Skip to content
Merged
142 changes: 128 additions & 14 deletions packages/barcodes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Rip Barcodes

> **QR and Code 128 generator and reader — packed-bitmap QR encoder, camera-budgeted decoder, scan-line Code 128, zero dependencies.**
> **QR, PDF417 and Code 128 generator and reader — packed-bitmap QR encoder, camera-budgeted decoder, ray-voted PDF417, scan-line Code 128, zero dependencies.**

The encoder keeps a symbol as one `Int32Array` with 32 modules per word,
builds the function-pattern template, placement order and the eight mask
Expand All @@ -15,19 +15,25 @@ allocated once per scanner so a camera frame never allocates. Code 128
lives in one file: the encoder chooses the shortest subset sequence by
dynamic programming, and the reader walks scan lines middle-out, matching
each eleven-module group to its nearest codeword in both directions and on
both axes.
both axes. PDF417 compacts text, bytes and digits the way the specification
recommends and lays the stream into stacked rows with GF(929) parity; its
reader finds a start or stop pattern on a scan line, tracks both edges up
and down the stack, samples rays across every row along the direction
between them, votes each codeword into the cell its row indicators and
cluster name, and corrects erasures and errors together.

**Runtime:** browser-safe (`rip.browser: true`). One file per symbology,
`qr.rip` and `code128.rip`, each holding its tables, encoder and reader; a
root entry that re-exports both; the camera and canvas plumbing in `dom.rip`;
and the GIF writer and image-input helpers both symbologies share.
`rip/barcodes/qr` and `rip/barcodes/code128` import one symbology alone.
`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. `rip/barcodes/qr`, `rip/barcodes/pdf417` and
`rip/barcodes/code128` import one symbology alone.

**Origin:** the QR encoder, decoder and browser layer are a port of
[paulmillr/qr](https://github.com/paulmillr/qr) by Paul Miller, itself
derived from ZXing, rewritten in Rip with the same algorithms and
byte-identical output on every format. The Code 128 symbology is original
to this package. See [Credits](#credits).
byte-identical output on every format. The PDF417 and Code 128 symbologies
are original to this package. See [Credits](#credits).

## Quick Start

Expand All @@ -49,6 +55,11 @@ import { encodeCode128, decodeCode128 } from 'rip/barcodes'

encodeCode128 'L2602852147', 'svg', scale: 2 # a Code 128 label
decodeCode128 { width, height, data } # the text, or throws

import { encodePDF417, decodePDF417 } from 'rip/barcodes'

encodePDF417 'Hello PDF417', 'svg', scale: 2 # a stacked symbol
decodePDF417 { width, height, data } # the text, or throws
```

## Features
Expand All @@ -66,6 +77,11 @@ decodeCode128 { width, height, data } # the text, or throws
- Code 128 with subsets A, B and C, shortest-sequence subset selection,
GS1-128 FNC1, the same six outputs, and a reader that handles both
directions, both axes and inverted symbols
- PDF417 with text, byte and numeric compaction, nine error-correction
levels, 1..30 columns and 3..90 rows, compact symbols, UTF-8 behind an
ECI, the same six outputs, and a reader that recovers from tilt, skew,
rotation, inversion, damaged rows, a cropped start pattern and holes,
and reads Macro PDF417 segment metadata

## Encoding

Expand Down Expand Up @@ -143,6 +159,68 @@ codewords and which scan line, axis, direction and polarity produced them.
Modules must be at least one pixel wide; a printed label filling a quarter
of a camera frame is plenty.

## PDF417

```coffee
import { encodePDF417, decodePDF417, readPDF417 } from 'rip/barcodes'

encodePDF417 text, output, opts
```

Text is compacted in the four text submodes, six-byte groups in base 900,
or 44-digit groups in base 900, switching where the specification's rules
say a switch pays: a run of thirteen digits, five text characters, or
anything else as bytes. Characters beyond Latin-1 send the whole message as
UTF-8 behind ECI 26. The outputs are the QR six: `raw` is `boolean[][]` with
`true` for a bar, one entry per module and row, quiet zone included;
`ascii`, `term`, `svg`, `gif` and `data-url` draw each row `rowHeight`
modules tall, `ascii` packing two module rows into each line of half-height
block characters.

