@sythos/js_barcode_universal 1.2.5 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/LICENSE +8 -1
  2. package/NOTICE.md +3 -0
  3. package/README.md +548 -463
  4. package/bundle/sythos-barcode.esm.js +2377 -3
  5. package/bundle/sythos-barcode.js +2365 -3
  6. package/examples/create.html +1009 -730
  7. package/licenses/README.md +35 -58
  8. package/licenses/aztec-code.license +15 -13
  9. package/licenses/codabar.license +18 -15
  10. package/licenses/code-11.license +11 -9
  11. package/licenses/code-128.license +10 -8
  12. package/licenses/code-39.license +10 -8
  13. package/licenses/code-93.license +16 -14
  14. package/licenses/data-matrix.license +15 -13
  15. package/licenses/ean-13.license +10 -8
  16. package/licenses/ean-8.license +10 -8
  17. package/licenses/frameqr.license +84 -0
  18. package/licenses/gs1-128.license +10 -8
  19. package/licenses/isbn.license +10 -8
  20. package/licenses/itf-14.license +10 -8
  21. package/licenses/itf.license +9 -7
  22. package/licenses/micro-qr.license +79 -0
  23. package/licenses/micropdf417.license +52 -67
  24. package/licenses/msi-plessey.license +13 -11
  25. package/licenses/pdf417.license +78 -37
  26. package/licenses/pharmacode.license +14 -12
  27. package/licenses/qr-code.license +9 -7
  28. package/licenses/rmqr.license +79 -0
  29. package/licenses/upc-a.license +12 -10
  30. package/licenses/upc-e.license +10 -8
  31. package/package.json +97 -88
  32. package/src/datamatrix/decoder.js +262 -262
  33. package/src/datamatrix/detector.js +225 -225
  34. package/src/datamatrix/encoder.js +191 -191
  35. package/src/datamatrix/index.js +42 -42
  36. package/src/datamatrix/tables.js +123 -123
  37. package/src/frameqr/decoder.js +239 -0
  38. package/src/frameqr/detector.js +192 -0
  39. package/src/frameqr/encoder.js +156 -0
  40. package/src/frameqr/index.js +42 -0
  41. package/src/frameqr/tables.js +270 -0
  42. package/src/index.js +91 -2
  43. package/src/microqr/decoder.js +245 -0
  44. package/src/microqr/detector.js +355 -0
  45. package/src/microqr/encoder.js +269 -0
  46. package/src/microqr/index.js +36 -0
  47. package/src/microqr/tables.js +316 -0
  48. package/src/oned/index.js +59 -59
  49. package/src/rmqr/decoder.js +101 -0
  50. package/src/rmqr/detector.js +90 -0
  51. package/src/rmqr/encoder.js +172 -0
  52. package/src/rmqr/index.js +37 -0
  53. package/src/rmqr/tables.js +154 -0
