@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
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
package/src/index.js CHANGED
@@ -4,6 +4,7 @@
4
4
  * MIT License
5
5
  *
6
6
  * Copyright (c) 2026 Sythos
7
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
7
8
  *
8
9
  * Permission is hereby granted, free of charge, to any person obtaining a copy
9
10
  * of this software and associated documentation files (the "Software"), to deal
@@ -56,6 +57,12 @@ import * as qr from './qr/index.js';
56
57
  import * as aztec from './aztec/index.js';
57
58
  import * as pdf417 from './pdf417/index.js';
58
59
  import * as micropdf417 from './micropdf417/index.js';
60
+ import * as microqr from './microqr/index.js';
61
+ import * as rmqr from './rmqr/index.js';
62
+ import * as frameqr from './frameqr/index.js';
63
+ import * as aztecRune from './aztecrune/index.js';
64
+ import * as compactPdf417 from './compactpdf417/index.js';
65
+ import * as databar from './databar/index.js';
59
66
 
60
67
  export { BitMatrix };
61
68
  export {
@@ -74,10 +81,20 @@ export {
74
81
  encodeDataMatrix, decodeDataMatrix, detectDataMatrix, detectAndDecodeDataMatrix,
75
82
  } from './datamatrix/index.js';
76
83
  export { encodeAztec, decodeAztec, detectAztec, detectAndDecodeAztec } from './aztec/index.js';
84
+ export * from './aztecrune/index.js';
77
85
  export { encodePDF417, decodePDF417, detectPDF417, detectAndDecodePDF417 } from './pdf417/index.js';
86
+ export * from './compactpdf417/index.js';
87
+ // DataBar currently exposes verified GS1 data-layer codecs only; physical
88
+ // symbol rendering and image detection remain deliberately out of scope.
89
+ export * from './databar/index.js';
78
90
  export {
79
91
  encodeMicroPDF417, decodeMicroPDF417, detectMicroPDF417, detectAndDecodeMicroPDF417,
80
92
  } from './micropdf417/index.js';
93
+ export { encodeMicroQR, decodeMicroQR, detectMicroQR, detectAndDecodeMicroQR } from './microqr/index.js';
94
+ export { encodeRMQR, decodeRMQR, detectRMQR, detectAndDecodeRMQR } from './rmqr/index.js';
95
+ export {
96
+ encodeFrameQR, decodeFrameQR, detectFrameQR, detectAndDecodeFrameQR,
97
+ } from './frameqr/index.js';
81
98
 
82
99
  /**
83
100
  * @typedef {object} FormatInfo
@@ -113,6 +130,17 @@ const pdf417CanEncode = typeof pdf417.encodePDF417 === 'function';
113
130
  const pdf417CanDecode = typeof pdf417.detectAndDecodePDF417 === 'function';
114
131
  const microPdf417CanEncode = typeof micropdf417.encodeMicroPDF417 === 'function';
115
132
  const microPdf417CanDecode = typeof micropdf417.detectAndDecodeMicroPDF417 === 'function';
133
+ const microQrCanEncode = typeof microqr.encodeMicroQR === 'function';
134
+ const microQrCanDecode = typeof microqr.detectAndDecodeMicroQR === 'function';
135
+ const rmqrCanEncode = typeof rmqr.encodeRMQR === 'function';
136
+ const rmqrCanDecode = typeof rmqr.detectAndDecodeRMQR === 'function';
137
+ const frameQrCanEncode = typeof frameqr.encodeFrameQR === 'function';
138
+ const frameQrCanDecode = typeof frameqr.detectAndDecodeFrameQR === 'function';
139
+ const aztecRuneCanEncode = typeof aztecRune.encodeAztecRune === 'function';
140
+ const aztecRuneCanDecode = typeof aztecRune.detectAndDecodeAztecRune === 'function';
141
+ const compactPdf417CanEncode = typeof compactPdf417.encodeCompactPDF417 === 'function';
142
+ const compactPdf417CanDecode = typeof compactPdf417.detectAndDecodeCompactPDF417 === 'function';
143
+ const dataBarCanEncode = typeof databar.encodeDataBar14 === 'function';
116
144
 
117
145
  /**
118
146
  * Every format this build supports.
@@ -154,6 +182,13 @@ export function listFormats() {
154
182
  canRead: aztecCanDecode,
155
183
  kind: /** @type {'2D'} */ ('2D'),
156
184
  });
