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)]