From 53a49f08d88830a8d3b968c8b902618ae2f86056 Mon Sep 17 00:00:00 2001 From: Steve Shreeve Date: Fri, 18 Sep 2026 03:03:15 -0600 Subject: [PATCH] barcodes: README audited against the code Every claim checked against main: the QR speed range reads from the race table (1.7 to 5.5x), the corpus sentence carries the measured mean and the slowest image's warm time, nativeEvery is described as the gate applies it (1 never forces the native search), the lock colors are the literal defaults, the ECI table is shared by QR and PDF417 only, the encoder is pinned against generated vectors rather than a shipped oracle, the probes paragraph states each probe's exact scope, and the race describes the misc/rip-prior checkout as main. The bench prints the README's row names verbatim, the race's baseline column is labeled rip main, and the PDF417 result type carries vertical, reversed and inverted as the reader sets them. --- packages/barcodes/README.md | 54 ++++++++++++++++-------------- packages/barcodes/pdf417.rip | 3 ++ packages/barcodes/test/bench.rip | 47 ++++++++++++++------------ packages/barcodes/test/compare.rip | 6 ++-- 4 files changed, 59 insertions(+), 51 deletions(-) diff --git a/packages/barcodes/README.md b/packages/barcodes/README.md index 2041fd8d..f7e4ee85 100644 --- a/packages/barcodes/README.md +++ b/packages/barcodes/README.md @@ -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 @@ -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` | @@ -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 | @@ -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 @@ -381,12 +382,12 @@ 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 | @@ -394,14 +395,14 @@ with how many times faster this package is: | 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 | @@ -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. @@ -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 @@ -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. @@ -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 diff --git a/packages/barcodes/pdf417.rip b/packages/barcodes/pdf417.rip index 414b2b78..3dc33498 100644 --- a/packages/barcodes/pdf417.rip +++ b/packages/barcodes/pdf417.rip @@ -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. diff --git a/packages/barcodes/test/bench.rip b/packages/barcodes/test/bench.rip index 16cdbb9e..4e7070b6 100644 --- a/packages/barcodes/test/bench.rip +++ b/packages/barcodes/test/bench.rip @@ -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' diff --git a/packages/barcodes/test/compare.rip b/packages/barcodes/test/compare.rip index d3dfa960..269f3961 100644 --- a/packages/barcodes/test/compare.rip +++ b/packages/barcodes/test/compare.rip @@ -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)] @@ -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 @@ -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)]