A pure-Elixir QR code generator — zero dependencies, full spec, beautiful output.
Installation · Quick Start · Styling · Backgrounds · Logos · Payloads · API
Qiroex generates valid, scannable QR codes entirely in Elixir with no external dependencies — no C NIFs, no system libraries, no ImageMagick. It implements the full ISO 18004 specification and outputs to SVG, PNG, and terminal.
Qiroex is sponsored by Qiro, a platform for dynamic QR codes. Use Qiroex when you want QR generation fully inside Elixir, and pair it with Qiro when you need a destination you can update without reprinting the code.
- Zero dependencies — pure Elixir, runs anywhere the BEAM runs
- Full QR spec — versions 1–40, error correction L/M/Q/H, all 4 encoding modes (numeric, alphanumeric, byte, kanji), 8 mask patterns
- Three output formats — SVG (vector), PNG (raster), terminal (Unicode art)
- Visual styling — module shapes (circle, rounded, diamond, leaf, shield), custom colors, whole-code SVG gradients, finder pattern colors and shapes
- Background images — embed photos or SVG artwork behind the QR body in SVG output with a convenient file-based helper
- Logo embedding — embed SVG or raster image logos (PNG, JPEG, WEBP, GIF, BMP, AVIF, TIFF) with automatic coverage validation
- 11 payload builders — WiFi, URL, Email, SMS, Phone, Geo, vCard, vEvent, MeCard, Bitcoin, WhatsApp
- Input validation — descriptive error messages for every misconfiguration
- Verified output — broad unit coverage plus decoder-backed conformance tests with a real QR decoder
Add qiroex to your list of dependencies in mix.exs:
def deps do
[
{:qiroex, "~> 1.0"}
]
endQiroex requires Elixir 1.18+.
Upgrading from a pre-1.0 release? See CHANGELOG.md for the small API cleanup around :level and :quiet_zone.
Then run mix deps.get.
Qiroex.save_svg("https://qiro.gg", "qr.svg")Qiroex.save_png("https://qiro.gg", "qr.png")Qiroex.print("https://qiro.gg")In a real terminal, the default renderer uses compact Unicode blocks for dense output. The browser preview below is illustrative and may not be reliably scannable because Markdown code blocks do not preserve terminal cell geometry exactly:
█▀▀▀▀▀█ ▄▀▀█▄▄█ █▀▀▀▀▀█
█ ███ █ █▀ ▀ ▄█▄ █ ███ █
█ ▀▀▀ █ █▀▀▄ ▀▄▄▀ █ ▀▀▀ █
▀▀▀▀▀▀▀ █ ▀ ▀ █ █ ▀▀▀▀▀▀▀
▀▄▀▀██▀ ▄ ▄▄▀▄ ▄▀█▀▀▀▄
██▀██▄▀ █▀▄▄ █▀██ ▄█ ▀ ▀█
▄███▄ ▀ ▄ ▄▀▀ ▀▀▄▀▀▄▀▄▀█▀
█ ▄ ▀█ ▀█▀ ▄█▄▄███▀ ▀█
▀ ▀ ▀▀ ██▀▀▄▄▄▄█▀▀▀█▄▀
█▀▀▀▀▀█ ▄▄ ▄█▀ █ ▀ █▄▀█▀
█ ███ █ █▀▀ ▀ ▀█▀██▀█▄█▄█
█ ▀▀▀ █ ▀▀▀█▀ ▄▀▄▄ ▄▄█▀ █
▀▀▀▀▀▀▀ ▀▀ ▀ ▀▀▀▀▀▀▀▀
If your terminal font makes compact mode too dense, render one QR row per terminal line instead:
Qiroex.print("https://qiro.gg", compact: false)# Get an SVG string
{:ok, svg} = Qiroex.to_svg("Hello")
# Get a PNG binary
{:ok, png} = Qiroex.to_png("Hello")
# Get a QR struct for inspection
{:ok, qr} = Qiroex.encode("Hello")
Qiroex.info(qr)
# => %{version: 1, ec_level: :m, mode: :byte, mask: 4, modules: 21, data_bytes: 5}
# Get the raw 0/1 matrix
{:ok, matrix} = Qiroex.to_matrix("Hello")Control the encoding process with these options (available on all functions):
# Error correction level (:l, :m, :q, :h)
Qiroex.save_svg("Hello", "qr.svg", level: :h)
# Force a specific version (1–40)
Qiroex.save_svg("Hello", "qr.svg", version: 5)
# Force encoding mode
Qiroex.save_svg("12345", "qr.svg", mode: :numeric)
# Force mask pattern (0–7)
Qiroex.save_svg("Hello", "qr.svg", mask: 2)
# Combine freely
Qiroex.save_svg("Hello", "qr.svg", level: :q, version: 3, mask: 0)Qiroex.save_svg("Hello", "qr.svg",
module_size: 12, # pixel size of each module (default: 10)
quiet_zone: 2, # modules of white border (default: 4)
dark_color: "#4B275F", # hex, rgb/rgba, hsl/hsla, or supported named color
light_color: "#F4F1F6" # background color
)Qiroex.save_png("Hello", "qr.png",
module_size: 20, # pixel size per module (default: 10)
quiet_zone: 3, # quiet zone modules (default: 4)
dark_color: {75, 39, 95}, # {r, g, b} tuple, 0–255
light_color: {244, 241, 246} # background color
)PNG intentionally keeps a narrower rendering surface: it supports finder colors, but not logos, background images, gradients, finder shapes, or custom module shapes.
- SVG — best for the web, print, branding, logos, backgrounds, and advanced styling
- PNG — best when you need a simple raster file for chat apps, social uploads, or systems that expect bitmaps
- Terminal — best for CLIs, debugging, demos, and scripts where writing files would be overkill
Qiroex supports rich visual customization through the Qiroex.Style struct. All style options apply to SVG output; PNG supports finder pattern colors.
| Feature | SVG | PNG | Terminal |
|---|---|---|---|
| Module shapes | Yes | No | No |
| Finder colors | Yes | Yes | No |
| Finder shapes | Yes | No | No |
| Gradients | Yes | No | No |
Choose how individual data modules are rendered:
# Circular dots
style = Qiroex.Style.new(module_shape: :circle)
Qiroex.save_svg("Hello", "circles.svg", style: style)
# Rounded squares
style = Qiroex.Style.new(module_shape: :rounded, module_radius: 0.4)
Qiroex.save_svg("Hello", "rounded.svg", style: style)
# Diamond (rotated squares)
style = Qiroex.Style.new(module_shape: :diamond)
Qiroex.save_svg("Hello", "diamond.svg", style: style)
# Leaf (asymmetric rounded corners)
style = Qiroex.Style.new(module_shape: :leaf)
Qiroex.save_svg("Hello", "leaf.svg", style: style)
# Shield (flat top, curved pointed bottom)
style = Qiroex.Style.new(module_shape: :shield)
Qiroex.save_svg("Hello", "shield.svg", style: style):circle |
:rounded |
:diamond |
:leaf |
:shield |
Customize the three concentric layers of each finder pattern independently:
style = Qiroex.Style.new(
module_shape: :rounded,
module_radius: 0.3,
finder: %{
outer: "#E63946", # 7×7 dark border ring
inner: "#F1FAEE", # 5×5 light ring
eye: "#1D3557" # 3×3 dark center
}
)
Qiroex.save_svg("Hello", "finder.svg", style: style)Customize the shape of each finder pattern layer independently. Finder layers are rendered as single compound SVG elements for clean visual output. Available shapes: :square, :rounded, :circle, :diamond, :leaf, :shield.
# Circle finders with rounded data modules
style = Qiroex.Style.new(
module_shape: :rounded,
module_radius: 0.3,
finder: %{
outer: "#E63946", outer_shape: :rounded,
inner: "#F1FAEE", inner_shape: :square,
eye: "#1D3557", eye_shape: :circle
}
)
Qiroex.save_svg("Hello", "finder_shapes.svg", style: style)You can also set shapes without custom colors — the default dark/light colors will be used:
style = Qiroex.Style.new(
finder: %{
outer_shape: :rounded,
inner_shape: :rounded,
eye_shape: :circle
}
)Rounded |
Circle |
Leaf |
Shield |
Mixed shapes |
Apply linear or radial gradients across the QR code's dark data modules. Finder patterns stay flat unless you style them separately. SVG only:
# Linear gradient across the whole QR code at 135°
style = Qiroex.Style.new(
module_shape: :circle,
gradient: %{
type: :linear,
start_color: "#0F172A",
end_color: "#22D3EE",
angle: 135
}
)
Qiroex.save_svg("Hello", "gradient.svg", style: style)Linear |
Radial |
Combined |
Use a real image such as a JPEG or PNG photo as a background inside the QR body. Qiroex embeds the source directly into the SVG output, just like raster logos, so the result stays self-contained. The image is clipped to the QR content area and the quiet zone remains plain for scan reliability.
background = Qiroex.BackgroundImage.from_file!("photo.jpg",
opacity: 0.3,
fit: :cover
)
Qiroex.save_svg("https://qiro.gg", "photo-background.svg",
level: :h,
dark_color: "#0F172A",
light_color: "#F8FAFC",
background_image: background
)background = Qiroex.BackgroundImage.new(
image: File.read!("hero.jpg"),
opacity: 0.28,
fit: :contain
)
Qiroex.save_svg("https://qiro.gg", "background.svg", background_image: background)The same API also supports raw SVG markup:
background = Qiroex.BackgroundImage.new(svg: "<svg>...</svg>", fit: :contain)Tip: Start around
opacity: 0.22–0.35, prefer photos with bold shapes instead of pale low-contrast scenes, and use error correction level:hfor busy backgrounds.
Combine everything for maximum visual impact:
style = Qiroex.Style.new(
module_shape: :rounded,
module_radius: 0.35,
finder: %{outer: "#0F172A", inner: "#F8FAFC", eye: "#F97316"},
gradient: %{type: :linear, start_color: "#0F172A", end_color: "#22D3EE", angle: 25}
)
Qiroex.save_svg("https://qiro.gg", "styled.svg", light_color: "#F8FAFC", style: style)This kind of branded styling pairs well with Qiro when the printed QR code needs a dynamic destination that can keep evolving after launch.
Embed a logo in the center of your QR code. Qiroex supports both SVG markup and raster images (PNG, JPEG, WEBP, GIF, BMP, AVIF, TIFF) — all with zero dependencies. It automatically clears the modules behind the logo area and validates that the logo doesn't exceed the error correction capacity.
logo = Qiroex.Logo.new(
svg: ~s(<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="40" fill="#9B59B6"/>
<text x="50" y="62" text-anchor="middle" font-size="36"
font-weight="bold" fill="white" font-family="sans-serif">Ex</text>
</svg>),
size: 0.22, # 22% of QR code size
shape: :circle, # background shape (:square, :rounded, :circle)
padding: 1 # padding in modules around the logo
)
# Use high EC level (:h) for best scan reliability with logos
Qiroex.save_svg("https://qiro.gg", "logo.svg", level: :h, logo: logo)Load any image file and embed it directly — the format is auto-detected from the binary:
logo = Qiroex.Logo.new(
image: File.read!("company_logo.png"),
size: 0.22,
shape: :circle,
padding: 1
)
Qiroex.save_svg("https://qiro.gg", "branded.svg", level: :h, logo: logo)Raster images are embedded as base64 data URIs inside the SVG — no external files or dependencies needed. When shape is :rounded or :circle, the image is clipped to that shape using an SVG <clipPath>, so the image itself appears rounded or circular — not just the background behind it. You can also specify the format explicitly:
Qiroex.Logo.new(image: jpeg_bytes, image_type: :jpeg, size: 0.2)SVG Logo |
Styled + Logo |
PNG Logo |
Logos work seamlessly with all styling options:
style = Qiroex.Style.new(
module_shape: :rounded,
module_radius: 0.3,
finder: %{outer: "#4B275F", inner: "#FFFFFF", eye: "#9B59B6"}
)
Qiroex.save_svg("https://qiro.gg", "branded.svg",
level: :h, style: style, logo: logo)| Option | Default | Description |
|---|---|---|
:svg |
— | SVG markup string (provide :svg or :image) |
:image |
— | Binary image data: PNG, JPEG, WEBP, GIF, BMP (provide :image or :svg) |
:image_type |
auto-detected | Image format: :png, :jpeg, :webp, :gif, :bmp |
:size |
0.2 |
Logo size as fraction of QR code (0.0–0.4) |
:padding |
1 |
Padding around logo in modules |
:background |
"#ffffff" |
Background color behind the logo |
:shape |
:square |
Background shape: :square, :rounded, :circle. For raster images, also clips the image itself to the chosen shape. |
:border_radius |
4 |
Corner radius for :rounded shape |
Qiroex automatically validates that the logo doesn't cover too many modules. If the logo is too large for the chosen error correction level, you'll get a clear error message:
large_logo = Qiroex.Logo.new(svg: "<svg/>", size: 0.4)
{:error, message} = Qiroex.to_svg("Hello", level: :l, logo: large_logo)
# => "Logo covers 28.3% of modules, but EC level :l safely supports only 5.6%.
# Use a higher EC level or a smaller logo size."Tip: Always use error correction level
:hwhen embedding logos for maximum scan reliability.
Generate structured data payloads for common QR code use cases with a single function call:
# WiFi network — scan to connect
{:ok, svg} = Qiroex.payload(:wifi,
[ssid: "CoffeeShop", password: "latte2024"],
:svg, dark_color: "#2C3E50")Qiroex ships with 11 payload builders covering the most common QR code use cases:
Scan to auto-connect to a network.
{:ok, svg} = Qiroex.payload(:wifi,
[ssid: "MyNetwork", password: "secret123", auth: :wpa],
:svg)Open a website in the browser.
If the destination behind the QR should stay editable after print, Qiro can manage the dynamic URL while Qiroex handles the rendering side.
{:ok, svg} = Qiroex.payload(:url,
[url: "https://qiro.gg"],
:svg)Compose an email with pre-filled fields.
{:ok, svg} = Qiroex.payload(:email,
[to: "hello@example.com", subject: "Hi!", body: "Nice to meet you."],
:svg)Open the messaging app with a pre-filled text.
{:ok, svg} = Qiroex.payload(:sms,
[number: "+1-555-0123", message: "Hello!"],
:svg)Initiate a phone call.
{:ok, svg} = Qiroex.payload(:phone,
[number: "+1-555-0199"],
:svg)Open a map to a specific location.
{:ok, svg} = Qiroex.payload(:geo,
[latitude: 48.8566, longitude: 2.3522, query: "Eiffel Tower"],
:svg)Share a full contact card.
{:ok, svg} = Qiroex.payload(:vcard,
[first_name: "Jane", last_name: "Doe",
phone: "+1-555-0199", email: "jane@example.com",
org: "Acme Corp", title: "Engineer"],
:svg)Add a calendar event.
{:ok, svg} = Qiroex.payload(:vevent,
[summary: "Team Standup",
start: ~U[2026-03-01 09:00:00Z],
end: ~U[2026-03-01 09:30:00Z],
location: "Conference Room A"],
:svg)Share a contact (simpler alternative to vCard, popular on mobile).
{:ok, svg} = Qiroex.payload(:mecard,
[name: "Doe,Jane", phone: "+1-555-0199", email: "jane@example.com"],
:svg)Request a Bitcoin payment (BIP-21).
{:ok, svg} = Qiroex.payload(:bitcoin,
[address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa",
amount: 0.001, label: "Donation"],
:svg)Open a WhatsApp chat with a pre-filled message.
{:ok, svg} = Qiroex.payload(:whatsapp,
[number: "+1234567890", message: "Hello from Qiroex!"],
:svg)The third argument is the output format: :svg, :png, :terminal, :matrix, or :encode.
All functions return {:ok, result} / {:error, reason} tuples. Bang variants raise ArgumentError:
# Safe — returns error tuple
{:error, message} = Qiroex.encode("")
# => "Data cannot be empty"
{:error, message} = Qiroex.to_svg("test", level: :x)
# => "invalid error correction level: :x. Must be one of [:l, :m, :q, :h]"
{:error, message} = Qiroex.to_png("test", dark_color: "#000")
# => "invalid dark_color: \"#000\". Must be an {r, g, b} tuple with values 0–255"
# Bang — raises on error
svg = Qiroex.to_svg!("Hello") # returns SVG string directly
png = Qiroex.to_png!("Hello") # returns PNG binary directly
qr = Qiroex.encode!("Hello") # returns QR struct directly| Function | Description |
|---|---|
Qiroex.encode(data, opts) |
Encode data into a %Qiroex.QR{} struct |
Qiroex.to_svg(data, opts) |
Generate SVG string |
Qiroex.to_png(data, opts) |
Generate PNG binary |
Qiroex.to_terminal(data, opts) |
Generate terminal-printable string |
Qiroex.to_matrix(data, opts) |
Generate 2D list of 0/1 |
Qiroex.save_svg(data, path, opts) |
Write SVG to file |
Qiroex.save_png(data, path, opts) |
Write PNG to file |
Qiroex.print(data, opts) |
Print QR code to terminal |
Qiroex.payload(type, opts, format) |
Generate payload QR code |
Qiroex.info(qr) |
Get metadata about an encoded QR |
Qiroex.scanability(qr) |
Score an encoded QR for scan reliability |
Qiroex.scanability(data, opts) |
Encode data and evaluate scanability in one step |
All functions have bang (!) variants that raise instead of returning error tuples.
| Option | Values | Default | Description |
|---|---|---|---|
:level |
:l, :m, :q, :h |
:m |
Error correction level |
:version |
1–40, :auto |
:auto |
QR version (size) |
:mode |
:numeric, :alphanumeric, :byte, :kanji, :auto |
:auto |
Encoding mode |
:mask |
0–7, :auto |
:auto |
Mask pattern |
| Option | Type | Default | Description |
|---|---|---|---|
:module_size |
integer | 10 |
Pixel size of each module |
:quiet_zone |
integer | 4 |
Quiet zone border in modules |
:dark_color |
string | "#000000" |
SVG color string in hex, rgb/rgba, hsl/hsla, or supported named-color form |
:light_color |
string | "#ffffff" |
SVG color string in hex, rgb/rgba, hsl/hsla, or supported named-color form |
:style |
%Style{} |
nil |
Visual styling configuration |
:logo |
%Logo{} |
nil |
Center logo configuration |
:background_image |
%BackgroundImage{} |
nil |
Embedded SVG/photo background for SVG output |
| Option | Type | Default | Description |
|---|---|---|---|
:module_size |
integer | 10 |
Pixel size of each module |
:quiet_zone |
integer | 4 |
Quiet zone border in modules |
:dark_color |
{r,g,b} |
{0,0,0} |
RGB tuple for dark modules |
:light_color |
{r,g,b} |
{255,255,255} |
RGB tuple for background |
:style |
%Style{} |
nil |
Finder pattern colors |
Qiroex implements the full QR code pipeline from scratch:
Data → Mode Detection → Version Selection → Bit Encoding
→ Reed-Solomon EC → Interleaving → Matrix Placement
→ Masking (8 patterns × 4 penalty rules) → Format Info
→ Render (SVG / PNG / Terminal)
Key implementation details:
- Galois Field GF(2⁸) with primitive polynomial 285 and compile-time lookup tables
- Reed-Solomon error correction with polynomial division
- BCH encoding for format and version information
- Matrix stored as a
Mapof{row, col} => :dark | :lightwithMapSetfor reserved positions - SVG built with IO lists for zero-copy string assembly
- PNG encoded with pure-Erlang
:zliband:erlang.crc32— no image libraries needed
Every generated QR code can be evaluated for how easy it will be to scan. The score is computed from five factors that reflect real-world scanning conditions:
| Factor | Weight | What it measures |
|---|---|---|
| Error Correction | 25% | Higher EC level → more resilience to damage |
| Version Complexity | 25% | Higher version → finer modules → harder for cameras |
| Capacity Utilization | 20% | How full the version is (sweet spot: 30–70%) |
| Mask Penalty | 15% | ISO 18004 pattern penalty (lower = better balanced) |
| Data Density | 15% | Ratio of EC codewords to total codewords |
The result is a %Qiroex.Scanability{} struct with an overall score (0–100), a rating (:excellent, :good, :moderate, :poor), a human-readable summary, and a per-factor breakdown.
{:ok, qr} = Qiroex.encode("Hello, World!", level: :m)
result = Qiroex.scanability(qr)
result.score #=> 72
result.rating #=> :good
result.summary #=> "Good (72/100) — version 1, EC level M, 38% capacity used"{:ok, result} = Qiroex.scanability("Hello, World!", level: :m)
# Or use the bang variant (raises on encode error)
result = Qiroex.scanability!("12345", level: :h, mode: :numeric)
result.rating #=> :excellentresult = Qiroex.scanability!("Hello")
for factor <- result.factors do
IO.puts("#{factor.name}: #{factor.score}/100 (#{factor.rating}) — #{factor.detail}")
end
# error_correction: 60/100 (good) — EC level M provides ~15% error recovery
# version_complexity: 100/100 (excellent) — Version 1 produces a 21×21 module matrix
# capacity_utilization: 100/100 (excellent) — 38% used (optimal range)
# mask_penalty: 85/100 (good) — Mask penalty 312 (normalized 0.71 per module; lower is better)
# data_density: 65/100 (good) — 10 of 26 codewords are EC (38% redundancy)- Use
:hor:qerror correction for outdoor, printed, or worn surfaces - Keep data compact: use numeric or alphanumeric mode when possible
- Avoid forcing a higher version than needed — lower versions produce larger, more scannable modules
- If embedding a logo, always use
:hEC level to maintain enough redundancy
Before opening a PR, run the same quality gates used for release hardening:
mix format
mix test
mix test --include conformance
mix test --cover
mix credo --strictThe conformance suite uses zbarimg, and SVG conformance also rasterizes through rsvg-convert.
On macOS, install both with brew install zbar librsvg.
Qiroex is sponsored by Qiro, which handles dynamic QR codes for teams that need flexible destinations after a code is already in the wild. If you are generating branded QR assets and want the landing target to stay editable without reprinting, Qiro is the natural companion.
MIT License. See LICENSE for details.