@sythos/js_barcode_universal 1.2.5 → 1.4.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 (124) hide show
  1. package/AI_USAGE.md +33 -0
  2. package/LICENSE +11 -2
  3. package/NOTICE.md +10 -4
  4. package/README.md +601 -465
  5. package/bundle/sythos-barcode.esm.js +4669 -74
  6. package/bundle/sythos-barcode.js +4575 -72
  7. package/examples/create.html +1010 -730
  8. package/examples/read.html +1 -0
  9. package/licenses/README.md +40 -58
  10. package/licenses/aztec-code.license +15 -13
  11. package/licenses/aztec-rune.license +76 -0
  12. package/licenses/codabar.license +18 -15
  13. package/licenses/code-11.license +11 -9
  14. package/licenses/code-128.license +10 -8
  15. package/licenses/code-39.license +10 -8
  16. package/licenses/code-93.license +16 -14
  17. package/licenses/compact-pdf417.license +76 -0
  18. package/licenses/data-matrix.license +15 -13
  19. package/licenses/ean-13.license +10 -8
  20. package/licenses/ean-2.license +74 -0
  21. package/licenses/ean-5.license +74 -0
  22. package/licenses/ean-8.license +10 -8
  23. package/licenses/frameqr.license +84 -0
  24. package/licenses/gs1-128.license +10 -8
  25. package/licenses/gs1-databar.license +81 -0
  26. package/licenses/isbn.license +10 -8
  27. package/licenses/itf-14.license +10 -8
  28. package/licenses/itf.license +9 -7
  29. package/licenses/micro-qr.license +79 -0
  30. package/licenses/micropdf417.license +52 -67
  31. package/licenses/msi-plessey.license +13 -11
  32. package/licenses/pdf417.license +78 -37
  33. package/licenses/pharmacode.license +14 -12
  34. package/licenses/qr-code.license +9 -7
  35. package/licenses/rmqr.license +79 -0
  36. package/licenses/upc-a.license +12 -10
  37. package/licenses/upc-e.license +10 -8
  38. package/package.json +109 -88
  39. package/src/aztec/decoder.js +1 -0
  40. package/src/aztec/detector.js +1 -0
  41. package/src/aztec/encoder.js +1 -0
  42. package/src/aztec/high-level.js +1 -0
  43. package/src/aztec/index.js +1 -0
  44. package/src/aztec/tables.js +1 -0
  45. package/src/aztecrune/decoder.js +156 -0
  46. package/src/aztecrune/detector.js +167 -0
  47. package/src/aztecrune/encoder.js +122 -0
  48. package/src/aztecrune/index.js +50 -0
  49. package/src/aztecrune/tables.js +138 -0
  50. package/src/compactpdf417/decoder.js +129 -0
  51. package/src/compactpdf417/detector.js +140 -0
  52. package/src/compactpdf417/encoder.js +141 -0
  53. package/src/compactpdf417/index.js +22 -0
  54. package/src/compactpdf417/tables.js +174 -0
  55. package/src/core/bit-buffer.js +1 -0
  56. package/src/core/bit-matrix.js +1 -0
  57. package/src/core/errors.js +1 -0
  58. package/src/core/galois-field.js +1 -0
  59. package/src/core/index.js +1 -0
  60. package/src/core/reed-solomon.js +1 -0
  61. package/src/databar/codec.js +182 -0
  62. package/src/databar/decoder.js +115 -0
  63. package/src/databar/encoder.js +97 -0
  64. package/src/databar/gs1.js +178 -0
  65. package/src/databar/index.js +69 -0
  66. package/src/databar/patterns.js +144 -0
  67. package/src/databar/tables.js +125 -0
  68. package/src/datamatrix/decoder.js +263 -262
  69. package/src/datamatrix/detector.js +226 -225
  70. package/src/datamatrix/encoder.js +192 -191
  71. package/src/datamatrix/index.js +43 -42
  72. package/src/datamatrix/tables.js +124 -123
  73. package/src/frameqr/decoder.js +240 -0
  74. package/src/frameqr/detector.js +193 -0
  75. package/src/frameqr/encoder.js +157 -0
  76. package/src/frameqr/index.js +43 -0
  77. package/src/frameqr/tables.js +271 -0
  78. package/src/image/binarizer.js +1 -0
  79. package/src/image/grid-sampler.js +1 -0
  80. package/src/image/index.js +1 -0
  81. package/src/image/luminance.js +1 -0
  82. package/src/image/perspective.js +1 -0
  83. package/src/index.js +155 -2
  84. package/src/micropdf417/compaction.js +1 -0
  85. package/src/micropdf417/decoder.js +1 -0
  86. package/src/micropdf417/detector.js +1 -0
  87. package/src/micropdf417/encoder.js +1 -0
  88. package/src/micropdf417/error-correction.js +1 -0
  89. package/src/micropdf417/index.js +1 -0
  90. package/src/micropdf417/tables.js +1 -0
  91. package/src/microqr/decoder.js +246 -0
  92. package/src/microqr/detector.js +356 -0
  93. package/src/microqr/encoder.js +270 -0
  94. package/src/microqr/index.js +37 -0
  95. package/src/microqr/tables.js +317 -0
  96. package/src/oned/addons.js +421 -0
  97. package/src/oned/index.js +77 -59
  98. package/src/oned/patterns.js +1 -0
  99. package/src/oned/reader.js +1 -0
  100. package/src/oned/writers.js +1 -0
  101. package/src/pdf417/compaction.js +1 -0
  102. package/src/pdf417/decoder.js +1 -0
  103. package/src/pdf417/detector.js +1 -0
  104. package/src/pdf417/encoder.js +1 -0
  105. package/src/pdf417/error-correction.js +1 -0
  106. package/src/pdf417/index.js +5 -0
  107. package/src/pdf417/tables.js +1 -0
  108. package/src/qr/decoder.js +1 -0
  109. package/src/qr/detector.js +1 -0
  110. package/src/qr/encoder.js +1 -0
  111. package/src/qr/index.js +1 -0
  112. package/src/qr/tables.js +1 -0
  113. package/src/render/image-data.js +1 -0
  114. package/src/render/index.js +1 -0
  115. package/src/render/options.js +1 -0
  116. package/src/render/png.js +1 -0
  117. package/src/render/svg.js +1 -0
  118. package/src/render/webgl.js +1 -0
  119. package/src/render/webgpu.js +1 -0
  120. package/src/rmqr/decoder.js +102 -0
  121. package/src/rmqr/detector.js +91 -0
  122. package/src/rmqr/encoder.js +173 -0
  123. package/src/rmqr/index.js +38 -0
  124. package/src/rmqr/tables.js +155 -0