@@ -0,0 +1,245 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * SPDX-License-Identifier: MIT
27
+ *
28
+ * Original work. No code from any other barcode implementation.
29
+ */
30
+
31
+ /**
32
+ * Micro QR decoder for an already sampled M1-M4 module matrix.
33
+ *
34
+ * @module microqr/decoder
35
+ */
36
+
37
+ import { ChecksumError, FormatError } from '../core/errors.js';
38
+ import { GF256_QR } from '../core/galois-field.js';
39
+ import { rsDecode } from '../core/reed-solomon.js';
40
+ import {
41
+ microQrBlockLayout,
42
+ microQrDataModuleOrder,
43
+ microQrDecodeFormatInfo,
44
+ microQrFormatInfoPositions,
45
+ microQrMaskBit,
46
+ } from './tables.js';
47
+
48
+ const ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
49
+ const MODE_NAMES = ['numeric', 'alphanumeric', 'byte', 'kanji'];
50
+ const COUNT_BITS = {
51
+ numeric: [0, 3, 4, 5, 6],
52
+ alphanumeric: [0, 0, 3, 4, 5],
53
+ byte: [0, 0, 0, 4, 5],
54
+ kanji: [0, 0, 0, 3, 4],
55
+ };
56
+
57
+ class LimitedBitReader {
58
+ constructor(bytes, limit) {
59
+ this.bytes = bytes;
60
+ this.limit = limit;
61
+ this.offset = 0;
62
+ }
63
+
64
+ available() { return this.limit - this.offset; }
65
+
66
+ read(count) {
67
+ if (!Number.isInteger(count) || count < 1 || count > 32 || count > this.available()) {
68
+ throw new FormatError(`Micro QR: needed ${count} bits, ${Math.max(0, this.available())} remain`);
69
+ }
70
+ let value = 0;
71
+ for (let i = 0; i < count; i++, this.offset++) {
72
+ value = (value << 1) | ((this.bytes[this.offset >>> 3] >>> (7 - (this.offset & 7))) & 1);
73
+ }
74
+ return value >>> 0;
75
+ }
76
+ }
77
+
78
+ function moduleAt(matrix, x, y, mirrored) {
79
+ return mirrored ? matrix.get(y, x) : matrix.get(x, y);
80
+ }
81
+
82
+ function readFormat(matrix, expectedVersion, mirrored) {
83
+ let bits = 0;
84
+ const positions = microQrFormatInfoPositions(matrix.width);
85
+ for (let i = 0; i < positions.length; i++) {
86
+ const [x, y] = positions[i];
87
+ if (moduleAt(matrix, x, y, mirrored)) bits |= 1 << i;
88
+ }
89
+ const format = microQrDecodeFormatInfo(bits);
90
+ if (!format) throw new FormatError('Micro QR: format information is unreadable');
91
+ if (format.version !== expectedVersion) {
92
+ throw new FormatError(
93
+ `Micro QR: format identifies ${format.version}, but the matrix dimension identifies ${expectedVersion}`,
94
+ );
95
+ }
96
+ return format;
97
+ }
98
+
99
+ function readCodewords(matrix, layout, mask, mirrored) {
100
+ const order = microQrDataModuleOrder(layout.version);
101
+ const data = new Array(layout.dataCodewords).fill(0);
102
+ const ecc = new Array(layout.eccCodewords).fill(0);
103
+ let streamOffset = 0;
104
+
105
+ const readBit = () => {
106
+ const x = order[streamOffset * 2];
107
+ const y = order[streamOffset * 2 + 1];
108
+ if (x === undefined || y === undefined) throw new FormatError('Micro QR: encoding region is truncated');
109
+ streamOffset++;
110
+ return moduleAt(matrix, x, y, mirrored) !== microQrMaskBit(mask, x, y) ? 1 : 0;
111
+ };
112
+ const readInto = (target, index, count, highBit = 7) => {
113
+ for (let bit = highBit; bit > highBit - count; bit--) target[index] |= readBit() << bit;
114
+ };
115
+
116
+ const fullData = layout.shortDataCodewordBits === 4 ? layout.dataCodewords - 1 : layout.dataCodewords;
117
+ for (let i = 0; i < fullData; i++) readInto(data, i, 8);
118
+ if (layout.shortDataCodewordBits === 4) readInto(data, data.length - 1, 4);
119
+ for (let i = 0; i < ecc.length; i++) readInto(ecc, i, 8);
120
+
121
+ if (streamOffset !== order.length / 2) {
122
+ throw new FormatError(`Micro QR: read ${streamOffset} of ${order.length / 2} encoding modules`);
123
+ }
124
+ return data.concat(ecc);
125
+ }
126
+
127
+ function correctCodewords(received, layout) {
128
+ const corrections = rsDecode(received, layout.eccCodewords, GF256_QR, 0);
129
+ if (layout.version === 'M1' && corrections !== 0) {
130
+ throw new ChecksumError('Micro QR: M1 provides error detection only');
131
+ }
132
+ return { data: Uint8Array.from(received.slice(0, layout.dataCodewords)), corrections };
133
+ }
134
+
135
+ function decodeKanjiValue(value) {
136
+ const combined = (Math.floor(value / 0xc0) << 8) | (value % 0xc0);
137
+ const sjis = combined + (combined < 0x1f00 ? 0x8140 : 0xc140);
138
+ const bytes = Uint8Array.of(sjis >>> 8, sjis & 0xff);
139
+ try {
140
+ return new TextDecoder('shift_jis', { fatal: true }).decode(bytes);
141
+ } catch {
142
+ throw new FormatError(`Micro QR: invalid Kanji value ${value}`);
143
+ }
144
+ }
145
+
146
+ function parsePayload(data, version, dataBits) {
147
+ const reader = new LimitedBitReader(data, dataBits);
148
+ const modeValue = version === 1 ? 0 : reader.read(version - 1);
149
+ if (modeValue > 3 || (version === 2 && modeValue > 1)) {
150
+ throw new FormatError(`Micro QR: mode indicator ${modeValue} is unavailable in M${version}`);
151
+ }
152
+ const mode = MODE_NAMES[modeValue];
153
+ const countWidth = COUNT_BITS[mode][version];
154
+ if (!countWidth) throw new FormatError(`Micro QR: ${mode} mode is unavailable in M${version}`);
155
+ const count = reader.read(countWidth);
156
+ if (count === 0) throw new FormatError('Micro QR: zero-length data segment');
157
+
158
+ let text = '';
159
+ const rawBytes = [];
160
+ if (mode === 'numeric') {
161
+ let remaining = count;
162
+ while (remaining >= 3) {
163
+ const value = reader.read(10);
164
+ if (value >= 1000) throw new FormatError(`Micro QR: invalid numeric triplet ${value}`);
165
+ text += String(value).padStart(3, '0');
166
+ remaining -= 3;
167
+ }
168
+ if (remaining === 2) {
169
+ const value = reader.read(7);
170
+ if (value >= 100) throw new FormatError(`Micro QR: invalid numeric pair ${value}`);
171
+ text += String(value).padStart(2, '0');
172
+ } else if (remaining === 1) {
173
+ const value = reader.read(4);
174
+ if (value >= 10) throw new FormatError(`Micro QR: invalid numeric digit ${value}`);
175
+ text += String(value);
176
+ }
177
+ } else if (mode === 'alphanumeric') {
178
+ let remaining = count;
179
+ while (remaining >= 2) {
180
+ const value = reader.read(11);
181
+ if (value >= 45 * 45) throw new FormatError(`Micro QR: invalid alphanumeric pair ${value}`);
182
+ text += ALPHANUMERIC[Math.floor(value / 45)] + ALPHANUMERIC[value % 45];
183
+ remaining -= 2;
184
+ }
185
+ if (remaining === 1) {
186
+ const value = reader.read(6);
187
+ if (value >= 45) throw new FormatError(`Micro QR: invalid alphanumeric value ${value}`);
188
+ text += ALPHANUMERIC[value];
189
+ }
190
+ } else if (mode === 'byte') {
191
+ for (let i = 0; i < count; i++) {
192
+ const value = reader.read(8);
193
+ rawBytes.push(value);
194
+ text += String.fromCharCode(value);
195
+ }
196
+ } else {
197
+ for (let i = 0; i < count; i++) text += decodeKanjiValue(reader.read(13));
198
+ }
199
+ return { text, bytes: Uint8Array.from(rawBytes), mode };
200
+ }
201
+
202
+ function decodeOrientation(matrix, expectedVersion, mirrored) {
203
+ const format = readFormat(matrix, expectedVersion, mirrored);
204
+ const layout = microQrBlockLayout(format.version, format.ecc);
205
+ const received = readCodewords(matrix, layout, format.mask, mirrored);
206
+ const { data, corrections } = correctCodewords(received, layout);
207
+ const payload = parsePayload(data, Number(format.version.slice(1)), layout.dataBits);
208
+ return {
209
+ text: payload.text,
210
+ bytes: payload.bytes,
211
+ mode: payload.mode,
212
+ version: format.version,
213
+ ecc: format.ecc,
214
+ mask: format.mask,
215
+ corrections,
216
+ formatCorrections: format.correctedBits,
217
+ mirrored,
218
+ };
219
+ }
220
+
221
+ /** Decode a sampled Micro QR Code symbol without its quiet zone. */
222
+ export function decodeMicroQR(matrix) {
223
+ if (!matrix || !Number.isInteger(matrix.width) || typeof matrix.get !== 'function') {
224
+ throw new FormatError('Micro QR: no matrix supplied');
225
+ }
226
+ if (matrix.height !== matrix.width) {
227
+ throw new FormatError(`Micro QR: symbol must be square, got ${matrix.width}x${matrix.height}`);
228
+ }
229
+ const version = (matrix.width - 9) / 2;
230
+ if (!Number.isInteger(version) || version < 1 || version > 4) {
231
+ throw new FormatError(`Micro QR: ${matrix.width} modules is not a valid M1-M4 symbol size`);
232
+ }
233
+ const expectedVersion = `M${version}`;
234
+ try {
235
+ return decodeOrientation(matrix, expectedVersion, false);
236
+ } catch (primaryError) {
237
+ try {
238
+ return decodeOrientation(matrix, expectedVersion, true);
239
+ } catch {
240
+ throw primaryError;
241
+ }
242
+ }
243
+ }
244
+
245
+ export { ChecksumError, FormatError };
@@ -0,0 +1,355 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ *
8
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
9
+ * of this software and associated documentation files (the "Software"), to deal
10
+ * in the Software without restriction, including without limitation the rights
11
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
+ * copies of the Software, and to permit persons to whom the Software is
13
+ * furnished to do so, subject to the following conditions:
14
+ *
15
+ * The above copyright notice and this permission notice shall be included in all
16
+ * copies or substantial portions of the Software.
17
+ *
18
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
+ * SOFTWARE.
25
+ *
26
+ * SPDX-License-Identifier: MIT
27
+ *
28
+ * Original work. No code from any other barcode implementation.
29
+ */
30
+
31
+ /**
32
+ * Micro QR detection in binarized rasters.
33
+ *
34
+ * A Micro QR symbol has one 7x7 finder in its top-left corner. That alone is
35
+ * not enough to distinguish it from one corner of a normal QR Code, so every
36
+ * candidate is also required to have the Micro QR timing arms and a format
37
+ * word which the decoder accepts. The decoder is deliberately the final
38
+ * geometric arbiter; BCH and Reed--Solomon verification make accidental
39
+ * acceptance of ordinary square artwork very unlikely.
40
+ *
41
+ * Finder geometry supplies two local axes. Timing arms refine their lengths,
42
+ * while a small fourth-corner search lets projective sampling absorb mild
43
+ * perspective despite the format having no remote alignment pattern.
44
+ *
45
+ * @module microqr/detector
46
+ */
47
+
48
+ import { BitMatrix } from '../core/bit-matrix.js';
49
+ import { NotFoundError } from '../core/errors.js';
50
+ import { sampleQuad } from '../image/grid-sampler.js';
51
+ import { decodeMicroQR } from './decoder.js';
52
+
53
+ /** Legal Micro QR side lengths (M1 through M4). */
54
+ const DIMENSIONS = [11, 13, 15, 17];
55
+
56
+ /** @typedef {{x:number, y:number}} Point */
57
+
58
+ /**
59
+ * @typedef {object} Detection
60
+ * @property {Point[]} corners Outer corners in reading order.
61
+ * @property {number} dimension Side length in modules.
62
+ * @property {'M1'|'M2'|'M3'|'M4'} version
63
+ * @property {number} moduleSize Estimated pixels per module at the finder.
64
+ * @property {number} rotation Clockwise orientation of the source raster.
65
+ * @property {boolean} inverted Whether the detected symbol used inverted polarity.
66
+ * @property {BitMatrix} matrix Rectified, normally polarised module matrix.
67
+ */
68
+
69
+ function rotateVector(vector) {
70
+ return { x: -vector.y, y: vector.x };
71
+ }
72
+
73
+ function add(point, a, av, b, bv) {
74
+ return { x: point.x + a.x * av + b.x * bv, y: point.y + a.y * av + b.y * bv };
75
+ }
76
+
77
+ function sample(image, point) {
78
+ const x = Math.round(point.x);
79
+ const y = Math.round(point.y);
80
+ if (x < 0 || y < 0 || x >= image.width || y >= image.height) return null;
81
+ return image.get(x, y);
82
+ }
83
+
84
+ function expectedFinder(x, y) {
85
+ return x === 0 || y === 0 || x === 6 || y === 6 ||
86
+ (x >= 2 && x <= 4 && y >= 2 && y <= 4);
87
+ }
88
+
89
+ /** Connected components matching one polarity, capped to plausible centre blocks. */
90
+ function components(image, value) {
91
+ const seen = new Uint8Array(image.width * image.height);
92
+ const result = [];
93
+ const maximumArea = Math.max(16, Math.floor(image.width * image.height * 0.08));
94
+
95
+ for (let y = 0; y < image.height; y++) for (let x = 0; x < image.width; x++) {
96
+ const start = y * image.width + x;
97
+ if (seen[start] || image.get(x, y) !== value) continue;
98
+
99
+ const queueX = [x];
100
+ const queueY = [y];
101
+ seen[start] = 1;
102
+ let head = 0;
103
+ let minX = x; let maxX = x; let minY = y; let maxY = y;
104
+
105
+ while (head < queueX.length) {
106
+ const px = queueX[head];
107
+ const py = queueY[head++];
108
+ if (px < minX) minX = px;
109
+ if (px > maxX) maxX = px;
110
+ if (py < minY) minY = py;
111
+ if (py > maxY) maxY = py;
112
+ for (const [nx, ny] of [[px - 1, py], [px + 1, py], [px, py - 1], [px, py + 1]]) {
113
+ if (nx < 0 || ny < 0 || nx >= image.width || ny >= image.height) continue;
114
+ const index = ny * image.width + nx;
115
+ if (!seen[index] && image.get(nx, ny) === value) {
116
+ seen[index] = 1;
117
+ queueX.push(nx);
118
+ queueY.push(ny);
119
+ }
120
+ }
121
+ }
122
+
123
+ const width = maxX - minX + 1;
124
+ const height = maxY - minY + 1;
125
+ const area = width * height;
126
+ if (queueX.length > maximumArea || Math.min(width, height) < 2) continue;
127
+ if (Math.max(width, height) > Math.min(width, height) * 1.7) continue;
128
+ if (queueX.length < area * 0.42) continue;
129
+ result.push({
130
+ x: (minX + maxX) / 2,
131
+ y: (minY + maxY) / 2,
132
+ width,
133
+ height,
134
+ pixels: queueX.length,
135
+ });
136
+ }
137
+
138
+ return result.sort((a, b) => b.pixels - a.pixels).slice(0, 256);
139
+ }
140
+
141
+ /** Score the complete 7x7 finder at module centres. */
142
+ function finderScore(image, centre, u, v, pitch, inverted) {
143
+ let correct = 0;
144
+ let total = 0;
145
+ for (let y = 0; y < 7; y++) for (let x = 0; x < 7; x++) {
146
+ const actual = sample(image, add(centre, u, (x - 3) * pitch, v, (y - 3) * pitch));
147
+ if (actual === null) continue;
148
+ const wanted = inverted ? !expectedFinder(x, y) : expectedFinder(x, y);
149
+ if (actual === wanted) correct++;
150
+ total++;
151
+ }
152
+ return total === 49 ? correct / total : 0;
153
+ }
154
+
155
+ /** Validate separator, timing arms and a sparse quiet-zone outline. */
156
+ function structureScore(image, centre, u, v, pitch, dimension, sx, sy, inverted) {
157
+ let correct = 0;
158
+ let total = 0;
159
+ const check = (x, y, dark) => {
160
+ const point = add(centre, u, (x - 3) * pitch * sx, v, (y - 3) * pitch * sy);
161
+ const actual = sample(image, point);
162
+ if (actual !== null && actual === (inverted ? !dark : dark)) correct++;
163
+ total++;
164
+ };
165
+
166
+ // The light separator lies between the finder and encoding region.
167
+ for (let i = 0; i <= 7; i++) {
168
+ check(7, i, false);
169
+ check(i, 7, false);
170
+ }
171
+ // Both timing arms start dark at coordinate 8 and alternate to the edge.
172
+ for (let i = 8; i < dimension; i++) {
173
+ check(i, 0, (i & 1) === 0);
174
+ check(0, i, (i & 1) === 0);
175
+ }
176
+ // A quiet-zone sample just beyond each edge rejects an isolated normal-QR
177
+ // finder and most decorative squares without requiring a perfect crop.
178
+ for (let i = 0; i < dimension; i += 2) {
179
+ check(i, -1.25, false);
180
+ check(-1.25, i, false);
181
+ check(i, dimension + 0.75, false);
182
+ check(dimension + 0.75, i, false);
183
+ }
184
+ return correct / total;
185
+ }
186
+
187
+ function invert(matrix) {
188
+ const out = matrix.clone();
189
+ for (let y = 0; y < out.height; y++) for (let x = 0; x < out.width; x++) out.flip(x, y);
190
+ return out;
191
+ }
192
+
193
+ function orientationDegrees(u) {
194
+ const degrees = Math.atan2(u.y, u.x) * 180 / Math.PI;
195
+ return ((Math.round(degrees / 90) * 90) % 360 + 360) % 360;
196
+ }
197
+
198
+ function cornersFor(centre, u, v, pitch, dimension, sx, sy, dx = 0, dy = 0) {
199
+ const tl = add(centre, u, -3.5 * pitch, v, -3.5 * pitch);
200
+ const tr = add(tl, u, dimension * pitch * sx, v, 0);
201
+ const bl = add(tl, u, 0, v, dimension * pitch * sy);
202
+ const br = add(add(tr, v, dimension * pitch * sy, u, 0), u, dx * pitch, v, dy * pitch);
203
+ return [tl, tr, br, bl];
204
+ }
205
+
206
+ function candidateKey(detection) {
207
+ const centre = detection.finderCentre;
208
+ return `${Math.round(centre.x)},${Math.round(centre.y)},${detection.dimension}`;
209
+ }
210
+
211
+ function sameCandidate(left, right) {
212
+ if (left.dimension !== right.dimension) return false;
213
+ const centre = (detection) => detection.corners.reduce(
214
+ (sum, point) => ({ x: sum.x + point.x / 4, y: sum.y + point.y / 4 }),
215
+ { x: 0, y: 0 },
216
+ );
217
+ const a = centre(left);
218
+ const b = centre(right);
219
+ const tolerance = Math.max(left.moduleSize, right.moduleSize) * 2;
220
+ return Math.hypot(a.x - b.x, a.y - b.y) < tolerance;
221
+ }
222
+
223
+ /**
224
+ * Find Micro QR symbols in a binarized raster.
225
+ *
226
+ * The search accepts arbitrary in-plane angles, including all quarter-turns.
227
+ * Non-integer scale is supported through centre sampling. Mild projective
228
+ * distortion is handled by searching the unconstrained fourth corner.
229
+ *
230
+ * @param {BitMatrix} binaryImage Set bit = dark.
231
+ * @returns {Detection[]} Best candidate first; empty when no symbol is found.
232
+ */
233
+ export function detectMicroQR(binaryImage) {
234
+ if (!binaryImage || !binaryImage.width || !binaryImage.height) {
235
+ throw new NotFoundError('detectMicroQR: no image supplied');
236
+ }
237
+
238
+ const detections = [];
239
+ const seen = new Set();
240
+
241
+ for (const inverted of [false, true]) {
242
+ for (const centre of components(binaryImage, !inverted)) {
243
+ for (let degrees = 0; degrees < 180; degrees += 3) {
244
+ const angle = degrees * Math.PI / 180;
245
+ const axis = { x: Math.cos(angle), y: Math.sin(angle) };
246
+ const perpendicular = rotateVector(axis);
247
+ const footprint = Math.abs(axis.x) + Math.abs(axis.y);
248
+ const pitch = ((centre.width + centre.height) / 2) / (3 * footprint);
249
+ if (pitch < 0.75) continue;
250
+
251
+ // The finder is rotationally symmetric; four turns decide which pair
252
+ // of arms points into the encoding region.
253
+ for (let turn = 0, u = axis, v = perpendicular; turn < 4; turn++) {
254
+ if (turn > 0) { u = v; v = { x: -u.y, y: u.x }; }
255
+ const fScore = finderScore(binaryImage, centre, u, v, pitch, inverted);
256
+ if (fScore < 0.9) continue;
257
+
258
+ for (const dimension of DIMENSIONS) {
259
+ const scales = [0.84, 0.92, 1, 1.08, 1.16];
260
+ const rankedX = scales.map((scale) => ({
261
+ scale,
262
+ score: structureScore(binaryImage, centre, u, v, pitch, dimension, scale, 1, inverted),
263
+ })).sort((a, b) => b.score - a.score).slice(0, 2);
264
+ const rankedY = scales.map((scale) => ({
265
+ scale,
266
+ score: structureScore(binaryImage, centre, u, v, pitch, dimension, 1, scale, inverted),
267
+ })).sort((a, b) => b.score - a.score).slice(0, 2);
268
+
269
+ for (const xs of rankedX) for (const ys of rankedY) {
270
+ const score = structureScore(binaryImage, centre, u, v, pitch, dimension, xs.scale, ys.scale, inverted);
271
+ if (score < 0.78) continue;
272
+
273
+ // With a single finder there is no direct bottom-right anchor.
274
+ // A compact search around the affine estimate lets the projective
275
+ // sampler account for convergence of the remote edges.
276
+ for (const delta of [[0, 0], [-0.75, 0], [0.75, 0], [0, -0.75], [0, 0.75],
277
+ [-0.75, -0.75], [0.75, -0.75], [-0.75, 0.75], [0.75, 0.75]]) {
278
+ const corners = cornersFor(
279
+ centre, u, v, pitch, dimension, xs.scale, ys.scale, delta[0], delta[1]
280
+ );
281
+ let matrix;
282
+ try { matrix = sampleQuad(binaryImage, dimension, corners, score < 0.9); }
283
+ catch (error) { continue; }
284
+ if (inverted) matrix = invert(matrix);
285
+
286
+ try {
287
+ const decoded = decodeMicroQR(matrix);
288
+ const version = decoded.version ?? `M${(dimension - 9) / 2}`;
289
+ const detection = {
290
+ corners,
291
+ dimension,
292
+ version,
293
+ moduleSize: pitch,
294
+ rotation: orientationDegrees(u),
295
+ inverted,
296
+ matrix,
297
+ finderCentre: { x: centre.x, y: centre.y },
298
+ score: fScore + score,
299
+ };
300
+ const key = candidateKey(detection);
301
+ if (!seen.has(key) && !detections.some((entry) => sameCandidate(entry, detection))) {
302
+ seen.add(key);
303
+ detections.push(detection);
304
+ }
305
+ // Decoder validation settled this dimension and orientation.
306
+ break;
307
+ } catch (error) {
308
+ /* Try the next perspective hypothesis. */
309
+ }
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ detections.sort((a, b) => b.score - a.score || b.moduleSize - a.moduleSize);
319
+ for (const detection of detections) {
320
+ delete detection.finderCentre;
321
+ delete detection.score;
322
+ }
323
+ return detections;
324
+ }
325
+
326
+ /**
327
+ * Detect and decode all Micro QR symbols in a binarized raster.
328
+ *
329
+ * @param {BitMatrix} binaryImage
330
+ * @returns {Array<object>}
331
+ */
332
+ export function detectAndDecodeMicroQR(binaryImage) {
333
+ let detections;
334
+ try { detections = detectMicroQR(binaryImage); }
335
+ catch (error) { return []; }
336
+
337
+ const results = [];
338
+ const seen = new Set();
339
+ for (const detection of detections) {
340
+ try {
341
+ const decoded = decodeMicroQR(detection.matrix);
342
+ const key = `${decoded.version ?? detection.version}|${decoded.text ?? ''}`;
343
+ if (seen.has(key)) continue;
344
+ seen.add(key);
345
+ results.push(Object.assign({
346
+ corners: detection.corners,
347
+ rotation: detection.rotation,
348
+ inverted: detection.inverted,
349
+ }, decoded));
350
+ } catch (error) {
351
+ /* A failed candidate is a normal no-symbol result. */
352
+ }
353
+ }
354
+ return results;
355
+ }