QQR
QR code encoder and decoder in pure Elixir. Zero dependencies — no NIFs, no ports, no C.
Installation
def deps do
[{:qqr, "~> 0.2.0"}]
endEncoding
{:ok, matrix} = QQR.encode("Hello World")
{:ok, matrix} = QQR.encode("12345", ec_level: :high, mode: :numeric)
Options: :ec_level (:low, :medium, :quartile, :high), :mode (:numeric, :alphanumeric, :byte, :auto), :version (1–40), :mask (0–7). All default to auto.
SVG
svg = QQR.to_svg("https://example.com")
svg = QQR.to_svg("Hello", dot_shape: :rounded, color: "#336699")
Styling: :dot_shape (:square, :rounded, :dots, :diamond), :finder_shape (:square, :rounded, :dots), :dot_size, :module_size, :quiet_zone, :color, :background, :logo. See QQR.SVG for details.
Phoenix LiveView
<div class="qr"><%= raw(QQR.to_svg_iodata(@url, dot_shape: :rounded)) %></div>to_svg_iodata/2 returns iodata — no extra binary copy, sent directly to the socket.
PNG with stb_image
{:ok, matrix} = QQR.encode("Hello World")
dim = matrix.width
scale = 10
quiet = 4
img_dim = (dim + quiet * 2) * scale
rgb =
for y <- 0..(img_dim - 1), x <- 0..(img_dim - 1), into: <<>> do
qr_x = div(x, scale) - quiet
qr_y = div(y, scale) - quiet
if QQR.BitMatrix.get(matrix, qr_x, qr_y),
do: <<0, 0, 0>>,
else: <<255, 255, 255>>
end
%StbImage{data: rgb, shape: {img_dim, img_dim, 3}, type: {:u, 8}}
|> StbImage.write_file!("qr.png")Decoding
From RGBA pixels
case QQR.decode(rgba_binary, width, height) do
{:ok, result} ->
result.text #=> "https://example.com"
result.version #=> 3
result.bytes #=> [104, 116, 116, 112, ...]
result.chunks #=> [%QQR.Chunk{mode: :byte, text: "https://example.com", bytes: [...]}]
result.location #=> %QQR.Location{top_left_corner: {10.5, 10.5}, ...}
:error ->
# no QR code found
endrgba_binary is a binary of RGBA pixels — 4 bytes per pixel, same format as ImageData in browsers.
From a file with stb_image
{:ok, img} = StbImage.read_file("photo.png")
{h, w, c} = img.shape
rgba =
case c do
4 -> img.data
3 -> for <<r, g, b <- img.data>>, into: <<>>, do: <<r, g, b, 255>>
end
case QQR.decode(rgba, w, h) do
{:ok, result} -> result.text
:error -> "no QR code found"
endFrom a module grid
Skip image processing when you already have a binarized grid:
QQR.decode_matrix(bit_matrix)Inversion
By default both normal and inverted (light-on-dark) images are tried. Pass inversion: :dont_invert for ~2× speedup when you know the background is white.
Features
- Versions 1–40, all error correction levels (L/M/Q/H)
- Numeric, alphanumeric, and byte encoding/decoding modes
- Kanji decoding (raw bytes — Shift-JIS to text conversion not yet implemented)
- ECI segment parsing (designators consumed, encoding not applied)
- Reed-Solomon error correction (encode and decode)
- Adaptive binarization, perspective correction
- Dark-background (inverted) and mirror/transposed QR codes
- SVG rendering with dot shapes (square, rounded, dots, diamond), finder pattern styling, and logo embedding
Benchmarks
Compared against qrex (Rust NIF, PNG input). Run with elixir bench/decode.exs.
| Input | QQR.decode_matrix | QRex (Rust NIF) | QQR.decode (RGBA) |
|---|---|---|---|
| Version 1, "Hello" | 30 µs | 51 µs | 1.5 ms |
| Version 2, URL | 55 µs | 70 µs | 2.1 ms |
| Version 6, 100 chars | 251 µs | 146 µs | 5.5 ms |
Grid-only decode (decode_matrix) is 1.3–1.7× faster than Rust for small and medium QR codes. The full RGBA pipeline is slower due to image processing overhead in the binarizer and locator.
How it works
Encode: text → data bits → RS error correction → matrix → mask → QR
Decode: RGBA → binarize → locate → extract → unmask → RS correct → text
GF(256) exp/log tables are compiled into pattern-matched function heads. The BitMatrix uses a flat tuple with :erlang.element/2 for constant-time access. No mutable state — zigzag traversal, Bresenham walks, and polynomial arithmetic are purely functional.
Encoder ported from etiket. Decoder ported from jsQR with algorithm verification against quirc.