aboutsummaryrefslogtreecommitdiff
path: root/test/codes/QrTest.mc
blob: 2f81887378f586ba1ec0b67f01f5d70bb0dc0c43 (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
// Author:  cirakg & claude ai
// Summary: Tests for the QR encoder: segno reference matrices (forced and automatic version and mask), packed mask scores against a per-module reference, decoding dumps, capacity, timing.

import Toybox.Lang;
import Toybox.System;
import Toybox.Test;

(:test)
module QrFixtures {

    // Row-major modules packed 8 per byte, as hex (the CodeVectors format).
    function packedHex(code as Qr.MatrixCode) as String {
        var m = code.modules;
        var out = new [(m.size() + 7) / 8]b;
        for (var i = 0; i < m.size(); i++) {
            if (m[i] == 1) {
                out[i / 8] = out[i / 8] | (0x80 >> (i % 8));
            }
        }
        return Bytes.toHex(out);
    }
}

// The mask penalty computed module by module, the way the encoder did
// before M8 packed its lines: the reference for the packed scorer. It
// scores a finished symbol (forced mask) with its format and version areas
// made light again, as the encoder scores them.
(:test)
module QrReference {

    function penalty(code as Qr.MatrixCode) as Number {
        var n = code.size;
        var m = Bytes.copy(code.modules);
        for (var i = 0; i <= 8; i++) {
            if (i != 6) {
                m[i * n + 8] = 0;
                m[8 * n + i] = 0;
            }
        }
        for (var i = 0; i < 8; i++) {
            m[8 * n + n - 1 - i] = 0;
            m[(n - 1 - i) * n + 8] = 0;
        }
        m[(n - 8) * n + 8] = 0;
        if (code.version >= 7) {
            for (var i = 0; i < 18; i++) {
                var a = n - 11 + i % 3;
                var b = i / 3;
                m[b * n + a] = 0;
                m[a * n + b] = 0;
            }
        }
        var score = 0;
        var dark = 0;
        for (var y = 0; y < n; y++) {
            var row = m.slice(y * n, y * n + n);
            score += line(row);
            for (var x = 0; x < n; x++) {
                dark += row[x];
                if (y > 0 && x > 0 && row[x] == row[x - 1] && row[x] == m[(y - 1) * n + x] && row[x] == m[(y - 1) * n + x - 1]) {
                    score += 3;
                }
            }
        }
        for (var x = 0; x < n; x++) {
            var column = new [n]b;
            for (var y = 0; y < n; y++) {
                column[y] = m[y * n + x];
            }
            score += line(column);
        }
        var total = n * n;
        return score + 10 * ((dark * 100 - 50 * total).abs() / (5 * total));
    }

    function line(l as ByteArray) as Number {
        var n = l.size();
        var score = 0;
        var run = 1;
        var window = l[0];
        var nextAllowed = 0;
        for (var k = 1; k < n; k++) {
            var c = l[k];
            if (c == l[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 || light(l, idx - 4 < 0 ? 0 : idx - 4, idx)
                            || light(l, after, after + 4 > n ? n : after + 4)) {
                        score += 40;
                        nextAllowed = idx + 7;
                    } else {
                        nextAllowed = idx + 4;
                    }
                }
            }
        }
        return run >= 5 ? score + run - 2 : score;
    }

    function light(l as ByteArray, from as Number, to as Number) as Boolean {
        for (var i = from; i < to; i++) {
            if (l[i] != 0) {
                return false;
            }
        }
        return true;
    }
}

// The packed scorer gives every mask exactly the per-module penalty, for
// small and large versions, every level, and versions with version areas.
(:test)
function qrPackedScoresMatchReference(logger as Test.Logger) as Boolean {
    var cases = [["https://example.com/member/12345", 1, 0], ["HELLO WORLD 1234567890", 3, 0], ["Ünïcödé · 2026", 0, 7],
        ["a", 2, 0], ["Version ten payload", 1, 10], ["0123456789", 0, 21]];
    for (var c = 0; c < cases.size(); c++) {
        var row = cases[c] as Array;
        var job = new Qr.QrJob(row[0] as String, row[1] as Number, row[2] as Number, -1);
        Jobs.runToCompletion(job, 1);
        if (job.scores.size() != 8) {
            logger.error("case " + c + ": " + job.scores.size() + " scores");
            return false;
        }
        for (var mask = 0; mask < 8; mask++) {
            var forced = Qr.encode(row[0] as String, row[1] as Number, (job.result as Qr.MatrixCode).version, mask);
            var expected = QrReference.penalty(forced as Qr.MatrixCode);
            if (job.scores[mask] != expected) {
                logger.error("case " + c + " mask " + mask + ": packed " + job.scores[mask] + ", reference " + expected);
                return false;
            }
        }
    }
    return true;
}

