Packages

Pure-Elixir QR code generator with zero dependencies — SVG, PNG, and terminal output with styling and logo support.

Current section

Files

Jump to
qiroex lib qiroex.ex
Raw

lib/qiroex.ex

defmodule Qiroex do
@moduledoc """
Pure-Elixir QR code generation library with zero dependencies.
Generates valid, scannable QR codes supporting all 40 versions,
4 encoding modes (numeric, alphanumeric, byte, kanji),
4 error correction levels (L/M/Q/H), and output in SVG, PNG, and
terminal formats.
## Quick Start
iex> {:ok, %Qiroex.QR{}} = Qiroex.encode("Hello, World!")
iex> {:ok, svg} = Qiroex.to_svg("Hello, World!")
iex> String.contains?(svg, "<svg")
true
iex> {:ok, png} = Qiroex.to_png("Hello, World!")
iex> :binary.part(png, 0, 8)
<<137, 80, 78, 71, 13, 10, 26, 10>>
iex> {:ok, rows} = Qiroex.to_matrix("Hello, World!")
iex> is_list(rows) and is_list(hd(rows))
true
## Encoding Options
All rendering functions accept encoding options as well:
- `:level` — error correction level (`:l`, `:m`, `:q`, `:h`). Default: `:m`
- `:version` — force a specific version (1–40) or `:auto`. Default: `:auto`
- `:mode` — force encoding mode (`:numeric`, `:alphanumeric`, `:byte`, `:kanji`)
or `:auto`. Default: `:auto`
- `:mask` — force mask pattern (0–7) or `:auto`. Default: `:auto`
## Styling
Customize QR code appearance with the `Qiroex.Style` struct:
style = Qiroex.Style.new(module_shape: :circle)
{:ok, svg} = Qiroex.to_svg("Hello", style: style)
## Logos
Embed an SVG or raster logo in the center of the code (SVG output only):
logo = Qiroex.Logo.new(svg: "<svg>...</svg>", size: 0.2)
{:ok, svg} = Qiroex.to_svg("Hello", level: :h, logo: logo)
## Background Images
Embed a photo-style background image inside the QR body (SVG output only):
background = Qiroex.BackgroundImage.from_file!("photo.jpg", opacity: 0.2)
{:ok, svg} = Qiroex.to_svg("Hello", level: :h, background_image: background)
## Payload Helpers
Generate structured QR payloads (WiFi, vCard, etc.) in one step:
{:ok, svg} = Qiroex.payload(:wifi, [ssid: "Net", password: "pass"], :svg)
"""
alias Qiroex.{QR, Scanability, Validate}
alias Qiroex.Render.{PNG, SVG, Terminal}
@encode_option_keys [:level, :version, :mode, :mask]
@matrix_render_keys [:quiet_zone]
@svg_render_keys [
:module_size,
:quiet_zone,
:dark_color,
:light_color,
:style,
:logo,
:background_image
]
@png_render_keys [:module_size, :quiet_zone, :dark_color, :light_color, :style]
@terminal_render_keys [:quiet_zone, :compact]
@matrix_option_keys @encode_option_keys ++ @matrix_render_keys
@svg_option_keys @encode_option_keys ++ @svg_render_keys
@png_option_keys @encode_option_keys ++ @png_render_keys
@terminal_option_keys @encode_option_keys ++ @terminal_render_keys
@png_unsupported_option_messages %{
logo:
"unsupported option :logo for Qiroex.to_png/2. Logo embedding is only available in SVG output.",
background_image:
"unsupported option :background_image for Qiroex.to_png/2. Background images are only available in SVG output."
}
# ─── Encode ──────────────────────────────────────────────────────────
@doc """
Encodes data into a QR code struct.
## Options
- `:level` — error correction level (`:l`, `:m`, `:q`, `:h`). Default: `:m`
- `:version` — force a specific version (1–40) or `:auto`. Default: `:auto`
- `:mode` — force encoding mode or `:auto`. Default: `:auto`
- `:mask` — force mask pattern (0–7) or `:auto`. Default: `:auto`
## Examples
iex> {:ok, %Qiroex.QR{}} = Qiroex.encode("Hello")
iex> {:ok, qr} = Qiroex.encode("12345", level: :h, mode: :numeric)
iex> qr.ec_level
:h
iex> Qiroex.encode("")
{:error, "Data cannot be empty"}
## Returns
`{:ok, %Qiroex.QR{}}` or `{:error, reason}`
"""
@spec encode(binary(), keyword()) :: {:ok, QR.t()} | {:error, String.t()}
def encode(data, opts \\ []) do
QR.encode(data, opts)
end
@doc """
Encodes data into a QR code struct, raising on error.
Same options as `encode/2`. Raises `ArgumentError` on invalid data or options.
"""
@spec encode!(binary(), keyword()) :: QR.t()
def encode!(data, opts \\ []) do
case encode(data, opts) do
{:ok, qr} -> qr
{:error, reason} -> raise ArgumentError, reason
end
end
# ─── Matrix ─────────────────────────────────────────────────────────
@doc """
Generates a QR code and returns it as a 2D list of 0s and 1s.
## Options
Same as `encode/2`, plus:
- `:quiet_zone` — quiet zone size in modules. Default: 4
## Examples
iex> {:ok, rows} = Qiroex.to_matrix("Hi")
iex> is_list(rows) and is_list(hd(rows))
true
"""
@spec to_matrix(binary(), keyword()) :: {:ok, list(list(0 | 1))} | {:error, String.t()}
def to_matrix(data, opts \\ []) do
{render_opts, encode_opts} = split_render_opts(opts, @matrix_render_keys)
quiet_zone = Keyword.get(render_opts, :quiet_zone, 4)
with :ok <- Validate.option_keys(opts, @matrix_option_keys, "Qiroex.to_matrix/2"),
:ok <- Validate.matrix_render_opts(render_opts),
{:ok, qr} <- encode(data, encode_opts) do
{:ok, QR.to_matrix(qr, quiet_zone)}
end
end
@doc """
Generates a QR code matrix, raising on error.
Same options as `to_matrix/2`.
"""
@spec to_matrix!(binary(), keyword()) :: list(list(0 | 1))
def to_matrix!(data, opts \\ []) do
case to_matrix(data, opts) do
{:ok, rows} ->
rows
{:error, reason} ->
raise ArgumentError, reason
end
end
# ─── SVG ─────────────────────────────────────────────────────────────
@doc """
Generates a QR code and renders it as an SVG string.
## Options
Same as `encode/2`, plus:
- `:module_size` — pixel size of each module (default: 10)
- `:quiet_zone` — quiet zone modules (default: 4)
- `:dark_color` — SVG color for dark modules in hex, rgb/rgba, hsl/hsla, or supported named-color form (default: `"#000000"`)
- `:light_color` — SVG color for background in hex, rgb/rgba, hsl/hsla, or supported named-color form (default: `"#ffffff"`)
- `:style` — a `%Qiroex.Style{}` struct for shapes, finder colors, gradients
- `:logo` — a `%Qiroex.Logo{}` struct for center logo embedding
- `:background_image` — a `%Qiroex.BackgroundImage{}` struct for embedded photo or SVG backgrounds
## Examples
iex> {:ok, svg} = Qiroex.to_svg("Hello")
iex> String.contains?(svg, "<svg")
true
iex> {:ok, svg} = Qiroex.to_svg("Hello", dark_color: "#336699", module_size: 5)
iex> String.contains?(svg, "#336699")
true
## Returns
`{:ok, svg_string}` or `{:error, reason}`
"""
@spec to_svg(binary(), keyword()) :: {:ok, String.t()} | {:error, String.t()}
def to_svg(data, opts \\ []) do
{render_opts, encode_opts} = split_render_opts(opts, @svg_render_keys)
with :ok <- Validate.option_keys(opts, @svg_option_keys, "Qiroex.to_svg/2"),
:ok <- Validate.svg_render_opts(render_opts),
{:ok, qr} <- encode(data, encode_opts),
:ok <- validate_logo_coverage(render_opts, qr) do
{:ok, SVG.render(qr.matrix, render_opts)}
end
end
@doc """
Generates a QR code SVG string, raising on error.
Same options as `to_svg/2`.
"""
@spec to_svg!(binary(), keyword()) :: String.t()
def to_svg!(data, opts \\ []) do
case to_svg(data, opts) do
{:ok, svg} -> svg
{:error, reason} -> raise ArgumentError, reason
end
end
# ─── PNG ─────────────────────────────────────────────────────────────
@doc """
Generates a QR code and renders it as a PNG binary.
## Options
Same as `encode/2`, plus:
- `:module_size` — pixel size of each module (default: 10)
- `:quiet_zone` — quiet zone modules (default: 4)
- `:dark_color` — `{r, g, b}` tuple with values 0–255 (default: `{0, 0, 0}`)
- `:light_color` — `{r, g, b}` tuple with values 0–255 (default: `{255, 255, 255}`)
- `:style` — a `%Qiroex.Style{}` struct for finder pattern colors
PNG output supports finder styling colors, but not SVG-only features such as
`:logo` or `:background_image`.
## Examples
iex> {:ok, png} = Qiroex.to_png("Hello")
iex> :binary.part(png, 0, 8)
<<137, 80, 78, 71, 13, 10, 26, 10>>
iex> {:ok, png} = Qiroex.to_png("Hello", module_size: 20)
iex> is_binary(png)
true
## Returns
`{:ok, png_binary}` or `{:error, reason}`
"""
@spec to_png(binary(), keyword()) :: {:ok, binary()} | {:error, String.t()}
def to_png(data, opts \\ []) do
{render_opts, encode_opts} = split_render_opts(opts, @png_render_keys)
with :ok <-
Validate.option_keys(
opts,
@png_option_keys,
"Qiroex.to_png/2",
@png_unsupported_option_messages
),
:ok <- Validate.png_render_opts(render_opts),
{:ok, qr} <- encode(data, encode_opts) do
{:ok, PNG.render(qr.matrix, render_opts)}
end
end
@doc """
Generates a QR code PNG binary, raising on error.
Same options as `to_png/2`.
"""
@spec to_png!(binary(), keyword()) :: binary()
def to_png!(data, opts \\ []) do
case to_png(data, opts) do
{:ok, png} -> png
{:error, reason} -> raise ArgumentError, reason
end
end
# ─── File Saving ─────────────────────────────────────────────────────
@doc """
Generates a QR code SVG and writes it to a file.
## Options
Same as `to_svg/2`.
## Examples
:ok = Qiroex.save_svg("Hello", "/tmp/hello.svg")
:ok = Qiroex.save_svg("Hello", "/tmp/hello.svg", dark_color: "#003366")
## Returns
`:ok` or `{:error, reason}`
"""
@spec save_svg(binary(), Path.t(), keyword()) :: :ok | {:error, term()}
def save_svg(data, path, opts \\ []) do
case to_svg(data, opts) do
{:ok, svg} -> File.write(path, svg)
error -> error
end
end
@doc """
Generates a QR code PNG and writes it to a file.
## Options
Same as `to_png/2`.
## Examples
:ok = Qiroex.save_png("Hello", "/tmp/hello.png")
:ok = Qiroex.save_png("Hello", "/tmp/hello.png", module_size: 20)
## Returns
`:ok` or `{:error, reason}`
"""
@spec save_png(binary(), Path.t(), keyword()) :: :ok | {:error, term()}
def save_png(data, path, opts \\ []) do
case to_png(data, opts) do
{:ok, png} -> File.write(path, png)
error -> error
end
end
# ─── Terminal ────────────────────────────────────────────────────────
@doc """
Generates a QR code and renders it as a terminal-printable string.
## Options
Same as `encode/2`, plus:
- `:quiet_zone` — quiet zone modules (default: 4)
- `:compact` — use compact 2-row-per-line rendering (default: `true`)
## Examples
{:ok, str} = Qiroex.to_terminal("Hello")
## Returns
`{:ok, terminal_string}` or `{:error, reason}`
"""
@spec to_terminal(binary(), keyword()) :: {:ok, String.t()} | {:error, String.t()}
def to_terminal(data, opts \\ []) do
{render_opts, encode_opts} = split_render_opts(opts, @terminal_render_keys)
with :ok <- Validate.option_keys(opts, @terminal_option_keys, "Qiroex.to_terminal/2"),
:ok <- Validate.terminal_render_opts(render_opts),
{:ok, qr} <- encode(data, encode_opts) do
{:ok, Terminal.render(qr.matrix, render_opts)}
end
end
@doc """
Generates a QR code terminal string, raising on error.
Same options as `to_terminal/2`.
"""
@spec to_terminal!(binary(), keyword()) :: String.t()
def to_terminal!(data, opts \\ []) do
case to_terminal(data, opts) do
{:ok, str} -> str
{:error, reason} -> raise ArgumentError, reason
end
end
@doc """
Generates a QR code and prints it directly to the terminal.
Same options as `to_terminal/2`.
## Examples
Qiroex.print("Hello")
Qiroex.print("Hello", compact: false)
"""
@spec print(binary(), keyword()) :: :ok
def print(data, opts \\ []) do
str = to_terminal!(data, opts)
IO.puts(str)
end
# ─── Payload Helpers ─────────────────────────────────────────────────
@doc """
Encodes a structured payload and generates a QR code in one step.
## Parameters
- `type` — payload type atom (`:wifi`, `:url`, `:email`, `:sms`, `:phone`, `:geo`,
`:vcard`, `:vevent`, `:mecard`, `:bitcoin`, `:whatsapp`)
- `payload_opts` — keyword list of payload-specific options
- `format` — output format (`:svg`, `:png`, `:terminal`, `:matrix`, `:encode`)
- `render_opts` — keyword list of render/encode options (optional)
## Examples
iex> {:ok, qr} = Qiroex.payload(:wifi, [ssid: "MyNet", password: "secret"], :encode)
iex> match?(%Qiroex.QR{}, qr)
true
iex> {:ok, rows} = Qiroex.payload(:url, [url: "https://qiro.gg"], :matrix)
iex> is_list(rows)
true
## Returns
`{:ok, result}` or `{:error, reason}`
"""
@spec payload(atom(), keyword(), atom(), keyword()) ::
{:ok, term()} | {:error, String.t()}
def payload(type, payload_opts, format, render_opts \\ []) do
with :ok <- Validate.payload_format(format),
{:ok, data} <- build_payload(type, payload_opts) do
case format do
:svg -> to_svg(data, render_opts)
:png -> to_png(data, render_opts)
:terminal -> to_terminal(data, render_opts)
:matrix -> to_matrix(data, render_opts)
:encode -> encode(data, render_opts)
end
end
end
@doc """
Same as `payload/4` but raises on error.
"""
@spec payload!(atom(), keyword(), atom(), keyword()) :: term()
def payload!(type, payload_opts, format, render_opts \\ []) do
case payload(type, payload_opts, format, render_opts) do
{:ok, result} -> result
{:error, reason} -> raise ArgumentError, reason
end
end
# ─── Introspection ──────────────────────────────────────────────────
@doc """
Returns metadata about an encoded QR code.
## Examples
iex> {:ok, qr} = Qiroex.encode("Hello")
iex> info = Qiroex.info(qr)
iex> info.ec_level
:m
iex> info.version >= 1
true
"""
@spec info(QR.t()) :: map()
def info(%QR{} = qr) do
size = qr.matrix.size
%{
version: qr.version,
ec_level: qr.ec_level,
mode: qr.mode,
mask: qr.mask,
modules: size,
data_bytes: byte_size(qr.data)
}
end
# ─── Scanability ─────────────────────────────────────────────────────
@doc """
Evaluates the scanability of an already-encoded `%Qiroex.QR{}` struct.
Returns a `%Qiroex.Scanability{}` with an overall score (0–100), a rating,
a human-readable summary, and a per-factor breakdown.
## Ratings
- `:excellent` — score 80–100
- `:good` — score 60–79
- `:moderate` — score 40–59
- `:poor` — score 0–39
## Examples
{:ok, qr} = Qiroex.encode("Hello")
result = Qiroex.scanability(qr)
result.rating #=> :good
result.summary #=> "Good (72/100) — version 1, EC level M, 38% capacity used"
"""
@spec scanability(QR.t()) :: Scanability.t()
def scanability(%QR{} = qr), do: Scanability.evaluate(qr)
@doc """
Encodes data and evaluates the scanability of the resulting QR code.
Accepts the same options as `encode/2`.
## Examples
iex> {:ok, result} = Qiroex.scanability("Hello", level: :h)
iex> match?(%Qiroex.Scanability{}, result)
true
## Returns
`{:ok, %Qiroex.Scanability{}}` or `{:error, reason}`
"""
@spec scanability(binary(), keyword()) :: {:ok, Scanability.t()} | {:error, String.t()}
def scanability(data, opts) when is_binary(data) do
case encode(data, opts) do
{:ok, qr} -> {:ok, Scanability.evaluate(qr)}
error -> error
end
end
@doc """
Encodes data and evaluates scanability, raising on error.
Accepts the same options as `encode/2`.
## Examples
iex> result = Qiroex.scanability!("Hello", level: :h)
iex> match?(%Qiroex.Scanability{}, result)
true
"""
@spec scanability!(binary(), keyword()) :: Scanability.t()
def scanability!(data, opts \\ []) when is_binary(data) do
case scanability(data, opts) do
{:ok, result} -> result
{:error, reason} -> raise ArgumentError, reason
end
end
# ─── Private ─────────────────────────────────────────────────────────
@payload_modules %{
wifi: Qiroex.Payload.WiFi,
url: Qiroex.Payload.URL,
email: Qiroex.Payload.Email,
sms: Qiroex.Payload.SMS,
phone: Qiroex.Payload.Phone,
geo: Qiroex.Payload.Geo,
vcard: Qiroex.Payload.VCard,
vevent: Qiroex.Payload.VEvent,
mecard: Qiroex.Payload.MeCard,
bitcoin: Qiroex.Payload.Bitcoin,
whatsapp: Qiroex.Payload.WhatsApp
}
defp build_payload(type, opts) do
case Map.fetch(@payload_modules, type) do
{:ok, module} ->
module.encode(opts)
:error ->
{:error,
"unknown payload type: #{inspect(type)}. Valid types: #{inspect(Map.keys(@payload_modules))}"}
end
end
defp split_render_opts(opts, render_keys) do
Keyword.split(opts, render_keys)
end
defp validate_logo_coverage(render_opts, qr) do
case Keyword.get(render_opts, :logo) do
nil ->
:ok
%Qiroex.Logo{} = logo ->
module_size = Keyword.get(render_opts, :module_size, 10)
Qiroex.Logo.validate_coverage(logo, qr.matrix.size, module_size, qr.ec_level)
end
end
end