| option | meaning | default |
| --- | --- | --- |
| `ecc` | error-correction level 0..8, 2 to 512 parity codewords | by message length, as the specification recommends |
| `columns` | data columns 1..30 | the pair nearest `aspect` |
| `rows` | rows 3..90 | the pair nearest `aspect` |
| `aspect` | integer width to height ratio the automatic layout aims for | `3` |
| `rowHeight` | modules per row in every drawn output | `3` |
| `encoding` | `'text'`, `'byte'`, `'numeric'` | `'auto'` |
| `compact` | omit the right row indicator and shorten the stop pattern | `false` |
| `scale` | pixels per module | `1` |
| `border` | quiet zone in modules, `0` allowed | `2` |
| `optimize` | `svg` only: one `<path>` instead of one `<rect>` per bar | `true` |

`decodePDF417` takes the same `{ width, height, data }` as `decodeQR`, with
the same `format` option, and returns the text or throws
`'PDF417 not found'`. `readPDF417` returns `null` on a miss and otherwise

```coffee
{ text, bytes, codewords, columns, rows, ecc, errors, erasures, corners,
readerInit, macro, vertical, reversed, inverted }
```

`bytes` is the decoded message before text decoding, `codewords` the
corrected stream, `errors` and `erasures` how many cells the parity
repaired, and `corners` the four symbol corners in image space. `macro` is
present for a Macro PDF417 segment:
`{ segmentIndex, fileId, last, segmentCount?, fileName?, timestamp?,
sender?, addressee?, fileSize?, checksum? }`; each segment decodes on its
own and the caller assembles a file from them.

The reader wants modules at least a pixel wide; rows a pixel tall read
from a clean raster, and photographs want a little more. It reads the
symbol in all four orientations and with the tilt and skew of a hand-held
photo, as a mirror image, inverted, with whole rows torn off the top or
bottom, with the start pattern cut off at the image edge, and with holes. One parity codeword is always held back to check the
correction and the padding after the message must read as pad, so a
damaged symbol misses rather than decoding to the wrong text. The row
direction comes from the start and stop edges tracked together, so a
photograph taken from the side, whose rows stay level while the edges
lean, reads as well as a rotated one. Against ZXing's PDF417 blackbox
photo sets 1 to 3 it decodes all 58 images at each of four rotations. Its
Macro set decodes one segment per image; images holding several symbols
return the first found.

## Scanner

