Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8c99ea2
barcodes: stage probes for the decoder, the pixel pipeline and every …
shreeve Sep 18, 2026
13b9203
barcodes: walk vertical finder runs down the column without bit()
shreeve Sep 18, 2026
0d51240
barcodes: leave cross() as soon as a run cannot pass the ratio
shreeve Sep 18, 2026
d7ed3a7
barcodes: keep the current word resident across finder runs in findRows
shreeve Sep 18, 2026
efc0e45
barcodes: sample the module grid without per-module calls
shreeve Sep 18, 2026
1cae133
barcodes: walk codewords with packed column masks and whole-byte stores
shreeve Sep 18, 2026
484d652
barcodes: read payload fields through a three-byte window
shreeve Sep 18, 2026
2901351
barcodes: fold the Reed-Solomon remainder four coefficients a word
shreeve Sep 18, 2026
1e1cbbc
barcodes: derive a damaged block's syndromes from the remainder register
shreeve Sep 18, 2026
77e2586
barcodes: slide the 5x5 threshold smoother along each block row
shreeve Sep 18, 2026
4cce472
barcodes: wipe each luma arena once in clean()
shreeve Sep 18, 2026
98cbdf9
barcodes: grow finder records on demand
shreeve Sep 18, 2026
347e857
barcodes: size version scratch to the symbol actually attempted
shreeve Sep 18, 2026
e87c573
barcodes: fold three-byte RGB input a word at a time
shreeve Sep 18, 2026
da87980
barcodes: cache SVG path commands per output width
shreeve Sep 18, 2026
9548d8e
barcodes: append a run of adjacent dark modules as one SVG string
shreeve Sep 18, 2026
55ce9cd
barcodes: append ASCII glyphs four cells at a time
shreeve Sep 18, 2026
de347e6
barcodes: copy GIF rows whole, then spread them into LZW chunks
shreeve Sep 18, 2026
7f3c5fa
barcodes: validate text against the value table
shreeve Sep 18, 2026
100cee3
barcodes: build raw rows from the packed words
shreeve Sep 18, 2026
67d61c2
barcodes: place data bits two at a time
shreeve Sep 18, 2026
c5b4bc1
barcodes: take numeric groups straight from the code points
shreeve Sep 18, 2026
ac416d6
barcodes: keep Code 128 line buffers across calls
shreeve Sep 18, 2026
cc31eb6
barcodes: sample axis scan lines through whole-span copies
shreeve Sep 18, 2026
f4f7851
barcodes: route PDF417 axis rays through sampleLine
shreeve Sep 18, 2026
419bbc7
barcodes: correlate four gradient products per step in barShift
shreeve Sep 18, 2026
b2104e0
barcodes: find edges without a closure per scan line
shreeve Sep 18, 2026
b877dd9
barcodes: test the widest pair of runs before a PDF417 anchor match
shreeve Sep 18, 2026
23f9b22
barcodes: one PDF417 anchor sweep per runs array
shreeve Sep 18, 2026
adc4c51
barcodes: performance tables remeasured
shreeve Sep 18, 2026
5b51783
barcodes: the race includes ZXing, qr with pull request 39 and the pa…
shreeve Sep 18, 2026
e624bb6
barcodes: the race states each column as a speedup of this package
shreeve Sep 18, 2026
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
18 changes: 18 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

110 changes: 68 additions & 42 deletions packages/barcodes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,33 +330,33 @@ with the rows it touches.
| Encode | |
|---------------------------|------:|
| QR raw, version 1 | 2 µs |
| QR raw, version 10 | 15 µs |
| QR raw, version 22 | 47 µs |
| QR svg, version 10 | 32 µs |
| QR gif, version 10 | 15 µs |
| QR raw, version 10 | 13 µs |
| QR raw, version 22 | 41 µs |
| QR svg, version 10 | 20 µs |
| QR gif, version 10 | 13 µs |
| Code 128 raw, 18 chars | 2 µs |
| PDF417 raw, 18 chars | 12 µs |
| PDF417 raw, 18 chars | 11 µs |

| Read a clean raster | |
|---------------------------|--------:|
| QR version 1, 132x132 | 36 µs |
| QR 1280x720 | 653 µs |
| QR version 10, 1920x1080 | 1.56 ms |
| Code 128, 1920x1080 | 171 µs |
| PDF417, 1920x1080 | 149 µs |
| QR version 1, 132x132 | 26 µs |
| QR 1280x720 | 591 µs |
| QR version 10, 1920x1080 | 1.41 ms |
| Code 128, 1920x1080 | 107 µs |
| PDF417, 1920x1080 | 114 µs |

