diff options
| author | cirakg <cirakg@cirakg.xyz> | 2026-10-20 06:56:58 +0200 |
|---|---|---|
| committer | cirakg <cirakg@cirakg.xyz> | 2026-10-20 06:56:58 +0200 |
| commit | 3548312586aeafbfca0d0b87db90f9bdd972c831 (patch) | |
| tree | b657f5b2884ffc506c2d9dd080407e5c6218edee /source/codes | |
| parent | e3cc458d3b9aadb9459b51ef0120621408093f38 (diff) | |
feat(codes): add a QR Code encoder (versions 1-40, levels L-H) as a resumable job
Optimal numeric/alphanumeric/byte segmentation, Reed-Solomon over GF(256),
all eight masks scored like segno, steps sized for the 120,000-bytecode
watchdog; block tables generated from segno; a generic Matrix type with a
compact form for caching.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'source/codes')
| -rw-r--r-- | source/codes/Codes.mc | 54 | ||||
| -rw-r--r-- | source/codes/Qr.mc | 764 | ||||
| -rw-r--r-- | source/codes/QrTables.mc | 97 | ||||
| -rw-r--r-- | source/codes/ReedSolomon.mc | 110 |
4 files changed, 1025 insertions, 0 deletions
diff --git a/source/codes/Codes.mc b/source/codes/Codes.mc index 70c6c2d..2d307ef 100644 --- a/source/codes/Codes.mc +++ b/source/codes/Codes.mc @@ -53,6 +53,60 @@ module Codes { } } + // A 2D code ready to draw: width x height modules, row-major, 1 = dark, + // plus the quiet zone its symbology needs (QR 4, Data Matrix 1, ...). + class Matrix { + var width as Number; + var height as Number; + var quiet as Number; + var modules as ByteArray; + + function initialize(w as Number, h as Number, quietModules as Number, bits as ByteArray) { + width = w; + height = h; + quiet = quietModules; + modules = bits; + } + + // Compact form for the encrypted cache: width, height, quiet (one byte + // each), then the modules packed 8 per byte. + function toBytes() as ByteArray { + var out = [width, height, quiet]b; + var packed = new [(modules.size() + 7) / 8]b; + for (var i = 0; i < modules.size(); i++) { + if (modules[i] == 1) { + packed[i / 8] = packed[i / 8] | (0x80 >> (i % 8)); + } + } + out.addAll(packed); + return out; + } + + static function fromBytes(data as ByteArray) as Matrix? { + if (data.size() < 3) { + return null; + } + var w = data[0]; + var h = data[1]; + if (w == 0 || h == 0 || data.size() != 3 + (w * h + 7) / 8) { + return null; + } + var bits = new [w * h]b; + for (var i = 0; i < w * h; i++) { + bits[i] = (data[3 + i / 8] >> (7 - i % 8)) & 1; + } + return new Matrix(w, h, data[2], bits); + } + } + + function isMatrix(symbology as Number) as Boolean { + return symbology == QR || symbology == DATAMATRIX || symbology == PDF417 || symbology == AZTEC; + } + + function ecLevel(options as Number) as Number { + return options & OPT_EC_MASK; + } + // Why a payload can't be encoded, in words for the user. class CodeError { var message as String; diff --git a/source/codes/Qr.mc b/source/codes/Qr.mc new file mode 100644 index 0000000..8af0f5a --- /dev/null +++ b/source/codes/Qr.mc @@ -0,0 +1,764 @@ +// Author: cirakg & claude ai +// Summary: QR Code encoder (ISO/IEC 18004): optimal mode segmentation, versions 1-40, levels L-H, all 8 masks, as a resumable job. +// +// Steps (README 12.1): +// 1. Segment the UTF-8 bytes optimally into numeric, alphanumeric and +// byte segments (dynamic programming, as in Nayuki's QR library), and +// pick the smallest version whose capacity at the chosen level fits. +// 2. Build the bit stream (mode, character count, data, terminator, pad +// codewords 0xEC 0x11), split it into blocks, add Reed-Solomon +// codewords (QrTables.BLOCKS), interleave. +// 3. Draw the function patterns, place the codewords in the zigzag. +// 4. Score all 8 masks with the ISO 18004:2015 penalty rules exactly as +// segno computes them (format and version areas still light), keep +// the lowest, then draw the format and version information. +// Each step does a bounded amount of work (one RS block, a few columns or +// rows), because version 40 is far beyond one event's watchdog budget. +// Verified against segno with forced version and mask, and by decoding. + +import Toybox.Lang; + +module Qr { + + const MODE_NUMERIC = 1; + const MODE_ALPHANUMERIC = 2; + const MODE_BYTE = 4; + const ALPHANUMERIC = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; // lint: allow-literal (QR alphanumeric set) + // Format bits per level L, M, Q, H. + const LEVEL_BITS = [1, 0, 3, 2]; + // Modules looked at per job step: well below the 120,000-bytecode + // watchdog limit of the smallest targets. + const MODULES_PER_STEP = 2500; + // Mask scoring costs more per module: the row pass applies the mask and + // scores N1-N3, the column pass N1 and N3. Sized for the Forerunner + // 255S's 120,000 bytecodes per event, where 1,200 row modules tripped + // the watchdog in the UI flow. + const ROW_MODULES_PER_STEP = 500; + const COLUMN_MODULES_PER_STEP = 1000; + + // The GF(256) field of QR (x^8 + x^4 + x^3 + x^2 + 1), shared. + var field as ReedSolomon.Field? = null; + + function theField() as ReedSolomon.Field { + var f = field; + if (f == null) { + f = new ReedSolomon.Field(0x11D, 256, 0); + field = f; + } + return f; + } + + // Data codewords of version v at level ec (0 L .. 3 H). + function dataCodewords(v as Number, ec as Number) as Number { + var i = ((v - 1) * 4 + ec) * 5; + var b = QrTables.BLOCKS; + return (b[i + 1] as Number) * (b[i + 2] as Number) + (b[i + 3] as Number) * (b[i + 4] as Number); + } + + function charCountBits(mode as Number, v as Number) as Number { + var c = v <= 9 ? 0 : (v <= 26 ? 1 : 2); + if (mode == MODE_NUMERIC) { + return [10, 12, 14][c] as Number; + } + if (mode == MODE_ALPHANUMERIC) { + return [9, 11, 13][c] as Number; + } + return [8, 16, 16][c] as Number; + } + + function isDigit(b as Number) as Boolean { + return b >= 0x30 && b <= 0x39; + } + + function alnumValue(b as Number) as Number { + if (b >= 0x80) { + return -1; + } + var at = ALPHANUMERIC.find(b.toChar().toString()); + return at == null ? -1 : at; + } + + // Optimal segmentation for a version class: [mode, start, end) triples + // flattened, by dynamic programming over the bytes (Nayuki's + // makeSegmentsOptimally). Costs are in sixths of a bit (numeric 10/3, + // alphanumeric 11/2, byte 8 bits per character). State j after + // character i: the cheapest encoding so far that continues in mode j; + // choice[i * 3 + j] is the mode character i itself was encoded in. + function segment(data as ByteArray, v as Number) as Array<Number> { + var n = data.size(); + var modes = [MODE_BYTE, MODE_ALPHANUMERIC, MODE_NUMERIC]; + var head = [(4 + charCountBits(MODE_BYTE, v)) * 6, (4 + charCountBits(MODE_ALPHANUMERIC, v)) * 6, + (4 + charCountBits(MODE_NUMERIC, v)) * 6]; + var unit = [48, 33, 20]; + var inf = 0x3FFFFFFF; + var prev = [head[0], head[1], head[2]]; + // choice[i * 3 + j] = the mode index the segment ending at i in mode j came from. + var choice = new [n * 3]b; + for (var i = 0; i < n; i++) { + var b = data[i]; + var cur = [prev[0] + unit[0], alnumValue(b) >= 0 ? prev[1] + unit[1] : inf, isDigit(b) ? prev[2] + unit[2] : inf]; + choice[i * 3] = 0; + choice[i * 3 + 1] = 1; + choice[i * 3 + 2] = 2; + for (var j = 0; j < 3; j++) { + for (var k = 0; k < 3; k++) { + if (k == j || (cur[k] as Number) >= inf) { + continue; + } + // State j may follow a character in mode k: a new segment + // in mode j starts after it (its header paid now). + var switched = ((cur[k] as Number) + 5) / 6 * 6 + (head[j] as Number); + if (switched < (cur[j] as Number)) { + cur[j] = switched; + choice[i * 3 + j] = k; + } + } + } + prev = cur; + } + // The cheapest final mode, then walk back. + var best = 0; + for (var j = 1; j < 3; j++) { + if ((prev[j] as Number) < (prev[best] as Number)) { + best = j; + } + } + // Walk back: the state after character i says which mode i was in. + var charModes = new [n]b; + var state = best; + for (var i = n - 1; i >= 0; i--) { + var charMode = choice[i * 3 + state]; + charModes[i] = charMode; + state = charMode; + } + var segments = [] as Array<Number>; + var start = 0; + for (var i = 1; i <= n; i++) { + if (i == n || charModes[i] != charModes[start]) { + segments.add(modes[charModes[start]] as Number); + segments.add(start); + segments.add(i); + start = i; + } + } + return segments; + } + + // Bits needed for segments at version v. + function segmentBits(segments as Array<Number>, v as Number) as Number { + var total = 0; + for (var s = 0; s < segments.size(); s += 3) { + var mode = segments[s]; + var count = segments[s + 2] - segments[s + 1]; + total += 4 + charCountBits(mode, v); + if (mode == MODE_NUMERIC) { + total += count / 3 * 10 + [0, 4, 7][count % 3]; + } else if (mode == MODE_ALPHANUMERIC) { + total += count / 2 * 11 + (count % 2) * 6; + } else { + total += count * 8; + } + } + return total; + } + + // A growing bit stream packed into codewords. + class BitWriter { + var bytes as ByteArray = []b; + var bitCount as Number = 0; + + function initialize() { + } + + function put(value as Number, count as Number) as Void { + for (var i = count - 1; i >= 0; i--) { + if (bitCount % 8 == 0) { + bytes.add(0); + } + if (((value >> i) & 1) == 1) { + var at = bitCount / 8; + bytes[at] = bytes[at] | (0x80 >> (bitCount % 8)); + } + bitCount++; + } + } + } + + // The QR result: a size x size matrix, row-major, 1 = dark. + class MatrixCode { + var size as Number; + var modules as ByteArray; + var version as Number; + var mask as Number; + + function initialize(n as Number, bits as ByteArray, v as Number, m as Number) { + size = n; + modules = bits; + version = v; + mask = m; + } + } + + // Encode synchronously (tests and small codes); the app uses QrJob. + function encode(text as String, ec as Number, forceVersion as Number, forceMask as Number) as MatrixCode or Codes.CodeError { + var job = new QrJob(text, ec, forceVersion, forceMask); + Jobs.runToCompletion(job, 1); + var err = job.error; + return err != null ? new Codes.CodeError(err) : job.result as MatrixCode; + } + + class QrJob extends Jobs.Job { + + var error as String? = null; + var result as MatrixCode? = null; + + private var _data as ByteArray; + private var _ec as Number; + private var _version as Number = 0; + private var _forceMask as Number; + private var _phase as Number = 0; + private var _size as Number = 0; + private var _m as ByteArray = []b; // modules, row-major + private var _function as ByteArray = []b; // 1 = function module + private var _blocks as Array<Array<Number>> = [] as Array<Array<Number>>; + private var _ecBlocks as Array<Array<Number>> = [] as Array<Array<Number>>; + private var _codewords as ByteArray = []b; + private var _cursor as Number = 0; + private var _bitIndex as Number = 0; + private var _maskIndex as Number = 0; + private var _bestMask as Number = 0; + private var _bestScore as Number = 0x7FFFFFFF; + private var _score as Number = 0; + private var _dark as Number = 0; + private var _maskPass as Number = 0; + private var _masked as ByteArray = []b; + private var _prevRow as ByteArray = []b; + + // ec: 0 L, 1 M, 2 Q, 3 H. forceVersion 0 = smallest that fits; + // forceMask -1 = best by penalty. + function initialize(text as String, ec as Number, forceVersion as Number, forceMask as Number) { + Job.initialize(); + _data = Bytes.fromUtf8(text); + _ec = ec; + _forceMask = forceMask; + _version = forceVersion; + } + + // Progress 0-100 for the UI. + function percent() as Number { + return _phase * 100 / 6; + } + + function step(budget as Number) as Boolean { + if (_phase == 0) { + buildCodewords(); + } else if (_phase == 1) { + eccStep(); + } else if (_phase == 2) { + interleaveAndDrawFunctions(); + } else if (_phase == 3) { + placeStep(); + } else if (_phase == 4) { + maskStep(); + } else if (_phase == 5) { + finish(); + } + return _phase >= 6 || error != null; + } + + // Phase 0: segments, version, bit stream, blocks. + private function buildCodewords() as Void { + if (_data.size() == 0) { + error = "QR needs some text"; + return; + } + var segments = null; + var v = _version; + if (v > 0) { + segments = segment(_data, v); + if (segmentBits(segments, v) > dataCodewords(v, _ec) * 8) { + error = "too long for QR version " + v; + return; + } + } else { + for (var c = 0; c < 3 && v == 0; c++) { + var lo = [1, 10, 27][c] as Number; + var hi = [9, 26, 40][c] as Number; + var s = segment(_data, lo); + var bits = segmentBits(s, lo); + for (var tryV = lo; tryV <= hi; tryV++) { + if (bits <= dataCodewords(tryV, _ec) * 8) { + v = tryV; + segments = s; + break; + } + } + } + if (v == 0) { + error = "too long for a QR code at this error correction level (" + _data.size() + " bytes, at most " + + dataCodewords(40, _ec) + " in byte mode)"; + return; + } + } + _version = v; + var capacity = dataCodewords(v, _ec); + var w = new BitWriter(); + var segs = segments as Array<Number>; + for (var s = 0; s < segs.size(); s += 3) { + writeSegment(w, segs[s], segs[s + 1], segs[s + 2]); + } + // Terminator, byte alignment, pad codewords. + var terminator = capacity * 8 - w.bitCount; + w.put(0, terminator < 4 ? terminator : 4); + if (w.bitCount % 8 != 0) { + w.put(0, 8 - w.bitCount % 8); + } + var pad = 0xEC; + while (w.bytes.size() < capacity) { + w.bytes.add(pad); + pad = pad == 0xEC ? 0x11 : 0xEC; + } + // Split into blocks. + var i = ((v - 1) * 4 + _ec) * 5; + var t = QrTables.BLOCKS; + var at = 0; + for (var g = 0; g < 2; g++) { + var count = t[i + 1 + 2 * g] as Number; + var len = t[i + 2 + 2 * g] as Number; + for (var b = 0; b < count; b++) { + var block = new [len] as Array<Number>; + for (var k = 0; k < len; k++) { + block[k] = w.bytes[at + k]; + } + at += len; + _blocks.add(block); + } + } + _cursor = 0; + _phase = 1; + } + + private function writeSegment(w as BitWriter, mode as Number, start as Number, end as Number) as Void { + var count = end - start; + w.put(mode, 4); + w.put(count, charCountBits(mode, _version)); + var d = _data; + if (mode == MODE_NUMERIC) { + var i = start; + while (i < end) { + var take = end - i >= 3 ? 3 : end - i; + var value = 0; + for (var k = 0; k < take; k++) { + value = value * 10 + d[i + k] - 0x30; + } + w.put(value, [0, 4, 7, 10][take] as Number); + i += take; + } + } else if (mode == MODE_ALPHANUMERIC) { + var i = start; + while (i < end) { + if (end - i >= 2) { + w.put(alnumValue(d[i]) * 45 + alnumValue(d[i + 1]), 11); + i += 2; + } else { + w.put(alnumValue(d[i]), 6); + i += 1; + } + } + } else { + for (var i = start; i < end; i++) { + w.put(d[i], 8); + } + } + } + + // Phase 1: Reed-Solomon codewords, one block per step. + private function eccStep() as Void { + var i = ((_version - 1) * 4 + _ec) * 5; + var n = QrTables.BLOCKS[i] as Number; + _ecBlocks.add(ReedSolomon.ecc(theField(), _blocks[_cursor], n)); + _cursor++; + if (_cursor >= _blocks.size()) { + _phase = 2; + } + } + + // Phase 2: interleave codewords; draw the function patterns. + private function interleaveAndDrawFunctions() as Void { + var out = []b; + var longest = 0; + for (var b = 0; b < _blocks.size(); b++) { + if (_blocks[b].size() > longest) { + longest = _blocks[b].size(); + } + } + for (var k = 0; k < longest; k++) { + for (var b = 0; b < _blocks.size(); b++) { + if (k < _blocks[b].size()) { + out.add(_blocks[b][k]); + } + } + } + var ecLen = _ecBlocks[0].size(); + for (var k = 0; k < ecLen; k++) { + for (var b = 0; b < _ecBlocks.size(); b++) { + out.add(_ecBlocks[b][k]); + } + } + _codewords = out; + _blocks = [] as Array<Array<Number>>; + _ecBlocks = [] as Array<Array<Number>>; + drawFunctionPatterns(); + _cursor = _size - 1; // right column of the current column pair + _bitIndex = 0; + _phase = 3; + } + + private function drawFunctionPatterns() as Void { + var n = 17 + 4 * _version; + _size = n; + _m = new [n * n]b; + _function = new [n * n]b; + // Timing patterns. + for (var i = 0; i < n; i++) { + setFunction(6, i, i % 2 == 0 ? 1 : 0); + setFunction(i, 6, i % 2 == 0 ? 1 : 0); + } + // Finder patterns with separators. + drawFinder(3, 3); + drawFinder(n - 4, 3); + drawFinder(3, n - 4); + // Alignment patterns, except where they'd overlap the finders. + var pos = QrTables.ALIGNMENT[_version - 1] as Array<Number>; + var count = pos.size(); + for (var a = 0; a < count; a++) { + for (var b = 0; b < count; b++) { + if ((a == 0 && b == 0) || (a == 0 && b == count - 1) || (a == count - 1 && b == 0)) { + continue; + } + drawAlignment(pos[a], pos[b]); + } + } + // Format areas and the dark module's place stay light until the + // mask is chosen (segno evaluates masks that way; ISO 18004:2015 + // 7.8 leaves format information out of the evaluation). + for (var i = 0; i <= 8; i++) { + if (i != 6) { + setFunction(8, i, 0); + setFunction(i, 8, 0); + } + } + for (var i = 0; i < 8; i++) { + setFunction(n - 1 - i, 8, 0); + setFunction(8, n - 1 - i, 0); + } + setFunction(8, n - 8, 0); + // Version areas (light for now). + if (_version >= 7) { + for (var i = 0; i < 18; i++) { + var a = n - 11 + i % 3; + var b = i / 3; + setFunction(a, b, 0); + setFunction(b, a, 0); + } + } + } + + private function setFunction(x as Number, y as Number, dark as Number) as Void { + var at = y * _size + x; + _m[at] = dark; + _function[at] = 1; + } + + private function drawFinder(cx as Number, cy as Number) as Void { + for (var dy = -4; dy <= 4; dy++) { + for (var dx = -4; dx <= 4; dx++) { + var x = cx + dx; + var y = cy + dy; + if (x < 0 || y < 0 || x >= _size || y >= _size) { + continue; + } + var d = (dx.abs() > dy.abs() ? dx.abs() : dy.abs()); + setFunction(x, y, d != 2 && d != 4 ? 1 : 0); + } + } + } + + private function drawAlignment(cx as Number, cy as Number) as Void { + for (var dy = -2; dy <= 2; dy++) { + for (var dx = -2; dx <= 2; dx++) { + var d = (dx.abs() > dy.abs() ? dx.abs() : dy.abs()); + setFunction(cx + dx, cy + dy, d != 1 ? 1 : 0); + } + } + } + + // Phase 3: place the codeword bits in the zigzag, a few column pairs + // per step. + private function placeStep() as Void { + var n = _size; + var totalBits = _codewords.size() * 8; + var budget = MODULES_PER_STEP; + while (_cursor >= 1 && budget > 0) { + var right = _cursor == 6 ? 5 : _cursor; + var upward = ((right + 1) & 2) == 0; + for (var vert = 0; vert < n; vert++) { + var y = upward ? n - 1 - vert : vert; + for (var j = 0; j < 2; j++) { + var x = right - j; + var at = y * n + x; + if (_function[at] == 0 && _bitIndex < totalBits) { + _m[at] = (_codewords[_bitIndex >> 3] >> (7 - (_bitIndex & 7))) & 1; + _bitIndex++; + } + } + } + budget -= 2 * n; + _cursor = right - 2; + } + if (_cursor < 1) { + _codewords = []b; + _maskIndex = _forceMask >= 0 ? _forceMask : 0; + _cursor = 0; + _score = 0; + _dark = 0; + _phase = 4; + } + } + + // Phase 4: score masks (or apply the forced one). Per mask, two + // passes: the rows (apply the mask into a copy, then N1, N2, N3 and + // the dark count), then the columns (N1, N3), each in steps sized to + // its cost per module. + private function maskStep() as Void { + if (_forceMask >= 0) { + _bestMask = _forceMask; + _phase = 5; + return; + } + var n = _size; + var lines = (_maskPass == 0 ? ROW_MODULES_PER_STEP : COLUMN_MODULES_PER_STEP) / n; + if (lines < 1) { + lines = 1; + } + for (var l = 0; l < lines && _cursor < n; l++) { + if (_maskPass == 0) { + applyMaskRow(_cursor); + scoreRow(_cursor); + } else { + scoreColumn(_cursor); + } + _cursor++; + } + if (_cursor < n) { + return; + } + _cursor = 0; + _maskPass++; + if (_maskPass < 2) { + return; + } + // N4: 10 points per 5 % away from half dark (integer form of segno's). + var total = n * n; + var deviation = (_dark * 100 - 50 * total).abs(); + _score += 10 * (deviation / (5 * total)); + if (_score < _bestScore) { + _bestScore = _score; + _bestMask = _maskIndex; + } + _maskIndex++; + _maskPass = 0; + _score = 0; + _dark = 0; + if (_maskIndex >= 8) { + _masked = []b; + _prevRow = []b; + _phase = 5; + } + } + + private function applyMaskRow(y as Number) as Void { + var n = _size; + if (_masked.size() != n * n) { + _masked = new [n * n]b; + } + var m = _maskIndex; + var row = y * n; + for (var x = 0; x < n; x++) { + var at = row + x; + var bit = _m[at]; + if (_function[at] == 0 && maskBit(m, x, y)) { + bit = bit ^ 1; + } + _masked[at] = bit; + } + } + + private function scoreRow(y as Number) as Void { + var n = _size; + var row = _masked.slice(y * n, y * n + n); + _score += lineScore(row); + var prev = _prevRow; + var dark = 0; + for (var x = 0; x < n; x++) { + var c = row[x]; + dark += c; + // N2: a 2x2 block of one colour with the row above. + if (y > 0 && x > 0 && c == row[x - 1] && c == prev[x] && c == prev[x - 1]) { + _score += 3; + } + } + _dark += dark; + _prevRow = row; + } + + private function scoreColumn(x as Number) as Void { + var n = _size; + var column = new [n]b; + for (var y = 0; y < n; y++) { + column[y] = _masked[y * n + x]; + } + _score += lineScore(column); + } + + // N1 and N3 of one row or column, in one pass. N1: 3 + (run - 5) + // for every run of 5 or more equal modules. N3 as segno counts it: + // 40 for each 1011101 with light modules (up to 4, fewer at the edge) + // before or after it; after a counted match the search continues + // behind it, otherwise 4 modules on (found with a rolling 7-bit window). + private function lineScore(line as ByteArray) as Number { + var n = line.size(); + var score = 0; + var run = 1; + var window = line[0]; + var nextAllowed = 0; + for (var k = 1; k < n; k++) { + var c = line[k]; + if (c == line[k - 1]) { + run++; + } else { + if (run >= 5) { + score += run - 2; + } + run = 1; + } + window = ((window << 1) | c) & 0x7F; + if (window == 0x5D && k >= 6) { + var idx = k - 6; + if (idx >= nextAllowed) { + var after = idx + 7; + if (idx == 0 || idx == n - 7 || allLight(line, idx - 4 < 0 ? 0 : idx - 4, idx) + || allLight(line, after, after + 4 > n ? n : after + 4)) { + score += 40; + nextAllowed = idx + 7; + } else { + nextAllowed = idx + 4; + } + } + } + } + if (run >= 5) { + score += run - 2; + } + return score; + } + + private function allLight(line as ByteArray, from as Number, to as Number) as Boolean { + for (var i = from; i < to; i++) { + if (line[i] != 0) { + return false; + } + } + return true; + } + + // Phase 5: apply the chosen mask, draw format and version info. + private function finish() as Void { + var n = _size; + var m = _bestMask; + for (var y = 0; y < n; y++) { + for (var x = 0; x < n; x++) { + var at = y * n + x; + if (_function[at] == 0 && maskBit(m, x, y)) { + _m[at] = _m[at] ^ 1; + } + } + } + drawFormat(m); + if (_version >= 7) { + drawVersion(); + } + result = new MatrixCode(n, _m, _version, m); + _function = []b; + _phase = 6; + } + + private function drawFormat(mask as Number) as Void { + var data = ((LEVEL_BITS[_ec] as Number) << 3) | mask; + var rem = data; + for (var i = 0; i < 10; i++) { + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + } + var bits = ((data << 10) | rem) ^ 0x5412; + var n = _size; + for (var i = 0; i <= 5; i++) { + setFunction(8, i, (bits >> i) & 1); + } + setFunction(8, 7, (bits >> 6) & 1); + setFunction(8, 8, (bits >> 7) & 1); + setFunction(7, 8, (bits >> 8) & 1); + for (var i = 9; i < 15; i++) { + setFunction(14 - i, 8, (bits >> i) & 1); + } + for (var i = 0; i < 8; i++) { + setFunction(n - 1 - i, 8, (bits >> i) & 1); + } + for (var i = 8; i < 15; i++) { + setFunction(8, n - 15 + i, (bits >> i) & 1); + } + setFunction(8, n - 8, 1); + } + + private function drawVersion() as Void { + var rem = _version; + for (var i = 0; i < 12; i++) { + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + } + var bits = (_version << 12) | rem; + var n = _size; + for (var i = 0; i < 18; i++) { + var bit = (bits >> i) & 1; + var a = n - 11 + i % 3; + var b = i / 3; + setFunction(a, b, bit); + setFunction(b, a, bit); + } + } + } + + // Mask pattern m at column x, row y: true flips a data module. + function maskBit(m as Number, x as Number, y as Number) as Boolean { + if (m == 0) { + return (x + y) % 2 == 0; + } + if (m == 1) { + return y % 2 == 0; + } + if (m == 2) { + return x % 3 == 0; + } + if (m == 3) { + return (x + y) % 3 == 0; + } + if (m == 4) { + return (x / 3 + y / 2) % 2 == 0; + } + if (m == 5) { + return x * y % 2 + x * y % 3 == 0; + } + if (m == 6) { + return (x * y % 2 + x * y % 3) % 2 == 0; + } + return ((x + y) % 2 + x * y % 3) % 2 == 0; + } +} diff --git a/source/codes/QrTables.mc b/source/codes/QrTables.mc new file mode 100644 index 0000000..d88f7b0 --- /dev/null +++ b/source/codes/QrTables.mc @@ -0,0 +1,97 @@ +// Author: cirakg & claude ai +// Summary: QR error-correction block structure per version and level (generated from segno; do not edit). +// +// Generated by tools/gen_qr_tables.py. For version v (1-40) and level e (0 L, 1 M, 2 Q, 3 H), the five +// numbers at index ((v - 1) * 4 + e) * 5 are: EC codewords per block, group-1 blocks, data codewords +// per group-1 block, group-2 blocks, data codewords per group-2 block (ISO/IEC 18004 table 9). + +import Toybox.Lang; + +module QrTables { + const BLOCKS = [ + 7, 1, 19, 0, 0, 10, 1, 16, 0, 0, 13, 1, 13, 0, 0, 17, 1, 9, 0, 0, // version 1: L, M, Q, H + 10, 1, 34, 0, 0, 16, 1, 28, 0, 0, 22, 1, 22, 0, 0, 28, 1, 16, 0, 0, // version 2: L, M, Q, H + 15, 1, 55, 0, 0, 26, 1, 44, 0, 0, 18, 2, 17, 0, 0, 22, 2, 13, 0, 0, // version 3: L, M, Q, H + 20, 1, 80, 0, 0, 18, 2, 32, 0, 0, 26, 2, 24, 0, 0, 16, 4, 9, 0, 0, // version 4: L, M, Q, H + 26, 1, 108, 0, 0, 24, 2, 43, 0, 0, 18, 2, 15, 2, 16, 22, 2, 11, 2, 12, // version 5: L, M, Q, H + 18, 2, 68, 0, 0, 16, 4, 27, 0, 0, 24, 4, 19, 0, 0, 28, 4, 15, 0, 0, // version 6: L, M, Q, H + 20, 2, 78, 0, 0, 18, 4, 31, 0, 0, 18, 2, 14, 4, 15, 26, 4, 13, 1, 14, // version 7: L, M, Q, H + 24, 2, 97, 0, 0, 22, 2, 38, 2, 39, 22, 4, 18, 2, 19, 26, 4, 14, 2, 15, // version 8: L, M, Q, H + 30, 2, 116, 0, 0, 22, 3, 36, 2, 37, 20, 4, 16, 4, 17, 24, 4, 12, 4, 13, // version 9: L, M, Q, H + 18, 2, 68, 2, 69, 26, 4, 43, 1, 44, 24, 6, 19, 2, 20, 28, 6, 15, 2, 16, // version 10: L, M, Q, H + 20, 4, 81, 0, 0, 30, 1, 50, 4, 51, 28, 4, 22, 4, 23, 24, 3, 12, 8, 13, // version 11: L, M, Q, H + 24, 2, 92, 2, 93, 22, 6, 36, 2, 37, 26, 4, 20, 6, 21, 28, 7, 14, 4, 15, // version 12: L, M, Q, H + 26, 4, 107, 0, 0, 22, 8, 37, 1, 38, 24, 8, 20, 4, 21, 22, 12, 11, 4, 12, // version 13: L, M, Q, H + 30, 3, 115, 1, 116, 24, 4, 40, 5, 41, 20, 11, 16, 5, 17, 24, 11, 12, 5, 13, // version 14: L, M, Q, H + 22, 5, 87, 1, 88, 24, 5, 41, 5, 42, 30, 5, 24, 7, 25, 24, 11, 12, 7, 13, // version 15: L, M, Q, H + 24, 5, 98, 1, 99, 28, 7, 45, 3, 46, 24, 15, 19, 2, 20, 30, 3, 15, 13, 16, // version 16: L, M, Q, H + 28, 1, 107, 5, 108, 28, 10, 46, 1, 47, 28, 1, 22, 15, 23, 28, 2, 14, 17, 15, // version 17: L, M, Q, H + 30, 5, 120, 1, 121, 26, 9, 43, 4, 44, 28, 17, 22, 1, 23, 28, 2, 14, 19, 15, // version 18: L, M, Q, H + 28, 3, 113, 4, 114, 26, 3, 44, 11, 45, 26, 17, 21, 4, 22, 26, 9, 13, 16, 14, // version 19: L, M, Q, H + 28, 3, 107, 5, 108, 26, 3, 41, 13, 42, 30, 15, 24, 5, 25, 28, 15, 15, 10, 16, // version 20: L, M, Q, H + 28, 4, 116, 4, 117, 26, 17, 42, 0, 0, 28, 17, 22, 6, 23, 30, 19, 16, 6, 17, // version 21: L, M, Q, H + 28, 2, 111, 7, 112, 28, 17, 46, 0, 0, 30, 7, 24, 16, 25, 24, 34, 13, 0, 0, // version 22: L, M, Q, H + 30, 4, 121, 5, 122, 28, 4, 47, 14, 48, 30, 11, 24, 14, 25, 30, 16, 15, 14, 16, // version 23: L, M, Q, H + 30, 6, 117, 4, 118, 28, 6, 45, 14, 46, 30, 11, 24, 16, 25, 30, 30, 16, 2, 17, // version 24: L, M, Q, H + 26, 8, 106, 4, 107, 28, 8, 47, 13, 48, 30, 7, 24, 22, 25, 30, 22, 15, 13, 16, // version 25: L, M, Q, H + 28, 10, 114, 2, 115, 28, 19, 46, 4, 47, 28, 28, 22, 6, 23, 30, 33, 16, 4, 17, // version 26: L, M, Q, H + 30, 8, 122, 4, 123, 28, 22, 45, 3, 46, 30, 8, 23, 26, 24, 30, 12, 15, 28, 16, // version 27: L, M, Q, H + 30, 3, 117, 10, 118, 28, 3, 45, 23, 46, 30, 4, 24, 31, 25, 30, 11, 15, 31, 16, // version 28: L, M, Q, H + 30, 7, 116, 7, 117, 28, 21, 45, 7, 46, 30, 1, 23, 37, 24, 30, 19, 15, 26, 16, // version 29: L, M, Q, H + 30, 5, 115, 10, 116, 28, 19, 47, 10, 48, 30, 15, 24, 25, 25, 30, 23, 15, 25, 16, // version 30: L, M, Q, H + 30, 13, 115, 3, 116, 28, 2, 46, 29, 47, 30, 42, 24, 1, 25, 30, 23, 15, 28, 16, // version 31: L, M, Q, H + 30, 17, 115, 0, 0, 28, 10, 46, 23, 47, 30, 10, 24, 35, 25, 30, 19, 15, 35, 16, // version 32: L, M, Q, H + 30, 17, 115, 1, 116, 28, 14, 46, 21, 47, 30, 29, 24, 19, 25, 30, 11, 15, 46, 16, // version 33: L, M, Q, H + 30, 13, 115, 6, 116, 28, 14, 46, 23, 47, 30, 44, 24, 7, 25, 30, 59, 16, 1, 17, // version 34: L, M, Q, H + 30, 12, 121, 7, 122, 28, 12, 47, 26, 48, 30, 39, 24, 14, 25, 30, 22, 15, 41, 16, // version 35: L, M, Q, H + 30, 6, 121, 14, 122, 28, 6, 47, 34, 48, 30, 46, 24, 10, 25, 30, 2, 15, 64, 16, // version 36: L, M, Q, H + 30, 17, 122, 4, 123, 28, 29, 46, 14, 47, 30, 49, 24, 10, 25, 30, 24, 15, 46, 16, // version 37: L, M, Q, H + 30, 4, 122, 18, 123, 28, 13, 46, 32, 47, 30, 48, 24, 14, 25, 30, 42, 15, 32, 16, // version 38: L, M, Q, H + 30, 20, 117, 4, 118, 28, 40, 47, 7, 48, 30, 43, 24, 22, 25, 30, 10, 15, 67, 16, // version 39: L, M, Q, H + 30, 19, 118, 6, 119, 28, 18, 47, 31, 48, 30, 34, 24, 34, 25, 30, 20, 15, 61, 16 // version 40: L, M, Q, H + ]; + + // Alignment pattern centres (row and column coordinates) per version, from version 1. + const ALIGNMENT = [ + [], + [6, 18], + [6, 22], + [6, 26], + [6, 30], + [6, 34], + [6, 22, 38], + [6, 24, 42], + [6, 26, 46], + [6, 28, 50], + [6, 30, 54], + [6, 32, 58], + [6, 34, 62], + [6, 26, 46, 66], + [6, 26, 48, 70], + [6, 26, 50, 74], + [6, 30, 54, 78], + [6, 30, 56, 82], + [6, 30, 58, 86], + [6, 34, 62, 90], + [6, 28, 50, 72, 94], + [6, 26, 50, 74, 98], + [6, 30, 54, 78, 102], + [6, 28, 54, 80, 106], + [6, 32, 58, 84, 110], + [6, 30, 58, 86, 114], + [6, 34, 62, 90, 118], + [6, 26, 50, 74, 98, 122], + [6, 30, 54, 78, 102, 126], + [6, 26, 52, 78, 104, 130], + [6, 30, 56, 82, 108, 134], + [6, 34, 60, 86, 112, 138], + [6, 30, 58, 86, 114, 142], + [6, 34, 62, 90, 118, 146], + [6, 30, 54, 78, 102, 126, 150], + [6, 24, 50, 76, 102, 128, 154], + [6, 28, 54, 80, 106, 132, 158], + [6, 32, 58, 84, 110, 136, 162], + [6, 26, 54, 82, 110, 138, 166], + [6, 30, 58, 86, 114, 142, 170] + ]; +} diff --git a/source/codes/ReedSolomon.mc b/source/codes/ReedSolomon.mc new file mode 100644 index 0000000..a69fb1a --- /dev/null +++ b/source/codes/ReedSolomon.mc @@ -0,0 +1,110 @@ +// Author: cirakg & claude ai +// Summary: Reed-Solomon error-correction codewords over GF(2^m) for QR (and later Data Matrix and Aztec). +// +// A Field is GF(2^m) with a primitive polynomial and the exponent the +// generator's roots start at (QR: 0x11D, base 0; Data Matrix: 0x12D, +// base 1). Generator polynomials are cached per field and length. ecc() +// is the remainder of data * x^n divided by the generator: the standard +// systematic encoding, one multiply-accumulate per data symbol and EC +// symbol, so a caller budgets blocks per job step. + +import Toybox.Lang; + +module ReedSolomon { + + class Field { + var size as Number; + var exp as Array<Number>; + var log as Array<Number>; + var base as Number; + private var _generators as Dictionary<Number, Array<Number>> = {} as Dictionary<Number, Array<Number>>; + + // primitive: the field polynomial including x^m (0x11D for GF(256)). + function initialize(primitive as Number, fieldSize as Number, generatorBase as Number) { + size = fieldSize; + base = generatorBase; + exp = new [2 * fieldSize] as Array<Number>; + log = new [fieldSize] as Array<Number>; + var x = 1; + for (var i = 0; i < fieldSize - 1; i++) { + exp[i] = x; + log[x] = i; + x <<= 1; + if (x >= fieldSize) { + x ^= primitive; + } + } + // Doubled table: exp[a + b] needs no modulo. + for (var i = fieldSize - 1; i < 2 * fieldSize; i++) { + exp[i] = exp[i - (fieldSize - 1)]; + } + log[0] = 0; + } + + function multiply(a as Number, b as Number) as Number { + if (a == 0 || b == 0) { + return 0; + } + return exp[log[a] + log[b]]; + } + + // Coefficients of the generator with n roots, highest degree first, + // without the leading 1. + function generator(n as Number) as Array<Number> { + var cached = _generators[n]; + if (cached != null) { + return cached; + } + var g = [1] as Array<Number>; + for (var i = 0; i < n; i++) { + // Multiply by (x - a^(base + i)); in GF(2^m) minus is plus. + var root = exp[(base + i) % (size - 1)]; + var next = new [g.size() + 1] as Array<Number>; + for (var k = 0; k < next.size(); k++) { + next[k] = 0; + } + for (var k = 0; k < g.size(); k++) { + next[k] = next[k] ^ g[k]; + next[k + 1] = next[k + 1] ^ multiply(g[k], root); + } + g = next; + } + var coefficients = g.slice(1, null) as Array<Number>; + _generators[n] = coefficients; + return coefficients; + } + } + + // The n error-correction symbols of data (both as symbol values). + function ecc(field as Field, data as Array<Number>, n as Number) as Array<Number> { + var g = field.generator(n); + var rem = new [n] as Array<Number>; + for (var k = 0; k < n; k++) { + rem[k] = 0; + } + var exp = field.exp; + var log = field.log; + var logG = new [n] as Array<Number>; + for (var k = 0; k < n; k++) { + logG[k] = g[k] == 0 ? -1 : log[g[k]]; + } + for (var i = 0; i < data.size(); i++) { + var factor = data[i] ^ rem[0]; + // Shift the remainder left by one symbol. + for (var k = 0; k < n - 1; k++) { + rem[k] = rem[k + 1]; + } + rem[n - 1] = 0; + if (factor != 0) { + var lf = log[factor]; + for (var k = 0; k < n; k++) { + var lg = logG[k]; + if (lg >= 0) { + rem[k] = rem[k] ^ exp[lf + lg]; + } + } + } + } + return rem; + } +} |
