# @leumas/qr

The QR engine: payload builders, a style model, and matrix → SVG / PNG / PDF rendering. Pure and browser-safe except for ./raster, which is the only subpath that touches sharp.


# @leumas/qr

The QR engine behind `qr.leumas.tech`: what text goes in a code, what it looks like, whether it will
actually scan, and how it comes out as SVG, PDF or PNG.

Absorbs and replaces the payload builders that used to live inside
`shared/engines/adapters/domain/qr/index.js` (themselves ported from `leumas-middleware/lib/a.qr`).
That adapter now imports this package rather than keeping its own copy, so a fix to WIFI escaping
lands everywhere at once.

## The split, and why it matters

Everything on the barrel is **pure and browser-safe**. `./raster` is the only subpath that touches
`sharp`, and it is separate for the same reason `@leumas/artwork` splits `./dpi` from `./prepare`: a
libvips binding of tens of megabytes must not sit on the boot path of a page that wanted a `mailto:`
string.

The practical payoff is that the generator page renders a **live, fully styled preview with no server
call**, and the download rasterises the *same* SVG string. Preview and export cannot drift, because
they are one function.

| step | subpath | needs |
|---|---|---|
| `buildPayload({type, …})` — what text goes in | `./payload` | nothing |
| `resolveStyle({…})` — what it looks like, and what's wrong with it | `./style` | nothing |
| `encodeMatrix(text, {…})` — the module grid | `./matrix` | `qrcode` |
| `planCode({matrix, style})` — format-neutral shapes | `./plan` | nothing |
| `renderSvg(plan)` · `renderPdf(plan)` | `./svg` · `./pdf` | nothing |
| `rasterize(svg, {…})` — PNG / JPEG / WebP | `./raster` | **sharp** — server only |

`generateSvg({payload, style})` runs the first five for the common case; `generatePlan(…)` stops one
step earlier so a caller exporting three formats encodes once.

## Payload types

`url` · `text` · `wifi` · `email` · `tel` · `sms` · `geo` · `vcard` · `event` · `whatsapp` ·
`crypto` · `dynamic`

**`dynamic` is the commercial one.** It builds `<base>/api/qr/r/<code>` — the short link whose
destination stays editable after the code is printed. It is a builder rather than a string assembled
at three call sites because the redirect route, the generator and the printed artwork must agree on
that URL *for ever*; a printed code cannot be reissued when they drift.

**There is deliberately no `pay` builder.** A payment link is a plain URL whose shape differs per
provider and changes without notice, and getting one wrong sends somebody's money nowhere. Build the
URL you have verified and pass it to `url`. `whatsapp` and `crypto` are here because both have a
published, stable URI scheme rather than a marketing URL that happens to work today.

[warning] **`crypto` amounts are not one unit.** BIP-21 (`bitcoin:` and friends) takes whole coins; EIP-681
(`ethereum:`) takes **wei**. This package passes each through unchanged and converts nothing — a form
must label its own field.

## Styling

Module shapes `square` · `dot` · `rounded` · `diamond`. Eye frames `square` · `rounded` · `circle` ·
`leaf`. Pupils `square` · `dot` · `rounded`. Plus flat or gradient foreground, per-eye colours, quiet
zone, error-correction level, a centred logo, and a caption bar.

Four module shapes, not six, and none of them joins to its neighbours. The connected "classy" look
every styling library ships needs each module to know which neighbours are filled, and gets subtly
wrong results at the finder boundaries and the timing rows. Four that are exactly right beat six with
two quietly deformed.

## The readability check is the point

`resolveStyle` **always returns a usable style plus a list of warnings** — it never throws. Almost
every bad style is still drawable: a pale code on white renders beautifully and no phone reads it; a
logo over a third of the modules produces a lovely image that fails at the till. Throwing would block
a preview the user is entitled to see; staying silent would ship the failure.

- `low_contrast` — **error** below 3:1, warning below 4.5:1.
- `inverted` — warning. Light-on-dark scans on modern phones and fails on older and industrial readers.
- `logo_too_large` — **error** above 25% width, where even level H starts to fail.
- `quiet_zone_small` — warning below the 4 modules ISO/IEC 18004 requires.
- `ec_raised` — informational: a logo silently raises error correction to `H`, because that repair has
  no downside and forgetting it has a large one. It happens *before* encoding; raising it afterwards
  would change nothing.

`scannable` is false when any warning is an error. **A save lane must read it.**

## Formats

- **SVG** — the vector deliverable, and what a printer wants.
- **PDF** — a real vector PDF, written here with no library: because `./plan.js` emits only
  rectangles-with-radii and polygons, every shape converts to Bézier operators with no loss.
  Gradients become PDF axial/radial shadings. **A logo is omitted** and reported as `logoOmitted` —
  embedding an arbitrary image means owning a PNG decoder or putting JPEG ringing on the hard edges a
  scanner samples. Export PNG or SVG for a logo'd code.
- **PNG / JPEG / WebP** — `./raster`, via sharp rendering the SVG. `rasterizeForPrint({mm, dpi})`
  sizes by physical width, because "2000px" says nothing about whether a module survives the press.

[warning] `rasterize`'s `density` is the grid sharp rasterises the SVG at *before* scaling to `width`. Left
low, a large `width` produces a big **fuzzy** image rather than a big sharp one — that is the actual
cause of soft QR exports.

## Tests

`node --test test/` — 24 checks. The central one rasterises at an odd number of pixels per module,
samples the centre of every module, and asserts the result **is** the source matrix, for every module
shape and at all three finders. There is no QR decoder in this workspace, and adding one would test
somebody else's library; what *this* package can get wrong is the drawing — a transposed matrix, an
eye one module out, a shape that misses its own cell — and every one of those produces a plausible
image that throws nothing.


---
Source: shared/packages/qr/README.md
Canonical: https://docs.leumas.tech/p/packages/qr
