diff --git a/packages/barcodes/README.md b/packages/barcodes/README.md index fcf807f9..d33ad79a 100644 --- a/packages/barcodes/README.md +++ b/packages/barcodes/README.md @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 `` instead of one `` 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 @@ -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 @@ -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 @@ -232,7 +336,7 @@ 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` @@ -240,8 +344,12 @@ 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 @@ -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. diff --git a/packages/barcodes/barcodes.rip b/packages/barcodes/barcodes.rip index 63e45f0b..22c83b5e 100644 --- a/packages/barcodes/barcodes.rip +++ b/packages/barcodes/barcodes.rip @@ -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 @@ -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 diff --git a/packages/barcodes/code128.rip b/packages/barcodes/code128.rip index 94ac5278..b5d43bc6 100644 --- a/packages/barcodes/code128.rip +++ b/packages/barcodes/code128.rip @@ -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[ @@ -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 diff --git a/packages/barcodes/dom.rip b/packages/barcodes/dom.rip index d32e0fba..4c21f423 100644 --- a/packages/barcodes/dom.rip +++ b/packages/barcodes/dom.rip @@ -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. @@ -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'] @@ -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) @@ -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'") diff --git a/packages/barcodes/eci.rip b/packages/barcodes/eci.rip new file mode 100644 index 00000000..b4b48986 --- /dev/null +++ b/packages/barcodes/eci.rip @@ -0,0 +1,18 @@ +# ============================================================================== +# rip/barcodes — Extended Channel Interpretation +# +# The AIM ECI designators for character sets, as text decoders shared by the +# readers; an ECI stays active until another replaces it. +# ============================================================================== + +ECI_ENCODINGS =! + 1: 'iso-8859-1', 2: 'ibm437', 3: 'iso-8859-1', 4: 'iso-8859-2', 5: 'iso-8859-3' + 6: 'iso-8859-4', 7: 'iso-8859-5', 8: 'iso-8859-6', 9: 'iso-8859-7', 10: 'iso-8859-8' + 11: 'iso-8859-9', 13: 'iso-8859-11', 15: 'iso-8859-13', 16: 'iso-8859-14' + 17: 'iso-8859-15', 18: 'iso-8859-16', 20: 'shift-jis', 21: 'windows-1250' + 22: 'windows-1251', 23: 'windows-1252', 24: 'windows-1256', 25: 'utf-16be' + 26: 'utf-8', 28: 'big5', 29: 'gbk', 30: 'euc-kr' + +export ECI_DECODERS =! {} +for id, name of ECI_ENCODINGS + ECI_DECODERS[id] = try TextDecoder.new(name) catch then undefined diff --git a/packages/barcodes/image.rip b/packages/barcodes/image.rip index 6fdfaf1e..e1ff2cc2 100644 --- a/packages/barcodes/image.rip +++ b/packages/barcodes/image.rip @@ -129,3 +129,57 @@ export darkToImage =! (width, height, isDark) -> data[i + 3] = 255 i += 4 { width, height, data } + +# ==[ Scan lines ]== + +# 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. +export 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. +export 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. +export 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 diff --git a/packages/barcodes/package.json b/packages/barcodes/package.json index e2d71d67..f0826074 100644 --- a/packages/barcodes/package.json +++ b/packages/barcodes/package.json @@ -3,12 +3,13 @@ "version": "0.0.0", "private": true, "type": "module", - "description": "QR and Code 128 generator and reader \u2014 packed-bitmap QR encoder, camera-budgeted decoder, scan-line Code 128, zero dependencies.", + "description": "QR, PDF417 and Code 128 generator and reader — packed-bitmap QR encoder, camera-budgeted decoder, ray-voted PDF417, scan-line Code 128, zero dependencies.", "exports": { ".": "./barcodes.rip", "./qr": "./qr.rip", "./code128": "./code128.rip", - "./dom": "./dom.rip" + "./dom": "./dom.rip", + "./pdf417": "./pdf417.rip" }, "scripts": { "test": "rip test.rip" @@ -18,10 +19,12 @@ }, "files": [ "barcodes.rip", + "eci.rip", "gif.rip", "image.rip", "qr.rip", "code128.rip", + "pdf417.rip", "dom.rip", "README.md" ] diff --git a/packages/barcodes/pdf417.rip b/packages/barcodes/pdf417.rip new file mode 100644 index 00000000..d1e977b4 --- /dev/null +++ b/packages/barcodes/pdf417.rip @@ -0,0 +1,1866 @@ +# ============================================================================== +# rip/barcodes — PDF417 (ISO/IEC 15438) +# +# A symbol is a stack of 3 to 90 rows, each a start pattern, a left row +# indicator, 1 to 30 data codewords, a right row indicator and the stop +# pattern; every codeword is four bars and four spaces spanning seventeen +# modules, drawn from one of three clusters that rotate row by row so a +# codeword's own widths tell which row it belongs to. The data stream is a +# length descriptor, the compacted message (text in four submodes, bytes in +# base-900 groups of six, digits in base-900 groups of 44), padding, and +# Reed-Solomon parity over GF(929) at one of nine levels. +# +# The encoder follows the specification's recommended mode switching and +# lays the codewords into module rows; renderers draw those rows at a row +# height and scale. The reader locates the start and stop patterns on scan +# lines, samples every codeword in the band between them, votes each into +# its row and column by the row indicators and cluster, then corrects +# erasures and errors together before parsing the stream. +# ============================================================================== + +import { gifDataUrl, writeGif } from './gif.rip' +import { ECI_DECODERS } from './eci.rip' +import { MAX_IMAGE_SIDE, asObject, asString, fail, reverseRuns, runLengths, sampleLine, validateImage } from './image.rip' + +# ==[ Spec tables ]== + +# Bar/space patterns of the 929 codewords in each of the three clusters, as +# seventeen bits with the first bar at the top; cluster-major. +SYMBOLS =! %w[ + 1d5c0 1eaf0 1f57c 1d4e0 1ea78 1f53e 1a8c0 1d470 1a860 15040 1a830 15020 1adc0 1d6f0 1eb7c 1ace0 + 1d678 1eb3e 158c0 1ac70 15860 15dc0 1aef0 1d77c 15ce0 1ae78 1d73e 15c70 1ae3c 15ef0 1af7c 15e78 + 1af3e 15f7c 1f5fa 1d2e0 1e978 1f4be 1a4c0 1d270 1e93c 1a460 1d238 14840 1a430 1d21c 14820 1a418 + 14810 1a6e0 1d378 1e9be 14cc0 1a670 1d33c 14c60 1a638 1d31e 14c30 1a61c 14ee0 1a778 1d3be 14e70 + 1a73c 14e38 1a71e 14f78 1a7be 14f3c 14f1e 1a2c0 1d170 1e8bc 1a260 1d138 1e89e 14440 1a230 1d11c + 14420 1a218 14410 14408 146c0 1a370 1d1bc 14660 1a338 1d19e 14630 1a31c 14618 1460c 14770 1a3bc + 14738 1a39e 1471c 147bc 1a160 1d0b8 1e85e 14240 1a130 1d09c 14220 1a118 1d08e 14210 1a10c 14208 + 1a106 14360 1a1b8 1d0de 14330 1a19c 14318 1a18e 1430c 14306 1a1de 1438e 14140 1a0b0 1d05c 14120 + 1a098 1d04e 14110 1a08c 14108 1a086 14104 141b0 14198 1418c 140a0 1d02e 1a04c 1a046 14082 1cae0 + 1e578 1f2be 194c0 1ca70 1e53c 19460 1ca38 1e51e 12840 19430 12820 196e0 1cb78 1e5be 12cc0 19670 + 1cb3c 12c60 19638 12c30 12c18 12ee0 19778 1cbbe 12e70 1973c 12e38 12e1c 12f78 197be 12f3c 12fbe + 1dac0 1ed70 1f6bc 1da60 1ed38 1f69e 1b440 1da30 1ed1c 1b420 1da18 1ed0e 1b410 1da0c 192c0 1c970 + 1e4bc 1b6c0 19260 1c938 1e49e 1b660 1db38 1ed9e 16c40 12420 19218 1c90e 16c20 1b618 16c10 126c0 + 19370 1c9bc 16ec0 12660 19338 1c99e 16e60 1b738 1db9e 16e30 12618 16e18 12770 193bc 16f70 12738 + 1939e 16f38 1b79e 16f1c 127bc 16fbc 1279e 16f9e 1d960 1ecb8 1f65e 1b240 1d930 1ec9c 1b220 1d918 + 1ec8e 1b210 1d90c 1b208 1b204 19160 1c8b8 1e45e 1b360 19130 1c89c 16640 12220 1d99c 1c88e 16620 + 12210 1910c 16610 1b30c 19106 12204 12360 191b8 1c8de 16760 12330 1919c 16730 1b39c 1918e 16718 + 1230c 12306 123b8 191de 167b8 1239c 1679c 1238e 1678e 167de 1b140 1d8b0 1ec5c 1b120 1d898 1ec4e + 1b110 1d88c 1b108 1d886 1b104 1b102 12140 190b0 1c85c 16340 12120 19098 1c84e 16320 1b198 1d8ce + 16310 12108 19086 16308 1b186 16304 121b0 190dc 163b0 12198 190ce 16398 1b1ce 1638c 12186 16386 + 163dc 163ce 1b0a0 1d858 1ec2e 1b090 1d84c 1b088 1d846 1b084 1b082 120a0 19058 1c82e 161a0 12090 + 1904c 16190 1b0cc 19046 16188 12084 16184 12082 120d8 161d8 161cc 161c6 1d82c 1d826 1b042 1902c + 12048 160c8 160c4 160c2 18ac0 1c570 1e2bc 18a60 1c538 11440 18a30 1c51c 11420 18a18 11410 11408 + 116c0 18b70 1c5bc 11660 18b38 1c59e 11630 18b1c 11618 1160c 11770 18bbc 11738 18b9e 1171c 117bc + 1179e 1cd60 1e6b8 1f35e 19a40 1cd30 1e69c 19a20 1cd18 1e68e 19a10 1cd0c 19a08 1cd06 18960 1c4b8 + 1e25e 19b60 18930 1c49c 13640 11220 1cd9c 1c48e 13620 19b18 1890c 13610 11208 13608 11360 189b8 + 1c4de 13760 11330 1cdde 13730 19b9c 1898e 13718 1130c 1370c 113b8 189de 137b8 1139c 1379c 1138e + 113de 137de 1dd40 1eeb0 1f75c 1dd20 1ee98 1f74e 1dd10 1ee8c 1dd08 1ee86 1dd04 19940 1ccb0 1e65c + 1bb40 19920 1eedc 1e64e 1bb20 1dd98 1eece 1bb10 19908 1cc86 1bb08 1dd86 19902 11140 188b0 1c45c + 13340 11120 18898 1c44e 17740 13320 19998 1ccce 17720 1bb98 1ddce 18886 17710 13308 19986 17708 + 11102 111b0 188dc 133b0 11198 188ce 177b0 13398 199ce 17798 1bbce 11186 13386 111dc 133dc 111ce + 177dc 133ce 1dca0 1ee58 1f72e 1dc90 1ee4c 1dc88 1ee46 1dc84 1dc82 198a0 1cc58 1e62e 1b9a0 19890 + 1ee6e 1b990 1dccc 1cc46 1b988 19884 1b984 19882 1b982 110a0 18858 1c42e 131a0 11090 1884c 173a0 + 13190 198cc 18846 17390 1b9cc 11084 17388 13184 11082 13182 110d8 1886e 131d8 110cc 173d8 131cc + 110c6 173cc 131c6 110ee 173ee 1dc50 1ee2c 1dc48 1ee26 1dc44 1dc42 19850 1cc2c 1b8d0 19848 1cc26 + 1b8c8 1dc66 1b8c4 19842 1b8c2 11050 1882c 130d0 11048 18826 171d0 130c8 19866 171c8 1b8e6 11042 + 171c4 130c2 171c2 130ec 171ec 171e6 1ee16 1dc22 1cc16 19824 19822 11028 13068 170e8 11022 13062 + 18560 10a40 18530 10a20 18518 1c28e 10a10 1850c 10a08 18506 10b60 185b8 1c2de 10b30 1859c 10b18 + 1858e 10b0c 10b06 10bb8 185de 10b9c 10b8e 10bde 18d40 1c6b0 1e35c 18d20 1c698 18d10 1c68c 18d08 + 1c686 18d04 10940 184b0 1c25c 11b40 10920 1c6dc 1c24e 11b20 18d98 1c6ce 11b10 10908 18486 11b08 + 18d86 10902 109b0 184dc 11bb0 10998 184ce 11b98 18dce 11b8c 10986 109dc 11bdc 109ce 11bce 1cea0 + 1e758 1f3ae 1ce90 1e74c 1ce88 1e746 1ce84 1ce82 18ca0 1c658 19da0 18c90 1c64c 19d90 1cecc 1c646 + 19d88 18c84 19d84 18c82 19d82 108a0 18458 119a0 10890 1c66e 13ba0 11990 18ccc 18446 13b90 19dcc + 10884 13b88 11984 10882 11982 108d8 1846e 119d8 108cc 13bd8 119cc 108c6 13bcc 119c6 108ee 119ee + 13bee 1ef50 1f7ac 1ef48 1f7a6 1ef44 1ef42 1ce50 1e72c 1ded0 1ef6c 1e726 1dec8 1ef66 1dec4 1ce42 + 1dec2 18c50 1c62c 19cd0 18c48 1c626 1bdd0 19cc8 1ce66 1bdc8 1dee6 18c42 1bdc4 19cc2 1bdc2 10850 + 1842c 118d0 10848 18426 139d0 118c8 18c66 17bd0 139c8 19ce6 10842 17bc8 1bde6 118c2 17bc4 1086c + 118ec 10866 139ec 118e6 17bec 139e6 17be6 1ef28 1f796 1ef24 1ef22 1ce28 1e716 1de68 1ef36 1de64 + 1ce22 1de62 18c28 1c616 19c68 18c24 1bce8 19c64 18c22 1bce4 19c62 1bce2 10828 18416 11868 18c36 + 138e8 11864 10822 179e8 138e4 11862 179e4 138e2 179e2 11876 179f6 1ef12 1de34 1de32 19c34 1bc74 + 1bc72 11834 13874 178f4 178f2 10540 10520 18298 10510 10508 10504 105b0 10598 1058c 10586 105dc + 105ce 186a0 18690 1c34c 18688 1c346 18684 18682 104a0 18258 10da0 186d8 1824c 10d90 186cc 10d88 + 186c6 10d84 10482 10d82 104d8 1826e 10dd8 186ee 10dcc 104c6 10dc6 104ee 10dee 1c750 1c748 1c744 + 1c742 18650 18ed0 1c76c 1c326 18ec8 1c766 18ec4 18642 18ec2 10450 10cd0 10448 18226 11dd0 10cc8 + 10444 11dc8 10cc4 10442 11dc4 10cc2 1046c 10cec 10466 11dec 10ce6 11de6 1e7a8 1e7a4 1e7a2 1c728 + 1cf68 1e7b6 1cf64 1c722 1cf62 18628 1c316 18e68 1c736 19ee8 18e64 18622 19ee4 18e62 19ee2 10428 + 18216 10c68 18636 11ce8 10c64 10422 13de8 11ce4 10c62 13de4 11ce2 10436 10c76 11cf6 13df6 1f7d4 + 1f7d2 1e794 1efb4 1e792 1efb2 1c714 1cf34 1c712 1df74 1cf32 1df72 18614 18e34 18612 19e74 18e32 + 1bef4 1f560 1fab8 1ea40 1f530 1fa9c 1ea20 1f518 1fa8e 1ea10 1f50c 1ea08 1f506 1ea04 1eb60 1f5b8 + 1fade 1d640 1eb30 1f59c 1d620 1eb18 1f58e 1d610 1eb0c 1d608 1eb06 1d604 1d760 1ebb8 1f5de 1ae40 + 1d730 1eb9c 1ae20 1d718 1eb8e 1ae10 1d70c 1ae08 1d706 1ae04 1af60 1d7b8 1ebde 15e40 1af30 1d79c + 15e20 1af18 1d78e 15e10 1af0c 15e08 1af06 15f60 1afb8 1d7de 15f30 1af9c 15f18 1af8e 15f0c 15fb8 + 1afde 15f9c 15f8e 1e940 1f4b0 1fa5c 1e920 1f498 1fa4e 1e910 1f48c 1e908 1f486 1e904 1e902 1d340 + 1e9b0 1f4dc 1d320 1e998 1f4ce 1d310 1e98c 1d308 1e986 1d304 1d302 1a740 1d3b0 1e9dc 1a720 1d398 + 1e9ce 1a710 1d38c 1a708 1d386 1a704 1a702 14f40 1a7b0 1d3dc 14f20 1a798 1d3ce 14f10 1a78c 14f08 + 1a786 14f04 14fb0 1a7dc 14f98 1a7ce 14f8c 14f86 14fdc 14fce 1e8a0 1f458 1fa2e 1e890 1f44c 1e888 + 1f446 1e884 1e882 1d1a0 1e8d8 1f46e 1d190 1e8cc 1d188 1e8c6 1d184 1d182 1a3a0 1d1d8 1e8ee 1a390 + 1d1cc 1a388 1d1c6 1a384 1a382 147a0 1a3d8 1d1ee 14790 1a3cc 14788 1a3c6 14784 14782 147d8 1a3ee + 147cc 147c6 147ee 1e850 1f42c 1e848 1f426 1e844 1e842 1d0d0 1e86c 1d0c8 1e866 1d0c4 1d0c2 1a1d0 + 1d0ec 1a1c8 1d0e6 1a1c4 1a1c2 143d0 1a1ec 143c8 1a1e6 143c4 143c2 143ec 143e6 1e828 1f416 1e824 + 1e822 1d068 1e836 1d064 1d062 1a0e8 1d076 1a0e4 1a0e2 141e8 1a0f6 141e4 141e2 1e814 1e812 1d034 + 1d032 1a074 1a072 1e540 1f2b0 1f95c 1e520 1f298 1f94e 1e510 1f28c 1e508 1f286 1e504 1e502 1cb40 + 1e5b0 1f2dc 1cb20 1e598 1f2ce 1cb10 1e58c 1cb08 1e586 1cb04 1cb02 19740 1cbb0 1e5dc 19720 1cb98 + 1e5ce 19710 1cb8c 19708 1cb86 19704 19702 12f40 197b0 1cbdc 12f20 19798 1cbce 12f10 1978c 12f08 + 19786 12f04 12fb0 197dc 12f98 197ce 12f8c 12f86 12fdc 12fce 1f6a0 1fb58 16bf0 1f690 1fb4c 169f8 + 1f688 1fb46 168fc 1f684 1f682 1e4a0 1f258 1f92e 1eda0 1e490 1fb6e 1ed90 1f6cc 1f246 1ed88 1e484 + 1ed84 1e482 1ed82 1c9a0 1e4d8 1f26e 1dba0 1c990 1e4cc 1db90 1edcc 1e4c6 1db88 1c984 1db84 1c982 + 1db82 193a0 1c9d8 1e4ee 1b7a0 19390 1c9cc 1b790 1dbcc 1c9c6 1b788 19384 1b784 19382 1b782 127a0 + 193d8 1c9ee 16fa0 12790 193cc 16f90 1b7cc 193c6 16f88 12784 16f84 12782 127d8 193ee 16fd8 127cc + 16fcc 127c6 16fc6 127ee 1f650 1fb2c 165f8 1f648 1fb26 164fc 1f644 1647e 1f642 1e450 1f22c 1ecd0 + 1e448 1f226 1ecc8 1f666 1ecc4 1e442 1ecc2 1c8d0 1e46c 1d9d0 1c8c8 1e466 1d9c8 1ece6 1d9c4 1c8c2 + 1d9c2 191d0 1c8ec 1b3d0 191c8 1c8e6 1b3c8 1d9e6 1b3c4 191c2 1b3c2 123d0 191ec 167d0 123c8 191e6 + 167c8 1b3e6 167c4 123c2 167c2 123ec 167ec 123e6 167e6 1f628 1fb16 162fc 1f624 1627e 1f622 1e428 + 1f216 1ec68 1f636 1ec64 1e422 1ec62 1c868 1e436 1d8e8 1c864 1d8e4 1c862 1d8e2 190e8 1c876 1b1e8 + 1d8f6 1b1e4 190e2 1b1e2 121e8 190f6 163e8 121e4 163e4 121e2 163e2 121f6 163f6 1f614 1617e 1f612 + 1e414 1ec34 1e412 1ec32 1c834 1d874 1c832 1d872 19074 1b0f4 19072 1b0f2 120f4 161f4 120f2 161f2 + 1f60a 1e40a 1ec1a 1c81a 1d83a 1903a 1b07a 1e2a0 1f158 1f8ae 1e290 1f14c 1e288 1f146 1e284 1e282 + 1c5a0 1e2d8 1f16e 1c590 1e2cc 1c588 1e2c6 1c584 1c582 18ba0 1c5d8 1e2ee 18b90 1c5cc 18b88 1c5c6 + 18b84 18b82 117a0 18bd8 1c5ee 11790 18bcc 11788 18bc6 11784 11782 117d8 18bee 117cc 117c6 117ee + 1f350 1f9ac 135f8 1f348 1f9a6 134fc 1f344 1347e 1f342 1e250 1f12c 1e6d0 1e248 1f126 1e6c8 1f366 + 1e6c4 1e242 1e6c2 1c4d0 1e26c 1cdd0 1c4c8 1e266 1cdc8 1e6e6 1cdc4 1c4c2 1cdc2 189d0 1c4ec 19bd0 + 189c8 1c4e6 19bc8 1cde6 19bc4 189c2 19bc2 113d0 189ec 137d0 113c8 189e6 137c8 19be6 137c4 113c2 + 137c2 113ec 137ec 113e6 137e6 1fba8 175f0 1bafc 1fba4 174f8 1ba7e 1fba2 1747c 1743e 1f328 1f996 + 132fc 1f768 1fbb6 176fc 1327e 1f764 1f322 1767e 1f762 1e228 1f116 1e668 1e224 1eee8 1f776 1e222 + 1eee4 1e662 1eee2 1c468 1e236 1cce8 1c464 1dde8 1cce4 1c462 1dde4 1cce2 1dde2 188e8 1c476 199e8 + 188e4 1bbe8 199e4 188e2 1bbe4 199e2 1bbe2 111e8 188f6 133e8 111e4 177e8 133e4 111e2 177e4 133e2 + 177e2 111f6 133f6 1fb94 172f8 1b97e 1fb92 1727c 1723e 1f314 1317e 1f734 1f312 1737e 1f732 1e214 + 1e634 1e212 1ee74 1e632 1ee72 1c434 1cc74 1c432 1dcf4 1cc72 1dcf2 18874 198f4 18872 1b9f4 198f2 + 1b9f2 110f4 131f4 110f2 173f4 131f2 173f2 1fb8a 1717c 1713e 1f30a 1f71a 1e20a 1e61a 1ee3a 1c41a + 1cc3a 1dc7a 1883a 1987a 1b8fa 1107a 130fa 171fa 170be 1e150 1f0ac 1e148 1f0a6 1e144 1e142 1c2d0 + 1e16c 1c2c8 1e166 1c2c4 1c2c2 185d0 1c2ec 185c8 1c2e6 185c4 185c2 10bd0 185ec 10bc8 185e6 10bc4 + 10bc2 10bec 10be6 1f1a8 1f8d6 11afc 1f1a4 11a7e 1f1a2 1e128 1f096 1e368 1e124 1e364 1e122 1e362 + 1c268 1e136 1c6e8 1c264 1c6e4 1c262 1c6e2 184e8 1c276 18de8 184e4 18de4 184e2 18de2 109e8 184f6 + 11be8 109e4 11be4 109e2 11be2 109f6 11bf6 1f9d4 13af8 19d7e 1f9d2 13a7c 13a3e 1f194 1197e 1f3b4 + 1f192 13b7e 1f3b2 1e114 1e334 1e112 1e774 1e332 1e772 1c234 1c674 1c232 1cef4 1c672 1cef2 18474 + 18cf4 18472 19df4 18cf2 19df2 108f4 119f4 108f2 13bf4 119f2 13bf2 17af0 1bd7c 17a78 1bd3e 17a3c + 17a1e 1f9ca 1397c 1fbda 17b7c 1393e 17b3e 1f18a 1f39a 1f7ba 1e10a 1e31a 1e73a 1ef7a 1c21a 1c63a + 1ce7a 1defa 1843a 18c7a 19cfa 1bdfa 1087a 118fa 139fa 17978 1bcbe 1793c 1791e 138be 179be 178bc + 1789e 1785e 1e0a8 1e0a4 1e0a2 1c168 1e0b6 1c164 1c162 182e8 1c176 182e4 182e2 105e8 182f6 105e4 + 105e2 105f6 1f0d4 10d7e 1f0d2 1e094 1e1b4 1e092 1e1b2 1c134 1c374 1c132 1c372 18274 186f4 18272 + 186f2 104f4 10df4 104f2 10df2 1f8ea 11d7c 11d3e 1f0ca 1f1da 1e08a 1e19a 1e3ba 1c11a 1c33a 1c77a + 1823a 1867a 18efa 1047a 10cfa 11dfa 13d78 19ebe 13d3c 13d1e 11cbe 13dbe 17d70 1bebc 17d38 1be9e + 17d1c 17d0e 13cbc 17dbc 13c9e 17d9e 17cb8 1be5e 17c9c 17c8e 13c5e 17cde 17c5c 17c4e 17c2e 1c0b4 + 1c0b2 18174 18172 102f4 102f2 1e0da 1c09a 1c1ba 1813a 1837a 1027a 106fa 10ebe 11ebc 11e9e 13eb8 + 19f5e 13e9c 13e8e 11e5e 13ede 17eb0 1bf5c 17e98 1bf4e 17e8c 17e86 13e5c 17edc 13e4e 17ece 17e58 + 1bf2e 17e4c 17e46 13e2e 17e6e 17e2c 17e26 10f5e 11f5c 11f4e 13f58 19fae 13f4c 13f46 11f2e 13f6e + 13f2c 13f26 1abe0 1d5f8 153c0 1a9f0 1d4fc 151e0 1a8f8 1d47e 150f0 1a87c 15078 1fad0 15be0 1adf8 + 1fac8 159f0 1acfc 1fac4 158f8 1ac7e 1fac2 1587c 1f5d0 1faec 15df8 1f5c8 1fae6 15cfc 1f5c4 15c7e + 1f5c2 1ebd0 1f5ec 1ebc8 1f5e6 1ebc4 1ebc2 1d7d0 1ebec 1d7c8 1ebe6 1d7c4 1d7c2 1afd0 1d7ec 1afc8 + 1d7e6 1afc4 14bc0 1a5f0 1d2fc 149e0 1a4f8 1d27e 148f0 1a47c 14878 1a43e 1483c 1fa68 14df0 1a6fc + 1fa64 14cf8 1a67e 1fa62 14c7c 14c3e 1f4e8 1fa76 14efc 1f4e4 14e7e 1f4e2 1e9e8 1f4f6 1e9e4 1e9e2 + 1d3e8 1e9f6 1d3e4 1d3e2 1a7e8 1d3f6 1a7e4 1a7e2 145e0 1a2f8 1d17e 144f0 1a27c 14478 1a23e 1443c + 1441e 1fa34 146f8 1a37e 1fa32 1467c 1463e 1f474 1477e 1f472 1e8f4 1e8f2 1d1f4 1d1f2 1a3f4 1a3f2 + 142f0 1a17c 14278 1a13e 1423c 1421e 1fa1a 1437c 1433e 1f43a 1e87a 1d0fa 14178 1a0be 1413c 1411e + 141be 140bc 1409e 12bc0 195f0 1cafc 129e0 194f8 1ca7e 128f0 1947c 12878 1943e 1283c 1f968 12df0 + 196fc 1f964 12cf8 1967e 1f962 12c7c 12c3e 1f2e8 1f976 12efc 1f2e4 12e7e 1f2e2 1e5e8 1f2f6 1e5e4 + 1e5e2 1cbe8 1e5f6 1cbe4 1cbe2 197e8 1cbf6 197e4 197e2 1b5e0 1daf8 1ed7e 169c0 1b4f0 1da7c 168e0 + 1b478 1da3e 16870 1b43c 16838 1b41e 1681c 125e0 192f8 1c97e 16de0 124f0 1927c 16cf0 1b67c 1923e + 16c78 1243c 16c3c 1241e 16c1e 1f934 126f8 1937e 1fb74 1f932 16ef8 1267c 1fb72 16e7c 1263e 16e3e + 1f274 1277e 1f6f4 1f272 16f7e 1f6f2 1e4f4 1edf4 1e4f2 1edf2 1c9f4 1dbf4 1c9f2 1dbf2 193f4 193f2 + 165c0 1b2f0 1d97c 164e0 1b278 1d93e 16470 1b23c 16438 1b21e 1641c 1640e 122f0 1917c 166f0 12278 + 1913e 16678 1b33e 1663c 1221e 1661e 1f91a 1237c 1fb3a 1677c 1233e 1673e 1f23a 1f67a 1e47a 1ecfa + 1c8fa 1d9fa 191fa 162e0 1b178 1d8be 16270 1b13c 16238 1b11e 1621c 1620e 12178 190be 16378 1213c + 1633c 1211e 1631e 121be 163be 16170 1b0bc 16138 1b09e 1611c 1610e 120bc 161bc 1209e 1619e 160b8 + 1b05e 1609c 1608e 1205e 160de 1605c 1604e 115e0 18af8 1c57e 114f0 18a7c 11478 18a3e 1143c 1141e + 1f8b4 116f8 18b7e 1f8b2 1167c 1163e 1f174 1177e 1f172 1e2f4 1e2f2 1c5f4 1c5f2 18bf4 18bf2 135c0 + 19af0 1cd7c 134e0 19a78 1cd3e 13470 19a3c 13438 19a1e 1341c 1340e 112f0 1897c 136f0 11278 1893e + 13678 19b3e 1363c 1121e 1361e 1f89a 1137c 1f9ba 1377c 1133e 1373e 1f13a 1f37a 1e27a 1e6fa 1c4fa + 1cdfa 189fa 1bae0 1dd78 1eebe 174c0 1ba70 1dd3c 17460 1ba38 1dd1e 17430 1ba1c 17418 1ba0e 1740c + 132e0 19978 1ccbe 176e0 13270 1993c 17670 1bb3c 1991e 17638 1321c 1761c 1320e 1760e 11178 188be + 13378 1113c 17778 1333c 1111e 1773c 1331e 1771e 111be 133be 177be 172c0 1b970 1dcbc 17260 1b938 + 1dc9e 17230 1b91c 17218 1b90e 1720c 17206 13170 198bc 17370 13138 1989e 17338 1b99e 1731c 1310e + 1730e 110bc 131bc 1109e 173bc 1319e 1739e 17160 1b8b8 1dc5e 17130 1b89c 17118 1b88e 1710c 17106 + 130b8 1985e 171b8 1309c 1719c 1308e 1718e 1105e 130de 171de 170b0 1b85c 17098 1b84e 1708c 17086 + 1305c 170dc 1304e 170ce 17058 1b82e 1704c 17046 1302e 1706e 1702c 17026 10af0 1857c 10a78 1853e + 10a3c 10a1e 10b7c 10b3e 1f0ba 1e17a 1c2fa 185fa 11ae0 18d78 1c6be 11a70 18d3c 11a38 18d1e 11a1c + 11a0e 10978 184be 11b78 1093c 11b3c 1091e 11b1e 109be 11bbe 13ac0 19d70 1cebc 13a60 19d38 1ce9e + 13a30 19d1c 13a18 19d0e 13a0c 13a06 11970 18cbc 13b70 11938 18c9e 13b38 1191c 13b1c 1190e 13b0e + 108bc 119bc 1089e 13bbc 1199e 13b9e 1bd60 1deb8 1ef5e 17a40 1bd30 1de9c 17a20 1bd18 1de8e 17a10 + 1bd0c 17a08 1bd06 17a04 13960 19cb8 1ce5e 17b60 13930 19c9c 17b30 1bd9c 19c8e 17b18 1390c 17b0c + 13906 17b06 118b8 18c5e 139b8 1189c 17bb8 1399c 1188e 17b9c 1398e 17b8e 1085e 118de 139de 17bde + 17940 1bcb0 1de5c 17920 1bc98 1de4e 17910 1bc8c 17908 1bc86 17904 17902 138b0 19c5c 179b0 13898 + 19c4e 17998 1bcce 1798c 13886 17986 1185c 138dc 1184e 179dc 138ce 179ce 178a0 1bc58 1de2e 17890 + 1bc4c 17888 1bc46 17884 17882 13858 19c2e 178d8 1384c 178cc 13846 178c6 1182e 1386e 178ee 17850 + 1bc2c 17848 1bc26 17844 17842 1382c 1786c 13826 17866 17828 1bc16 17824 17822 13816 17836 10578 + 182be 1053c 1051e 105be 10d70 186bc 10d38 1869e 10d1c 10d0e 104bc 10dbc 1049e 10d9e 11d60 18eb8 + 1c75e 11d30 18e9c 11d18 18e8e 11d0c 11d06 10cb8 1865e 11db8 10c9c 11d9c 10c8e 11d8e 1045e 10cde + 11dde 13d40 19eb0 1cf5c 13d20 19e98 1cf4e 13d10 19e8c 13d08 19e86 13d04 13d02 11cb0 18e5c 13db0 + 11c98 18e4e 13d98 19ece 13d8c 11c86 13d86 10c5c 11cdc 10c4e 13ddc 11cce 13dce 1bea0 1df58 1efae + 1be90 1df4c 1be88 1df46 1be84 1be82 13ca0 19e58 1cf2e 17da0 13c90 19e4c 17d90 1becc 19e46 17d88 + 13c84 17d84 13c82 17d82 11c58 18e2e 13cd8 11c4c 17dd8 13ccc 11c46 17dcc 13cc6 17dc6 10c2e 11c6e + 13cee 17dee 1be50 1df2c 1be48 1df26 1be44 1be42 13c50 19e2c 17cd0 13c48 19e26 17cc8 1be66 17cc4 + 13c42 17cc2 11c2c 13c6c 11c26 17cec 13c66 17ce6 1be28 1df16 1be24 1be22 13c28 19e16 17c68 13c24 + 17c64 13c22 17c62 11c16 13c36 17c76 1be14 1be12 13c14 17c34 13c12 17c32 102bc 1029e 106b8 1835e + 1069c 1068e 1025e 106de 10eb0 1875c 10e98 1874e 10e8c 10e86 1065c 10edc 1064e 10ece 11ea0 18f58 + 1c7ae 11e90 18f4c 11e88 18f46 11e84 11e82 10e58 1872e 11ed8 18f6e 11ecc 10e46 11ec6 1062e 10e6e + 11eee 19f50 1cfac 19f48 1cfa6 19f44 19f42 11e50 18f2c 13ed0 19f6c 18f26 13ec8 11e44 13ec4 11e42 + 13ec2 10e2c 11e6c 10e26 13eec 11e66 13ee6 1dfa8 1efd6 1dfa4 1dfa2 19f28 1cf96 1bf68 19f24 1bf64 + 19f22 1bf62 11e28 18f16 13e68 11e24 17ee8 13e64 11e22 17ee4 13e62 17ee2 10e16 11e36 13e76 17ef6 + 1df94 1df92 19f14 1bf34 19f12 1bf32 11e14 13e34 11e12 17e74 13e32 17e72 1df8a 19f0a 1bf1a 11e0a + 13e1a 17e3a 1035c 1034e 10758 183ae 1074c 10746 1032e 1076e 10f50 187ac 10f48 187a6 10f44 10f42 + 1072c 10f6c 10726 10f66 18fa8 1c7d6 18fa4 18fa2 10f28 18796 11f68 18fb6 11f64 10f22 11f62 10716 + 10f36 11f76 1cfd4 1cfd2 18f94 19fb4 18f92 19fb2 10f14 11f34 10f12 13f74 11f32 13f72 1cfca 18f8a + 19f9a 10f0a 11f1a 13f3a 103ac 103a6 107a8 183d6 107a4 107a2 10396 107b6 187d4 187d2 10794 10fb4 + 10792 10fb2 1c7ea +] + +PATTERNS =! do -> + out = Int32Array.new(SYMBOLS.length) + out[i] = parseInt(SYMBOLS[i], 16) for i in [0...SYMBOLS.length] + out + +START_PATTERN =! 0x1fea8 # 17 modules: 8 1 1 1 1 1 1 3 +STOP_PATTERN =! 0x3fa29 # 18 modules: 7 1 1 3 1 1 1 2 1 + +MAX_COLUMNS =! 30 +MIN_ROWS =! 3 +MAX_ROWS =! 90 +MAX_CODEWORDS =! 928 # per symbol, indicators excluded +MODULUS =! 929 + +# Mode codewords. +PAD =! 900 +LATCH_TEXT =! 900 +LATCH_BYTE =! 901 +LATCH_NUMERIC =! 902 +SHIFT_BYTE =! 913 +LATCH_BYTE6 =! 924 # byte compaction, a multiple of six bytes +ECI_CHARSET =! 927 +ECI_UTF8 =! 26 + +# Text submodes and the symbols that move between them: a latch changes the +# submode, a shift applies to the next character only. +ALPHA =! 0 +LOWER =! 1 +MIXED =! 2 +PUNCT =! 3 +PL =! 25 # mixed -> punct latch +LL =! 27 # alpha or mixed -> lower latch +AS =! 27 # lower: alpha shift +ML =! 28 # alpha or lower -> mixed latch +AL =! 28 # mixed -> alpha latch +PS =! 29 # punct shift +PAL =! 29 # punct -> alpha latch + +MIXED_CHARS =! '0123456789&\r\t,:#-.$/+%*=^' +PUNCT_CHARS =! ';<>@[\\]_`~!\r\t,:\n-.$/"|*()?{}\'' + +UPPER_CLASS =! 1 +LOWER_CLASS =! 2 +MIXED_CLASS =! 4 +PUNCT_CLASS =! 8 + +# Submode membership by char code, and each character's value in the mixed +# and punctuation submodes; space is value 26 in the other three. +CLASS =! Uint8Array.new(128) +MIXED_INDEX =! Uint8Array.new(128) +PUNCT_INDEX =! Uint8Array.new(128) +do -> + CLASS[c] |= UPPER_CLASS for c in [65..90] + CLASS[c] |= LOWER_CLASS for c in [97..122] + CLASS[32] = UPPER_CLASS | LOWER_CLASS | MIXED_CLASS + MIXED_INDEX[32] = 26 + for i in [0...MIXED_CHARS.length] + c = MIXED_CHARS.charCodeAt i + CLASS[c] |= MIXED_CLASS + MIXED_INDEX[c] = i + for i in [0...PUNCT_CHARS.length] + c = PUNCT_CHARS.charCodeAt i + CLASS[c] |= PUNCT_CLASS + PUNCT_INDEX[c] = i + +# ==[ GF(929) ]== + +# The prime field with generator 3: EXP[i] = 3^i, doubled so a summed pair of +# logs indexes it without a modulo. +GF929 =! do -> + exp = Int32Array.new(2 * 928) + log = Int32Array.new(929) + x = 1 + for i in [0...928] + exp[i] = exp[i + 928] = x + log[x] = i + x = (x * 3) % MODULUS + { exp, log } + +EXP =! GF929.exp +LOG =! GF929.log + +mul =! (a, b) -> (a * b) % MODULUS +inv =! (a) -> EXP[928 - LOG[a]] + +# ==[ Reed-Solomon ]== + +# Generator polynomial per level, ascending coefficients with the leading 1 +# dropped: the product of (x - 3^i) for i in 1..2^(level + 1). +GENERATORS =! [] + +generator =! (level) -> + return GENERATORS[level] if GENERATORS[level] + k = 2 << level + gen = Int32Array.new(k + 1) + gen[0] = 1 + root = 1 + for i in [0...k] + root = mul root, 3 + for j in [i + 1..1] by -1 + gen[j] = (gen[j - 1] + MODULUS - mul(root, gen[j])) % MODULUS + gen[0] = (MODULUS - mul(root, gen[0])) % MODULUS + GENERATORS[level] = gen.subarray(0, k) + +# Parity codewords via LFSR remainder, negated so the whole codeword +# polynomial divides by the generator; highest degree first. +eccCodewords =! (data, level) -> + gen = generator level + k = gen.length + last = k - 1 + reg = Int32Array.new(k) + for i in [0...data.length] + f = (data[i] + reg[last]) % MODULUS + for j in [last..1] by -1 + reg[j] = (reg[j - 1] + MODULUS - mul(f, gen[j])) % MODULUS + reg[0] = (MODULUS - mul(f, gen[0])) % MODULUS + out = Int32Array.new(k) + out[i] = (MODULUS - reg[last - i]) % MODULUS for i in [0...k] + out + +# ==[ Compaction ]== + +isDigit =! (c) -> c >= 48 and c <= 57 +isText =! (c) -> c is 9 or c is 10 or c is 13 or (c >= 32 and c < 127) + +# Count of consecutive digits at `at`. +digitRun =! (bytes, at, end) -> + i = at + i++ while i < end and isDigit(bytes[i]) + i - at + +# Count of consecutive text characters at `at`; a run of thirteen or more +# digits ends it, since those go numeric. +textRun =! (bytes, at, end) -> + i = at + while i < end + d = digitRun bytes, i, end + break if d >= 13 + if d + i += d + else if isText bytes[i] + i++ + else + break + i - at + +# Count of bytes to compact as binary at `at`: up to a text run of five or +# a digit run of thirteen. +byteRun =! (bytes, at, end) -> + i = at + while i < end + break if digitRun(bytes, i, end) >= 13 + break if textRun(bytes, i, end) >= 5 + i++ + i - at + +# Whether the punctuation run at `at` is worth a latch costing `cost` +# symbols over a shift per character: the latch also pays the way back +# out, one symbol to alpha and two to anything else. +punctLatch =! (bytes, at, end, cost) -> + i = at + i++ while i < end and CLASS[bytes[i]] & PUNCT_CLASS + back = if i is end then 0 else if CLASS[bytes[i]] & UPPER_CLASS then 1 else 2 + i - at > cost + back + +# Base-30 symbols for count bytes at `at`, starting in submode `mode`; +# returns the submode latched at the end. Characters outside the current +# submode latch when a longer stay is likely and shift otherwise, as the +# specification recommends. +textSymbols =! (bytes, at, count, mode, syms) -> + end = at + count + i = at + while i < end + c = bytes[i] + cls = CLASS[c] + switch mode + when ALPHA + if cls & UPPER_CLASS + syms.push (if c is 32 then 26 else c - 65) + i++ + else if cls & LOWER_CLASS + syms.push LL + mode = LOWER + else if cls & MIXED_CLASS or punctLatch(bytes, i, end, 2) + syms.push ML + mode = MIXED + else + syms.push PS, PUNCT_INDEX[c] + i++ + when LOWER + if cls & LOWER_CLASS + syms.push (if c is 32 then 26 else c - 97) + i++ + else if cls & UPPER_CLASS + syms.push AS, c - 65 + i++ + else if cls & MIXED_CLASS or punctLatch(bytes, i, end, 2) + syms.push ML + mode = MIXED + else + syms.push PS, PUNCT_INDEX[c] + i++ + when MIXED + if cls & MIXED_CLASS + syms.push MIXED_INDEX[c] + i++ + else if cls & UPPER_CLASS + syms.push AL + mode = ALPHA + else if cls & LOWER_CLASS + syms.push LL + mode = LOWER + else if punctLatch bytes, i, end, 1 + syms.push PL + mode = PUNCT + else + syms.push PS, PUNCT_INDEX[c] + i++ + else + if cls & PUNCT_CLASS + syms.push PUNCT_INDEX[c] + i++ + else + syms.push PAL + mode = ALPHA + mode + +# Text compaction of count bytes at `at` into codes: two symbols per +# codeword, an odd tail padded with a punctuation shift, which in the +# punctuation submode is the latch back to alpha and leaves the stream +# there. +textCodewords =! (bytes, at, count, mode, codes) -> + syms = [] + mode = textSymbols bytes, at, count, mode, syms + if syms.length & 1 + syms.push PS + mode = ALPHA if mode is PUNCT + codes.push 30 * syms[i] + syms[i + 1] for i in [0...syms.length] by 2 + mode + +# Byte compaction: a single byte from text shifts, otherwise a latch and +# six-byte groups as five base-900 codewords, the remainder one per byte. +byteCodewords =! (bytes, at, count, inText, codes) -> + if count is 1 and inText + codes.push SHIFT_BYTE, bytes[at] + return + codes.push (if count % 6 is 0 then LATCH_BYTE6 else LATCH_BYTE) + end = at + count + while at + 6 <= end + # Six bytes are 48 bits, exact in a double. + v = 0 + v = v * 256 + bytes[at + k] for k in [0...6] + place = codes.length + codes.push 0, 0, 0, 0, 0 + for k in [4..0] by -1 + codes[place + k] = v % 900 + v = Math.floor v / 900 + at += 6 + codes.push bytes[at++] while at < end + +# Numeric compaction: groups of 44 digits, each read as 1 followed by the +# digits and written in base 900, most significant codeword first. +numericCodewords =! (bytes, at, count, codes) -> + end = at + count + while at < end + n = Math.min 44, end - at + limbs = [1] + for i in [0...n] + carry = bytes[at + i] - 48 + for j in [0...limbs.length] + t = limbs[j] * 10 + carry + limbs[j] = t % 900 + carry = (t - limbs[j]) / 900 + limbs.push carry if carry + codes.push limbs[j] for j in [limbs.length - 1..0] by -1 + at += n + +TEXT =! 0 +BYTE =! 1 +NUMERIC =! 2 + +# Message codewords: the specification's recommended switching between the +# three compaction modes, or one mode throughout. +codewords =! (bytes, encoding, codes) -> + n = bytes.length + fail 'PDF417 text is empty' unless n + switch encoding + when 'text' + for i in [0...n] + fail "PDF417 text compaction cannot encode byte #{bytes[i]}" unless isText bytes[i] + textCodewords bytes, 0, n, ALPHA, codes + when 'byte' + byteCodewords bytes, 0, n, false, codes + when 'numeric' + fail 'PDF417 numeric compaction expects digits only' unless digitRun(bytes, 0, n) is n + codes.push LATCH_NUMERIC + numericCodewords bytes, 0, n, codes + else + mode = TEXT + sub = ALPHA + i = 0 + while i < n + d = digitRun bytes, i, n + if d >= 13 + codes.push LATCH_NUMERIC + numericCodewords bytes, i, d, codes + mode = NUMERIC + sub = ALPHA + i += d + continue + t = textRun bytes, i, n + if t >= 5 or t is n - i + codes.push LATCH_TEXT if mode isnt TEXT + sub = textCodewords bytes, i, t, (if mode is TEXT then sub else ALPHA), codes + mode = TEXT + i += t + continue + b = byteRun bytes, i, n + byteCodewords bytes, i, b, mode is TEXT, codes + mode = BYTE unless b is 1 and mode is TEXT + sub = ALPHA unless mode is TEXT + i += b + codes + +# ==[ Symbol layout ]== + +# Specification's recommended minimum level by message length, lowered only +# when nothing higher fits. +autoLevel =! (m) -> + level = if m <= 40 then 2 else if m <= 160 then 3 else if m <= 320 then 4 else 5 + level-- while level > 0 and m + 1 + (2 << level) > MAX_CODEWORDS + level + +# Columns and rows holding `need` codewords: as requested, or the pair whose +# width to height ratio at the given row height is nearest `aspect`. A +# symbol holds at most 928 codewords whatever its shape. +dimensions =! (need, columns, rows, rowHeight, aspect) -> + if columns isnt undefined and rows isnt undefined + fail "PDF417 #{columns}x#{rows} holds #{columns * rows} codewords, needs #{need}", RangeError if columns * rows < need + fail "PDF417 #{columns}x#{rows} holds #{columns * rows} codewords, max #{MAX_CODEWORDS}", RangeError if columns * rows > MAX_CODEWORDS + return { columns, rows } + if columns isnt undefined + rows = Math.max MIN_ROWS, Math.ceil(need / columns) + fail "PDF417 needs #{rows} rows at #{columns} columns, max #{MAX_ROWS}", RangeError if rows > MAX_ROWS + fail "PDF417 #{columns}x#{rows} holds #{columns * rows} codewords, max #{MAX_CODEWORDS}", RangeError if columns * rows > MAX_CODEWORDS + return { columns, rows } + if rows isnt undefined + columns = Math.ceil need / rows + fail "PDF417 needs #{columns} columns at #{rows} rows, max #{MAX_COLUMNS}", RangeError if columns > MAX_COLUMNS + fail "PDF417 #{columns}x#{rows} holds #{columns * rows} codewords, max #{MAX_CODEWORDS}", RangeError if columns * rows > MAX_CODEWORDS + return { columns, rows } + best = 0 + bestRows = 0 + bestDiff = Infinity + for c in [1..MAX_COLUMNS] + r = Math.max MIN_ROWS, Math.ceil(need / c) + continue if r > MAX_ROWS or c * r > MAX_CODEWORDS + diff = Math.abs (17 * c + 69) / (r * rowHeight) - aspect + if diff < bestDiff + bestDiff = diff + best = c + bestRows = r + { columns: best, rows: bestRows } + +# The full stream: length descriptor, message, padding, parity. +symbolCodewords =! (codes, columns, rows, level) -> + k = 2 << level + data = Int32Array.new(columns * rows) + m = codes.length + size = data.length - k + data[0] = size + data[i + 1] = codes[i] for i in [0...m] + data.fill PAD, m + 1, size + data.set eccCodewords(data.subarray(0, size), level), size + data + +# Row indicator value for cluster c of row r: the three clusters carry the +# row count, the level with the row remainder, and the column count, in +# that order on the left and rotated one place on the right. +indicator =! (c, r, rows, columns, level, right) -> + field = if right then (c + 2) % 3 else c + base = 30 * (r // 3) + base + switch field + when 0 then (rows - 1) // 3 + when 1 then 3 * level + (rows - 1) % 3 + else columns - 1 + +# Module rows, 1 dark; a compact symbol drops the right indicator and ends +# each row with a single stop bar. +modules =! (data, columns, rows, level, compact) -> + width = 17 * (columns + (if compact then 2 else 4)) + 1 + out = [] + pos = 0 + for r in [0...rows] + row = Uint8Array.new(width) + c = r % 3 + x = 0 + put = (bits, n) -> + for k in [n - 1..0] by -1 + row[x++] = (bits >>> k) & 1 + put START_PATTERN, 17 + put PATTERNS[929 * c + indicator(c, r, rows, columns, level, false)], 17 + put PATTERNS[929 * c + data[pos++]], 17 for k in [0...columns] + if compact + row[x] = 1 + else + put PATTERNS[929 * c + indicator(c, r, rows, columns, level, true)], 17 + put STOP_PATTERN, 18 + out.push row + out + +# ==[ Validation ]== + +asInt =! (n, title, min, max = Infinity) -> + fail "#{title} expected number, got type=#{typeof n}", TypeError unless typeof n is 'number' + range = if max is Infinity then ">= #{min}" else "in #{min}..#{max}" + fail "#{title} expected integer #{range}, got #{n}", RangeError unless Number.isSafeInteger(n) and n >= min and n <= max + n + +# ==[ Renderers ]== + +# A raster is the module rows plus output geometry: `bars` holds each module +# row's dark runs [x, width, ...] in output pixels, every module row covers +# `rowPx` pixel rows, and the border is `top` pixels on every side. +raster =! (mods, rowHeight, scale, border) -> + cols = mods[0].length + W = (cols + 2 * border) * scale + H = (mods.length * rowHeight + 2 * border) * scale + fail "PDF417 output #{W}x#{H} exceeds #{MAX_IMAGE_SIDE}" if W > MAX_IMAGE_SIDE or H > MAX_IMAGE_SIDE + bars = for row in mods + runs = [] + x = 0 + while x < cols + x++ while x < cols and row[x] is 0 + break if x is cols + start = x + x++ while x < cols and row[x] is 1 + runs.push (border + start) * scale, (x - start) * scale + runs + { W, H, bars, rowPx: rowHeight * scale, top: border * scale } + +# Pixel rows, one per module row, built on first use. +pixelRows =! (r) -> + blank = Uint8Array.new(r.W) + rows = for runs in r.bars + row = Uint8Array.new(r.W) + row.fill 1, runs[k], runs[k] + runs[k + 1] for k in [0...runs.length] by 2 + row + (y) -> + i = (y - r.top) // r.rowPx + if y >= r.top and i < rows.length then rows[i] else blank + +renderRaw =! (r) -> + rowAt = pixelRows r + for y in [0...r.H] + row = rowAt y + (row[x] is 1 for x in [0...r.W]) + +# Half-block glyphs with dark modules printed blank and light ones as blocks, +# which reads correctly on a dark terminal with light text. +renderAscii =! (r) -> + rowAt = pixelRows r + out = '' + for y in [0...r.H] by 2 + first = rowAt y + # Past the bottom edge the missing row reads as dark. + second = if y + 1 >= r.H then null else rowAt(y + 1) + for x in [0...r.W] + a = first[x] is 1 + b = second is null or second[x] is 1 + out += if a then (if b then ' ' else '▄') else (if b then '▀' else '█') + out += '\n' + out + +renderTerm =! (r) -> + rowAt = pixelRows r + black = '\x1b[40m \x1b[0m' + white = '\x1b[1;47m \x1b[0m' + out = '' + for y in [0...r.H] + row = rowAt y + out += (if row[x] then black else white) for x in [0...r.W] + out += '\n' + out + +renderSvg =! (r, optimize) -> + out = '' + path = '' + h = r.rowPx + for runs, i in r.bars + y = r.top + i * h + for k in [0...runs.length] by 2 + x = runs[k] + w = runs[k + 1] + if optimize + path += "M#{x} #{y}h#{w}v#{h}h-#{w}Z" + else + out += '' + out += '' if optimize + out + '' + +renderGif =! (r) -> writeGif r.W, r.H, pixelRows(r) + +# ==[ Encoder ]== + +ENCODINGS =! ['auto', 'text', 'byte', 'numeric'] + +# Message bytes: Latin-1 when every character fits, otherwise UTF-8 behind +# its ECI. +messageBytes =! (text, codes) -> + wide = false + for i in [0...text.length] + if text.charCodeAt(i) > 255 + wide = true + break + return Uint8Array.from(text, (ch) -> ch.charCodeAt 0) unless wide + codes.push ECI_CHARSET, ECI_UTF8 + TextEncoder.new().encode text + +export encodePDF417 =! (text, output = 'raw', opts = {}) -> + asString text, 'text' + asString output, 'output' + asObject opts, 'opts' + scale = if opts.scale is undefined then 1 else asInt(opts.scale, 'opts.scale', 1) + border = if opts.border is undefined then 2 else asInt(opts.border, 'opts.border', 0) + rowHeight = if opts.rowHeight is undefined then 3 else asInt(opts.rowHeight, 'opts.rowHeight', 1) + aspect = if opts.aspect is undefined then 3 else asInt(opts.aspect, 'opts.aspect', 1) + columns = if opts.columns is undefined then undefined else asInt(opts.columns, 'opts.columns', 1, MAX_COLUMNS) + rows = if opts.rows is undefined then undefined else asInt(opts.rows, 'opts.rows', MIN_ROWS, MAX_ROWS) + level = if opts.ecc is undefined then undefined else asInt(opts.ecc, 'opts.ecc', 0, 8) + encoding = if opts.encoding is undefined then 'auto' else opts.encoding + fail "invalid encoding=#{encoding}" unless ENCODINGS.includes encoding + codes = [] + bytes = messageBytes text, codes + codewords bytes, encoding, codes + m = codes.length + level = autoLevel m if level is undefined + need = m + 1 + (2 << level) + fail 'Capacity overflow' if need > MAX_CODEWORDS + {columns, rows} = dimensions need, columns, rows, rowHeight, aspect + data = symbolCodewords codes, columns, rows, level + mods = modules data, columns, rows, level, !!opts.compact + r = raster mods, rowHeight, scale, border + switch output + when 'raw' then renderRaw r + when 'ascii' then renderAscii r + when 'term' then renderTerm r + when 'svg' then renderSvg r, (if opts.optimize is undefined then true else opts.optimize) + when 'gif' then renderGif r + when 'data-url' then gifDataUrl renderGif(r) + else fail "Unknown output: #{output}" + +# ==[ Reader ]== + +MAX_CELLS =! 32 # left indicator, thirty columns, right indicator +START_RUNS =! [8, 1, 1, 1, 1, 1, 1, 3] +STOP_RUNS =! [7, 1, 1, 3, 1, 1, 1, 2, 1] +STOP_BACK =! [1, 2, 1, 1, 1, 3, 1, 1, 7] # the stop pattern read toward the start +START_BACK =! [3, 1, 1, 1, 1, 1, 1, 8] # the start pattern read the same way, space first + +# A row is read from its start pattern toward the stop, or when the start +# is out of the picture, from the stop pattern toward the start with the +# codewords mirrored. Each mode names the pattern at the row's other end +# as an anchor of its own, tracked to find the row direction. +STOP_END =! { anchor: STOP_RUNS, modules: 18 } +START_END =! { anchor: START_BACK, modules: 17 } +FORWARD =! { anchor: START_RUNS, modules: 17, end: STOP_RUNS, endModules: 18, tail: STOP_END, mirror: false } +MIRROR =! { anchor: STOP_BACK, modules: 18, end: START_BACK, endModules: 17, tail: START_END, mirror: true } + +# Symbol index by seventeen-module pattern (index + 1, zero for none) and by +# edge-to-similar-edge sequence, built on first use. The edge sequence is +# the six distances from each bar or space to the next of its kind, which +# print gain leaves unchanged. The mirror tables answer for rows read from +# the stop pattern toward the start, when the start is out of the picture. +TABLES =! { bits: null, edges: null, mirror: null } + +edgeKey =! (runs, at, total) -> + key = 0 + for i in [0...6] + t = Math.round (runs[at + i] + runs[at + i + 1]) * 17 / total + return -1 if t < 2 or t > 15 + key |= t << (4 * i) + key + +tables =! -> + return TABLES if TABLES.bits + TABLES.bits = Int16Array.new(1 << 17) + TABLES.edges = Map.new() + TABLES.mirror = { bits: Int16Array.new(1 << 17), edges: Map.new() } + runs = Int32Array.new(8) + back = Int32Array.new(8) + for i in [0...PATTERNS.length] + pattern = PATTERNS[i] + n = 0 + len = 0 + dark = 1 + for k in [16..0] by -1 + bit = (pattern >>> k) & 1 + if bit isnt dark + runs[n++] = len + len = 0 + dark = bit + len++ + runs[n] = len + TABLES.bits[pattern] = i + 1 + TABLES.edges.set edgeKey(runs, 0, 17), i + 1 + # Read backward the trailing space leads and the first bar trails, so + # the mirrored eight runs start one run in. + back[k] = runs[7 - k] for k in [0...8] + reversed = 0 + reversed |= ((pattern >>> k) & 1) << (16 - k) for k in [0...17] + TABLES.mirror.bits[reversed] = i + 1 + TABLES.mirror.edges.set edgeKey(back, 0, 17), i + 1 + TABLES + +# Symbol index of the eight runs at `at`, or -1: by edge sequence, which +# survives print gain, then by the widths sampled at the seventeen module +# centers when that misses. Mirrored runs begin with the codeword's last +# space. +lookup =! (runs, at, total, mirror) -> + {bits, edges} = if mirror then tables().mirror else tables() + key = edgeKey runs, at, total + if key >= 0 + code = edges.get key + return code - 1 if code + pattern = 0 + k = 0 + bound = runs[at] + for m in [0...17] + center = (2 * m + 1) * total + while 34 * bound <= center and k < 7 + k++ + bound += runs[at + k] + pattern = (pattern << 1) | (if mirror then k & 1 else 1 - (k & 1)) + bits[pattern] - 1 + +# Width in pixels of the runs at `at` when they match a fixed pattern, or 0. +# Each distance from a run's leading edge to the next run of its kind is +# compared, since print gain moves every edge by the same amount and leaves +# those distances alone: every one within 0.8 module of its ideal and the +# summed error under a quarter of the width. +matchRuns =! (runs, at, pattern, modules) -> + n = pattern.length + total = 0 + total += runs[at + k] for k in [0...n] + return 0 if total < modules + unit = total / modules + limit = unit * 0.8 + err = 0 + for k in [0...n - 1] + d = runs[at + k] + runs[at + k + 1] - (pattern[k] + pattern[k + 1]) * unit + d = -d if d < 0 + return 0 if d > limit + err += d + if err < total * 0.25 then total else 0 + +# First run index at or after `from`, same parity, where the pattern matches. +# A sliding sum of the window rejects one narrower than a module a run +# before the full comparison. +findRuns =! (runs, n, from, pattern, modules) -> + len = pattern.length + last = n - len + return -1 if from > last + total = 0 + total += runs[from + k] for k in [0...len] + at = from + loop + return at if total >= modules and matchRuns(runs, at, pattern, modules) + at += 2 + return -1 if at > last + total += runs[at + len - 2] + runs[at + len - 1] - runs[at - 2] - runs[at - 1] + +# Luma along a ray from (x, y) in `count` steps of the unit vector (dx, dy); +# samples outside the image read as light, the quiet zone a symbol at the +# edge would have had. +sampleRay! =! (luma, img, format, x, y, dx, dy, count) -> + {width, height} = img + # Samples lo...hi land inside the image and the rest read as paper; the + # sample at each end is checked, so the inner loops need no bounds. + lo = 0 + hi = count + if dx > 0 + lo = Math.max lo, Math.ceil(-x / dx) + hi = Math.min hi, Math.ceil((width - x) / dx) + else if dx < 0 + lo = Math.max lo, Math.floor((x - width) / -dx) + 1 + hi = Math.min hi, Math.floor(x / -dx) + 1 + else if x < 0 or x >= width + hi = lo + if dy > 0 + lo = Math.max lo, Math.ceil(-y / dy) + hi = Math.min hi, Math.ceil((height - y) / dy) + else if dy < 0 + lo = Math.max lo, Math.floor((y - height) / -dy) + 1 + hi = Math.min hi, Math.floor(y / -dy) + 1 + else if y < 0 or y >= height + hi = lo + lo = Math.min count, Math.max(0, lo) + hi = Math.min count, Math.max(lo, hi) + luma.fill 255, 0, lo + luma.fill 255, hi, count + sampleChecked luma, img, format, x, y, dx, dy, lo, Math.min(hi, lo + 1) + sampleChecked luma, img, format, x, y, dx, dy, Math.max(lo, hi - 1), hi + lo = Math.min hi, lo + 1 + hi = Math.max lo, hi - 1 + step = format.step + row = width * step + data = img.data + if step is 1 + for n in [lo...hi] + luma[n] = data[(y + n * dy | 0) * row + (x + n * dx | 0)] + else if step is 2 + shift = format.bits - 8 + for n in [lo...hi] + at = (y + n * dy | 0) * row + (x + n * dx | 0) * 2 + luma[n] = (data[at] | (data[at + 1] << 8)) >>> shift + else + for n in [lo...hi] + at = (y + n * dy | 0) * row + (x + n * dx | 0) * step + luma[n] = (data[at] + 2 * data[at + 1] + data[at + 2]) >> 2 + +# The samples from...to of a ray, each checked against the image bounds. +sampleChecked! =! (luma, img, format, x, y, dx, dy, from, to) -> + {width, height, data} = img + step = format.step + row = width * step + shift = format.bits - 8 + for n in [from...to] + px = x + n * dx + py = y + n * dy + xi = px | 0 + yi = py | 0 + if px < 0 or py < 0 or xi >= width or yi >= height + luma[n] = 255 + else + at = yi * row + xi * step + luma[n] = if step is 1 then data[at] + else if step is 2 then (data[at] | (data[at + 1] << 8)) >>> shift + else (data[at] + 2 * data[at + 1] + data[at + 2]) >> 2 + +# Run lengths of one ray, even slots light and odd dark, from the edges +# between local extrema: an extremum counts once the luma has swung a +# third of the ray's range away from the last one, and each edge sits +# where the luma crosses the midpoint of its own valley and peak, placed +# between samples by interpolation. Blur leaves a narrow space short of +# paper white and print gain widens every bar; both move the midpoint +# crossing far less than they move a fixed threshold, and the fractional +# positions carry sub-pixel width. Returns the run count, zero for a flat +# ray. +edgeRuns =! (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 + return 0 if hi - lo < 32 + swing = (hi - lo) / 3 + # Until the first swing the direction is unknown: track both extremes. + minV = luma[0] + maxV = luma[0] + minAt = 0 + maxAt = 0 + k = 1 + while k < count + v = luma[k] + if v < minV + minV = v + minAt = k + else if v > maxV + maxV = v + maxAt = k + break if maxV - minV >= swing + k++ + return 0 if k is count + rising = minAt < maxAt + n = 0 + runs[n++] = 0 if rising # the ray opens dark + if rising + at = minAt + value = minV + best = maxV + bestAt = maxAt + else + at = maxAt + value = maxV + best = minV + bestAt = minAt + last = 0 + # The edge between the extremum at `at` and the candidate at `bestAt`. + edge = -> + level = (value + best) / 2 + i = at + if rising + i++ while luma[i] < level + else + i++ while luma[i] > level + # Cross between samples i - 1 and i. + a = luma[i - 1] + b = luma[i] + pos = i - 1 + (if b is a then 0.5 else (level - a) / (b - a)) + runs[n++] = pos - last + last = pos + k++ + while k < count + v = luma[k] + if rising + if v > best + best = v + bestAt = k + else if v <= best - swing + edge() + at = bestAt + value = best + best = v + bestAt = k + rising = false + else + if v < best + best = v + bestAt = k + else if v >= best + swing + edge() + at = bestAt + value = best + best = v + bestAt = k + rising = true + k++ + edge() if Math.abs(best - value) >= swing + runs[n++] = count - last + n + +# Anchor pattern on a sampled ray whose leading edge lies nearest `expect` +# pixels along it, within `slack`; returns the run index or -1. +anchorNear =! (runs, n, first, expect, slack, mode) -> + best = -1 + bestD = slack + pos = 0 + pos += runs[k] for k in [0...first] + for at in [first...n - mode.anchor.length + 1] by 2 + d = pos - expect + d = -d if d < 0 + if d < bestD and matchRuns(runs, at, mode.anchor, mode.modules) + best = at + bestD = d + break if pos > expect + slack + pos += runs[at] + runs[at + 1] + best + +# Codewords along one ray into cells, the first expected to begin `expect` +# pixels in with modules of `unit` pixels and bars on runs of parity +# `first`: the symbol index, or -1 for a cell that would not read. Each next +# codeword is expected seventeen modules on; the nearest bar edge +# resynchronizes, so a merged or split run costs one cell rather than the +# rest of the row. Stops at the pattern that ends the row or when sync is +# lost. Returns the count. +readFrom =! (runs, n, first, expect, unit, cells, base, mode) -> + pos = 0 + pos += runs[k] for k in [0...first] + i = first + count = 0 + while count < MAX_CELLS + while i + 2 < n and Math.abs(pos + runs[i] + runs[i + 1] - expect) < Math.abs(pos - expect) + pos += runs[i] + runs[i + 1] + i += 2 + while i - 2 >= first and Math.abs(pos - runs[i - 1] - runs[i - 2] - expect) < Math.abs(pos - expect) + pos -= runs[i - 1] + runs[i - 2] + i -= 2 + break if Math.abs(pos - expect) > 2 * unit + break if i + mode.end.length <= n and matchRuns(runs, i, mode.end, mode.endModules) + break if i + 8 > n + total = 0 + total += runs[i + k] for k in [0...8] + code = if total < 13 * unit or total > 21 * unit then -1 else lookup(runs, i, total, mode.mirror) + cells[base + count++] = code + # Only a read codeword vouches for its own width; after a miss the next + # is expected a nominal codeword on, since a window slipped by two runs + # over repeated codewords reads as a valid wrong one. + if code < 0 + expect = pos + 17 * unit + else + unit = (unit + total / 17) / 2 + expect = pos + total + pos += total + i += 8 + count + +# Codewords along one ray from the anchor pattern at run `at`. +readRay =! (runs, n, at, cells, base, mode) -> + pos = 0 + pos += runs[k] for k in [0...at] + width = 0 + width += runs[at + k] for k in [0...mode.anchor.length] + readFrom runs, n, at + (mode.anchor.length & 1), pos + width, width / mode.modules, cells, base, mode + +# ==[ Error correction ]== + +# Errors-and-erasures decoding of the n codewords in place, k of them +# parity, with `e` erased positions listed in `erasures`. The erasure +# locator folds into the syndromes so the erased cells cost half what an +# unknown error does, then Berlekamp-Massey finds the rest, Chien locates +# and Forney evaluates. Returns the number of errors corrected, or -1. +correct =! (c, n, k, erasures, e) -> + return -1 if e > k + S = Int32Array.new(k) + dirty = 0 + for i in [0...k] + x = EXP[i + 1] + v = 0 + v = (v * x + c[j]) % MODULUS for j in [0...n] + S[i] = v + dirty |= v + return 0 unless dirty + # Erasure locator, ascending. + G = Int32Array.new(k + 1) + G[0] = 1 + for l in [0...e] + y = EXP[n - 1 - erasures[l]] + for j in [l + 1..1] by -1 + G[j] = (G[j] + MODULUS - mul(y, G[j - 1])) % MODULUS + # Forney syndromes: the erasures drop out, leaving a syndrome sequence of + # the unknown errors alone. + N = k - e + U = Int32Array.new(N) + for m in [0...N] + v = 0 + v = (v + mul(G[j], S[m + e - j])) % MODULUS for j in [0..e] + U[m] = v + # Berlekamp-Massey over U. + sigma = Int32Array.new(N + 2) + previous = Int32Array.new(N + 2) + next = Int32Array.new(N + 2) + sigma[0] = 1 + previous[0] = 1 + sigmaLength = 1 + previousLength = 1 + degree = 0 + shift = 1 + discrepancy = 1 + for r in [0...N] + delta = U[r] + delta = (delta + mul(sigma[i], U[r - i])) % MODULUS for i in [1..degree] + unless delta + shift++ + continue + coefficient = mul delta, inv(discrepancy) + nextLength = Math.max sigmaLength, previousLength + shift + for i in [0...nextLength] + a = if i < sigmaLength then sigma[i] else 0 + b = if i >= shift and i - shift < previousLength then mul(coefficient, previous[i - shift]) else 0 + next[i] = (a + MODULUS - b) % MODULUS + if 2 * degree <= r + previous[i] = sigma[i] for i in [0...sigmaLength] + previousLength = sigmaLength + degree = r + 1 - degree + discrepancy = delta + shift = 1 + else + shift++ + sigma[i] = next[i] for i in [0...nextLength] + sigmaLength = nextLength + sigmaLength-- while sigmaLength > 1 and not sigma[sigmaLength - 1] + errors = sigmaLength - 1 + # One parity codeword stays free to detect a miscorrection: a decode that + # spends every one solves the erased cells exactly, right or wrong. + return -1 if 2 * errors > N - 1 + # Full locator: the found errors times the erasures. + total = errors + e + L = Int32Array.new(total + 1) + for i in [0...sigmaLength] + for j in [0..e] + L[i + j] = (L[i + j] + mul(sigma[i], G[j])) % MODULUS + # Evaluator, S times the locator below x^k. + O = Int32Array.new(k) + for i in [0..total] + continue unless L[i] + O[i + j] = (O[i + j] + mul(L[i], S[j])) % MODULUS for j in [0...k - i] + # Chien search over every position, Forney at each root. + found = 0 + for pos in [0...n] + xinv = EXP[928 - (n - 1 - pos)] + v = 0 + v = (v * xinv + L[i]) % MODULUS for i in [total..0] by -1 + continue if v + found++ + o = 0 + o = (o * xinv + O[i]) % MODULUS for i in [k - 1..0] by -1 + d = 0 + d = (d * xinv + mul(i, L[i])) % MODULUS for i in [total..1] by -1 + return -1 unless d + c[pos] = (c[pos] + mul(o, inv(d))) % MODULUS + return -1 if found isnt total + errors + +# ==[ Decompaction ]== + +TEXT_SHIFT_NONE =! -1 + +# Decoded stream state: the codewords, a cursor, the byte sink and the +# parts already closed by an ECI change. +class Stream + constructor: (@c, @count) -> + @i = 1 + @out = [] + @parts = [] + @eci = -1 + @sub = ALPHA + @shift = TEXT_SHIFT_NONE + + switchEci: (eci) -> + @parts.push [@out, @eci] if @out.length + @out = [] + @eci = eci + + # One base-30 text symbol in the current submode. + symbol: (v) -> + if @shift isnt TEXT_SHIFT_NONE + mode = @shift + @shift = TEXT_SHIFT_NONE + else + mode = @sub + switch mode + when ALPHA + if v < 26 then @out.push 65 + v + else if v is 26 then @out.push 32 + else if v is LL then @sub = LOWER + else if v is ML then @sub = MIXED + else @shift = PUNCT + when LOWER + if v < 26 then @out.push 97 + v + else if v is 26 then @out.push 32 + else if v is AS then @shift = ALPHA + else if v is ML then @sub = MIXED + else @shift = PUNCT + when MIXED + if v < 25 then @out.push MIXED_CHARS.charCodeAt(v) + else if v is PL then @sub = PUNCT + else if v is 26 then @out.push 32 + else if v is LL then @sub = LOWER + else if v is AL then @sub = ALPHA + else @shift = PUNCT + else + if v < 29 then @out.push PUNCT_CHARS.charCodeAt(v) + else @sub = ALPHA + + # Text compaction from the cursor to the next mode codeword. + text: -> + @sub = ALPHA + @shift = TEXT_SHIFT_NONE + while @i < @count + v = @c[@i] + if v < 900 + @symbol v // 30 + @symbol v % 30 + else if v is SHIFT_BYTE + fail 'PDF417 byte shift at end of data' if @i + 1 >= @count + @out.push @c[++@i] + @shift = TEXT_SHIFT_NONE + else if v is ECI_CHARSET + fail 'PDF417 ECI at end of data' if @i + 1 >= @count + @switchEci @c[++@i] + else if v is LATCH_TEXT + @sub = ALPHA + @shift = TEXT_SHIFT_NONE + else + return + @i++ + + # Byte compaction: five-codeword groups are six bytes, the tail under 901 + # one byte per codeword. + bytes: (latch) -> + @i++ + loop + start = @i + @i++ while @i < @count and @c[@i] < 900 + n = @i - start + break unless n + groups = if latch is LATCH_BYTE6 then n // 5 else (n - 1) // 5 + for g in [0...groups] + v = 0 + v = v * 900 + @c[start + 5 * g + j] for j in [0...5] + place = @out.length + @out.push 0, 0, 0, 0, 0, 0 + for j in [5..0] by -1 + @out[place + j] = v % 256 + v = Math.floor v / 256 + @out.push @c[j] for j in [start + 5 * groups...@i] + if @i + 1 < @count and @c[@i] is ECI_CHARSET + @switchEci @c[@i + 1] + @i += 2 + else + break + + # Numeric compaction: groups of up to fifteen codewords, each a base-900 + # number whose decimal form drops its leading 1. + numeric: -> + @i++ + while @i < @count and @c[@i] < 900 + start = @i + @i++ while @i < @count and @c[@i] < 900 and @i - start < 15 + @decimal start, @i + @i++ if @i < @count and @c[@i] is LATCH_NUMERIC + + # Digits of the base-900 codewords in [start, end) into the sink. + decimal: (start, end) -> + limbs = [0] # base 10^7, least significant first + for j in [start...end] + carry = @c[j] + for l in [0...limbs.length] + t = limbs[l] * 900 + carry + limbs[l] = t % 10000000 + carry = (t - limbs[l]) / 10000000 + limbs.push carry if carry + s = String limbs[limbs.length - 1] + s += String(limbs[l]).padStart(7, '0') for l in [limbs.length - 2..0] by -1 + fail 'PDF417 numeric group does not start with 1' unless s.charCodeAt(0) is 49 + @out.push s.charCodeAt(j) for j in [1...s.length] + + # Number in the base-900 codewords in [start, end), leading 1 dropped. + number: (start, end) -> + sink = @out + @out = [] + @decimal start, end + digits = @out + @out = sink + Number String.fromCharCode(...digits) + + # Text of a macro field from the cursor to the next field or terminator. + field: -> + sink = @out + @out = [] + @sub = ALPHA + @shift = TEXT_SHIFT_NONE + while @i < @count and @c[@i] < 900 + v = @c[@i++] + @symbol v // 30 + @symbol v % 30 + text = String.fromCharCode ...@out + @out = sink + text + + # Macro PDF417 control block: segment index, file id, optional fields. + macro: -> + fail 'PDF417 macro block is truncated' if @i + 2 >= @count + macro = { segmentIndex: @number(@i + 1, @i + 3), fileId: '', last: false } + @i += 3 + while @i < @count and @c[@i] < 900 + macro.fileId += String(@c[@i++]).padStart(3, '0') + fail 'PDF417 macro block has no file id' unless macro.fileId + while @i < @count + v = @c[@i] + if v is 922 + macro.last = true + @i++ + else if v is 923 + fail 'PDF417 macro field is truncated' if @i + 1 >= @count + kind = @c[@i + 1] + @i += 2 + start = @i + switch kind + when 0 then macro.fileName = @field() + when 3 then macro.sender = @field() + when 4 then macro.addressee = @field() + when 1, 2, 5, 6 + @i++ while @i < @count and @c[@i] < 900 + n = @number start, @i + switch kind + when 1 then macro.segmentCount = n + when 2 then macro.timestamp = n + when 5 then macro.fileSize = n + else macro.checksum = n + else fail "PDF417 macro field #{kind} is unknown" + else + fail "PDF417 codeword #{v} inside macro block" + macro + +# Data codewords into bytes, an ECI-decoded text and any macro block. +parse =! (c, count) -> + s = Stream.new c, count + result = { readerInit: false } + if count > 1 and c[1] is 921 + result.readerInit = true + s.i = 2 + while s.i < count + v = c[s.i] + if v < 900 or v is LATCH_TEXT or v is SHIFT_BYTE + s.text() + else switch v + when LATCH_BYTE, LATCH_BYTE6 then s.bytes v + when LATCH_NUMERIC then s.numeric() + when ECI_CHARSET + fail 'PDF417 ECI at end of data' if s.i + 1 >= count + s.switchEci c[s.i + 1] + s.i += 2 + when 926 then s.i += 3 + when 925 then s.i += 2 + when 928 then result.macro = s.macro() + else fail "PDF417 codeword #{v} is not a mode" + s.parts.push [s.out, s.eci] if s.out.length + total = 0 + total += part[0].length for part in s.parts + bytes = Uint8Array.new(total) + text = '' + at = 0 + for [out, eci] in s.parts + bytes.set out, at + at += out.length + if eci < 0 + text += String.fromCharCode ...out + else + decoder = ECI_DECODERS[eci] + fail "PDF417 ECI #{eci} is not supported" unless decoder + text += decoder.decode Uint8Array.from(out) + result.text = text + result.bytes = bytes + result + +# ==[ Symbol reading ]== + +# Follow the anchor pattern from the hit line in direction `sign` along the +# stacking axis (sx, sy), one pixel at a time, re-finding it near where the +# last line had it. Every line where it held adds its leading edge to the +# fit in `edge`: the signed distance t along the axis and the offset u +# along the reading direction, so the edge line comes from all of them +# rather than its two ends. +trackEdge =! (s, x, y, dx, dy, sx, sy, unit, first, sign, edge, mode) -> + {img, format, rluma, rruns} = s + margin = 3 * unit + 3 + length = Math.min rluma.length, Math.ceil(mode.modules * unit + 2 * margin) + slack = Math.max 2, unit + allow = Math.ceil 3 * unit + misses = 0 + t = 0 + u = 0 + loop + t += sign + sampleRay rluma, img, format, x + t * sx + (u - margin) * dx + 0.5, y + t * sy + (u - margin) * dy + 0.5, dx, dy, length + m = edgeRuns rluma, length, rruns + at = if m < 8 then -1 else anchorNear(rruns, m, first, margin, slack, mode) + if at < 0 + break if ++misses > allow + continue + misses = 0 + pos = 0 + pos += rruns[k] for k in [0...at] + u += pos - margin + total = 0 + total += rruns[at + k] for k in [0...mode.anchor.length] + unit = (unit + total / mode.modules) / 2 + edge.n++ + edge.st += t + edge.su += u + edge.stt += t * t + edge.stu += t * u + edge.min = t if t < edge.min + edge.max = t if t > edge.max + unit + +# Row count, column count and level from the first indicator of every ray: +# the cluster names the field, the value mod 30 carries it; the right +# indicator carries the fields rotated one place. +metadata =! (s, rays, mode) -> + {cells, hist} = s + hist.fill 0 + for i in [0...rays] + code = cells[i * MAX_CELLS] + continue if code < 0 + hist[30 * ((code // 929 + (if mode.mirror then 2 else 0)) % 3) + code % 929 % 30]++ + pick = (field) -> + best = -1 + count = 0 + for v in [0...30] + if hist[30 * field + v] > count + count = hist[30 * field + v] + best = v + best + upper = pick 0 + mixed = pick 1 + cols = pick 2 + return null if upper < 0 or mixed < 0 or cols < 0 + rows = 3 * upper + mixed % 3 + 1 + level = mixed // 3 + columns = cols + 1 + return null if rows < MIN_ROWS or rows > MAX_ROWS or level > 8 + return null if columns * rows < 2 + (2 << level) or columns * rows > MAX_CODEWORDS + { rows, columns, level } + +# One located anchor pattern through to a decoded symbol, or null. +# The leading edge of an anchor pattern, tracked up and down the stack +# from (x, y) and fitted as a line u = a + b t across the tracked span; +# returns its two ends, the mean unit and the count of lines held, or null. +edgeEnds =! (s, x, y, dx, dy, sx, sy, unit, first, mode) -> + edge = { n: 0, st: 0, su: 0, stt: 0, stu: 0, min: 0, max: 0 } + unitUp = trackEdge s, x, y, dx, dy, sx, sy, unit, first, -1, edge, mode + unitDown = trackEdge s, x, y, dx, dy, sx, sy, unit, first, 1, edge, mode + return null if edge.n < 2 + mt = edge.st / edge.n + mu = edge.su / edge.n + den = edge.stt - edge.n * mt * mt + b = if den > 0 then (edge.stu - edge.n * mt * mu) / den else 0 + a = mu - b * mt + { + top: { x: x + edge.min * sx + (a + b * edge.min) * dx, y: y + edge.min * sy + (a + b * edge.min) * dy } + bottom: { x: x + edge.max * sx + (a + b * edge.max) * dx, y: y + edge.max * sy + (a + b * edge.max) * dy } + unit: (unitUp + unitDown) / 2 + lines: edge.n + } + +tryDecode =! (s, x, y, dx, dy, unit, first, endAt, extent, mode) -> + {img, format, rluma, rruns, cells, rayRow, rayT, votes} = s + # Rows stack a quarter turn from the symbol's reading direction, which a + # mirrored read runs against. + flip = if mode.mirror then -1 else 1 + sx = -dy * flip + sy = dx * flip + lead = edgeEnds s, x, y, dx, dy, sx, sy, unit, first, mode + return null unless lead + {top, bottom, unit} = lead + ex = bottom.x - top.x + ey = bottom.y - top.y + len = Math.hypot ex, ey + return null if len < 6 * unit + ux = ex / len + uy = ey / len + # Rows run from this edge to the one the pattern at the row's other end + # makes, when that pattern was on the scan line and its edge holds up as + # well as this one; a picture taken from the side skews the two edges + # without turning the rows. Otherwise the rows are taken square to the + # edge. + rx = uy * flip + ry = -ux * flip + if endAt < Infinity + tail = edgeEnds s, x + endAt * dx, y + endAt * dy, dx, dy, sx, sy, unit, first ^ (mode.anchor.length & 1), mode.tail + if tail and tail.lines * 2 >= lead.lines + vx = (tail.top.x - top.x + tail.bottom.x - bottom.x) / 2 + vy = (tail.top.y - top.y + tail.bottom.y - bottom.y) / 2 + vl = Math.hypot vx, vy + if vl > 0 and vx * rx + vy * ry > 0.7 * vl + rx = vx / vl + ry = vy / vl + margin = 3 * unit + 3 + length = Math.min rluma.length, Math.ceil((MAX_CELLS + 2) * 17 * unit + 2 * margin), Math.ceil(extent + 2 * margin + 3 * unit) + step = Math.max 1, 0.75 * unit + slack = Math.max 2, unit + # One ray at distance t along the edge: the anchor pattern is found near + # its expected place, or taken to be there when it is damaged, and the + # codewords after it are read. + rays = 0 + ray = (t) -> + return if rays is s.maxRays + base = rays * MAX_CELLS + rayT[rays] = t + rayRow[rays] = -1 + rays++ + sampleRay rluma, img, format, top.x + ux * t - rx * margin + 0.5, top.y + uy * t - ry * margin + 0.5, rx, ry, length + m = edgeRuns rluma, length, rruns + count = 0 + if m >= 16 + at = anchorNear rruns, m, first, margin, slack, mode + count = if at < 0 then readFrom(rruns, m, first + (mode.anchor.length & 1), margin + mode.modules * unit, unit, cells, base, mode) else readRay(rruns, m, at, cells, base, mode) + cells.fill -1, base + count, base + MAX_CELLS + ray i * step for i in [0..Math.floor(len / step)] + meta = metadata s, rays, mode + return null unless meta + {rows, columns, level} = meta + # Row of each ray from its left indicator, fitted against position along + # the edge so a misread indicator cannot move a whole line; then rays + # beyond the tracked edge while the fit still lands inside the symbol, + # which reads rows whose start pattern is damaged. + rowOf = (i) -> + code = cells[i * MAX_CELLS] + return -1 if code < 0 + row = 3 * (code % 929 // 30) + code // 929 + if row < rows then row else -1 + fit = null + backward = false + fitRows = -> + valid = 0 + st = 0 + sr = 0 + for i in [0...rays] + rayRow[i] = rowOf i + continue if rayRow[i] < 0 + valid++ + st += rayT[i] + sr += rayRow[i] + return false unless valid + mt = st / valid + mr = sr / valid + num = 0 + den = 0 + for i in [0...rays] when rayRow[i] >= 0 + num += (rayT[i] - mt) * (rayRow[i] - mr) + den += (rayT[i] - mt) * (rayT[i] - mt) + slope = if den > 0 then num / den else 0 + backward = valid >= 3 and slope < 0 + slope = rows / len if slope <= 0 + fit = (t) -> mr + slope * (t - mt) + true + return null unless fitRows() + # Rows numbered against the walk: a mirror image of the reading + # direction, so the stack is walked from its other end. + if backward + [top, bottom] = [bottom, top] + ux = -ux + uy = -uy + rays = 0 + ray i * step for i in [0..Math.floor(len / step)] + return null unless fitRows() + return null if backward + # The edge is short next to the width, so its fit leaves the row + # direction a little off, and a wide symbol's rays then drift into the + # neighbor rows. The rays measure that drift themselves: how the row + # named by each codeword's cluster moves along the ray, in rows per + # column. Past a fraction of a row across the width the direction is + # corrected and every ray sampled again. + drift = -> + num = 0 + den = 0 + for i in [0...rays] when rayRow[i] >= 0 + base = i * MAX_CELLS + r = rayRow[i] + for j in [1..columns] + code = cells[base + j] + continue if code < 0 + cluster = code // 929 + unless cluster is r % 3 + r = if cluster is (r + 1) % 3 then r + 1 else r - 1 + num += j * (r - rayRow[i]) + den += j * j + if den then num / den else 0 + d = drift() + if Math.abs(d * columns) > 0.25 + k = d / ((fit(1) - fit(0)) * 17 * unit) + nx = rx - k * ux + ny = ry - k * uy + nl = Math.hypot nx, ny + rx = nx / nl + ry = ny / nl + rays = 0 + ray i * step for i in [0..Math.floor(len / step)] + return null unless fitRows() + # One row past each end as well: a slight tilt carries a ray's far end + # into the neighbor row, and the row the fit names at its start would + # otherwise leave those cells unsampled. + t = -step + while fit(t) > -1.5 and rays < s.maxRays + ray t + t -= step + t = (Math.floor(len / step) + 1) * step + while fit(t) < rows + 0.5 and rays < s.maxRays + ray t + t += step + fitRows() + # One more ray through the center of every row, which matters when rows + # are only a pixel or two tall and the regular spacing straddles them. + ray (r - fit(0)) / (fit(1) - fit(0)) for r in [0...rows] when rays < s.maxRays + fitRows() + # Vote every data cell into its row and column. The ray's row comes from + # its indicator; along the ray a codeword whose cluster disagrees with + # the row so far belongs to the neighbor row it names, and the row is + # carried on from there, so a ray that drifts across rows still lands + # every cell. + votes.fill 0, 0, rows * columns * 8 + for i in [0...rays] + guess = fit rayT[i] + row = rayRow[i] + row = Math.round guess if row < 0 or Math.abs(row - guess) > 1.5 + base = i * MAX_CELLS + r = row + for j in [1..columns] + code = cells[base + j] + continue if code < 0 + cluster = code // 929 + unless cluster is r % 3 + r = if cluster is (r + 1) % 3 then r + 1 else r - 1 + continue if r < 0 or r >= rows + value = code % 929 + col = if mode.mirror then columns - j else j - 1 + slot = (r * columns + col) * 8 + for k in [0...8] by 2 + if votes[slot + k + 1] is 0 + votes[slot + k] = value + 1 + votes[slot + k + 1] = 1 + break + if votes[slot + k] is value + 1 + votes[slot + k + 1]++ + break + n = rows * columns + k = 2 << level + c = s.codewords + erasures = s.erasures + grid = s.grid + e = 0 + ties = [] + for cell in [0...n] + slot = cell * 8 + best = 0 + count = 0 + other = 0 + for q in [0...8] by 2 + break unless votes[slot + q + 1] + if votes[slot + q + 1] > count + count = votes[slot + q + 1] + best = votes[slot + q] + other = 0 + else if votes[slot + q + 1] is count + other = votes[slot + q] + if count and not other + grid[cell] = best - 1 + else + grid[cell] = 0 + if other then ties.push cell, best - 1, other - 1 else erasures[e++] = cell + # Tied cells are erasures first; failing that, every way of choosing + # between their two candidates, while there are few enough to try. + tied = ties.length // 3 + combos = if tied <= 3 then 1 << tied else 0 + errors = -1 + erased = 0 + for combo in [0..combos] + c.set grid.subarray(0, n) + erased = e + for i in [0...tied] + cell = ties[3 * i] + if combo + c[cell] = ties[3 * i + 1 + ((combo - 1) >> i & 1)] + else + erasures[erased++] = cell + errors = correct c, n, k, erasures, erased + break if errors >= 0 or tied is 0 + return null if errors < 0 + e = erased + # The padding after the data must read as pad. + size = c[0] + return null if size < 1 or size > n - k + for cell in [size...n - k] + return null unless c[cell] is PAD + result = try parse(c, size) catch then null + return null unless result + # Corners: the tracked edge and its projection across the symbol width, + # which in mirror mode lies on the anchor's other side. + w = (17 * (columns + 4) + 1) * unit + result.columns = columns + result.rows = rows + result.ecc = level + result.errors = errors + result.erasures = e + result.codewords = Array.from c.subarray(0, size) + near = [{ x: top.x, y: top.y }, { x: bottom.x, y: bottom.y }] + far = [{ x: top.x + rx * w, y: top.y + ry * w }, { x: bottom.x + rx * w, y: bottom.y + ry * w }] + result.corners = if mode.mirror then [far[0], near[0], near[1], far[1]] else [near[0], far[0], far[1], near[1]] + result + +# Start patterns on a sampled line, forward then reversed, both polarities, +# then the same for stop patterns read backward; each is tried through to a +# decode. +scanLine =! (s, vertical, line, count) -> + {runs, rev} = s + n = runLengths s.luma, count, runs + # A start pattern and an indicator need sixteen runs. + return null if n < 16 + m = reverseRuns runs, n, rev + for mode in [FORWARD, MIRROR] then for reversed in [false, true] + r = if reversed then rev else runs + rn = if reversed then m else n + for inverted in [false, true] + first = if inverted then 0 else 1 + at = first + loop + at = findRuns r, rn, at, mode.anchor, mode.modules + break if at < 0 + pos = 0 + pos += r[k] for k in [0...at] + pixel = if reversed then count - 1 - pos else pos + dir = if reversed then -1 else 1 + x = if vertical then line else pixel + y = if vertical then pixel else line + dx = if vertical then 0 else dir + dy = if vertical then dir else 0 + unit = matchRuns(r, at, mode.anchor, mode.modules) / mode.modules + # The end pattern on this line bounds the symbol's width, so the + # rays need not run out to the widest symbol there is. + end = findRuns r, rn, at + mode.anchor.length, mode.end, mode.endModules + endAt = if end < 0 then Infinity else 0 + endAt += r[k] for k in [at...end] if end >= 0 + extent = endAt + extent += r[k] for k in [end...end + mode.end.length] if end >= 0 + hit = tryDecode s, x, y, dx, dy, unit, first, endAt, extent, mode + if hit + hit.vertical = vertical + hit.reversed = reversed + hit.inverted = inverted + return hit + at += 2 + null + +# Scan lines middle-out at a stride that covers the axis in about 32 lines. +scanLines =! (s, vertical) -> + {img, format, luma} = s + {width, height, data} = img + lines = if vertical then width else height + count = if vertical then height else width + row = width * format.step + advance = if vertical then row else format.step + stride = Math.max 1, lines >> 5 + middle = lines >> 1 + k = 0 + loop + offset = stride * ((k + 1) >> 1) + line = if (k & 1) is 0 then middle + offset else middle - offset + break if line < 0 and middle + offset >= lines + k++ + continue if line < 0 or line >= lines + sampleLine luma, data, (if vertical then line * format.step else line * row), advance, count, format + hit = scanLine s, vertical, line, count + return hit if hit + null + +# Reader buffers sized for the longest image side seen so far, so a camera +# loop feeding frames of one size allocates once. +state =! { side: 0 } + +buffers =! (side) -> + maxRays = 4 * side + state.side = side + state.luma = Uint8Array.new(side) + state.runs = Int32Array.new(side) + state.rev = Int32Array.new(side) + state.rluma = Uint8Array.new(2 * side) + state.rruns = Float64Array.new(2 * side + 2) + state.cells = Int16Array.new(maxRays * MAX_CELLS) + state.rayRow = Int16Array.new(maxRays) + state.rayT = Float64Array.new(maxRays) + state.maxRays = maxRays + state.hist = Int32Array.new(90) + state.votes = Int32Array.new(MAX_ROWS * MAX_COLUMNS * 8) + state.codewords = Int32Array.new(MAX_CODEWORDS) + state.grid = Int32Array.new(MAX_CODEWORDS) + state.erasures = Int32Array.new(MAX_CODEWORDS) + +# First PDF417 found on any scan line; null on a miss. +export readPDF417 =! (img, opts = {}) -> + asObject opts, 'opts' + format = validateImage img, opts.format + side = Math.max(img.width, img.height) + 2 + buffers side if side > state.side + s = state + s.img = img + s.format = format + scanLines(s, false) or scanLines(s, true) + +# Text only; throws on a miss. +export decodePDF417 =! (img, opts = {}) -> + hit = readPDF417 img, opts + fail 'PDF417 not found' unless hit + hit.text + +# Internals for the test suite. +export _tests =! { codewords, eccCodewords, generator, symbolCodewords, indicator, autoLevel, dimensions, correct, parse } diff --git a/packages/barcodes/qr.rip b/packages/barcodes/qr.rip index 15475689..33258ee3 100644 --- a/packages/barcodes/qr.rip +++ b/packages/barcodes/qr.rip @@ -23,6 +23,7 @@ # reused. Coarse layers run first; native resolution last. # ============================================================================== +import { ECI_DECODERS } from './eci.rip' import { gifDataUrl, writeGif } from './gif.rip' import { FORMATS, LITTLE_ENDIAN, asObject, asString, copyLuma, darkToImage, fail, @@ -820,20 +821,6 @@ evalLow =! (poly, length, x) -> # ==[ Payload ]== -# §7.4.3: six-digit ECI designators; an ECI stays active until another one -# replaces it. -ECI_ENCODINGS =! - 1: 'iso-8859-1', 2: 'ibm437', 3: 'iso-8859-1', 4: 'iso-8859-2', 5: 'iso-8859-3' - 6: 'iso-8859-4', 7: 'iso-8859-5', 8: 'iso-8859-6', 9: 'iso-8859-7', 10: 'iso-8859-8' - 11: 'iso-8859-9', 13: 'iso-8859-11', 15: 'iso-8859-13', 16: 'iso-8859-14' - 17: 'iso-8859-15', 18: 'iso-8859-16', 20: 'shift-jis', 21: 'windows-1250' - 22: 'windows-1251', 23: 'windows-1252', 24: 'windows-1256', 25: 'utf-16be' - 26: 'utf-8', 28: 'big5', 29: 'gbk', 30: 'euc-kr' - -ECI_DECODERS =! {} -for id, name of ECI_ENCODINGS - ECI_DECODERS[id] = try TextDecoder.new(name) catch then undefined - # Bit reader over corrected data codewords plus a scratch byte buffer with # one prefix view per length, so byte segments decode without allocating. class Payload diff --git a/packages/barcodes/test.rip b/packages/barcodes/test.rip index 215b010f..50da9a18 100644 --- a/packages/barcodes/test.rip +++ b/packages/barcodes/test.rip @@ -12,6 +12,8 @@ import * as qr from 'rip/barcodes/qr' import * as dom from 'rip/barcodes/dom' import { encodeCode128, readCode128, decodeCode128, _tests as c128 } from 'rip/barcodes/code128' import * as code128 from 'rip/barcodes/code128' +import { encodePDF417, readPDF417, decodePDF417, _tests as p417 } from 'rip/barcodes/pdf417' +import * as pdf417 from 'rip/barcodes/pdf417' import { readFileSync } from 'fs' { BYTES, alignmentPatterns, formatBits, versionBits, maskBits, popcnt, encodeData, detectType } = _tests @@ -75,13 +77,14 @@ gifPixels =! (gif) -> p "\nPackage" test "exports", -> - eq Object.keys(enc).sort(), ['QRScanner', 'decodeCode128', 'decodeQR', 'decodeQRBatch', 'default', 'encodeCode128', 'encodeQR', 'readCode128'] + eq Object.keys(enc).sort(), ['QRScanner', 'decodeCode128', 'decodePDF417', 'decodeQR', 'decodeQRBatch', 'default', 'encodeCode128', 'encodePDF417', 'encodeQR', 'readCode128', 'readPDF417'] eq enc.default, encodeQR eq enc.decodeQR, decodeQR eq enc.encodeCode128, encodeCode128 eq enc.decodeCode128, decodeCode128 eq Object.keys(qr).sort(), ['QRScanner', '_tests', 'decodeQR', 'decodeQRBatch', 'encodeQR'] eq Object.keys(code128).sort(), ['_tests', 'decodeCode128', 'encodeCode128', 'readCode128'] + eq Object.keys(pdf417).sort(), ['_tests', 'decodePDF417', 'encodePDF417', 'readPDF417'] eq Object.keys(dom).sort(), ['BarcodeDetector', 'QRCamera', 'QRCanvas', 'frameLoop', 'getSize', 'gifToPng', 'rearCamera', 'selfieCamera', 'svgToPng'] test "no runtime deps, declares browser safety and earns it", -> @@ -548,3 +551,302 @@ test "text: shifts, FNC4 high characters, latches", -> eq text([103, 65, 100, 65, 99, 1, 101, 66, 0, 106]), '\x01a01\x02' eq text([105, 0, 100, 33, 0, 106]), '00A' eq c128.decodeText([105, 102, 1, 102, 2, 0, 106], 7), { text: '01\x1d02', gs1: true } + +# ==[ PDF417 ]== + +p "\nPDF417" + +# Message codewords of Latin-1 text. +cw =! (text, encoding = 'auto') -> p417.codewords Uint8Array.from(text, (ch) -> ch.charCodeAt 0), encoding, [] + +test "codewords: modes, submodes, switching", -> + eq cw('ABCD', 'text'), [1, 63] + eq cw('1234', 'numeric'), [902, 12, 434] + eq cw('abcd', 'byte'), [901, 97, 98, 99, 100] + eq cw('abcdef', 'byte'), [924, 163, 179, 507, 603, 522] + # Specification worked examples: nine bytes under 901 keep the tail one + # per codeword; text drops to numeric at thirteen digits. + eq cw('\x01\x02\x03\x04\x05\x06\x07\x08\x04', 'byte'), [901, 1, 620, 89, 74, 846, 7, 8, 4] + eq (cw('1'.repeat(n)).length for n in [1..22]), [1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9] + eq (cw('Ä'.repeat(n)).length for n in [1..7]), [2, 3, 4, 5, 6, 6, 7] + eq cw('A'), [0 * 30 + 29] # alpha, punctuation-shift pad + eq cw('a'), [27 * 30 + 0] # lower latch + eq cw('aB'), [27 * 30 + 0, 27 * 30 + 1] # alpha shift from lower + eq cw('A;'), [0 * 30 + 29, 0 * 30 + 29] # punctuation shift, pad + eq cw('1,2'), [28 * 30 + 1, 13 * 30 + 2] # mixed latch; comma is mixed + eq cw('1;;'), [28 * 30 + 1, 25 * 30 + 0, 0 * 30 + 29] # punctuation latch when two follow + eq cw('Hello\x01World'), [7 * 30 + 27, 4 * 30 + 11, 11 * 30 + 14, 913, 1, 27 * 30 + 22, 14 * 30 + 17, 11 * 30 + 3] + eq cw('ab\x01\x02\x03cd'), [901, 163, 177, 269, 787, 135, 100] # short text runs stay binary + throws (-> cw 'é', 'text'), Error, 'cannot encode' + # An odd tail padded in the punctuation submode latches to alpha, and + # the text after a byte shift is encoded from there. + eq cw('AB1;;\x01;;;;;'), [1, 841, 750, 29, 913, 1, 865, 0, 0, 29] + eq cw('AB1;;\x01ABCDE'), [1, 841, 750, 29, 913, 1, 1, 63, 149] + throws (-> cw 'x', 'numeric'), Error, 'digits only' + throws (-> cw ''), Error, 'empty' + +test "Reed-Solomon over GF(929)", -> + eq Array.from(p417.generator 0), [27, 917] + eq Array.from(p417.generator 1), [522, 568, 723, 809] + eq p417.generator(8).length, 512 + # Any corrected codeword polynomial has zero syndromes: encode, corrupt + # within capacity, correct back; one parity codeword always stays free. + codes = cw 'Errors and erasures over the prime field' + data = p417.symbolCodewords codes, 4, 10, 3 # 40 cells, 16 parity + n = data.length + k = 16 + scramble = (errors, erased) -> + c = Int32Array.from data + c[i] = (c[i] + 100 + i) % 929 for i in [1..errors] + e = Int32Array.from [20...20 + erased] + c[i] = 0 for i in e + [c, e] + [c, e] = scramble 3, 9 # 2*3 + 9 = 15 <= 15 + eq p417.correct(c, n, k, e, e.length), 3 + eq Array.from(c), Array.from(data) + [c, e] = scramble 0, 15 + eq p417.correct(c, n, k, e, e.length), 0 + eq Array.from(c), Array.from(data) + [c, e] = scramble 4, 8 # 2*4 + 8 = 16: nothing left to detect with + eq p417.correct(c, n, k, e, e.length), -1 + [c, e] = scramble 0, 17 + eq p417.correct(c, n, k, e, e.length), -1 + c = Int32Array.from data + eq p417.correct(c, n, k, e, 0), 0 + +test "layout: level, dimensions, indicators", -> + eq (p417.autoLevel m for m in [1, 40, 41, 160, 161, 320, 321, 863, 864, 925]), [2, 2, 3, 3, 4, 4, 5, 5, 4, 0] + eq p417.dimensions(20, undefined, undefined, 3, 3), { columns: 2, rows: 10 } + eq p417.dimensions(20, 5, undefined, 3, 3), { columns: 5, rows: 4 } + eq p417.dimensions(20, undefined, 4, 3, 3), { columns: 5, rows: 4 } + eq p417.dimensions(20, 5, 4, 3, 3), { columns: 5, rows: 4 } + eq p417.dimensions(20, undefined, undefined, 3, 1), { columns: 1, rows: 20 } + eq p417.dimensions(20, undefined, undefined, 3, 8), { columns: 4, rows: 5 } + eq encodePDF417('Hello PDF417', 'raw', aspect: 1).length > encodePDF417('Hello PDF417', 'raw', aspect: 8).length, true + throws (-> p417.dimensions 21, 5, 4, 3, 3), RangeError, 'holds 20' + throws (-> p417.dimensions 2000, 1, undefined, 3, 3), RangeError, 'max 90' + throws (-> p417.dimensions 2000, undefined, 3, 3, 3), RangeError, 'max 30' + # Never more than 928 cells, whatever the shape. + throws (-> p417.dimensions 2, 30, 40, 3, 3), RangeError, 'max 928' + throws (-> p417.dimensions 928, 30, undefined, 3, 3), RangeError, 'max 928' + throws (-> p417.dimensions 928, undefined, 31, 3, 3), RangeError, 'max 928' + for need in [921..928] + dims = p417.dimensions need, undefined, undefined, 3, 3 + cells = dims.columns * dims.rows + ok cells >= need and cells <= 928, "#{need} fits #{dims.columns}x#{dims.rows}" + # Row 4 of a 13-row, 2-column, level 2 symbol: cluster 1 carries the + # level and row remainder on the left, the row count on the right. + eq p417.indicator(1, 4, 13, 2, 2, false), 30 + 3 * 2 + 0 + eq p417.indicator(1, 4, 13, 2, 2, true), 30 + 4 + eq p417.indicator(0, 4, 13, 2, 2, false), 30 + 4 + eq p417.indicator(2, 4, 13, 2, 2, false), 30 + 1 + data = p417.symbolCodewords [1, 63], 2, 3, 0 + eq Array.from(data), [4, 1, 63, 900, ...Array.from(p417.eccCodewords(Int32Array.from([4, 1, 63, 900]), 0))] + +HELLO =! """ + ########.#.#.#...#####.#.#..#####.##.#.#.....##....###.#.#.###......#######.#...#.#..# + ########.#.#.#...####.#.#....#....##..#.###..#.....####.#.#...#.....#######.#...#.#..# + ########.#.#.#...##.#.#.#####.....###..#.#.######..#.#.#....####....#######.#...#.#..# + ########.#.#.#...###.#..#.###.....#..#.....##.##...##.#.####.#####..#######.#...#.#..# + ########.#.#.#...##.#.###.....#...###.....#.##...#.####.#.###...###.#######.#...#.#..# + ########.#.#.#...#####.#.###....#.#...##.#....###..####.#.#####.##..#######.#...#.#..# + ########.#.#.#...#.#..###...###...##.#....##..###..#.#..###.###.....#######.#...#.#..# + ########.#.#.#...######.#..#.###..##.#....###...#..#.#.######...###.#######.#...#.#..# + ########.#.#.#...#.#..##.#####....###.#....#####.#.#####.#..###.#...#######.#...#.#..# + ########.#.#.#...##.#...###.####..###.##..#...##...#.#...##...##....#######.#...#.#..# + ########.#.#.#...##.#..###....#...##.##..####...#..####.#..###..###.#######.#...#.#..# + ########.#.#.#...##.#...#..#####..#..##..##..####..######.#...##..#.#######.#...#.#..# + ########.#.#.#...##.#.....#.##....##.###...##.#....#.#....##....##..#######.#...#.#..# + ########.#.#.#...####.#...#....#..#.#...######.###.#####.#...#..##..#######.#...#.#..# + ########.#.#.#...####.#....####.#.#.#####..##..#...#.#......#..####.#######.#...#.#..# + ########.#.#.#...##..#.##.###.....#.....#.###.###..###..#.#...###...#######.#...#.#..# + ########.#.#.#...##.#...#####.###.##.##.####...#...#.#...####....#..#######.#...#.#..# + ########.#.#.#...######..#.###.##.#.#####..##..#...####..#.####...#.#######.#...#.#..# + """ + +# Every case here decodes with zxing-cpp. +PINS =! [ + ['Hello PDF417', {}, '16759322038964277220', 86, 18] + ['The quick brown fox jumps over the lazy dog, 42 times; really? (yes) {ok} [fine] "quoted" #1 @home', {}, '7846982029010869402', 103, 37] + ['12345678901234567890123456789012345678901234567890', {}, '5965295783082383056', 86, 28] + ['ORDER-2026-09-16 qty 00001234567890123 ref abc', {}, '12089213095314550506', 86, 34] + ['\x00\x01\x02\xff\xfe\xfd binary \x80\x81 tail', {}, '5388649561758448112', 86, 28] + ['abcdef', { encoding: 'byte' }, '16385500419708780100', 86, 15] + ['abcdefg', { encoding: 'byte' }, '2847640181012233553', 86, 16] + ['Hello\x01World', {}, '10501875650112426562', 86, 17] + ['Grüße, 世界! ünïcödé', {}, '5450240929259030254', 86, 36] + ['level zero', { ecc: 0 }, '229268292100416905', 86, 9] + ['level eight ' + 'x'.repeat(50), { ecc: 8 }, '16611835918714643843', 205, 69] + ['one column', { columns: 1 }, '12025549302922334139', 86, 15] + ['thirty columns ' + 'y'.repeat(200), { columns: 30 }, '10289456403934732096', 579, 5] + ['ninety rows ' + 'z'.repeat(300), { rows: 90 }, '6998225270771950661', 103, 90] + ['compact symbol here', { compact: true }, '17172449583246438028', 52, 19] + ['B'.repeat(800) + '1'.repeat(400) + 'c'.repeat(300), {}, '3702506550965028681', 239, 76] + ['0000000000000000000000000000000000000000000000000000000000001', { encoding: 'numeric' }, '13842950279047227124', 86, 31] + [';;;;;;;;;', {}, '4889440228876649914', 86, 15] + ["line one\nline two\r\n\ttabbed", {}, '3017900233706190855', 86, 25] +] + +test "symbols: pinned module rows", -> + eq rows(encodePDF417('Hello PDF417', 'raw', rowHeight: 1, border: 0)), HELLO.replaceAll('#', '1').replaceAll('.', '0') + for [text, opts, hash, W, H] in PINS + raw = encodePDF417 text, 'raw', { ...opts, rowHeight: 1, border: 0 } + eq [Bun.hash(rows raw).toString(), raw[0].length, raw.length], [hash, W, H], text.slice(0, 20) + +test "outputs agree and options apply", -> + # The row height shapes the symbol: at three modules a row the same + # message fits nine rows of four columns. + raw = encodePDF417 'Hello PDF417', 'raw' + eq [raw.length, raw[0].length], [9 * 3 + 4, 17 * 6 + 1 + 4] + eq raw[2 + 3][2 + 8], false + eq raw[2 + 3][2], true + eq encodePDF417('Hello PDF417', 'raw', scale: 2, border: 1, rowHeight: 2, columns: 2).length, 2 * (9 * 2 + 2) + ascii = encodePDF417 'Hello PDF417', 'ascii', rowHeight: 1, border: 0 + eq ascii.split('\n').length, 10 + term = encodePDF417 'Hello PDF417', 'term', rowHeight: 1, border: 0 + eq term, ansi(HELLO) + svg = encodePDF417 'Hello PDF417', 'svg', rowHeight: 1, border: 0 + ok svg.startsWith(' + text = 'Rip reads PDF417 symbols in a few hundred microseconds.' + for scale in [1, 2, 3] + eq decodePDF417(stack(text, { scale }, 400, 200)), text, "scale #{scale}" + for rowHeight in [1, 2, 3, 5] + eq decodePDF417(stack(text, { rowHeight, scale: 2 }, 400, 300)), text, "row height #{rowHeight}" + for rotate in [0, 90, 180, 270] + hit = readPDF417 stack(text, { scale: 2 }, 400, 400, false, rotate) + eq hit.text, text, "rotate #{rotate}" + eq hit.vertical, rotate is 90 or rotate is 270 + eq hit.reversed, rotate is 180 or rotate is 270 + eq hit.inverted, false + eq [hit.rows, hit.columns, hit.ecc, hit.errors, hit.erasures], [14, 3, 2, 0, 0] + hit = readPDF417 stack(text, { scale: 2 }, 400, 200, true) + eq [hit.text, hit.inverted], [text, true] + eq decodePDF417(stack(text, { scale: 2, compact: true }, 400, 200)), text + eq decodePDF417(stack(text, { scale: 2, columns: 1 }, 200, 700)), text + eq decodePDF417(stack(text, { scale: 1, columns: 30, rows: 3, ecc: 0 }, 700, 100)), text + eq decodePDF417(stack(text, { scale: 1, rows: 90 }, 300, 400)), text + eq decodePDF417(stack(text, { scale: 2, ecc: 8 }, 800, 400)), text + eq decodePDF417(stack('Grüße, 世界!', { scale: 2 }, 300, 200)), 'Grüße, 世界!' + eq decodePDF417(toLuma(stack(text, { scale: 2 }, 400, 200)), format: 'I420'), text + hit = readPDF417 stack('\x00\x01\xff\xfe', { scale: 2 }, 300, 200) + eq Array.from(hit.bytes), [0, 1, 255, 254] + eq hit.text, '\x00\x01\xff\xfe' + eq hit.codewords, [6, 901, 0, 1, 255, 254] + +test "geometry: skew, corners, cropped and damaged start patterns", -> + text = 'Symbols read along the true row direction' + for skew in [-6, -3, 3, 6] + eq decodePDF417(stack(text, { scale: 2 }, 500, 300, false, 0, skew)), text, "skew #{skew}" + raw = encodePDF417 text, 'raw', scale: 2, border: 4 + hit = readPDF417 paint(raw, 400, 300) + [tl, tr, br, bl] = hit.corners + ok Math.abs(tl.x - ((400 - raw[0].length) >> 1) - 8) < 1.5, "left edge at #{tl.x}" + ok Math.abs(tl.y - ((300 - raw.length) >> 1) - 8) < 2, "top at #{tl.y}" + ok Math.abs(bl.y - tl.y - (raw.length - 16)) < 3, "height #{bl.y - tl.y}" + ok Math.abs(tr.x - tl.x - (raw[0].length - 16)) < 3, "width #{tr.x - tl.x}" + # Start pattern cut off the left: the stop pattern anchors the read. + cropped = (row.slice(60) for row in raw) + hit = readPDF417 paint(cropped, 400, 300) + eq hit.text, text + right = ((400 - cropped[0].length) >> 1) + cropped[0].length - 8 + ok Math.abs(hit.corners[1].x - right) < 2, "stop edge at #{hit.corners[1].x}" + ok Math.abs(hit.corners[1].x - hit.corners[0].x - (raw[0].length - 16)) < 3, "width #{hit.corners[1].x - hit.corners[0].x}" + # Start pattern erased on the top and bottom rows: the rows still read. + torn = (row.slice() for row in raw) + for y in [0...torn.length] when y < 8 + 12 or y >= torn.length - 8 - 12 + torn[y][x] = false for x in [8...8 + 40] + hit = readPDF417 paint(torn, 400, 300) + eq hit.text, text + # Holes become erasures, not errors. + holed = (row.slice() for row in raw) + for y in [8 + 12...8 + 18] + holed[y][x] = false for x in [60...110] + hit = readPDF417 paint(holed, 400, 300) + eq hit.text, text + ok hit.erasures > 0 and hit.errors <= 1, "erasures #{hit.erasures} errors #{hit.errors}" + # Whole rows torn off the top and bottom are erasures across the width. + cut = raw.slice 8 + 12, raw.length - 8 - 6 + hit = readPDF417 paint(cut, 400, 300) + eq hit.text, text + eq [hit.columns, hit.erasures, hit.errors], [2, 6, 0] + # A mirror image reads with the stack walked from its other end. + mirror = ([...row].reverse() for row in raw) + eq readPDF417(paint(mirror, 400, 300)).text, text + # Rows a pixel tall on a clean raster. + eq decodePDF417(stack(text, { rowHeight: 1, scale: 1 }, 400, 200)), text + blank = flat 60, 60, 200 + eq readPDF417(blank), null + throws (-> decodePDF417 blank), Error, 'not found' + throws (-> readPDF417 blank, null), TypeError, 'opts' + throws (-> readPDF417 { width: 2, height: 2, data: Uint8Array.new(3) }), RangeError, 'img.data' + +test "decompaction: text, bytes, numbers, ECI, macro", -> + parse = (codes) -> p417.parse Int32Array.from([codes.length + 1, ...codes]), codes.length + 1 + eq parse([1, 63]).text, 'ABCD' + eq parse([902, 12, 434]).text, '1234' + eq parse([901, 97, 98, 99, 100]).text, 'abcd' + eq parse([924, 163, 179, 507, 603, 522]).text, 'abcdef' + eq parse([901, 1, 620, 89, 74, 846, 7, 8, 4]).text, '\x01\x02\x03\x04\x05\x06\x07\x08\x04' + eq parse([7 * 30 + 27, 4 * 30 + 11, 11 * 30 + 14, 913, 1, 27 * 30 + 22, 14 * 30 + 17, 11 * 30 + 3]).text, 'Hello\x01World' + eq parse([28 * 30 + 1, 25 * 30 + 0, 0 * 30 + 29]).text, '1;;' + eq parse([927, 26, 901, 0xc3, 0xa9]).text, 'é' + eq parse([927, 4, 901, 0xb1]).text, 'ą' + eq Array.from(parse([927, 26, 901, 0xc3, 0xa9]).bytes), [0xc3, 0xa9] + eq parse([921, 1, 63]).readerInit, true + # Macro control block after the data: segment index, file id, fields. + r = parse [1, 63, 928, 111, 103, 17, 53, 923, 1, 14, 923, 0, 12 * 30 + 4, 21 * 30 + 4, 922] + eq r.text, 'ABCD' + eq r.macro, { segmentIndex: 3, fileId: '017053', last: true, segmentCount: 4, fileName: 'MEVE' } + throws (-> parse [913]), Error, 'byte shift at end' + throws (-> parse [927]), Error, 'ECI at end' + throws (-> parse [922]), Error, 'not a mode' + throws (-> parse [928, 111]), Error, 'truncated' + throws (-> parse [928, 111, 103]), Error, 'no file id' + throws (-> parse [902, 899]), Error, 'start with 1' diff --git a/packages/sites/demos/scan/README.md b/packages/sites/demos/scan/README.md index 4e7ef643..d2a68e24 100644 --- a/packages/sites/demos/scan/README.md +++ b/packages/sites/demos/scan/README.md @@ -1,6 +1,6 @@ # Scan App -A camera scanner for QR codes and Code 128 labels, and the browser example +A camera scanner for QR codes, PDF417 symbols and Code 128 labels, and the browser example for [`rip/barcodes`](../../../barcodes/README.md). One page, no API beyond the Hub admit in `index.rip`, served over HTTPS on the LAN by Janus so a phone can use its camera. @@ -8,9 +8,9 @@ phone can use its camera. `app/routes/index.rip` opens the rear camera with `rearCamera`, runs `frameLoop`, and hands every frame to a `QRCanvas`, which decodes QR through the canvas path and paints the finder overlay. The same frame is -drawn into a working canvas whose pixels go to `readCode128`. A hit is -announced once until a different code appears; the last twenty stay in a -list. +drawn into a working canvas whose pixels go to `readCode128` and then to +`readPDF417`. A hit is announced once until a different code appears; the +last twenty stay in a list. ## Add and start @@ -41,7 +41,8 @@ Camera permission is per origin, so the phone asks once. ## Reading tips -Fill a good part of the frame with the code. The Code 128 reader wants -modules at least one pixel wide in the camera's native resolution, so a -label across a quarter of the view is plenty; QR decodes from further away. +Fill a good part of the frame with the code. The Code 128 and PDF417 +readers want modules at least one pixel wide in the camera's native +resolution, so a label across a quarter of the view is plenty; QR decodes +from further away. Live watch is on, so editing `app/routes/index.rip` remounts the page. diff --git a/packages/sites/demos/scan/app/routes/index.rip b/packages/sites/demos/scan/app/routes/index.rip index 6204d5a7..b427b8a4 100644 --- a/packages/sites/demos/scan/app/routes/index.rip +++ b/packages/sites/demos/scan/app/routes/index.rip @@ -1,9 +1,10 @@ -# One camera, two readers. QRCanvas and QRCamera decode QR through the +# One camera, three readers. QRCanvas and QRCamera decode QR through the # canvas path and paint the finder overlay; the same frame is drawn once -# more into a working canvas whose pixels go to readCode128. Every hit is -# announced once until a different code appears. +# more into a working canvas whose pixels go to readCode128 and then +# readPDF417. Every hit is announced once until a different code appears. import { QRCanvas, rearCamera, frameLoop } from 'rip/barcodes/dom' import { readCode128 } from 'rip/barcodes/code128' +import { readPDF417 } from 'rip/barcodes/pdf417' export Home = component player := null @@ -22,7 +23,7 @@ export Home = component return canvas = QRCanvas.new { overlay }, cropToSquare: false work = document.createElement('canvas').getContext '2d', willReadFrequently: true - status = 'Point the camera at a QR code or a Code 128 label' + status = 'Point the camera at a QR code, a PDF417 symbol or a Code 128 label' cancel = frameLoop (-> scan camera, canvas, work), player scan: (camera, canvas, work) -> @@ -33,8 +34,11 @@ export Home = component work.canvas.width = videoWidth if work.canvas.width isnt videoWidth work.canvas.height = videoHeight if work.canvas.height isnt videoHeight work.drawImage player, 0, 0 - hit = readCode128 work.getImageData(0, 0, videoWidth, videoHeight) - found 'Code 128', hit.text if hit + pixels = work.getImageData 0, 0, videoWidth, videoHeight + hit = readCode128 pixels + return found('Code 128', hit.text) if hit + hit = readPDF417 pixels + found 'PDF417', hit.text if hit found: (kind, text) -> return if latest? and latest.kind is kind and latest.text is text