summaryrefslogtreecommitdiff
path: root/source/codes/Pdf417.mc
diff options
context:
space:
mode:
authorcirakg <cirakg@cirakg.xyz>2026-10-20 11:24:58 +0200
committercirakg <cirakg@cirakg.xyz>2026-10-20 11:24:58 +0200
commitcc2fa8917b65eeda8d222dcbd69e9342884bb545 (patch)
tree910b9852ac4c29d5cb43cd475dc2426b0de4dddb /source/codes/Pdf417.mc
parent0a39c695e1fc78a7c8b8af0b301040f422303953 (diff)
feat(codes): add Data Matrix (ECC 200, 10x10-144x144) and PDF417 encoders as jobs
Data Matrix: ASCII with digit pairs, Base256 or Upper Shift for UTF-8, interleaved Reed-Solomon in steps, utah placement; matches ppf.datamatrix. PDF417: pdf417gen's compaction, automatic or fixed EC level, columns fitted to the screen, pattern tables as generated JSON resources; matches pdf417gen. 2D matrices gain a row height and 8-at-a-time packing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/codes/Pdf417.mc')
-rw-r--r--source/codes/Pdf417.mc428
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;
+ }
+ }
+}