diff options
Diffstat (limited to 'source/codes/Pdf417.mc')
| -rw-r--r-- | source/codes/Pdf417.mc | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/source/codes/Pdf417.mc b/source/codes/Pdf417.mc new file mode 100644 index 0000000..b17b5df --- /dev/null +++ b/source/codes/Pdf417.mc @@ -0,0 +1,428 @@ +// Author: cirakg & claude ai +// Summary: PDF417 encoder (ISO/IEC 15438): text, byte and numeric compaction as pdf417gen does it, columns fitted to the screen. +// +// Steps: +// 1. Compaction, mirroring pdf417gen (the test reference): the bytes are +// split into runs of digits, text-compactable characters and other +// bytes; digit runs under 13 next to text become text; equal neighbours +// merge. Text uses the four submodes (switch codes, pairs of values, +// 29 as padding), numeric packs 44 digits per base-900 group, byte +// packs 6 bytes into 5 codewords. +// 2. Error-correction level: the option's fixed level, or automatic +// (ISO 15438 annex E: 2 up to 40 data codewords, then 3, 4, 5). +// 3. Columns and module size: the largest module size, then the fewest +// columns, for which the symbol (with a 2-module quiet zone and rows 3 +// modules high) fits the screen (Codes.fits). +// 4. Padding (900), length descriptor, Reed-Solomon mod 929 in steps. +// 5. Rows: start, left row indicator, data, right row indicator, stop, +// each codeword drawn from its cluster's 17-module pattern. +// The 2,787 patterns, EC factors and text table are JSON resources +// generated from pdf417gen (tools/gen_pdf417_tables.py), loaded only while +// encoding. + +import Toybox.Lang; +import Toybox.Math; +import Toybox.WatchUi; + +module Pdf417 { + + const START = 0x1fea8; + const STOP = 0x3fa29; + const PAD = 900; + const TEXT_LATCH = 900; + const BYTE_LATCH = 901; + const BYTE_LATCH_6 = 924; + const NUMERIC_LATCH = 902; + const ROW_HEIGHT = 3; + const QUIET = 2; + const MAX_CODEWORDS = 928; + + const KIND_TEXT = 0; + const KIND_BYTE = 1; + const KIND_NUMERIC = 2; + + // Text submodes and the switch codes between them [from][to]. + const UPPER = 0; + const LOWER = 1; + const MIXED = 2; + const PUNCT = 3; + const SWITCHES = [ + [[], [27], [28], [28, 25]], + [[28, 28], [], [28], [28, 25]], + [[28], [27], [], [25]], + [[29], [29, 27], [29, 28], []] + ]; + const PREFERENCE = [1, 0, 2, 3]; // lower, upper, mixed, punctuation + const EC_STEP_WORDS = 20; + + // One of the generated table resources ("v" holds the values). + function table(id as ResourceId) as Array { + return (WatchUi.loadResource(id) as Dictionary)["v"] as Array; + } + + // Data codewords (without length descriptor and padding). + function dataCodewords(data as ByteArray, text as Array<Array<Number>>) as Array<Number> { + // Runs of one kind: [kind, start, end) triples. + var runs = [] as Array<Number>; + for (var i = 0; i < data.size(); i++) { + var kind = kindOf(data[i], text); + var n = runs.size(); + if (n > 0 && runs[n - 3] == kind) { + runs[n - 1] = i + 1; + } else { + runs.add(kind); + runs.add(i); + runs.add(i + 1); + } + } + // Short digit runs next to text become text. + for (var r = 0; r < runs.size(); r += 3) { + if (runs[r] == KIND_NUMERIC && runs[r + 2] - runs[r + 1] < 13 + && ((r > 0 && runs[r - 3] == KIND_TEXT) || (r + 3 < runs.size() && runs[r + 3] == KIND_TEXT))) { + runs[r] = -KIND_TEXT - 10; // marked; resolved below so later checks see the original kinds + } + } + for (var r = 0; r < runs.size(); r += 3) { + if (runs[r] == -KIND_TEXT - 10) { + runs[r] = KIND_TEXT; + } + } + // Merge equal neighbours. + var merged = [] as Array<Number>; + for (var r = 0; r < runs.size(); r += 3) { + var n = merged.size(); + if (n > 0 && merged[n - 3] == runs[r]) { + merged[n - 1] = runs[r + 2]; + } else { + merged.add(runs[r]); + merged.add(runs[r + 1]); + merged.add(runs[r + 2]); + } + } + var out = [] as Array<Number>; + for (var r = 0; r < merged.size(); r += 3) { + var kind = merged[r]; + var start = merged[r + 1]; + var end = merged[r + 2]; + if (r > 0 || kind != KIND_TEXT) { + out.add(kind == KIND_TEXT ? TEXT_LATCH : (kind == KIND_NUMERIC ? NUMERIC_LATCH + : ((end - start) % 6 == 0 ? BYTE_LATCH_6 : BYTE_LATCH))); + } + if (kind == KIND_TEXT) { + compactText(data, start, end, text, out); + } else if (kind == KIND_NUMERIC) { + compactNumeric(data, start, end, out); + } else { + compactBytes(data, start, end, out); + } + } + return out; + } + + function kindOf(b as Number, text as Array<Array<Number>>) as Number { + if (b >= 0x30 && b <= 0x39) { + return KIND_NUMERIC; + } + if (b < 127) { + for (var m = 0; m < 4; m++) { + if ((text[m][b] as Number) >= 0) { + return KIND_TEXT; + } + } + } + return KIND_BYTE; + } + + function compactText(data as ByteArray, start as Number, end as Number, text as Array<Array<Number>>, out as Array<Number>) as Void { + var interim = [] as Array<Number>; + var submode = UPPER; + for (var i = start; i < end; i++) { + var c = data[i]; + if ((text[submode][c] as Number) < 0) { + var next = submode; + for (var p = 0; p < 4; p++) { + var m = PREFERENCE[p] as Number; + if ((text[m][c] as Number) >= 0) { + next = m; + break; + } + } + interim.addAll((SWITCHES[submode] as Array)[next] as Array<Number>); + submode = next; + } + interim.add(text[submode][c] as Number); + } + for (var i = 0; i < interim.size(); i += 2) { + out.add(30 * interim[i] + (i + 1 < interim.size() ? interim[i + 1] : 29)); + } + } + + // 44 digits per group: the value of "1" followed by the digits, in base 900. + function compactNumeric(data as ByteArray, start as Number, end as Number, out as Array<Number>) as Void { + for (var g = start; g < end; g += 44) { + var stop = g + 44 < end ? g + 44 : end; + var decimal = [1] as Array<Number>; + for (var i = g; i < stop; i++) { + decimal.add(data[i] - 0x30); + } + var base900 = [] as Array<Number>; + while (decimal.size() > 0) { + // Divide the decimal number by 900, keeping the quotient. + var quotient = [] as Array<Number>; + var rem = 0; + for (var k = 0; k < decimal.size(); k++) { + var cur = rem * 10 + decimal[k]; + var q = cur / 900; + rem = cur % 900; + if (quotient.size() > 0 || q > 0) { + quotient.add(q); + } + } + base900.add(rem); + decimal = quotient; + } + for (var k = base900.size() - 1; k >= 0; k--) { + out.add(base900[k]); + } + } + } + + // 6 bytes per group as 5 base-900 codewords; a shorter last group as is. + function compactBytes(data as ByteArray, start as Number, end as Number, out as Array<Number>) as Void { + for (var g = start; g < end; g += 6) { + if (g + 6 > end) { + for (var i = g; i < end; i++) { + out.add(data[i]); + } + return; + } + var value = 0l; + for (var i = g; i < g + 6; i++) { + value = value * 256 + data[i]; + } + var digits = new [5] as Array<Number>; + for (var k = 4; k >= 0; k--) { + digits[k] = (value % 900).toNumber(); + value = value / 900; + } + out.addAll(digits); + } + } + + // ISO 15438 annex E recommended level for the data codewords. + function autoLevel(dataCount as Number) as Number { + if (dataCount <= 40) { + return 2; + } + if (dataCount <= 160) { + return 3; + } + return dataCount <= 320 ? 4 : 5; + } + + // The option's level: Low 2, Medium automatic, Quartile 5, High 6. + function levelFor(option as Number, dataCount as Number) as Number { + return [2, autoLevel(dataCount), 5, 6][option] as Number; + } + + function leftIndicator(row as Number, rows as Number, cols as Number, level as Number) as Number { + var t = row % 3; + var x = t == 0 ? (rows - 1) / 3 : (t == 1 ? level * 3 + (rows - 1) % 3 : cols - 1); + return 30 * (row / 3) + x; + } + + function rightIndicator(row as Number, rows as Number, cols as Number, level as Number) as Number { + var t = row % 3; + var x = t == 0 ? cols - 1 : (t == 1 ? (rows - 1) / 3 : level * 3 + (rows - 1) % 3); + return 30 * (row / 3) + x; + } + + // Encode synchronously with given columns and level (tests). + function encode(text as String, cols as Number, level as Number) as Codes.Matrix or Codes.CodeError { + var job = new Pdf417Job(text, 0, 0, 0, false); + job.force(cols, level); + Jobs.runToCompletion(job, 1); + var err = job.error; + return err != null ? new Codes.CodeError(err) : job.matrix() as Codes.Matrix; + } + + class Pdf417Job extends Codes.MatrixJob { + + var columns as Number = 0; + var level as Number = -1; + var rows as Number = 0; + + private var _data as ByteArray; + private var _option as Number; + private var _screenW as Number; + private var _screenH as Number; + private var _round as Boolean; + private var _phase as Number = 0; + private var _words as Array<Number> = [] as Array<Number>; + private var _ec as Array<Number> = [] as Array<Number>; + private var _factors as Array<Number> = [] as Array<Number>; + private var _codes as Array<Array<Number>> = [] as Array<Array<Number>>; + private var _at as Number = 0; + private var _modules as ByteArray = []b; + private var _result as Codes.Matrix? = null; + + // option: the EC option (0-3). The screen size decides the columns. + function initialize(text as String, option as Number, screenW as Number, screenH as Number, round as Boolean) { + MatrixJob.initialize(); + _data = Bytes.fromUtf8(text); + _option = option; + _screenW = screenW; + _screenH = screenH; + _round = round; + } + + // Tests: fixed columns and level instead of the screen and option. + function force(cols as Number, ecLevel as Number) as Void { + columns = cols; + level = ecLevel; + } + + function percent() as Number { + return _phase == 1 && _words.size() > 0 ? 20 + 60 * _at / _words.size() : _phase * 30; + } + + function matrix() as Codes.Matrix? { + return _result; + } + + function step(budget as Number) as Boolean { + if (_phase == 0) { + prepare(); + } else if (_phase == 1) { + ecStep(); + } else if (_phase == 2) { + rowsStep(); + } + return _phase >= 3 || error != null; + } + + private function prepare() as Void { + if (_data.size() == 0) { + error = "PDF417 needs some text"; + return; + } + var data = dataCodewords(_data, table(Rez.JsonData.Pdf417Text) as Array<Array<Number>>); + if (level < 0) { + level = levelFor(_option, data.size()); + } + var ecCount = 1 << (level + 1); + if (columns == 0 && !chooseLayout(data.size() + ecCount + 1)) { + error = "too large for this screen as PDF417 (" + data.size() + " codewords)"; + return; + } + var total = data.size() + ecCount + 1; + var padding = total % columns == 0 ? 0 : columns - total % columns; + var length = data.size() + padding + 1; + rows = (total + padding) / columns; + if (length > MAX_CODEWORDS || rows > 90) { + error = "too long for PDF417 (" + data.size() + " codewords)"; + return; + } + if (rows < 3) { + error = "PDF417 needs at least 3 rows; use fewer columns"; + return; + } + _words = [length] as Array<Number>; + _words.addAll(data); + for (var i = 0; i < padding; i++) { + _words.add(PAD); + } + _factors = (table(Rez.JsonData.Pdf417Ec) as Array<Array<Number>>)[level]; + _codes = [table(Rez.JsonData.Pdf417Codes0) as Array<Number>, table(Rez.JsonData.Pdf417Codes1) as Array<Number>, + table(Rez.JsonData.Pdf417Codes2) as Array<Number>]; + _ec = new [ecCount] as Array<Number>; + for (var i = 0; i < ecCount; i++) { + _ec[i] = 0; + } + _at = 0; + _phase = 1; + } + + // The largest module size, then the fewest columns, that fit. + private function chooseLayout(codewords as Number) as Boolean { + for (var px = 6; px >= 1; px--) { + for (var cols = 1; cols <= 30; cols++) { + var r = (codewords + cols - 1) / cols; + if (r < 3 || r > 90) { + continue; + } + var w = 17 * (cols + 4) + 1 + 2 * QUIET; + var h = r * ROW_HEIGHT + 2 * QUIET; + if (Codes.fits(w, h, px, _screenW, _screenH, _round)) { + columns = cols; + return true; + } + } + } + return false; + } + + // Reed-Solomon mod 929 (pdf417gen's form), EC_STEP_WORDS words per step. + private function ecStep() as Void { + var n = _ec.size(); + var f = _factors; + var end = _at + EC_STEP_WORDS < _words.size() ? _at + EC_STEP_WORDS : _words.size(); + for (var i = _at; i < end; i++) { + var temp = (_words[i] + _ec[n - 1]) % 929; + for (var x = n - 1; x >= 0; x--) { + var word = x > 0 ? _ec[x - 1] : 0; + _ec[x] = (word + 929 - (temp * f[x]) % 929) % 929; + } + } + _at = end; + if (_at < _words.size()) { + return; + } + for (var x = n - 1; x >= 0; x--) { + var v = _ec[x]; + _words.add(v > 0 ? 929 - v : 0); + } + _ec = [] as Array<Number>; + var width = 17 * (columns + 4) + 1; + _modules = new [width * rows]b; + _at = 0; + _phase = 2; + } + + // A few rows per step: start, indicators, data, stop. + private function rowsStep() as Void { + var width = 17 * (columns + 4) + 1; + for (var k = 0; k < 4 && _at < rows; k++) { + var row = _at; + var cluster = _codes[row % 3]; + var x = put(row, 0, START, 17, width); + x = put(row, x, cluster[leftIndicator(row, rows, columns, level)] as Number, 17, width); + for (var c = 0; c < columns; c++) { + x = put(row, x, cluster[_words[row * columns + c]] as Number, 17, width); + } + x = put(row, x, cluster[rightIndicator(row, rows, columns, level)] as Number, 17, width); + put(row, x, STOP, 18, width); + _at++; + } + if (_at < rows) { + return; + } + _codes = [] as Array<Array<Number>>; + _words = [] as Array<Number>; + var result = new Codes.Matrix(width, rows, QUIET, _modules); + result.rowHeight = ROW_HEIGHT; + _result = result; + _phase = 3; + } + + // Draw count bits of pattern (most significant first) at column x of + // row `row`; returns the next column. + private function put(row as Number, x as Number, pattern as Number, count as Number, width as Number) as Number { + var at = row * width + x; + for (var i = 0; i < count; i++) { + _modules[at + i] = (pattern >> (count - 1 - i)) & 1; + } + return x + count; + } + } +} |