```coffee
Expand Down Expand Up @@ -183,8 +261,10 @@ given. `rearCamera` and `selfieCamera` open a stream into a video element;
`camera.listDevices()` and `camera.setDevice(id)` switch cameras. When the
browser exposes `VideoFrame`, `camera.readFrame! canvas, true` copies
frames plane-for-plane into the scanner arena without a canvas round trip.
`svgToPng` resolves to a PNG data URL and `gifToPng` to a `Blob`, and `BarcodeDetector` is a Shape Detection
API ponyfill over `decodeQR`. Camera access needs a secure context.
`svgToPng` resolves to a PNG data URL and `gifToPng` to a `Blob`, and
`BarcodeDetector` is a Shape Detection API ponyfill over `decodeQR` and
`readPDF417` that reports `qr_code` and `pdf417`. Camera access needs a
secure context.

## Performance

Expand Down Expand Up @@ -214,6 +294,30 @@ inputs are synthetic RGBA frames with one symbol centered on a flat
background, plus a full-frame noise image for the miss case, which is
dominated by the finder search; this package walks it on packed words.

PDF417 has no reference implementation in the same runtime to race, so
its figures stand alone, measured the same way on the same machine:

| PDF417 encode (µs) | rip |
|-----------------------------|------:|
| raw, `Hello PDF417` | 10.4 |
| raw, 192 bytes of text | 39.5 |
| raw, 768 bytes of text | 135.7 |
| raw, 200 digits | 39.4 |
| svg, 192 bytes | 26.0 |
| gif, 192 bytes | 25.3 |

| PDF417 decode (µs) | rip |
|----------------------------------|------:|
| 300x100 raster, 9 rows x 2 cols | 83 |
| 1280x720 frame, one symbol | 232 |
| 1920x1080 frame, 192 bytes | 343 |
| 1920x1080 noise, no symbol | 302 |

The frames are the same kind as the QR rows above, with the symbol drawn
at two to four pixels per module. Photographs from ZXing's corpus decode
in 0.4 to 8 ms, the largest a 2390x2220 image of 74 rows by 12 columns at
level 8.

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
a JIT that meets it first specializes the encoder on small integers; the
Expand All @@ -232,16 +336,20 @@ in the world, and a great deal of measuring.

## Credits

This package began as a port of [paulmillr/qr](https://github.com/paulmillr/qr)
The QR half of this package is a port of [paulmillr/qr](https://github.com/paulmillr/qr)
0.7.0 by [Paul Miller](https://paulmillr.com), released under MIT OR
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 is new here and shares only the image input
and GIF writer.
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 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 PDF417 blackbox
corpus.

## Test

Expand All @@ -255,4 +363,10 @@ implementation, then round-trips synthetic rasters through the decoder
across versions, levels, rotations, inverted symbols, input formats, batch
decoding and scanner reuse. Code 128 is pinned against the published
`Wikipedia` vector, shortest-subset choices, every output, and round trips
at four scales, four rotations, inverted, luma input and GS1-128.
at four scales, four rotations, inverted, luma input and GS1-128. PDF417
pins codeword streams for every compaction mode, the GF(929) generator and
parity, symbol layout and row indicators, a full module matrix, every
output, errors-and-erasures correction to its limit, and decompaction
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.
10 changes: 7 additions & 3 deletions packages/barcodes/barcodes.rip
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ==============================================================================
# barcodes — QR Code and Code 128 encoder and decoder for Rip
# barcodes — QR Code, PDF417 and Code 128 encoder and decoder for Rip
#
# Author: Steve Shreeve (steve.shreeve@gmail.com)
# Date: September 15, 2026
Expand All @@ -14,13 +14,17 @@
# Encodes to raw, ascii, term, svg, gif and data-url; reads RGB,
# RGBA and planar YUV frames, with a browser layer for canvas,
# camera and VideoFrame input. Code 128 is a one-line reader over
# the same luma sampling. Ported from paulmillr/qr, byte-identical
# on its outputs.
# the same luma sampling. PDF417 tracks a start or stop pattern
# down the symbol, samples rays along the true row direction with
# sub-pixel edges, votes codewords into rows by their cluster, and
# corrects erasures and errors together over GF(929). The QR half
# is ported from paulmillr/qr, byte-identical on its outputs.
# ==============================================================================

import { encodeQR } from './qr.rip'

export { encodeQR, decodeQR, decodeQRBatch, QRScanner } from './qr.rip'
export { encodeCode128, decodeCode128, readCode128 } from './code128.rip'
export { encodePDF417, decodePDF417, readPDF417 } from './pdf417.rip'

export default encodeQR
54 changes: 1 addition & 53 deletions packages/barcodes/code128.rip
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
# ==============================================================================

import { gifDataUrl, writeGif } from './gif.rip'
import { MAX_IMAGE_SIDE, asObject, asString, fail, validateImage } from './image.rip'
import { MAX_IMAGE_SIDE, asObject, asString, fail, reverseRuns, runLengths, 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 @@ -283,58 +283,6 @@ nearest =! (runs, at, lo, hi) ->
code = c
code

# One scan line as 8-bit luma straight from the source pixels: `at` is the
# first pixel's byte and `advance` the bytes to the next pixel. Packed RGB
# takes (r + 2g + b) / 4, symmetric in r and b so every byte order reads
# alike; wide luma planes take their top eight bits.
sampleLine! =! (luma, data, at, advance, count, format) ->
switch format.step
when 1
for k in [0...count]
luma[k] = data[at]
at += advance
when 2
shift = format.bits - 8
for k in [0...count]
luma[k] = (data[at] | (data[at + 1] << 8)) >>> shift
at += advance
else
for k in [0...count]
luma[k] = (data[at] + 2 * data[at + 1] + data[at + 2]) >> 2
at += advance

# Run lengths of one thresholded scan line: even slots light, odd dark, the
# first possibly zero. Returns the run count, zero for a flat line.
runLengths =! (luma, count, runs) ->
lo = 255
hi = 0
for k in [0...count]
v = luma[k]
lo = v if v < lo
hi = v if v > hi
# Under 32 levels of contrast the line is flat.
return 0 if hi - lo < 32
threshold = (lo + hi) >> 1
n = 0
dark = false
len = 0
for k in [0...count]
isDark = luma[k] < threshold
if isDark isnt dark
runs[n++] = len
len = 0
dark = isDark
len++
runs[n++] = len
n

# The same line read right to left, light run first.
reverseRuns =! (runs, n, out) ->
m = 0
out[m++] = 0 if (n & 1) is 0
out[m++] = runs[k] for k in [n - 1..0] by -1
m

# Codewords from a start at `at` through stop, check verified; the count, or 0.
readFrom =! (runs, n, at, start, codes) ->
len = 0
Expand Down
40 changes: 25 additions & 15 deletions packages/barcodes/dom.rip
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
# canvases; QRCamera feeds it frames, preferring a zero-copy VideoFrame path
# that writes native planes straight into the scanner arena. Overlay identity
# changes are suppressed for three frames so camera jitter never flickers.
# BarcodeDetector is a ponyfill over decodeQR for the Shape Detection API.
# BarcodeDetector is a ponyfill over decodeQR and readPDF417 for the Shape
# Detection API.
# ==============================================================================

import { QRScanner, decodeQR } from './qr.rip'
import { readPDF417 } from './pdf417.rip'
import { fail } from './image.rip'

# Plane descriptors per VideoFrame format: x shift, y shift, bytes per sample.
Expand Down Expand Up @@ -821,7 +823,7 @@ export gifToPng =! (gifBytes) ->

# ==[ BarcodeDetector ]==
# Ponyfill: importing has no side effects; assign it to the global name to
# install it. Only qr_code is detected, at most one per image, and
# install it. qr_code and pdf417 are detected, at most one per image, and
# cornerPoints are the decoder's projected bounds.

BARCODE_FORMATS =! ['aztec', 'code_128', 'code_39', 'code_93', 'codabar', 'data_matrix', 'ean_13', 'ean_8', 'itf', 'pdf417', 'qr_code', 'unknown', 'upc_a', 'upc_e']
Expand Down Expand Up @@ -907,9 +909,9 @@ toImageData =! (image) ->
throw TypeError.new "The provided value is not of type '(Blob or HTMLCanvasElement or HTMLImageElement or HTMLVideoElement or ImageBitmap or ImageData or OffscreenCanvas or SVGImageElement or VideoFrame)'."

# Top-left, top-right, bottom-right, bottom-left in image space, rotated
# from the decoder's clockwise projected boundary.
symbolCorners =! (p) ->
points = p.bounds.map (pt) -> { x: pt.x, y: pt.y }
# from a decoder's clockwise projected boundary.
symbolCorners =! (bounds) ->
points = bounds.map (pt) -> { x: pt.x, y: pt.y }
first = 0
for i in [1...points.length]
first = i if points[i].y < points[first].y or (points[i].y is points[first].y and points[i].x < points[first].x)
Expand All @@ -932,20 +934,28 @@ export class BarcodeDetector
catch e
throw prefixed(e, "Failed to construct 'BarcodeDetector'")

@getSupportedFormats: -> Promise.resolve ['qr_code']
@getSupportedFormats: -> Promise.resolve ['pdf417', 'qr_code']

detect: (image) ->
try
data = toImageData! image
return [] if data is null
return [] if @formats.length isnt 0 and not @formats.includes('qr_code')
points = undefined
rawValue = undefined
try
rawValue = decodeQR data, effort: Infinity, timeLimit: Infinity, pointsOnDetect: (p, result) -> points = p if typeof result is 'string'
catch
return []
box = points.boundingBox
[{ boundingBox: DOMRectReadOnly.new(box.x, box.y, box.width, box.height), rawValue, format: 'qr_code', cornerPoints: symbolCorners(points) }]
formats = @formats
wanted = (format) -> formats.length is 0 or formats.includes(format)
if wanted 'qr_code'
points = undefined
rawValue = try decodeQR(data, effort: Infinity, timeLimit: Infinity, pointsOnDetect: (p, result) -> points = p if typeof result is 'string') catch then undefined
if rawValue isnt undefined
box = points.boundingBox
return [{ boundingBox: DOMRectReadOnly.new(box.x, box.y, box.width, box.height), rawValue, format: 'qr_code', cornerPoints: symbolCorners(points.bounds) }]
if wanted 'pdf417'
hit = readPDF417 data
if hit
xs = hit.corners.map (pt) -> pt.x
ys = hit.corners.map (pt) -> pt.y
x = Math.min ...xs
y = Math.min ...ys
return [{ boundingBox: DOMRectReadOnly.new(x, y, Math.max(...xs) - x, Math.max(...ys) - y), rawValue: hit.text, format: 'pdf417', cornerPoints: symbolCorners(hit.corners) }]
[]
catch e
throw prefixed(e, "Failed to execute 'detect' on 'BarcodeDetector'")
Loading