185
+ formats.push({
186
+ id: 'aztecrune',
187
+ label: 'Aztec Rune',
188
+ canWrite: aztecRuneCanEncode,
189
+ canRead: aztecRuneCanDecode,
190
+ kind: /** @type {'2D'} */ ('2D'),
191
+ });
157
192
  formats.push({
158
193
  id: 'pdf417',
159
194
  label: 'PDF417',
@@ -161,6 +196,13 @@ export function listFormats() {
161
196
  canRead: pdf417CanDecode,
162
197
  kind: /** @type {'2D'} */ ('2D'),
163
198
  });
199
+ formats.push({
200
+ id: 'compactpdf417',
201
+ label: 'Compact PDF417',
202
+ canWrite: compactPdf417CanEncode,
203
+ canRead: compactPdf417CanDecode,
204
+ kind: /** @type {'2D'} */ ('2D'),
205
+ });
164
206
  formats.push({
165
207
  id: 'micropdf417',
166
208
  label: 'MicroPDF417',
@@ -168,6 +210,34 @@ export function listFormats() {
168
210
  canRead: microPdf417CanDecode,
169
211
  kind: /** @type {'2D'} */ ('2D'),
170
212
  });
213
+ formats.push({
214
+ id: 'microqr',
215
+ label: 'Micro QR Code',
216
+ canWrite: microQrCanEncode,
217
+ canRead: microQrCanDecode,
218
+ kind: /** @type {'2D'} */ ('2D'),
219
+ });
220
+ formats.push({
221
+ id: 'rmqr',
222
+ label: 'rMQR Code',
223
+ canWrite: rmqrCanEncode,
224
+ canRead: rmqrCanDecode,
225
+ kind: /** @type {'2D'} */ ('2D'),
226
+ });
227
+ formats.push({
228
+ id: 'frameqr',
229
+ label: 'FrameQR Code',
230
+ canWrite: frameQrCanEncode,
231
+ canRead: frameQrCanDecode,
232
+ kind: /** @type {'2D'} */ ('2D'),
233
+ });
234
+ formats.push({
235
+ id: 'gs1databar14',
236
+ label: 'GS1 DataBar Omnidirectional / Truncated',
237
+ canWrite: dataBarCanEncode,
238
+ canRead: false,
239
+ kind: /** @type {'1D'} */ ('1D'),
240
+ });
171
241
 
172
242
  return formats;
173
243
  }
@@ -198,6 +268,14 @@ export function listFormats() {
198
268
  * @param {'auto'|'text'|'byte'|'numeric'} [options.compaction] PDF417 compaction mode.
199
269
  * @param {number} [options.eci] MicroPDF417 byte-compaction ECI assignment (3 or 26).
200
270
  * @param {number} [options.aspectRatio] Preferred MicroPDF417 symbol aspect ratio.
271
+ * @param {object} [options.canvas] FrameQR Code artwork reservation.
272
+ * @param {'square'|'circle'|'diamond'} [options.canvas.shape] Canvas shape.
273
+ * @param {number} [options.canvas.size] Odd canvas size in QR modules.
274
+ * @param {number} [options.canvas.width] Canvas width in QR modules.
275
+ * @param {number} [options.canvas.height] Canvas height in QR modules.
276
+ * @param {number} [options.canvas.centerX] Canvas centre X in QR modules.
277
+ * @param {number} [options.canvas.centerY] Canvas centre Y in QR modules.
278
+ * @param {0|90|180|270} [options.canvas.angle] Canvas quarter-turn.
201
279
  * @returns {BitMatrix}
202
280
  */
203
281
  export function encode(text, options = {}) {
@@ -213,16 +291,34 @@ export function encode(text, options = {}) {
213
291
  if (format === 'aztec' || format === 'aztec-code') {
214
292
  return aztec.encodeAztec(value, options);
215
293
  }
294
+ if (format === 'aztecrune' || format === 'aztec-rune' || format === 'rune') {
295
+ return aztecRune.encodeAztecRune(value, options);
296
+ }
216
297
  if (format === 'pdf417' || format === 'pdf-417') {
217
298
  return pdf417.encodePDF417(value, options);
218
299
  }
300
+ if (format === 'compactpdf417' || format === 'compact-pdf417' || format === 'compact-pdf-417') {
301
+ return compactPdf417.encodeCompactPDF417(value, options);
302
+ }
219
303
  if (format === 'micropdf417' || format === 'micro-pdf417' || format === 'micro-pdf-417') {
220
304
  return micropdf417.encodeMicroPDF417(value, options);
221
305
  }
306
+ if (format === 'microqr' || format === 'micro-qr') {
307
+ return microqr.encodeMicroQR(value, options);
308
+ }
309
+ if (format === 'rmqr' || format === 'r-mqr' || format === 'rectangular-micro-qr') {
310
+ return rmqr.encodeRMQR(value, options);
311
+ }
312
+ if (format === 'frameqr' || format === 'frame-qr' || format === 'canvas-qr') {
313
+ return frameqr.encodeFrameQR(value, options);
314
+ }
315
+ if (format === 'gs1databar14' || format === 'gs1-databar14' || format === 'databar') {
316
+ return databar.encodeDataBar14(value, options);
317
+ }
222
318
 
223
319
  const entry = ONED_FORMATS[format];
224
320
  if (!entry) {
225
- const known = [...Object.keys(ONED_FORMATS), 'qr', 'datamatrix', 'aztec', 'pdf417', 'micropdf417'].join(', ');
321
+ const known = [...Object.keys(ONED_FORMATS), 'qr', 'datamatrix', 'aztec', 'aztecrune', 'pdf417', 'compactpdf417', 'micropdf417', 'microqr', 'rmqr', 'frameqr', 'gs1databar14'].join(', ');
226
322
  throw new EncodeError(`Unknown format "${format}". Known formats: ${known}`);
227
323
  }
228
324
  return entry.encode(value, options);
@@ -245,6 +341,9 @@ export function encode(text, options = {}) {
245
341
  * @property {number} [rowHeight] PDF417 row height in modules.
246
342
  * @property {number} [variant] MicroPDF417 predefined variant number.
247
343
  * @property {number} [eccCodewords] MicroPDF417 fixed error-correction codewords.
344
+ * @property {string} [profile] FrameQR Code profile identifier.
345
+ * @property {boolean} [certified] Whether the profile is certified by its originator.
346
+ * @property {object} [canvas] Canvas reservation metadata for the FrameQR Code profile.
248
347
  */
249
348
 
250
349
  /**
@@ -259,6 +358,8 @@ export function encode(text, options = {}) {
259
358
  * @param {string[]} [options.formats] Restrict to these format ids.
260
359
  * @param {boolean} [options.tryHarder] Retry inverted and rotated. Default true.
261
360
  * @param {'global'|'hybrid'|'auto'} [options.binarizer]
361
+ * @param {object} [options.frameqr] FrameQR Code detector options when
362
+ * the profile marker is not preserved through image rendering.
262
363
  * @returns {DecodeResult[]}
263
364
  */
264
365
  export function decode(image, options = {}) {
@@ -267,8 +368,13 @@ export function decode(image, options = {}) {
267
368
  const wantQR = !want || want.has('qr') || want.has('qrcode');
268
369
  const wantDataMatrix = !want || want.has('datamatrix') || want.has('data-matrix');
269
370
  const wantAztec = !want || want.has('aztec') || want.has('aztec-code');
371
+ const wantAztecRune = !want || want.has('aztecrune') || want.has('aztec-rune') || want.has('rune');
270
372
  const wantPDF417 = !want || want.has('pdf417') || want.has('pdf-417');
373
+ const wantCompactPDF417 = !want || want.has('compactpdf417') || want.has('compact-pdf417') || want.has('compact-pdf-417');
271
374
  const wantMicroPDF417 = !want || want.has('micropdf417') || want.has('micro-pdf417') || want.has('micro-pdf-417');
375
+ const wantMicroQR = !want || want.has('microqr') || want.has('micro-qr');
376
+ const wantRMQR = !want || want.has('rmqr') || want.has('r-mqr') || want.has('rectangular-micro-qr');
377
+ const wantFrameQR = !want || want.has('frameqr') || want.has('frame-qr') || want.has('canvas-qr');
272
378
  const wantOneD = !want || [...want].some((f) => f in ONED_FORMATS);
273
379
 
274
380
  const source = LuminanceSource.fromImageData(image);
@@ -321,6 +427,15 @@ export function decode(image, options = {}) {
321
427
  }
322
428
  }
323
429
 
430
+ if (wantAztecRune && aztecRuneCanDecode) {
431
+ try {
432
+ const found = aztecRune.detectAndDecodeAztecRune(bits);
433
+ if (found) results.push({ ...found, format: 'aztecrune' });
434
+ } catch {
435
+ /* no Aztec Rune in this pass */
436
+ }
437
+ }
438
+
324
439
  if (wantPDF417 && pdf417CanDecode) {
325
440
  try {
326
441
  const found = pdf417.detectAndDecodePDF417(bits);
@@ -330,6 +445,15 @@ export function decode(image, options = {}) {
330
445
  }
331
446
  }
332
447
 
448
+ if (wantCompactPDF417 && compactPdf417CanDecode) {
449
+ try {
450
+ const found = compactPdf417.detectAndDecodeCompactPDF417(bits);
451
+ if (found) results.push({ ...found, format: 'compactpdf417' });
452
+ } catch {
453
+ /* no Compact PDF417 in this pass */
454
+ }
455
+ }
456
+
333
457
  if (wantMicroPDF417 && microPdf417CanDecode) {
334
458
  // MicroPDF417 detection measures runs across the whole raster. Hybrid
335
459
  // thresholding can alter uniform modules near local-window boundaries,
@@ -345,6 +469,35 @@ export function decode(image, options = {}) {
345
469
  }
346
470
  }
347
471
 
472
+ if (wantMicroQR && microQrCanDecode) {
473
+ try {
474
+ for (const found of microqr.detectAndDecodeMicroQR(bits)) {
475
+ results.push({ ...found, format: 'microqr' });
476
+ }
477
+ } catch {
478
+ /* no Micro QR in this pass */
479
+ }
480
+ }
481
+
482
+ if (wantRMQR && rmqrCanDecode) {
483
+ try {
484
+ const found = rmqr.detectAndDecodeRMQR(bits);
485
+ if (found) results.push({ ...found, format: 'rmqr' });
486
+ } catch {
487
+ /* no rMQR in this pass */
488
+ }
489
+ }
490
+
491
+ if (wantFrameQR && frameQrCanDecode) {
492
+ try {
493
+ for (const found of frameqr.detectAndDecodeFrameQR(bits, options.frameqr ?? {})) {
494
+ results.push({ ...found, format: 'frameqr' });
495
+ }
496
+ } catch {
497
+ /* no FrameQR Code in this pass */
498
+ }
499
+ }
500
+
348
501
  if (wantOneD) {
349
502
  const oneDFormats = want ? [...want].filter((f) => f in ONED_FORMATS) : null;
350
503
  for (const found of decodeOneD(bits, { formats: oneDFormats, tryHarder })) {
@@ -379,4 +532,4 @@ export function decodeStrict(image, options) {
379
532
  }
380
533
 
381
534
  /** Library version, matching package.json. */
382
- export const VERSION = '1.2.5';
535
+ export const VERSION = '1.4.1';
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -0,0 +1,246 @@
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 decoder for an already sampled M1-M4 module matrix.
34
+ *
35
+ * @module microqr/decoder
36
+ */
37
+
38
+ import { ChecksumError, FormatError } from '../core/errors.js';
39
+ import { GF256_QR } from '../core/galois-field.js';
40
+ import { rsDecode } from '../core/reed-solomon.js';
41
+ import {
42
+ microQrBlockLayout,
43
+ microQrDataModuleOrder,
44
+ microQrDecodeFormatInfo,
45
+ microQrFormatInfoPositions,
46
+ microQrMaskBit,
47
+ } from './tables.js';
48
+
49
+ const ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
50
+ const MODE_NAMES = ['numeric', 'alphanumeric', 'byte', 'kanji'];
51
+ const COUNT_BITS = {
52
+ numeric: [0, 3, 4, 5, 6],
53
+ alphanumeric: [0, 0, 3, 4, 5],
54
+ byte: [0, 0, 0, 4, 5],
55
+ kanji: [0, 0, 0, 3, 4],
56
+ };
57
+
58
+ class LimitedBitReader {
59
+ constructor(bytes, limit) {
60
+ this.bytes = bytes;
61
+ this.limit = limit;
62
+ this.offset = 0;
63
+ }
64
+
65
+ available() { return this.limit - this.offset; }
66
+
67
+ read(count) {
68
+ if (!Number.isInteger(count) || count < 1 || count > 32 || count > this.available()) {
69
+ throw new FormatError(`Micro QR: needed ${count} bits, ${Math.max(0, this.available())} remain`);
70
+ }
71
+ let value = 0;
72
+ for (let i = 0; i < count; i++, this.offset++) {
73
+ value = (value << 1) | ((this.bytes[this.offset >>> 3] >>> (7 - (this.offset & 7))) & 1);
74
+ }
75
+ return value >>> 0;
76
+ }
77
+ }
78
+
79
+ function moduleAt(matrix, x, y, mirrored) {
80
+ return mirrored ? matrix.get(y, x) : matrix.get(x, y);
81
+ }
82
+
83
+ function readFormat(matrix, expectedVersion, mirrored) {
84
+ let bits = 0;
85
+ const positions = microQrFormatInfoPositions(matrix.width);
86
+ for (let i = 0; i < positions.length; i++) {
87
+ const [x, y] = positions[i];
88
+ if (moduleAt(matrix, x, y, mirrored)) bits |= 1 << i;
89
+ }
90
+ const format = microQrDecodeFormatInfo(bits);
91
+ if (!format) throw new FormatError('Micro QR: format information is unreadable');
92
+ if (format.version !== expectedVersion) {
93
+ throw new FormatError(
94
+ `Micro QR: format identifies ${format.version}, but the matrix dimension identifies ${expectedVersion}`,
95
+ );
96
+ }
97
+ return format;
98
+ }
99
+
100
+ function readCodewords(matrix, layout, mask, mirrored) {
101
+ const order = microQrDataModuleOrder(layout.version);
102
+ const data = new Array(layout.dataCodewords).fill(0);
103
+ const ecc = new Array(layout.eccCodewords).fill(0);
104
+ let streamOffset = 0;
105
+
106
+ const readBit = () => {
107
+ const x = order[streamOffset * 2];
108
+ const y = order[streamOffset * 2 + 1];
109
+ if (x === undefined || y === undefined) throw new FormatError('Micro QR: encoding region is truncated');
110
+ streamOffset++;
111
+ return moduleAt(matrix, x, y, mirrored) !== microQrMaskBit(mask, x, y) ? 1 : 0;
112
+ };
113
+ const readInto = (target, index, count, highBit = 7) => {
114
+ for (let bit = highBit; bit > highBit - count; bit--) target[index] |= readBit() << bit;
115
+ };
116
+
117
+ const fullData = layout.shortDataCodewordBits === 4 ? layout.dataCodewords - 1 : layout.dataCodewords;
118
+ for (let i = 0; i < fullData; i++) readInto(data, i, 8);
119
+ if (layout.shortDataCodewordBits === 4) readInto(data, data.length - 1, 4);
120
+ for (let i = 0; i < ecc.length; i++) readInto(ecc, i, 8);
121
+
122
+ if (streamOffset !== order.length / 2) {
123
+ throw new FormatError(`Micro QR: read ${streamOffset} of ${order.length / 2} encoding modules`);
124
+ }
125
+ return data.concat(ecc);
126
+ }
127
+
128
+ function correctCodewords(received, layout) {
129
+ const corrections = rsDecode(received, layout.eccCodewords, GF256_QR, 0);
130
+ if (layout.version === 'M1' && corrections !== 0) {
131
+ throw new ChecksumError('Micro QR: M1 provides error detection only');
132
+ }
133
+ return { data: Uint8Array.from(received.slice(0, layout.dataCodewords)), corrections };
134
+ }
135
+
136
+ function decodeKanjiValue(value) {
137
+ const combined = (Math.floor(value / 0xc0) << 8) | (value % 0xc0);
138
+ const sjis = combined + (combined < 0x1f00 ? 0x8140 : 0xc140);
139
+ const bytes = Uint8Array.of(sjis >>> 8, sjis & 0xff);
140
+ try {
141
+ return new TextDecoder('shift_jis', { fatal: true }).decode(bytes);
142
+ } catch {
143
+ throw new FormatError(`Micro QR: invalid Kanji value ${value}`);
144
+ }
145
+ }
146
+
147
+ function parsePayload(data, version, dataBits) {
148
+ const reader = new LimitedBitReader(data, dataBits);
149
+ const modeValue = version === 1 ? 0 : reader.read(version - 1);
150
+ if (modeValue > 3 || (version === 2 && modeValue > 1)) {
151
+ throw new FormatError(`Micro QR: mode indicator ${modeValue} is unavailable in M${version}`);
152
+ }
153
+ const mode = MODE_NAMES[modeValue];
154
+ const countWidth = COUNT_BITS[mode][version];
155
+ if (!countWidth) throw new FormatError(`Micro QR: ${mode} mode is unavailable in M${version}`);
156
+ const count = reader.read(countWidth);
157
+ if (count === 0) throw new FormatError('Micro QR: zero-length data segment');
158
+
159
+ let text = '';
160
+ const rawBytes = [];
161
+ if (mode === 'numeric') {
162
+ let remaining = count;
163
+ while (remaining >= 3) {
164
+ const value = reader.read(10);
165
+ if (value >= 1000) throw new FormatError(`Micro QR: invalid numeric triplet ${value}`);
166
+ text += String(value).padStart(3, '0');
167
+ remaining -= 3;
168
+ }
169
+ if (remaining === 2) {
170
+ const value = reader.read(7);
171
+ if (value >= 100) throw new FormatError(`Micro QR: invalid numeric pair ${value}`);
172
+ text += String(value).padStart(2, '0');
173
+ } else if (remaining === 1) {
174
+ const value = reader.read(4);
175
+ if (value >= 10) throw new FormatError(`Micro QR: invalid numeric digit ${value}`);
176
+ text += String(value);
177
+ }
178
+ } else if (mode === 'alphanumeric') {
179
+ let remaining = count;
180
+ while (remaining >= 2) {
181
+ const value = reader.read(11);
182
+ if (value >= 45 * 45) throw new FormatError(`Micro QR: invalid alphanumeric pair ${value}`);
183
+ text += ALPHANUMERIC[Math.floor(value / 45)] + ALPHANUMERIC[value % 45];
184
+ remaining -= 2;
185
+ }
186
+ if (remaining === 1) {
187
+ const value = reader.read(6);
188
+ if (value >= 45) throw new FormatError(`Micro QR: invalid alphanumeric value ${value}`);
189
+ text += ALPHANUMERIC[value];
190
+ }
191
+ } else if (mode === 'byte') {
192
+ for (let i = 0; i < count; i++) {
193
+ const value = reader.read(8);
194
+ rawBytes.push(value);
195
+ text += String.fromCharCode(value);
196
+ }
197
+ } else {
198
+ for (let i = 0; i < count; i++) text += decodeKanjiValue(reader.read(13));
199
+ }
200
+ return { text, bytes: Uint8Array.from(rawBytes), mode };
201
+ }
202
+
203
+ function decodeOrientation(matrix, expectedVersion, mirrored) {
204
+ const format = readFormat(matrix, expectedVersion, mirrored);
205
+ const layout = microQrBlockLayout(format.version, format.ecc);
206
+ const received = readCodewords(matrix, layout, format.mask, mirrored);
207
+ const { data, corrections } = correctCodewords(received, layout);
208
+ const payload = parsePayload(data, Number(format.version.slice(1)), layout.dataBits);
209
+ return {
210
+ text: payload.text,
211
+ bytes: payload.bytes,
212
+ mode: payload.mode,
213
+ version: format.version,
214
+ ecc: format.ecc,
215
+ mask: format.mask,
216
+ corrections,
217
+ formatCorrections: format.correctedBits,
218
+ mirrored,
219
+ };
220
+ }
221
+
222
+ /** Decode a sampled Micro QR Code symbol without its quiet zone. */
223
+ export function decodeMicroQR(matrix) {
224
+ if (!matrix || !Number.isInteger(matrix.width) || typeof matrix.get !== 'function') {
225
+ throw new FormatError('Micro QR: no matrix supplied');
226
+ }
227
+ if (matrix.height !== matrix.width) {
228
+ throw new FormatError(`Micro QR: symbol must be square, got ${matrix.width}x${matrix.height}`);
229
+ }
230
+ const version = (matrix.width - 9) / 2;
231
+ if (!Number.isInteger(version) || version < 1 || version > 4) {
232
+ throw new FormatError(`Micro QR: ${matrix.width} modules is not a valid M1-M4 symbol size`);
233
+ }
234
+ const expectedVersion = `M${version}`;
235
+ try {
236
+ return decodeOrientation(matrix, expectedVersion, false);
237
+ } catch (primaryError) {
238
+ try {
239
+ return decodeOrientation(matrix, expectedVersion, true);
240
+ } catch {
241
+ throw primaryError;
242
+ }
243
+ }
244
+ }
245
+
246
+ export { ChecksumError, FormatError };