summaryrefslogtreecommitdiff
path: root/source/codes/Codes.mc
blob: 9527a75b9f6964869995c101d9f1accf20d3dfd8 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
// Author:  cirakg & claude ai
// Summary: Barcode symbology IDs, option bits, result types and the dispatcher to the encoders (README 12).
//
// A code entry stores its symbology in Entry.type and its options in
// Entry.options (bits below). encode() turns the payload text into either a
// LinearCode (one row of modules) or, from M6b, a matrix; validation errors
// come back as a CodeError with a message for the user, never an exception.

import Toybox.Lang;
import Toybox.System;

module Codes {

    // Symbology IDs (Entry.type of code entries). Stored: never renumber.
    const QR = 0;
    const CODE128 = 1;
    const CODE39 = 2;
    const EAN13 = 3;
    const EAN8 = 4;
    const UPCA = 5;
    const UPCE = 6;
    const ITF = 7;
    const CODABAR = 8;
    const DATAMATRIX = 9;
    const PDF417 = 10;
    const AZTEC = 11;

    const NAMES = ["QR", "Code 128", "Code 39", "EAN-13", "EAN-8", "UPC-A", "UPC-E", "ITF", "Codabar", "Data Matrix",
        "PDF417", "Aztec"];

    // Entry.options bits.
    const OPT_EC_MASK = 0x3;         // error correction: 0 low, 1 medium, 2 quartile, 3 high
    const OPT_CHECK_SHIFT = 2;       // check digit: 0 add or verify, 1 fix if wrong, 2 off
    const OPT_CHECK_MASK = 0xC;
    const OPT_SHOW_TEXT = 0x10;      // human-readable text under linear codes
    const DEFAULT_OPTIONS = 0x11;    // medium, add or verify, text on

    const CHECK_ADD_VERIFY = 0;
    const CHECK_FIX = 1;
    const CHECK_OFF = 2;

    // A row of modules: modules[i] is 1 for a bar, 0 for a space. The quiet
    // zone is not included (the renderer adds it).
    class LinearCode {
        var modules as ByteArray;
        // The human-readable text (digits as encoded, check digit included).
        var text as String;
        // A remark for the user (check digit added or fixed), or null.
        var note as String? = null;

        function initialize(rowModules as ByteArray, readable as String) {
            modules = rowModules;
            text = readable;
        }
    }

    // A 2D code ready to draw: width x height stored rows of modules,
    // row-major, 1 = dark; each stored row is rowHeight modules high (PDF417
    // rows are 3); plus the quiet zone its symbology needs (QR 4, Data
    // Matrix 1, PDF417 2).
    class Matrix {
        var width as Number;
        var height as Number;
        var quiet as Number;
        var rowHeight as Number = 1;
        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;
        }

        // Height in modules as drawn.
        function drawnHeight() as Number {
            return height * rowHeight;
        }

        // The cache form (Packed), in one go: tests and small codes. Jobs
        // use Packer.
        function toBytes() as ByteArray {
            var packer = new Packer(self);
            Jobs.runToCompletion(packer, 1);
            return packer.result;
        }