// README 12.4 check 1 for QR: identical to segno with forced version and
// mask, and the same automatic version and mask choice.
(:test)
function qrMatchesSegno(logger as Test.Logger) as Boolean {
    var rows = CodeVectors.QR;
    for (var i = 0; i < rows.size(); i++) {
        var row = rows[i] as Array;
        var payload = row[0] as String;
        var result = Qr.encode(payload, row[1] as Number, row[2] as Number, row[3] as Number);
        if (result instanceof Codes.CodeError) {
            logger.error("row " + i + ": " + result.message);
            return false;
        }
        var expectedVersion = row[4] as Number;
        var expectedMask = row[5] as Number;
        if (result.version != expectedVersion || result.mask != expectedMask) {
            logger.error("row " + i + " '" + Text.clip(payload, 20) + "': version " + result.version + " mask " + result.mask
                + ", segno " + expectedVersion + " / " + expectedMask);
            return false;
        }
        var got = QrFixtures.packedHex(result);
        var expected = row[6] as String;
        if (!got.equals(expected)) {
            logger.error("row " + i + " '" + Text.clip(payload, 20) + "' (version " + result.version + ") matrix differs");
            logger.debug("QRGOT " + got);
            return false;
        }
    }
    logger.debug("CODES qr reference rows=" + rows.size());
    return true;
}

// README 12.4 check 2: mixed-mode (segmented), Unicode and long payloads,
// decoded by tools/check_codes.py with zxing-cpp.
(:test)
function qrDumpForDecoding(logger as Test.Logger) as Boolean {
    var payloads = [
        "ABC12345678901234xyz", "Membership 0042-7731-55 · Zürich", "12345678901234567890HELLO WORLD hello world",
        "WIFI:T:WPA;S:FakeNet;P:not-a-real-password;;", "😀 emoji and Ünïcödé", "a"
    ];
    var seed = 777;
    for (var i = 0; i < 24; i++) {
        var text = "";
        var length = 1 + i * 13 % 90;
        for (var k = 0; k < length; k++) {
            seed = (seed * 1103515245 + 12345) & 0x7FFFFFFF;
            var pool = (seed >> 16) % 3 == 0 ? "0123456789" : ((seed >> 16) % 3 == 1 ? "ABCXYZ $%*+-./:" : "abcxyz,!?é");
            var at = (seed >> 4) % pool.length();
            text += pool.substring(at, at + 1);
        }
        payloads.add(text);
    }
    for (var i = 0; i < payloads.size(); i++) {
        var text = payloads[i] as String;
        var result = Qr.encode(text, i % 4, 0, -1);
        if (result instanceof Codes.CodeError) {
            logger.error("'" + text + "': " + result.message);
            return false;
        }
        logger.debug("QRDUMP " + result.size + " " + Bytes.toHex(Bytes.fromUtf8(text)) + " " + QrFixtures.packedHex(result));
    }
    return true;
}

(:test)
function qrReportsCapacity(logger as Test.Logger) as Boolean {
    var big = "";
    for (var i = 0; i < 300; i++) {
        big += "abcdefghij";
    }
    var tooLong = Qr.encode(big, 3, 0, -1);
    var forced = Qr.encode("hello world, too long for version 1", 3, 1, -1);
    var empty = Qr.encode("", 1, 0, -1);
    return tooLong instanceof Codes.CodeError && (tooLong.message.find("too long") != null)
        && forced instanceof Codes.CodeError && empty instanceof Codes.CodeError;
}

// Timing and step counts for PERFORMANCE.md (simulator).
(:test)
function qrBenchmark(logger as Test.Logger) as Boolean {
    var text = "";
    for (var i = 0; i < 20; i++) {
        text += "abcdefghij";
    }
    var inputs = ["https://example.com/member/12345", text, text + text + text + text + text];
    for (var i = 0; i < inputs.size(); i++) {
        var job = new Qr.QrJob(inputs[i] as String, 1, 0, -1);
        var steps = 0;
        var start = System.getTimer();
        while (!job.step(1)) {
            steps++;
        }
        var code = job.result as Qr.MatrixCode;
        logger.debug("BENCH qr_v" + code.version + " ms=" + (System.getTimer() - start) + " steps=" + steps);
    }
    return true;
}