@@ -0,0 +1,356 @@
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-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+
32
+ /**
33
+ * Micro QR detection in binarized rasters.
34
+ *
35
+ * A Micro QR symbol has one 7x7 finder in its top-left corner. That alone is
36
+ * not enough to distinguish it from one corner of a normal QR Code, so every
37
+ * candidate is also required to have the Micro QR timing arms and a format
38
+ * word which the decoder accepts. The decoder is deliberately the final
39
+ * geometric arbiter; BCH and Reed--Solomon verification make accidental
40
+ * acceptance of ordinary square artwork very unlikely.
41
+ *
42
+ * Finder geometry supplies two local axes. Timing arms refine their lengths,
43
+ * while a small fourth-corner search lets projective sampling absorb mild
44
+ * perspective despite the format having no remote alignment pattern.
45
+ *
46
+ * @module microqr/detector
47
+ */
48
+
49
+ import { BitMatrix } from '../core/bit-matrix.js';
50
+ import { NotFoundError } from '../core/errors.js';
51
+ import { sampleQuad } from '../image/grid-sampler.js';
52
+ import { decodeMicroQR } from './decoder.js';
53
+
54
+ /** Legal Micro QR side lengths (M1 through M4). */
55
+ const DIMENSIONS = [11, 13, 15, 17];
56
+
57
+ /** @typedef {{x:number, y:number}} Point */
58
+
59
+ /**
60
+ * @typedef {object} Detection
61
+ * @property {Point[]} corners Outer corners in reading order.
62
+ * @property {number} dimension Side length in modules.
63
+ * @property {'M1'|'M2'|'M3'|'M4'} version
64
+ * @property {number} moduleSize Estimated pixels per module at the finder.
65
+ * @property {number} rotation Clockwise orientation of the source raster.
66
+ * @property {boolean} inverted Whether the detected symbol used inverted polarity.
67
+ * @property {BitMatrix} matrix Rectified, normally polarised module matrix.
68
+ */
69
+
70
+ function rotateVector(vector) {
71
+ return { x: -vector.y, y: vector.x };
72
+ }
73
+
74
+ function add(point, a, av, b, bv) {
75
+ return { x: point.x + a.x * av + b.x * bv, y: point.y + a.y * av + b.y * bv };
76
+ }
77
+
78
+ function sample(image, point) {
79
+ const x = Math.round(point.x);
80
+ const y = Math.round(point.y);
81
+ if (x < 0 || y < 0 || x >= image.width || y >= image.height) return null;
82
+ return image.get(x, y);
83
+ }
84
+
85
+ function expectedFinder(x, y) {
86
+ return x === 0 || y === 0 || x === 6 || y === 6 ||
87
+ (x >= 2 && x <= 4 && y >= 2 && y <= 4);
88
+ }
89
+
90
+ /** Connected components matching one polarity, capped to plausible centre blocks. */
91
+ function components(image, value) {
92
+ const seen = new Uint8Array(image.width * image.height);
93
+ const result = [];
94
+ const maximumArea = Math.max(16, Math.floor(image.width * image.height * 0.08));
95
+
96
+ for (let y = 0; y < image.height; y++) for (let x = 0; x < image.width; x++) {
97
+ const start = y * image.width + x;
98
+ if (seen[start] || image.get(x, y) !== value) continue;
99
+
100
+ const queueX = [x];
101
+ const queueY = [y];
102
+ seen[start] = 1;
103
+ let head = 0;
104
+ let minX = x; let maxX = x; let minY = y; let maxY = y;
105
+
106
+ while (head < queueX.length) {
107
+ const px = queueX[head];
108
+ const py = queueY[head++];
109
+ if (px < minX) minX = px;
110
+ if (px > maxX) maxX = px;
111
+ if (py < minY) minY = py;
112
+ if (py > maxY) maxY = py;
113
+ for (const [nx, ny] of [[px - 1, py], [px + 1, py], [px, py - 1], [px, py + 1]]) {
114
+ if (nx < 0 || ny < 0 || nx >= image.width || ny >= image.height) continue;
115
+ const index = ny * image.width + nx;
116
+ if (!seen[index] && image.get(nx, ny) === value) {
117
+ seen[index] = 1;
118
+ queueX.push(nx);
119
+ queueY.push(ny);
120
+ }
121
+ }
122
+ }
123
+
124
+ const width = maxX - minX + 1;
125
+ const height = maxY - minY + 1;
126
+ const area = width * height;
127
+ if (queueX.length > maximumArea || Math.min(width, height) < 2) continue;
128
+ if (Math.max(width, height) > Math.min(width, height) * 1.7) continue;
129
+ if (queueX.length < area * 0.42) continue;
130
+ result.push({
131
+ x: (minX + maxX) / 2,
132
+ y: (minY + maxY) / 2,
133
+ width,
134
+ height,
135
+ pixels: queueX.length,
136
+ });
137
+ }
138
+
139
+ return result.sort((a, b) => b.pixels - a.pixels).slice(0, 256);
140
+ }
141
+
142
+ /** Score the complete 7x7 finder at module centres. */
143
+ function finderScore(image, centre, u, v, pitch, inverted) {
144
+ let correct = 0;
145
+ let total = 0;
146
+ for (let y = 0; y < 7; y++) for (let x = 0; x < 7; x++) {
147
+ const actual = sample(image, add(centre, u, (x - 3) * pitch, v, (y - 3) * pitch));
148
+ if (actual === null) continue;
149
+ const wanted = inverted ? !expectedFinder(x, y) : expectedFinder(x, y);
150
+ if (actual === wanted) correct++;
151
+ total++;
152
+ }
153
+ return total === 49 ? correct / total : 0;
154
+ }
155
+
156
+ /** Validate separator, timing arms and a sparse quiet-zone outline. */
157
+ function structureScore(image, centre, u, v, pitch, dimension, sx, sy, inverted) {
158
+ let correct = 0;
159
+ let total = 0;
160
+ const check = (x, y, dark) => {
161
+ const point = add(centre, u, (x - 3) * pitch * sx, v, (y - 3) * pitch * sy);
162
+ const actual = sample(image, point);
163
+ if (actual !== null && actual === (inverted ? !dark : dark)) correct++;
164
+ total++;
165
+ };
166
+
167
+ // The light separator lies between the finder and encoding region.
168
+ for (let i = 0; i <= 7; i++) {
169
+ check(7, i, false);
170
+ check(i, 7, false);
171
+ }
172
+ // Both timing arms start dark at coordinate 8 and alternate to the edge.
173
+ for (let i = 8; i < dimension; i++) {
174
+ check(i, 0, (i & 1) === 0);
175
+ check(0, i, (i & 1) === 0);
176
+ }
177
+ // A quiet-zone sample just beyond each edge rejects an isolated normal-QR
178
+ // finder and most decorative squares without requiring a perfect crop.
179
+ for (let i = 0; i < dimension; i += 2) {
180
+ check(i, -1.25, false);
181
+ check(-1.25, i, false);
182
+ check(i, dimension + 0.75, false);
183
+ check(dimension + 0.75, i, false);
184
+ }
185
+ return correct / total;
186
+ }
187
+
188
+ function invert(matrix) {
189
+ const out = matrix.clone();
190
+ for (let y = 0; y < out.height; y++) for (let x = 0; x < out.width; x++) out.flip(x, y);
191
+ return out;
192
+ }
193
+
194
+ function orientationDegrees(u) {
195
+ const degrees = Math.atan2(u.y, u.x) * 180 / Math.PI;
196
+ return ((Math.round(degrees / 90) * 90) % 360 + 360) % 360;
197
+ }
198
+
199
+ function cornersFor(centre, u, v, pitch, dimension, sx, sy, dx = 0, dy = 0) {
200
+ const tl = add(centre, u, -3.5 * pitch, v, -3.5 * pitch);
201
+ const tr = add(tl, u, dimension * pitch * sx, v, 0);
202
+ const bl = add(tl, u, 0, v, dimension * pitch * sy);
203
+ const br = add(add(tr, v, dimension * pitch * sy, u, 0), u, dx * pitch, v, dy * pitch);
204
+ return [tl, tr, br, bl];
205
+ }
206
+
207
+ function candidateKey(detection) {
208
+ const centre = detection.finderCentre;
209
+ return `${Math.round(centre.x)},${Math.round(centre.y)},${detection.dimension}`;
210
+ }
211
+
212
+ function sameCandidate(left, right) {
213
+ if (left.dimension !== right.dimension) return false;
214
+ const centre = (detection) => detection.corners.reduce(
215
+ (sum, point) => ({ x: sum.x + point.x / 4, y: sum.y + point.y / 4 }),
216
+ { x: 0, y: 0 },
217
+ );
218
+ const a = centre(left);
219
+ const b = centre(right);
220
+ const tolerance = Math.max(left.moduleSize, right.moduleSize) * 2;
221
+ return Math.hypot(a.x - b.x, a.y - b.y) < tolerance;
222
+ }
223
+
224
+ /**
225
+ * Find Micro QR symbols in a binarized raster.
226
+ *
227
+ * The search accepts arbitrary in-plane angles, including all quarter-turns.
228
+ * Non-integer scale is supported through centre sampling. Mild projective
229
+ * distortion is handled by searching the unconstrained fourth corner.
230
+ *
231
+ * @param {BitMatrix} binaryImage Set bit = dark.
232
+ * @returns {Detection[]} Best candidate first; empty when no symbol is found.
233
+ */
234
+ export function detectMicroQR(binaryImage) {
235
+ if (!binaryImage || !binaryImage.width || !binaryImage.height) {
236
+ throw new NotFoundError('detectMicroQR: no image supplied');
237
+ }
238
+
239
+ const detections = [];
240
+ const seen = new Set();
241
+
242
+ for (const inverted of [false, true]) {
243
+ for (const centre of components(binaryImage, !inverted)) {
244
+ for (let degrees = 0; degrees < 180; degrees += 3) {
245
+ const angle = degrees * Math.PI / 180;
246
+ const axis = { x: Math.cos(angle), y: Math.sin(angle) };
247
+ const perpendicular = rotateVector(axis);
248
+ const footprint = Math.abs(axis.x) + Math.abs(axis.y);
249
+ const pitch = ((centre.width + centre.height) / 2) / (3 * footprint);
250
+ if (pitch < 0.75) continue;
251
+
252
+ // The finder is rotationally symmetric; four turns decide which pair
253
+ // of arms points into the encoding region.
254
+ for (let turn = 0, u = axis, v = perpendicular; turn < 4; turn++) {
255
+ if (turn > 0) { u = v; v = { x: -u.y, y: u.x }; }
256
+ const fScore = finderScore(binaryImage, centre, u, v, pitch, inverted);
257
+ if (fScore < 0.9) continue;
258
+
259
+ for (const dimension of DIMENSIONS) {
260
+ const scales = [0.84, 0.92, 1, 1.08, 1.16];
261
+ const rankedX = scales.map((scale) => ({
262
+ scale,
263
+ score: structureScore(binaryImage, centre, u, v, pitch, dimension, scale, 1, inverted),
264
+ })).sort((a, b) => b.score - a.score).slice(0, 2);
265
+ const rankedY = scales.map((scale) => ({
266
+ scale,
267
+ score: structureScore(binaryImage, centre, u, v, pitch, dimension, 1, scale, inverted),
268
+ })).sort((a, b) => b.score - a.score).slice(0, 2);
269
+
270
+ for (const xs of rankedX) for (const ys of rankedY) {
271
+ const score = structureScore(binaryImage, centre, u, v, pitch, dimension, xs.scale, ys.scale, inverted);
272
+ if (score < 0.78) continue;
273
+
274
+ // With a single finder there is no direct bottom-right anchor.
275
+ // A compact search around the affine estimate lets the projective
276
+ // sampler account for convergence of the remote edges.
277
+ for (const delta of [[0, 0], [-0.75, 0], [0.75, 0], [0, -0.75], [0, 0.75],
278
+ [-0.75, -0.75], [0.75, -0.75], [-0.75, 0.75], [0.75, 0.75]]) {
279
+ const corners = cornersFor(
280
+ centre, u, v, pitch, dimension, xs.scale, ys.scale, delta[0], delta[1]
281
+ );
282
+ let matrix;
283
+ try { matrix = sampleQuad(binaryImage, dimension, corners, score < 0.9); }
284
+ catch (error) { continue; }
285
+ if (inverted) matrix = invert(matrix);
286
+
287
+ try {
288
+ const decoded = decodeMicroQR(matrix);
289
+ const version = decoded.version ?? `M${(dimension - 9) / 2}`;
290
+ const detection = {
291
+ corners,
292
+ dimension,
293
+ version,
294
+ moduleSize: pitch,
295
+ rotation: orientationDegrees(u),
296
+ inverted,
297
+ matrix,
298
+ finderCentre: { x: centre.x, y: centre.y },
299
+ score: fScore + score,
300
+ };
301
+ const key = candidateKey(detection);
302
+ if (!seen.has(key) && !detections.some((entry) => sameCandidate(entry, detection))) {
303
+ seen.add(key);
304
+ detections.push(detection);
305
+ }
306
+ // Decoder validation settled this dimension and orientation.
307
+ break;
308
+ } catch (error) {
309
+ /* Try the next perspective hypothesis. */
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+
319
+ detections.sort((a, b) => b.score - a.score || b.moduleSize - a.moduleSize);
320
+ for (const detection of detections) {
321
+ delete detection.finderCentre;
322
+ delete detection.score;
323
+ }
324
+ return detections;
325
+ }
326
+
327
+ /**
328
+ * Detect and decode all Micro QR symbols in a binarized raster.
329
+ *
330
+ * @param {BitMatrix} binaryImage
331
+ * @returns {Array<object>}
332
+ */
333
+ export function detectAndDecodeMicroQR(binaryImage) {
334
+ let detections;
335
+ try { detections = detectMicroQR(binaryImage); }
336
+ catch (error) { return []; }
337
+
338
+ const results = [];
339
+ const seen = new Set();
340
+ for (const detection of detections) {
341
+ try {
342
+ const decoded = decodeMicroQR(detection.matrix);
343
+ const key = `${decoded.version ?? detection.version}|${decoded.text ?? ''}`;
344
+ if (seen.has(key)) continue;
345
+ seen.add(key);
346
+ results.push(Object.assign({
347
+ corners: detection.corners,
348
+ rotation: detection.rotation,
349
+ inverted: detection.inverted,
350
+ }, decoded));
351
+ } catch (error) {
352
+ /* A failed candidate is a normal no-symbol result. */
353
+ }
354
+ }
355
+ return results;
356
+ }
@@ -0,0 +1,270 @@
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-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+
32
+ import { BitMatrix } from '../core/bit-matrix.js';
33
+ import { BitWriter } from '../core/bit-buffer.js';
34
+ import { EncodeError } from '../core/errors.js';
35
+ import { GF256_QR } from '../core/galois-field.js';
36
+ import { rsEncode } from '../core/reed-solomon.js';
37
+ import {
38
+ MICROQR_VERSIONS, microQrBlockLayout, microQrDataModuleOrder,
39
+ microQrFormatInfo, microQrFormatInfoPositions, microQrMaskBit,
40
+ microQrSymbolNumber, microQrVersionSize,
41
+ } from './tables.js';
42
+
43
+ export const MICROQR_ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
44
+ const MODE = { numeric: 0, alphanumeric: 1, byte: 2, kanji: 3 };
45
+ const MODE_MIN_VERSION = { numeric: 1, alphanumeric: 2, byte: 3, kanji: 3 };
46
+ const COUNT_BITS = {
47
+ numeric: [0, 3, 4, 5, 6], alphanumeric: [0, 0, 3, 4, 5],
48
+ byte: [0, 0, 0, 4, 5], kanji: [0, 0, 0, 3, 4],
49
+ };
50
+
51
+ function parseVersion(value) {
52
+ if (value == null) return null;
53
+ const match = /^M?([1-4])$/i.exec(String(value));
54
+ if (!match) throw new EncodeError(`Micro QR: version must be M1-M4, got ${value}`);
55
+ return Number(match[1]);
56
+ }
57
+
58
+ function versionNumber(version) {
59
+ return typeof version === 'number' ? version : Number(String(version).slice(1));
60
+ }
61
+
62
+ function latin1Bytes(text) {
63
+ const bytes = new Uint8Array(text.length);
64
+ for (let i = 0; i < text.length; i++) {
65
+ const cp = text.charCodeAt(i);
66
+ if (cp > 0xff) throw new EncodeError('Micro QR: byte mode supports ISO-8859-1 only (ECI is unavailable)');
67
+ bytes[i] = cp;
68
+ }
69
+ return bytes;
70
+ }
71
+
72
+ let sjisReverseMap;
73
+
74
+ function sjisToThirteenBits(sjis) {
75
+ const trail = sjis & 0xff;
76
+ if (trail < 0x40 || trail === 0x7f || trail > 0xfc) return -1;
77
+ let adjusted;
78
+ if (sjis >= 0x8140 && sjis <= 0x9ffc) adjusted = sjis - 0x8140;
79
+ else if (sjis >= 0xe040 && sjis <= 0xebbf) adjusted = sjis - 0xc140;
80
+ else return -1;
81
+ const packed = (adjusted >>> 8) * 0xc0 + (adjusted & 0xff);
82
+ return packed <= 0x1fff ? packed : -1;
83
+ }
84
+
85
+ function getSjisReverseMap() {
86
+ if (sjisReverseMap !== undefined) return sjisReverseMap;
87
+ let decoder;
88
+ try {
89
+ decoder = new TextDecoder('shift_jis', { fatal: true });
90
+ if (decoder.decode(new Uint8Array([0x82, 0xa0])) !== 'あ') return (sjisReverseMap = null);
91
+ } catch {
92
+ return (sjisReverseMap = null);
93
+ }
94
+ const result = new Map();
95
+ const bytes = new Uint8Array(2);
96
+ for (const [start, end] of [[0x8140, 0x9ffc], [0xe040, 0xebbf]]) {
97
+ for (let value = start; value <= end; value++) {
98
+ if (sjisToThirteenBits(value) < 0) continue;
99
+ bytes[0] = value >>> 8;
100
+ bytes[1] = value & 0xff;
101
+ let character;
102
+ try { character = decoder.decode(bytes); } catch { continue; }
103
+ if (Array.from(character).length === 1 && !result.has(character)) result.set(character, value);
104
+ }
105
+ }
106
+ sjisReverseMap = result;
107
+ return result;
108
+ }
109
+
110
+ function kanjiValues(text) {
111
+ const reverse = getSjisReverseMap();
112
+ if (!reverse) return null;
113
+ const values = [];
114
+ for (const character of text) {
115
+ const sjis = reverse.get(character);
116
+ if (sjis == null) return null;
117
+ values.push(sjisToThirteenBits(sjis));
118
+ }
119
+ return values;
120
+ }
121
+
122
+ function chooseMode(text, forced) {
123
+ const mode = forced == null ? (/^\d+$/.test(text) ? 'numeric' :
124
+ [...text].every((ch) => MICROQR_ALPHANUMERIC.includes(ch)) ? 'alphanumeric' :
125
+ kanjiValues(text) ? 'kanji' : 'byte') :
126
+ String(forced).toLowerCase();
127
+ if (!(mode in MODE)) throw new EncodeError(`Micro QR: unsupported mode "${forced}"`);
128
+ if (mode === 'numeric' && !/^\d+$/.test(text)) throw new EncodeError('Micro QR: numeric mode accepts digits only');
129
+ if (mode === 'alphanumeric' && ![...text].every((ch) => MICROQR_ALPHANUMERIC.includes(ch))) {
130
+ throw new EncodeError('Micro QR: alphanumeric mode contains an unsupported character');
131
+ }
132
+ if (mode === 'kanji' && !kanjiValues(text)) {
133
+ throw new EncodeError('Micro QR: kanji mode requires characters representable in the QR Shift_JIS ranges');
134
+ }
135
+ return mode;
136
+ }
137
+
138
+ function encodePayload(text, mode) {
139
+ const w = new BitWriter();
140
+ if (mode === 'numeric') {
141
+ for (let i = 0; i < text.length; i += 3) {
142
+ const n = Math.min(3, text.length - i);
143
+ w.put(Number(text.slice(i, i + n)), n === 3 ? 10 : n === 2 ? 7 : 4);
144
+ }
145
+ } else if (mode === 'alphanumeric') {
146
+ let i = 0;
147
+ for (; i + 1 < text.length; i += 2) {
148
+ w.put(MICROQR_ALPHANUMERIC.indexOf(text[i]) * 45 + MICROQR_ALPHANUMERIC.indexOf(text[i + 1]), 11);
149
+ }
150
+ if (i < text.length) w.put(MICROQR_ALPHANUMERIC.indexOf(text[i]), 6);
151
+ } else if (mode === 'byte') {
152
+ w.putBytes(latin1Bytes(text));
153
+ } else {
154
+ for (const value of kanjiValues(text)) w.put(value, 13);
155
+ }
156
+ return w;
157
+ }
158
+
159
+ function getBit(writer, index) {
160
+ return ((writer.bytes[index >>> 3] >>> (7 - (index & 7))) & 1) === 1;
161
+ }
162
+
163
+ function writeData(text, mode, version, layout) {
164
+ const numericVersion = versionNumber(version);
165
+ const payload = encodePayload(text, mode);
166
+ const writer = new BitWriter();
167
+ if (numericVersion > 1) writer.put(MODE[mode], numericVersion - 1);
168
+ const count = mode === 'byte' ? latin1Bytes(text).length : [...text].length;
169
+ const countWidth = COUNT_BITS[mode][numericVersion];
170
+ if (countWidth === 0 || count >= 2 ** countWidth) return null;
171
+ writer.put(count, countWidth);
172
+ for (let i = 0; i < payload.length; i++) writer.putBit(getBit(payload, i));
173
+ if (writer.length > layout.dataBits) return null;
174
+ for (let i = 0, n = Math.min(2 * numericVersion + 1, layout.dataBits - writer.length); i < n; i++) writer.putBit(false);
175
+ if (numericVersion !== 1 && numericVersion !== 3) {
176
+ while ((writer.length & 7) && writer.length < layout.dataBits) writer.putBit(false);
177
+ }
178
+ if (numericVersion === 1 || numericVersion === 3) {
179
+ while (writer.length < layout.dataBits) writer.putBit(false);
180
+ return writer;
181
+ }
182
+ let pad = 0;
183
+ while (writer.length + 8 <= layout.dataBits) writer.put(pad++ & 1 ? 0x11 : 0xec, 8);
184
+ while (writer.length < layout.dataBits) writer.putBit(false);
185
+ return writer;
186
+ }
187
+
188
+ function finalMessage(dataWriter, layout) {
189
+ const bytes = Array.from(dataWriter.toBytes());
190
+ if (layout.shortDataCodewordBits === 4) bytes[bytes.length - 1] &= 0xf0;
191
+ const ecc = rsEncode(bytes, layout.eccCodewords, GF256_QR, 0);
192
+ const out = [];
193
+ const full = layout.shortDataCodewordBits === 4 ? bytes.length - 1 : bytes.length;
194
+ for (let i = 0; i < full; i++) for (let b = 7; b >= 0; b--) out.push((bytes[i] >>> b) & 1);
195
+ if (layout.shortDataCodewordBits === 4) for (let b = 7; b >= 4; b--) out.push((bytes[bytes.length - 1] >>> b) & 1);
196
+ for (const value of ecc) for (let b = 7; b >= 0; b--) out.push((value >>> b) & 1);
197
+ return out;
198
+ }
199
+
200
+ function drawFunctions(matrix) {
201
+ const size = matrix.width;
202
+ for (let y = 0; y < 7; y++) for (let x = 0; x < 7; x++) {
203
+ const ring = x === 0 || x === 6 || y === 0 || y === 6;
204
+ const core = x >= 2 && x <= 4 && y >= 2 && y <= 4;
205
+ matrix.setValue(x, y, ring || core);
206
+ }
207
+ for (let i = 0; i < 8; i++) { matrix.unset(7, i); matrix.unset(i, 7); }
208
+ for (let i = 8; i < size; i++) if ((i & 1) === 0) { matrix.set(i, 0); matrix.set(0, i); }
209
+ }
210
+
211
+ function microMaskScore(matrix) {
212
+ let right = 0, bottom = 0;
213
+ for (let i = 1; i < matrix.width; i++) {
214
+ if (matrix.get(matrix.width - 1, i)) right++;
215
+ if (matrix.get(i, matrix.height - 1)) bottom++;
216
+ }
217
+ return Math.min(right, bottom) * 16 + Math.max(right, bottom);
218
+ }
219
+
220
+ function buildMatrix(version, ecc, mask, bits) {
221
+ const matrix = new BitMatrix(microQrVersionSize(version));
222
+ drawFunctions(matrix);
223
+ const order = microQrDataModuleOrder(version);
224
+ for (let i = 0; i < bits.length; i++) {
225
+ const x = order[i * 2], y = order[i * 2 + 1];
226
+ matrix.setValue(x, y, (bits[i] === 1) !== microQrMaskBit(mask, x, y));
227
+ }
228
+ const format = microQrFormatInfo(microQrSymbolNumber(version, ecc), mask);
229
+ const positions = microQrFormatInfoPositions(matrix.width);
230
+ for (let i = 0; i < 15; i++) matrix.setValue(positions[i][0], positions[i][1], ((format >>> i) & 1) === 1);
231
+ return matrix;
232
+ }
233
+
234
+ export function encodeMicroQR(text, options = {}) {
235
+ text = String(text);
236
+ if (!text) throw new EncodeError('Micro QR: payload must not be empty');
237
+ if (options.eci != null || options.gs1 === true) throw new EncodeError('Micro QR: ECI and GS1/FNC1 are unavailable');
238
+ const mode = chooseMode(text, options.mode);
239
+ const wantedVersion = parseVersion(options.version);
240
+ const wantedEcc = options.ecc == null ? null : String(options.ecc).toUpperCase();
241
+ if (wantedEcc === 'H') throw new EncodeError('Micro QR: error correction level H is unavailable');
242
+ if (options.mask != null && (!Number.isInteger(options.mask) || options.mask < 0 || options.mask > 3)) {
243
+ throw new EncodeError(`Micro QR: mask must be an integer 0-3, got ${options.mask}`);
244
+ }
245
+
246
+ let selected;
247
+ for (const version of MICROQR_VERSIONS) {
248
+ if (wantedVersion != null && version !== wantedVersion) continue;
249
+ const numericVersion = versionNumber(version);
250
+ if (numericVersion < MODE_MIN_VERSION[mode]) continue;
251
+ const levels = numericVersion === 1 ? ['DETECT'] : numericVersion < 4 ? ['L', 'M'] : ['L', 'M', 'Q'];
252
+ for (const ecc of levels) {
253
+ if (wantedEcc != null && ecc !== wantedEcc) continue;
254
+ const layout = microQrBlockLayout(version, ecc);
255
+ const data = writeData(text, mode, version, layout);
256
+ if (data) { selected = { version, ecc, layout, data }; break; }
257
+ }
258
+ if (selected) break;
259
+ }
260
+ if (!selected) throw new EncodeError('Micro QR: payload does not fit the requested version/error level');
261
+ const bits = finalMessage(selected.data, selected.layout);
262
+ if (options.mask != null) return buildMatrix(selected.version, selected.ecc, options.mask, bits);
263
+ let best, score = -1;
264
+ for (let mask = 0; mask < 4; mask++) {
265
+ const candidate = buildMatrix(selected.version, selected.ecc, mask, bits);
266
+ const candidateScore = microMaskScore(candidate);
267
+ if (candidateScore > score) { score = candidateScore; best = candidate; }
268
+ }
269
+ return best;
270
+ }
@@ -0,0 +1,37 @@
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-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+
32
+ /** Micro QR Code M1-M4 public module surface. @module microqr */
33
+
34
+ export { encodeMicroQR } from './encoder.js';
35
+ export { decodeMicroQR } from './decoder.js';
36
+ export { detectMicroQR, detectAndDecodeMicroQR } from './detector.js';
37
+ export { validateMicroQrTables } from './tables.js';