        // A matrix from its cache form, unpacked in one go (tests); the app
        // draws from Packed through RunFinder instead.
        static function fromBytes(data as ByteArray) as Matrix? {
            var packed = Packed.parse(data);
            if (packed == null) {
                return null;
            }
            var n = packed.width * packed.height;
            var bits = new [n]b;
            for (var i = 0; i < n; i++) {
                bits[i] = packed.moduleAt(i);
            }
            var matrix = new Matrix(packed.width, packed.height, packed.quiet, bits);
            matrix.rowHeight = packed.rowHeight;
            return matrix;
        }
    }

    // A matrix packed 8 modules per byte, as cached (sealed) in the vault:
    //     FORMAT (2) | width (2 bytes) | height (2 bytes) | quiet | rowHeight
    //     | modules, row-major, most significant bit first
    // Version 0.7 cached one byte of width, too few for wide PDF417
    // symbols; such a cache doesn't parse, and the code is encoded again.
    class Packed {
        static const FORMAT = 2;
        static const HEADER = 7;

        var width as Number;
        var height as Number;
        var quiet as Number;
        var rowHeight as Number;
        var bytes as ByteArray;

        function initialize(data as ByteArray) {
            bytes = data;
            width = (data[1] << 8) | data[2];
            height = (data[3] << 8) | data[4];
            quiet = data[5];
            rowHeight = data[6];
        }

        // The cache bytes as a Packed, or null when they are not format 2 or
        // the size doesn't match.
        static function parse(data as ByteArray?) as Packed? {
            if (data == null || data.size() < HEADER || data[0] != FORMAT) {
                return null;
            }
            var p = new Packed(data);
            var n = p.width * p.height;
            if (p.width == 0 || p.height == 0 || p.rowHeight == 0 || data.size() != HEADER + (n + 7) / 8) {
                return null;
            }
            return p;
        }

        function drawnHeight() as Number {
            return height * rowHeight;
        }

        // Module i (row-major), 1 = dark.
        function moduleAt(i as Number) as Number {
            return (bytes[HEADER + (i >> 3)] >> (7 - (i & 7))) & 1;
        }
    }

    // Matrix -> Packed bytes, BYTES_PER_STEP output bytes (8 modules each)
    // per step.
    class Packer extends Jobs.Job {
        static const BYTES_PER_STEP = 300;

        var result as ByteArray = []b;

        private var _matrix as Matrix;
        private var _i as Number = 0;

        function initialize(matrix as Matrix) {
            Job.initialize();
            _matrix = matrix;
            var w = matrix.width;
            var h = matrix.height;
            result = new [Packed.HEADER + (w * h + 7) / 8]b;
            result[0] = Packed.FORMAT;
            result[1] = (w >> 8) & 0xFF;
            result[2] = w & 0xFF;
            result[3] = (h >> 8) & 0xFF;
            result[4] = h & 0xFF;
            result[5] = matrix.quiet;
            result[6] = matrix.rowHeight;
        }

        // Progress 0-99 through the bytes.
        function percent() as Number {
            var full = _matrix.modules.size() / 8;
            return full > 0 ? 99 * _i / full : 0;
        }

        function step(budget as Number) as Boolean {
            var m = _matrix.modules;
            var n = m.size();
            var full = n / 8;
            var out = result;
            var stop = _i + BYTES_PER_STEP < full ? _i + BYTES_PER_STEP : full;
            for (var i = _i; i < stop; i++) {
                var k = i * 8;
                out[Packed.HEADER + i] = (m[k] << 7) | (m[k + 1] << 6) | (m[k + 2] << 5) | (m[k + 3] << 4)
                    | (m[k + 4] << 3) | (m[k + 5] << 2) | (m[k + 6] << 1) | m[k + 7];
            }
            _i = stop;
            if (_i < full) {
                return false;
            }
            for (var k = full * 8; k < n; k++) {
                out[Packed.HEADER + full] = out[Packed.HEADER + full] | (m[k] << (7 - k % 8));
            }
            return true;
        }
    }

    // A code as the dark runs of each stored row, ready to draw with one
    // fillRectangle per run (drawing module by module in onUpdate would be
    // too much for the watchdog with large codes). Each run is
    // (row << 20) | (first column << 10) | length.
    class Runs {
        var width as Number;
        var height as Number;
        var quiet as Number;
        var rowHeight as Number;
        var runs as Array<Number> = [] as Array<Number>;

        function initialize(packed as Packed) {
            width = packed.width;
            height = packed.height;
            quiet = packed.quiet;
            rowHeight = packed.rowHeight;
        }

        function drawnHeight() as Number {
            return height * rowHeight;
        }
    }

    // Packed -> Runs, about MODULES_PER_STEP modules (whole rows) per step;
    // all-light and all-dark bytes are taken eight modules at a time.
    class RunFinder extends Jobs.Job {
        static const MODULES_PER_STEP = 800;

        var result as Runs;

        private var _packed as Packed;
        private var _row as Number = 0;

        function initialize(packed as Packed) {
            Job.initialize();
            _packed = packed;
            result = new Runs(packed);
        }

        // Progress 0-99 through the rows.
        function percent() as Number {
            return 99 * _row / _packed.height;
        }

        function step(budget as Number) as Boolean {
            var p = _packed;
            var w = p.width;
            var bytes = p.bytes;
            var runs = result.runs;
            var rows = MODULES_PER_STEP / w > 0 ? MODULES_PER_STEP / w : 1;
            var last = _row + rows < p.height ? _row + rows : p.height;
            for (var y = _row; y < last; y++) {
                var bit = y * w;
                var end = bit + w;
                while (bit < end) {
                    var b = bytes[Packed.HEADER + (bit >> 3)];
                    if ((bit & 7) == 0 && bit + 8 <= end && b == 0) {
                        bit += 8;
                        continue;
                    }
                    if (((b >> (7 - (bit & 7))) & 1) == 0) {
                        bit++;
                        continue;
                    }
                    var start = bit;
                    while (bit < end) {
                        var c = bytes[Packed.HEADER + (bit >> 3)];
                        if ((bit & 7) == 0 && bit + 8 <= end && c == 0xFF) {
                            bit += 8;
                        } else if (((c >> (7 - (bit & 7))) & 1) == 1) {
                            bit++;
                        } else {
                            break;
                        }
                    }
                    runs.add((y << 20) | ((start - y * w) << 10) | (bit - start));
                }
            }
            _row = last;
            return _row >= p.height;
        }
    }

    // A 2D code from the cache or a fresh encoding to Runs: encode (unless
    // cached), pack (`cache` then holds the bytes to store), find the runs.
    // percent() follows the work: the encoder's share of 0-100 comes from
    // its stageEnds(), then the packer's, then the run finder's; a cached
    // code only runs the finder, over the whole range.
    class DisplayJob extends Jobs.Job {
        var error as String? = null;
        var cache as ByteArray? = null;
        var runs as Runs? = null;
        // Milliseconds spent in each stage's steps (encode, pack, runs), for
        // the debug log.
        var stageMs as Array<Number> = [0, 0, 0];

        private var _encoder as MatrixJob?;
        private var _packer as Packer? = null;
        private var _finder as RunFinder? = null;
        private var _encodeEnd as Number = 0;
        private var _packEnd as Number = 0;

        // Either an encoder (fresh) or the cached bytes.
        function initialize(encoder as MatrixJob?, packed as Packed?) {
            Job.initialize();
            _encoder = encoder;
            if (encoder != null) {
                var ends = encoder.stageEnds();
                _encodeEnd = ends[0];
                _packEnd = ends[1];
            }
            if (packed != null) {
                _finder = new RunFinder(packed);
            }
        }

        // Progress 0-99 (100 is never shown: the code replaces this screen),
        // never going back: each stage starts where the one before ended.
        function percent() as Number {
            var e = _encoder;
            var p = _packer;
            var f = _finder;
            var done = 0;
            if (e != null) {
                done = e.percent() * _encodeEnd / 100;
            } else if (p != null) {
                done = _encodeEnd + (_packEnd - _encodeEnd) * p.percent() / 100;
            } else if (f != null) {
                done = _packEnd + (100 - _packEnd) * f.percent() / 100;
            }
            return done < 99 ? done : 99;
        }

        function step(budget as Number) as Boolean {
            var started = System.getTimer();
            var stage = _finder != null ? 2 : (_packer != null ? 1 : 0);
            var done = advance(budget);
            stageMs[stage] += System.getTimer() - started;
            return done;
        }

        private function advance(budget as Number) as Boolean {
            var finder = _finder;
            if (finder != null) {
                if (!finder.step(budget)) {
                    return false;
                }
                runs = finder.result;
                _finder = null;
                return true;
            }
            var packer = _packer;
            if (packer != null) {
                if (packer.step(budget)) {
                    cache = packer.result;
                    _packer = null;
                    _finder = new RunFinder(new Packed(packer.result));
                }
                return false;
            }
            var encoder = _encoder as MatrixJob;
            if (!encoder.step(budget)) {
                return false;
            }
            var matrix = encoder.matrix();
            var err = encoder.error;
            if (err != null || matrix == null) {
                error = err != null ? err : "could not encode";
                return true;
            }
            _packer = new Packer(matrix);
            _encoder = null;
            return false;
        }
    }

    // A 2D encoder run as a resumable job (watchdog): when step() returns
    // true, either error is set or matrix() is the code.
    class MatrixJob extends Jobs.Job {
        var error as String? = null;

        function initialize() {
            Job.initialize();
        }

        // Progress 0-100 for the UI, following the work done so far.
        function percent() as Number {
            return 0;
        }

        // Where the pack and the run stages begin in a display job's 0-100
        // (the encoding is [0, ends[0]], packing [ends[0], ends[1]], the
        // runs the rest), weighted by their time in the simulator's
        // benchmark: encoding takes by far the most, except for PDF417,
        // whose wide matrices make the runs a third.
        function stageEnds() as Array<Number> {
            return [90, 92];
        }

        function matrix() as Matrix? {
            return null;
        }
    }

    // The job that encodes a 2D symbology, or null for linear ones. PDF417
    // chooses its columns for the screen, so it gets the screen's size.
    function matrixJob(symbology as Number, payload as String, options as Number, screenW as Number, screenH as Number,
            round as Boolean) as MatrixJob? {
        if (symbology == QR) {
            return new Qr.QrJob(payload, ecLevel(options), 0, -1);
        }
        if (symbology == DATAMATRIX) {
            return new DataMatrix.DataMatrixJob(payload);
        }
        if (symbology == PDF417) {
            return new Pdf417.Pdf417Job(payload, ecLevel(options), screenW, screenH, round);
        }
        if (symbology == AZTEC) {
            return new Aztec.AztecJob(payload, ecLevel(options));
        }
        return null;
    }

    // Smallest module size allowed (README 12.2): 1 px for linear codes and
    // PDF417 (a stacked linear code), 2 px for the square 2D codes.
    function minModulePx(symbology as Number) as Number {
        return isLinear(symbology) || symbology == PDF417 ? 1 : 2;
    }

    // Progress 0-100 through phases: ends[i] is the percent at the end of
    // phase i; `done` of `total` is how far the current phase has got (it
    // may be a flat 0 for a phase that reports no progress). A finished job
    // (phase past the last) is 100. Never goes back while `done` doesn't.
    function phased(ends as Array<Number>, phase as Number, done as Number, total as Number) as Number {
        if (phase >= ends.size()) {
            return 100;
        }
        var from = phase > 0 ? ends[phase - 1] : 0;
        var inside = total > 0 && done > 0 ? (done < total ? done : total) : 0;
        return from + (ends[phase] - from) * inside / (total > 0 ? total : 1);
    }

    // True when a code of w x h modules (quiet zone included) at px pixels
    // per module fits the screen: inside the circle (less a 4 px margin) on
    // round screens; elsewhere the full width and the height less room for
    // the name and ID lines.
    function fits(w as Number, h as Number, px as Number, screenW as Number, screenH as Number, round as Boolean) as Boolean {
        var width = w * px;
        var height = h * px;
        if (round) {
            var r = screenW / 2 - 4;
            return width.toLong() * width + height.toLong() * height <= 4l * r * r;
        }
        return width <= screenW - 4 && height <= screenH * 76 / 100;
    }

    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;

        function initialize(msg as String) {
            message = msg;
        }
    }

    function checkMode(options as Number) as Number {
        return (options & OPT_CHECK_MASK) >> OPT_CHECK_SHIFT;
    }

    function name(symbology as Number) as String {
        return symbology >= 0 && symbology < NAMES.size() ? NAMES[symbology] as String : "Code";
    }

    function isLinear(symbology as Number) as Boolean {
        return symbology >= CODE128 && symbology <= CODABAR;
    }

    // Encode a payload; unsupported symbologies give a CodeError until
    // their milestone (README 16, M6b-M6d).
    function encode(symbology as Number, payload as String, options as Number) as LinearCode or CodeError {
        var check = checkMode(options);
        if (symbology == EAN13) {
            return Ean.ean13(payload, check, "EAN-13", false);
        }
        if (symbology == UPCA) {
            return Ean.ean13(payload, check, "UPC-A", true);
        }
        if (symbology == EAN8) {
            return Ean.ean8(payload, check);
        }
        if (symbology == UPCE) {
            return Ean.upce(payload, check);
        }
        if (symbology == CODE128) {
            return Code128.encode(payload);
        }
        if (symbology == CODE39) {
            return Code39.encode(payload, check != CHECK_OFF);
        }
        if (symbology == ITF) {
            return Itf.encode(payload, check != CHECK_OFF);
        }
        if (symbology == CODABAR) {
            return Codabar.encode(payload);
        }
        return new CodeError(name(symbology) + " is not supported yet");
    }

    // Append bar/space runs to modules: widths alternate bar, space, ...,
    // starting with a bar when startWithBar.
    function addRuns(modules as ByteArray, widths as Array<Number>, startWithBar as Boolean) as Void {
        var bar = startWithBar;
        for (var i = 0; i < widths.size(); i++) {
            var w = widths[i];
            for (var k = 0; k < w; k++) {
                modules.add(bar ? 1 : 0);
            }
            bar = !bar;
        }
    }

    // Append a bit pattern of `count` modules, most significant bit first.
    function addBits(modules as ByteArray, bits as Number, count as Number) as Void {
        for (var i = count - 1; i >= 0; i--) {
            modules.add((bits >> i) & 1);
        }
    }

    // The digit values of text, or null when it holds anything else.
    function digits(text as String) as Array<Number>? {
        var chars = text.toCharArray();
        var out = [] as Array<Number>;
        for (var i = 0; i < chars.size(); i++) {
            var c = chars[i];
            if (c < '0' || c > '9') {
                return null;
            }
            out.add(c.toNumber() - 48);
        }
        return out;
    }

    function digitsText(values as Array<Number>) as String {
        var s = "";
        for (var i = 0; i < values.size(); i++) {
            s += values[i].toString();
        }
        return s;
    }
}