Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 28 additions & 26 deletions packages/barcodes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ camera, fast enough to scan on a phone at thirty frames a second.
length.
- **It is fast.** A 1080p frame with no symbol in it costs a few
milliseconds for QR and under two for the linear and stacked readers, so
a camera loop never falls behind. It reads QR 1.5 to 4x faster than
a camera loop never falls behind. It reads QR 1.7 to 5.5x faster than
the TypeScript library it was ported from.
- **It is small and self-contained.** Plain code, no WebAssembly, no native
module, nothing to install beyond the package. One import gives you all
Expand Down Expand Up @@ -273,8 +273,8 @@ decoded symbol, `resultQR` for the sampled module grid (scaled by
| `async` | decode with `decodeAsync`, yielding to the host between chunks | `false` |
| `decodeAll` | decode every symbol on the frame and draw every lock | `false` |
| `effort`, `timeLimit`, `nativeLimit`, `textDecoder` | passed to the scanner (see [Decoding](#decoding)) | |
| `nativeEvery` | with `nativeLimit`: the frame interval at which the native layer is searched regardless | `1` |
| `overlayColor`, `overlayFailedColor` | the lock, and the lock with `drawFailed` on a symbol found but not read | green, red |
| `nativeEvery` | with `nativeLimit`: every `nativeEvery`-th frame searches the native layer regardless; `1` never does | `1` |
| `overlayColor`, `overlayFailedColor` | the lock, and the lock with `drawFailed` on a symbol found but not read | `#4ade80`, `#f87171` |
| `overlaySideColor` | dims the frame outside the scanned region | `black` |
| `overlayEase` | of the way to a lock's new corners per display frame; `1` snaps | `0.3` |
| `overlayTimeout` | milliseconds a lock outlives its last sighting before fading | `500` |
Expand Down Expand Up @@ -351,7 +351,7 @@ with the rows it touches.
| QR, printed page | 2.55 ms | 7.12 ms |
| QR, noise | 8.95 ms | 21.94 ms |
| QR, fine weave | 3.13 ms | 8.73 ms |
| QR, desk, `nativeLimit: 1500` | | 4.05 ms |
| QR, desk, nativeLimit 1500 | | 4.05 ms |
| Code 128, desk | 764 µs | 1.16 ms |
| Code 128, printed page | | 742 µs |
| Code 128, noise | | 1.73 ms |
Expand All @@ -360,8 +360,9 @@ with the rows it touches.

A camera runs the miss rows thirty times a second, so they are what
sets the frame budget; the phone is about twice as slow as this machine.
Photographs from ZXing's corpus decode in a fraction of a millisecond to
11 ms, the slowest a 2390x2220 PDF417 of 74 rows by 12 columns at level 8.
Photographs from ZXing's corpus read in 1.2 ms on average across the
scorecard's 1332 decodes; the slowest, a 2390x2220 PDF417 of 74 rows by
12 columns at level 8, takes 6 ms once warm.

`rip test/corpus.rip` scores the readers on ZXing's blackbox photographs
against the counts ZXing's own tests require; `test/corpus.txt` is that
Expand All @@ -381,27 +382,27 @@ and its C++ build ([zxing-wasm](https://github.com/Sec-ant/zxing-wasm),
this package; [paulmillr/qr](https://github.com/paulmillr/qr), the
implementation the QR half was ported from, as 0.7.0 was released and with
[pull request 39](https://github.com/paulmillr/qr/pull/39) applied, checked
out at `misc/qr` and `misc/qr-perf`; and this package as it stood before
its own review round, checked out at `misc/rip-prior`, and as it is. A row
out at `misc/qr` and `misc/qr-perf`; and this package twice, as it stands
on main, checked out at `misc/rip-prior`, and as the working tree. A row
is the best of three 400 ms means, the columns timed left to right, each
with how many times faster this package is:

| Encode (µs) | qr 0.7.0 | qr + PR 39 | rip prior | rip |
| Encode (µs) | qr 0.7.0 | qr + PR 39 | rip main | rip |
|-----------------------------|-------------:|-------------:|-------------:|-------:|
| raw, version 1 | 3.1 (1.4x) | 2.2 (1.0x) | 2.3 (1.1x) | 2.2 |
| raw, version 10 | 18.5 (1.4x) | 12.9 (1.0x) | 14.6 (1.1x) | 12.9 |
| raw, version 22 | 53.0 (1.3x) | 41.2 (1.0x) | 46.5 (1.2x) | 40.1 |
| svg, version 10 | 45.5 (2.4x) | 21.2 (1.1x) | 29.4 (1.5x) | 19.3 |
| gif, version 10 | 18.4 (1.4x) | 12.7 (1.0x) | 14.8 (1.2x) | 12.9 |

| Decode (µs) | ZXing | zxing-wasm | qr 0.7.0 | qr + PR 39 | rip prior | rip |
| Decode (µs) | ZXing | zxing-wasm | qr 0.7.0 | qr + PR 39 | rip main | rip |
|-----------------------------|---------------:|---------------:|---------------:|-------------:|-------------:|-------:|
| 132x132 raster, version 1 | 49.2 (2.0x) | 79.8 (3.3x) | 117 (4.8x) | 21.9 (0.9x) | 34.5 (1.4x) | 24.4 |
| 1280x720 frame, one symbol | 2040 (3.6x) | 2102 (3.7x) | 1004 (1.7x) | 554 (1.0x) | 621 (1.1x) | 574 |
| 1920x1080 frame, one symbol | 4509 (3.3x) | 4774 (3.5x) | 2276 (1.7x) | 1232 (0.9x) | 1498 (1.1x) | 1378 |
| 1920x1080 weave, no symbol | 4516 (1.0x) | 8122 (1.8x) | 24623 (5.5x) | 4437 (1.0x) | 5536 (1.2x) | 4452 |

| Read (µs) | ZXing | zxing-wasm | rip prior | rip |
| Read (µs) | ZXing | zxing-wasm | rip main | rip |
|-----------------------------|---------------:|---------------:|-------------:|-------:|
| Code 128, 1920x1080 | 2028 (18.8x) | 1648 (15.3x) | 170 (1.6x) | 108 |
| PDF417, 1920x1080 | 4448 (39.4x) | 4276 (37.9x) | 146 (1.3x) | 113 |
Expand All @@ -410,10 +411,10 @@ Encode inputs are `Hello world`, 192 bytes and 768 bytes of text. Decode
inputs are synthetic RGBA frames with one symbol centered on a flat
background, plus a full-frame weave for the miss case, which is dominated
by the finder search; this package walks it on packed words. The Code 128
and PDF417 rasters are the bench's 1080p hit frames; ZXing reads them
and PDF417 rasters are built as the bench's 1080p hit frames are; ZXing reads them
through its own luma conversion, as its benchmark does. Pull request 39
carries this package's QR decoder work back to its origin, so those two
columns trade blows; the one algorithmic difference left between them is
columns trade blows; the one algorithmic difference between them is
the grid sampler, which reads the nearest pixel there and interpolates
here.

Expand Down Expand Up @@ -456,12 +457,13 @@ and torn start columns and holes.

Three probes under `test/` print one hash line per case, so two builds can
be shown identical beyond the corpus counts: `probe-stages.rip` hashes the
decoder's grid, codewords and result for every version, level, mask, text
kind, scale and damage level; `probe-pixels.rip` hashes every pyramid
layer's luma, cuts, blocks, packed bitmap and finder count across sizes,
input formats, padding and offsets; `probe-encode.rip` hashes every
encoder output in every format and option. Each header carries its diff
recipe.
decoder's grid, codewords and result for every version and level, the
automatic mask and one explicit mask, three text kinds, scales 1 to 3 and
six damage levels; `probe-pixels.rip` hashes every pyramid layer's luma,
cuts, blocks, packed bitmap and finder count across sizes, input formats,
padding and offsets; `probe-encode.rip` hashes every output format across
payload alphabets, ecc levels, six border/scale pairs and both mask
choices. Each header carries its diff recipe.

## Design

Expand All @@ -486,8 +488,8 @@ together.

One file per symbology, `qr.rip`, `pdf417.rip` and `code128.rip`, each
holding its tables, encoder and reader; a root entry that re-exports all
three; the camera and canvas plumbing in `dom.rip`; and the GIF writer, ECI
table and image-input helpers the symbologies share. The package is
three; the camera and canvas plumbing in `dom.rip`; and the GIF writer,
image-input helpers and ECI table shared between symbologies. The package is
browser-safe (`rip.browser: true`). The QR half is a port of
[paulmillr/qr](https://github.com/paulmillr/qr); PDF417 and Code 128 are
original to this package.
Expand Down Expand Up @@ -578,11 +580,11 @@ The QR half of this package is a port of [paulmillr/qr](https://github.com/paulm
Apache-2.0 and derived in turn from the ZXing project. The QR tables,
encoder, decoder pipeline, scanner, camera plumbing and `BarcodeDetector`
ponyfill follow his design; the port keeps his algorithms, restructures
them for Rip, and verifies itself against his implementation with an
oracle that compares every output format byte for byte and every decode
result on synthetic frames. The performance work described above is on top
of that foundation. Code 128 and PDF417 are original to this package and
share only the image input, ECI table and GIF writer. The PDF417 encoder
them for Rip, and pins its encoder against vectors generated from his
implementation and its decoder against round trips over synthetic rasters.
The performance work described above is on top of that foundation. Code
128 and PDF417 are original to this package; PDF417 shares the image input,
ECI table and GIF writer, Code 128 the image input and GIF writer. The PDF417 encoder
is checked against [zxing-cpp](https://github.com/zxing-cpp/zxing-cpp),
which decodes its output byte for byte across compaction modes, levels,
shapes and scales, and the reader against the photographs in ZXing's
Expand Down
3 changes: 3 additions & 0 deletions packages/barcodes/pdf417.rip
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,9 @@ export type PDF417Result = {
erasures?: number
codewords?: number[]
corners?: { x: number, y: number }[]
vertical?: boolean
reversed?: boolean
inverted?: boolean
}

# Data codewords into bytes, an ECI-decoded text and any macro block.
Expand Down
47 changes: 25 additions & 22 deletions packages/barcodes/test/bench.rip
Original file line number Diff line number Diff line change
Expand Up @@ -64,43 +64,46 @@ scenes =! (W, H) ->

miss =! (fn) -> -> try fn() catch then null

# Scene names as the README's tables print them.
label =! { desk: 'desk', text: 'printed page', noise: 'noise', weave: 'fine weave' }

t8 = 'x'.repeat 192
t18 = 'x'.repeat 768
p 'Encode'
row 'QR raw v1', time -> encodeQR 'Hello world', 'raw'
row 'QR raw v10', time -> encodeQR t8, 'raw'
row 'QR raw v22', time -> encodeQR t18, 'raw'
row 'QR svg v10', time -> encodeQR t8, 'svg'
row 'QR gif v10', time -> encodeQR t8, 'gif'
row 'Code 128', time -> encodeCode128 'Hello, World! 2026', 'raw'
row 'PDF417', time -> encodePDF417 'Hello, World! 2026', 'raw'
row 'QR raw, version 1', time -> encodeQR 'Hello world', 'raw'
row 'QR raw, version 10', time -> encodeQR t8, 'raw'
row 'QR raw, version 22', time -> encodeQR t18, 'raw'
row 'QR svg, version 10', time -> encodeQR t8, 'svg'
row 'QR gif, version 10', time -> encodeQR t8, 'gif'
row 'Code 128 raw, 18 chars', time -> encodeCode128 'Hello, World! 2026', 'raw'
row 'PDF417 raw, 18 chars', time -> encodePDF417 'Hello, World! 2026', 'raw'

p '\nRead a clean raster'
small = qrRaster 'Hello world', 4, 132, 132
hd = qrRaster 'https://example.com/some/path?with=query', 8, 1280, 720
fhd = qrRaster t8, 6, 1920, 1080
row 'QR v1 132x132', time -> decodeQR small
row 'QR 720p', time -> decodeQR hd
row 'QR v10 1080p', time -> decodeQR fhd
row 'QR version 1, 132x132', time -> decodeQR small
row 'QR 1280x720', time -> decodeQR hd
row 'QR version 10, 1920x1080', time -> decodeQR fhd
bars = barsRaster 'Hello, World! 2026', 3, 120, 1920, 1080
stack = stackRaster 'Hello, World! 2026', 3, 1920, 1080
row 'Code 128 1080p', time -> readCode128 bars
row 'PDF417 1080p', time -> readPDF417 stack
row 'Code 128, 1920x1080', time -> readCode128 bars
row 'PDF417, 1920x1080', time -> readPDF417 stack

p '\nMiss at 1080p (I420)'
s = scenes 1920, 1080
for name in ['desk', 'text', 'noise', 'weave']
row "QR #{name}", time miss -> decodeQR s[name], format: 'I420'
row 'Code 128 desk', time -> readCode128 s.desk, format: 'I420'
row 'PDF417 desk', time -> readPDF417 s.desk, format: 'I420'
row "QR, #{label[name]}", time miss -> decodeQR s[name], format: 'I420'
row 'Code 128, desk', time -> readCode128 s.desk, format: 'I420'
row 'PDF417, desk', time -> readPDF417 s.desk, format: 'I420'

p '\nMiss at the phone arena 2160x2592 (I420)'
s = scenes 2160, 2592
for name in ['desk', 'text', 'noise', 'weave']
row "QR #{name}", time miss -> decodeQR s[name], format: 'I420'
row 'QR desk, nativeLimit 1500', time miss -> decodeQR s.desk, format: 'I420', nativeLimit: 1500
row 'Code 128 desk', time -> readCode128 s.desk, format: 'I420'
row 'Code 128 text', time -> readCode128 s.text, format: 'I420'
row 'Code 128 noise', time -> readCode128 s.noise, format: 'I420'
row 'PDF417 desk', time -> readPDF417 s.desk, format: 'I420'
row 'PDF417 noise', time -> readPDF417 s.noise, format: 'I420'
row "QR, #{label[name]}", time miss -> decodeQR s[name], format: 'I420'
row 'QR, desk, nativeLimit 1500', time miss -> decodeQR s.desk, format: 'I420', nativeLimit: 1500
row 'Code 128, desk', time -> readCode128 s.desk, format: 'I420'
row 'Code 128, printed page', time -> readCode128 s.text, format: 'I420'
row 'Code 128, noise', time -> readCode128 s.noise, format: 'I420'
row 'PDF417, desk', time -> readPDF417 s.desk, format: 'I420'
row 'PDF417, noise', time -> readPDF417 s.noise, format: 'I420'
6 changes: 3 additions & 3 deletions packages/barcodes/test/compare.rip
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ C128_JS =! zxing.BarcodeFormat.CODE_128
PDF_JS =! zxing.BarcodeFormat.PDF_417

encodeWidths =! [12, 12, 12, 6]
head 'Encode (µs)', ['qr 0.7.0', 'qr + PR 39', 'rip prior', 'rip'], encodeWidths
head 'Encode (µs)', ['qr 0.7.0', 'qr + PR 39', 'rip main', 'rip'], encodeWidths
for [name, text, format] in [['raw, version 1', 'Hello world', 'raw'], ['raw, version 10', t10, 'raw'], ['raw, version 22', t22, 'raw'], ['svg, version 10', t10, 'svg'], ['gif, version 10', t10, 'gif']]
row! name, encodeWidths, [(-> encode070 text, format), (-> encode39 text, format), (-> encodePrior text, format), (-> encodeQR text, format)]

Expand All @@ -146,7 +146,7 @@ fhd = qrRaster t10, 6, 1920, 1080
noise = { width: 1920, height: 1080, data: Uint8Array.new(1920 * 1080 * 4).map((_, i) -> if (i & 3) is 3 then 255 else (i * 7919) & 0xff) }
decodeWidths =! [14, 14, 14, 12, 12, 6]
p ''
head 'Decode (µs)', ['ZXing', 'zxing-wasm', 'qr 0.7.0', 'qr + PR 39', 'rip prior', 'rip'], decodeWidths
head 'Decode (µs)', ['ZXing', 'zxing-wasm', 'qr 0.7.0', 'qr + PR 39', 'rip main', 'rip'], decodeWidths
for [name, img, text] in [['132x132 raster, version 1', small, 'Hello world'], ['1280x720 frame, one symbol', hd, 'https://example.com/some/path?with=query'], ['1920x1080 frame, one symbol', fhd, t10]]
expect name, zxingRead(img, QR_JS), text
expect name, (wasmRead! img, 'QRCode'), text
Expand All @@ -166,6 +166,6 @@ for [name, img, format, wasm, prior, read] in [['Code 128', bars, C128_JS, 'Code
expect name, prior(img).text, 'Hello, World! 2026'
expect name, read(img).text, 'Hello, World! 2026'
p ''
head 'Read (µs)', ['ZXing', 'zxing-wasm', 'rip prior', 'rip'], readWidths
head 'Read (µs)', ['ZXing', 'zxing-wasm', 'rip main', 'rip'], readWidths
row! 'Code 128, 1920x1080', readWidths, [(-> zxingRead bars, C128_JS), later(-> wasmRead bars, 'Code128'), (-> readCode128Prior bars), (-> readCode128 bars)]
row! 'PDF417, 1920x1080', readWidths, [(-> zxingRead stack, PDF_JS), later(-> wasmRead stack, 'PDF417'), (-> readPDF417Prior stack), (-> readPDF417 stack)]
Loading