QR code encoder and decoder in pure Elixir. Zero dependencies — no NIFs, no ports, no C.

Installation

def deps do
  [{:qqr, "~> 0.2.0"}]
end

Encoding

{: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
end

rgba_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"
end

From 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.

InputQQR.decode_matrixQRex (Rust NIF)QQR.decode (RGBA)
Version 1, "Hello"30 µs51 µs1.5 ms
Version 2, URL55 µs70 µs2.1 ms
Version 6, 100 chars251 µs146 µs5.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.

License

MIT