diff --git a/packages/barcodes/README.md b/packages/barcodes/README.md index d33ad79a..bbadbffe 100644 --- a/packages/barcodes/README.md +++ b/packages/barcodes/README.md @@ -101,15 +101,14 @@ encodeQR text, output, opts | `textEncoder` | custom text-to-bytes for `byte` mode | UTF-8 | Single-segment encoding is always used, and penalty scoring runs on the -reserved test form, so output matches python-qrcode module for module. +reserved test form, so output matches the reference implementation module +for module. ## Decoding `decodeQR` takes `{ width, height, data }` and returns the decoded string. It throws when no symbol decodes; in a camera loop that is a frame miss, feed -the next frame. Clean one-pixel-per-module rasters are too small for -run-length finder detection, so upscale the encoder's `raw` output at least -twice before decoding it. +the next frame. A clean raster decodes at one pixel per module. | option | meaning | default | | --- | --- | --- | @@ -211,15 +210,15 @@ 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. +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 @@ -344,12 +343,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 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. +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 diff --git a/packages/barcodes/code128.rip b/packages/barcodes/code128.rip index b5d43bc6..37101884 100644 --- a/packages/barcodes/code128.rip +++ b/packages/barcodes/code128.rip @@ -298,8 +298,9 @@ readFrom =! (runs, n, at, start, codes) -> # The termination bar is two modules; accept one to four. bar = runs[at + 6] * 11 return 0 if bar < width or bar > 4 * width - # A quiet zone of at least half a codeword, or the edge of the line. - return 0 unless at + 7 >= n or 2 * runs[at + 7] >= width + # A quiet zone of at least half a codeword, or the edge of the line, + # which a reversed line that opened dark carries as an empty run. + return 0 unless at + 7 >= n or runs[at + 7] is 0 or 2 * runs[at + 7] >= width break at += 6 # Start, at least one data codeword, check, stop. @@ -404,10 +405,10 @@ scanLines =! (img, format, vertical, luma, runs, rev, codes) -> export readCode128 =! (img, opts = {}) -> asObject opts, 'opts' format = validateImage img, opts.format - side = Math.max(img.width, img.height) + 2 - luma = Uint8Array.new(side) - runs = Int32Array.new(side) - rev = Int32Array.new(side) + side = Math.max(img.width, img.height) + 2 + luma = Uint8Array.new(side) + runs = Int32Array.new(side) + rev = Int32Array.new(side) codes = [] vertical = false hit = scanLines img, format, false, luma, runs, rev, codes @@ -425,4 +426,4 @@ export decodeCode128 =! (img, opts = {}) -> hit.text # Internals for the test suite. -export _tests =! { codewords, decodeText, checksum } +export _tests =! { codewords, decodeText } diff --git a/packages/barcodes/eci.rip b/packages/barcodes/eci.rip index b4b48986..b86944e6 100644 --- a/packages/barcodes/eci.rip +++ b/packages/barcodes/eci.rip @@ -8,7 +8,7 @@ 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' + 11: 'iso-8859-9', 12: 'iso-8859-10', 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' diff --git a/packages/barcodes/pdf417.rip b/packages/barcodes/pdf417.rip index d1e977b4..184e4a68 100644 --- a/packages/barcodes/pdf417.rip +++ b/packages/barcodes/pdf417.rip @@ -12,8 +12,10 @@ # # 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 +# height and scale. The reader finds a start pattern on a scan line, or a +# stop pattern read backward when the start is out of the picture, tracks +# its edge along the stack and the edge at the row's other end with it, +# reads every row along rays between the two, votes each codeword into # its row and column by the row indicators and cluster, then corrects # erasures and errors together before parsing the stream. # ============================================================================== @@ -212,9 +214,9 @@ PATTERNS =! do -> 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_COLUMNS =! 30 +MIN_ROWS =! 3 +MAX_ROWS =! 90 MAX_CODEWORDS =! 928 # per symbol, indicators excluded MODULUS =! 929 @@ -226,7 +228,7 @@ LATCH_NUMERIC =! 902 SHIFT_BYTE =! 913 LATCH_BYTE6 =! 924 # byte compaction, a multiple of six bytes ECI_CHARSET =! 927 -ECI_UTF8 =! 26 +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. @@ -234,13 +236,13 @@ 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 +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-.$/"|*()?{}\'' @@ -275,7 +277,7 @@ do -> # logs indexes it without a modulo. GF929 =! do -> exp = Int32Array.new(2 * 928) - log = Int32Array.new(929) + log = Int32Array.new( 929) x = 1 for i in [0...928] exp[i] = exp[i + 928] = x @@ -601,7 +603,7 @@ modules =! (data, columns, rows, level, compact) -> row = Uint8Array.new(width) c = r % 3 x = 0 - put = (bits, n) -> + put! = (bits, n) -> for k in [n - 1..0] by -1 row[x++] = (bits >>> k) & 1 put START_PATTERN, 17 @@ -727,14 +729,14 @@ 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 + 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 @@ -1004,7 +1006,7 @@ edgeRuns =! (luma, count, runs) -> # 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)) + pos = i - 1 + (level - a) / (b - a) runs[n++] = pos - last last = pos k++ @@ -1055,12 +1057,12 @@ anchorNear =! (runs, n, first, expect, slack, mode) -> 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. +# pixels in with modules of `unit` pixels, read from run `first`, which +# also floors the resync: 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] @@ -1258,7 +1260,7 @@ class Stream v = @c[@i] if v < 900 @symbol v // 30 - @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] @@ -1309,7 +1311,7 @@ class Stream @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) -> + decimal!: (start, end) -> limbs = [0] # base 10^7, least significant first for j in [start...end] carry = @c[j] @@ -1433,8 +1435,8 @@ parse =! (c, count) -> 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 + 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 @@ -1459,8 +1461,8 @@ trackEdge =! (s, x, y, dx, dy, sx, sy, unit, first, sign, edge, mode) -> 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 + 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: @@ -1492,7 +1494,6 @@ metadata =! (s, rays, mode) -> 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. @@ -1513,6 +1514,7 @@ edgeEnds =! (s, x, y, dx, dy, sx, sy, unit, first, mode) -> lines: edge.n } +# One located anchor pattern through to a decoded symbol, or null. 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 @@ -1534,17 +1536,19 @@ tryDecode =! (s, x, y, dx, dy, unit, first, endAt, extent, mode) -> # 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 + rx = uy * flip + ry = -ux * flip + tail = null 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 + far = edgeEnds s, x + endAt * dx, y + endAt * dy, dx, dy, sx, sy, unit, first ^ (mode.anchor.length & 1), mode.tail + if far and far.lines * 2 >= lead.lines + vx = (far.top.x - top.x + far.bottom.x - bottom.x) / 2 + vy = (far.top.y - top.y + far.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 + rx = vx / vl + ry = vy / vl + tail = far 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 @@ -1570,7 +1574,7 @@ tryDecode =! (s, x, y, dx, dy, unit, first, endAt, extent, mode) -> 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 + # Row of each ray from its first 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. @@ -1744,17 +1748,23 @@ tryDecode =! (s, x, y, dx, dy, unit, first, endAt, extent, mode) -> 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.columns = columns + result.rows = rows + result.ecc = level + result.errors = errors + result.erasures = e result.codewords = Array.from c.subarray(0, size) + # Corners: the tracked edge and, across the symbol, the tail edge plus + # its pattern's width, or the nominal width when the tail was not + # tracked; in mirror mode the far side is the start of the row. + if tail + w = mode.tail.modules * unit + ends = [tail.top, tail.bottom] + else + w = (17 * (columns + 4) + 1) * unit + ends = [top, bottom] 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 }] + far = ({ x: e.x + rx * w, y: e.y + ry * w } for e in ends) result.corners = if mode.mirror then [far[0], near[0], near[1], far[1]] else [near[0], far[0], far[1], near[1]] result diff --git a/packages/barcodes/qr.rip b/packages/barcodes/qr.rip index 33258ee3..c2e35f6e 100644 --- a/packages/barcodes/qr.rip +++ b/packages/barcodes/qr.rip @@ -544,7 +544,7 @@ buildSymCache =! (ver) -> # Template copy, data-bit scatter along the cached zigzag order, then mask # selection over XOR candidates; the first lowest score wins. -drawSymbol =! (ver, ecc, data, maskIdx, test = false) -> +drawSymbol =! (ver, ecc, data, maskIdx) -> symCache = buildSymCache ver unless symCache?.ver is ver {tpl, pos, planes, planesT, work} = symCache [m, t, cand, candT] = work @@ -570,7 +570,7 @@ drawSymbol =! (ver, ecc, data, maskIdx, test = false) -> mask = pl chosen = planes[mask] m.v[i] ^= chosen[i] for i in [0...m.v.length] - drawInfo m, ver, ecc, mask unless test + drawInfo m, ver, ecc, mask m # ==[ Validation ]== @@ -2003,7 +2003,8 @@ export class QRScanner true # Reject wrong triples and dimensions from the timing tracks before full - # sampling: 75% agreement across the horizontal and vertical tracks. + # sampling: 75% agreement over the horizontal and vertical tracks + # together, or one track read perfectly. timing: (s, map, size) -> gh = 0 gv = 0 @@ -2129,7 +2130,6 @@ export class QRScanner correctBlock: (offset, length, eccWords) -> blockBytes = @blockBytes syndromes = @syndromes - hasError = false # The generator divides an intact block: a zero LFSR remainder, computed # exactly as the encoder does, settles the common case without syndromes. {products} = rsCached eccWords @@ -2147,8 +2147,6 @@ export class QRScanner value = 0 value = mul(value, EXP[i]) ^ blockBytes[offset + j] for j in [0...length] syndromes[i] = value - hasError = true if value - return true unless hasError sigma = @sigma previous = @previous next = @next @@ -2540,7 +2538,11 @@ export class QRScanner failed = @projectTiles sp, sm, size, version, fine, brPoint, t, ms, ctx return failed unless failed instanceof Error # A false local alignment can corrupt a valid global projection. - failed = @projectMap sp, sm, size + # The tiles built their homographies in the same matrices, so the + # global map and its fine-plane form are derived again. + @mapFinderQuad map, size, t, brX, brY + fine = @upgrade plane, map, ctx + failed = @projectMap (if fine then @finePlane else plane), (if fine then @to else map), size return failed unless failed instanceof Error if fine failed = @projectMap plane, map, size @@ -2771,7 +2773,8 @@ export class QRScanner continue @retries-- if not mandatory and @retries isnt Infinity attempted = true - # Large blurred symbols get template-fit refinement first; other + # Large blurred symbols turned past forty-five degrees get + # template-fit refinement first, unless the layer is blocked; other # projection failures retry once after a cross refinement pass. steep = Math.abs(triple.tr.y - triple.tl.y) > Math.abs(triple.tr.x - triple.tl.x) pitch = if not @blocked and steep then edgePitch(layer, triple.tl, triple.tr, triple.inverted) else 0 diff --git a/packages/barcodes/test.rip b/packages/barcodes/test.rip index 50da9a18..4835bfc5 100644 --- a/packages/barcodes/test.rip +++ b/packages/barcodes/test.rip @@ -516,6 +516,10 @@ bars =! (text, opts, scale, W, H, invert = false, rotate = 0) -> test "round trips: scales, rotations, inversion, GS1", -> for scale in [1, 2, 3, 5] eq decodeCode128(bars('Hello, World! 2026', {}, scale, 1400, 200)), 'Hello, World! 2026', "scale #{scale}" + # No quiet zone at all: the symbol fills the line in both directions. + edge = encodeCode128('Wikipedia', 'raw', border: 0, scale: 2).length + for rotate in [0, 180] + eq decodeCode128(bars('Wikipedia', { border: 0 }, 2, edge, 100, false, rotate)), 'Wikipedia', "flush #{rotate}" for rotate in [0, 90, 180, 270] hit = readCode128 bars('Wikipedia', {}, 2, 500, 500, false, rotate) eq hit.text, 'Wikipedia', "rotate #{rotate}" @@ -695,7 +699,7 @@ test "symbols: pinned module rows", -> 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. + # message fits nine rows of two 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