| Miss, no symbol | 1920x1080 | 2160x2592 |
|-------------------|----------:|----------:|
| QR, desk | 5.01 ms | 13.59 ms |
| QR, printed page | 2.83 ms | 8.21 ms |
| QR, noise | 10.78 ms | 27.54 ms |
| QR, fine weave | 3.60 ms | 10.44 ms |
| QR, desk, `nativeLimit: 1500` | | 4.70 ms |
| Code 128, desk | 1.04 ms | 1.43 ms |
| Code 128, printed page | | 927 µs |
| Code 128, noise | | 2.20 ms |
| PDF417, desk | 288 µs | 464 µs |
| PDF417, noise | | 1.31 ms |
| QR, desk | 4.64 ms | 12.55 ms |
| 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 |
| Code 128, desk | 764 µs | 1.16 ms |
| Code 128, printed page | | 742 µs |
| Code 128, noise | | 1.73 ms |
| PDF417, desk | 223 µs | 401 µs |
| PDF417, noise | | 785 µs |

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.
Expand All @@ -374,31 +374,48 @@ output, committed:
| code128-1 to 3, where tested | 49 | 96 | 90 |
| falsepositives-1 and 2 | 47 | 0 false | up to 6 allowed |

`rip test/compare.rip` races the QR half against
[paulmillr/qr](https://github.com/paulmillr/qr) 0.7.0, the TypeScript
implementation it was ported from, with Paul's package checked out at
`misc/qr`. Both run in one process on the same inputs; each row is the
best of three 400 ms means, this package timed first:

| Encode (µs) | rip | paulmillr/qr | speedup |
|-----------------------------|-------:|-------------:|--------:|
| raw, version 1 | 2.3 | 3.0 | 1.33x |
| raw, version 10 | 14.1 | 16.8 | 1.19x |
| raw, version 22 | 45.5 | 51.2 | 1.13x |
| svg, version 10 | 29.7 | 46.6 | 1.57x |
| gif, version 10 | 14.4 | 17.8 | 1.23x |

| Decode (µs) | rip | paulmillr/qr | speedup |
|-----------------------------|-------:|-------------:|--------:|
| 132x132 raster, version 1 | 33.0 | 112 | 3.38x |
| 1280x720 frame, one symbol | 620 | 996 | 1.61x |
| 1920x1080 frame, one symbol | 1489 | 2190 | 1.47x |
| 1920x1080 weave, no symbol | 5403 | 23660 | 4.38x |
`rip test/compare.rip` races everyone in one process on the same inputs:
ZXing as its JavaScript port ([@zxing/library](https://github.com/zxing-js/library))
and its C++ build ([zxing-wasm](https://github.com/Sec-ant/zxing-wasm),
`tryHarder` on as its own benchmark runs it), both dev dependencies of
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
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 |
|-----------------------------|-------------:|-------------:|-------------:|-------:|
| 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 |
|-----------------------------|---------------:|---------------:|---------------:|-------------:|-------------:|-------:|
| 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 |
|-----------------------------|---------------:|---------------:|-------------:|-------:|
| 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 |

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.
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
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
the grid sampler, which reads the nearest pixel there and interpolates
here.

Encode timings are sensitive to which symbol size a process sees first.
A version 1 symbol fits one 32-bit word per row and never fills a word, so
Expand All @@ -420,7 +437,7 @@ in the world, and a great deal of measuring.
bun run test # rip test.rip, the contract
bun run corpus # rip test/corpus.rip, the ZXing blackbox scorecard; --record matches test/corpus.txt
bun run bench # rip test/bench.rip, the timing contract
bun run compare # rip test/compare.rip, the race against paulmillr/qr (needs misc/qr)
bun run compare # rip test/compare.rip, the race against ZXing and paulmillr/qr (checkouts named in its header)
```

The suite pins spec tables, encoded codewords, every output format, every
Expand All @@ -437,6 +454,15 @@ including Macro PDF417, then round-trips rasters across scales, row
heights, rotations, inversion, compact and extreme shapes, skew, cropped
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.

## Design

The QR encoder keeps a symbol as one `Int32Array` with 32 modules per word,
Expand Down
28 changes: 23 additions & 5 deletions packages/barcodes/code128.rip
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
# ==============================================================================

import { gifDataUrl, writeGif } from './gif.rip'
import { MAX_IMAGE_SIDE, asInt, asObject, asString, contrast, edgeRuns, fail, reverseRuns, validateImage } from './image.rip'
import { MAX_IMAGE_SIDE, asInt, asObject, asString, contrast, edgeRuns, fail, reverseRuns, sampleLine, validateImage } from './image.rip'

# Bar/space widths for values 0..106; 106 is Stop with its termination bar.
PATTERNS =! %w[
Expand Down Expand Up @@ -562,9 +562,16 @@ barShift =! (luma, luma2, lo, hi) ->
gradB[k] = luma2[x + 1] - luma2[x]
best = -Infinity
bestAt = 0
# Integer products, four per step; the sums are exact in any order.
for s in [0..2 * TILT_SHIFT]
sum = 0
sum += gradA[k + s] * gradB[k] for k in [0...span]
k = 0
while k + 3 < span
sum += gradA[k + s] * gradB[k] + gradA[k + s + 1] * gradB[k + 1] + gradA[k + s + 2] * gradB[k + 2] + gradA[k + s + 3] * gradB[k + 3]
k += 4
while k < span
sum += gradA[k + s] * gradB[k]
k++
corr[s] = sum
if sum > best
best = sum
Expand Down Expand Up @@ -706,6 +713,10 @@ sampleAlong =! (luma, data, width, height, format, ox, oy, dx, dy) ->
y = Math.round((oy + t0 * dy) * 65536) + 32768
sx = Math.round dx * 65536
sy = Math.round dy * 65536
# An axis line steps one whole pixel per sample: a row segment or a column.
if (sx is 65536 and sy is 0) or (sx is 0 and sy is 65536)
sampleLine luma, data, (y >> 16) * row + (x >> 16) * step, (if sy then row else step), count, format
return count
switch step
when 1
for k in [0...count]
Expand Down Expand Up @@ -816,6 +827,10 @@ trackEdges =! (img, format, hit, codes, luma, runs, rev) ->
[oa, ob] = edges
[point(oa, a0 + b0 * oa), point(oa, a1 + b1 * oa), point(ob, a1 + b1 * ob), point(ob, a0 + b0 * ob)]

# Line buffers sized for the longest diagonal seen so far, so a camera loop
# feeding frames of one size allocates once.
lines =! { side: 0, luma: Uint8Array.new(0), runs: Float64Array.new(0), rev: Float64Array.new(0) }

# First Code 128 on any scan line, where it was read and its corners; null
# on a miss. Lines run along both axes; after each axis pass, lines along
# the tilt estimated from its best unread crossings, unless `opts.tilt` is
Expand All @@ -827,9 +842,12 @@ export readCode128 =! (img, opts = {}) ->
fail "invalid opts.tilt=#{tilt} (#{typeof tilt})", TypeError unless typeof tilt is 'boolean'
# A line along a diagonal is as long as the frame's diagonal.
side = Math.ceil(Math.hypot(img.width, img.height)) + 2
luma = Uint8Array.new side
runs = Float64Array.new side
rev = Float64Array.new side
if side > lines.side
lines.side = side
lines.luma = Uint8Array.new side
lines.runs = Float64Array.new side
lines.rev = Float64Array.new side
{luma, runs, rev} = lines
codes = []
more = []
hit = null
Expand Down
33 changes: 17 additions & 16 deletions packages/barcodes/gif.rip
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@
# Uncompressed LZW stream: 8-bit codes and a clear code every 126 pixels, so
# no dictionary state exists. Palette entry 0 is white, every other entry
# black; `rowFor(y)` returns the 0/1 byte row for output row y (identical rows
# may return the same buffer) and each row is copied in spans bounded
# by the chunk boundaries.
# may return the same buffer).
# ==============================================================================

SIGNATURE =! Uint8Array.new [0x47, 0x49, 0x46, 0x38, 0x37, 0x61] # GIF87a
Expand Down Expand Up @@ -37,21 +36,23 @@ export writeGif =! (W, H, rowFor) ->
u16 H
out[pos++] = 0x00
out[pos++] = 0x07 # LZW minimum code size 7: clear is 0x80, end is 0x81
i = 0
# Pixel rows are copied back to back into the unused tail of the output,
# then spread forward chunk by chunk behind their two-byte headers: every
# chunk lands at or before its source, so the moves never clobber pixels
# still to be copied.
src = out.length - 4 - pixels
q = src
for y in [0...H]
row = rowFor y
x = 0
while x < W
if i % CHUNK is 0
rem = pixels - i
out[pos++] = (if rem < CHUNK then rem else CHUNK) + 1
out[pos++] = 0x80
n = Math.min(CHUNK - i % CHUNK, W - x)
# A span is at most 126 bytes; a byte loop beats a view per span.
out[pos + k] = row[x + k] for k in [0...n]
pos += n
x += n
i += n
out.set rowFor(y), q
q += W
s = src
for i in [0...pixels] by CHUNK
n = if pixels - i < CHUNK then pixels - i else CHUNK
out[pos++] = n + 1
out[pos++] = 0x80
out.copyWithin pos, s, s + n
pos += n
s += n
if tail is 0
out[pos++] = 1
out[pos++] = 0x80
Expand Down
Loading
Loading