@sythos/js_barcode_universal 1.4.1 → 1.5.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.
@@ -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
@@ -23,7 +24,6 @@
23
24
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
25
  * SOFTWARE.
25
26
  *
26
- * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
27
27
  * SPDX-License-Identifier: MIT
28
28
  *
29
29
  * Original work. No code from any other barcode implementation.
@@ -58,8 +58,14 @@ import {
58
58
  CODE128_STOP, CODE128_FNC1, CODE128_CODE_A, CODE128_CODE_B, CODE128_CODE_C,
59
59
  CODE128_SHIFT,
60
60
  ITF, CODABAR, CODABAR_START_STOP,
61
+ CODE11, CODE11_START_STOP, MSI_START, MSI_STOP, MSI_BIT,
61
62
  } from './patterns.js';
62
63
  import { ean13CheckDigit, upceToUpcaBody } from './writers.js';
64
+ import {
65
+ EAN2_PARITY, EAN5_PARITY, EAN_ADDON_START, EAN_ADDON_SEPARATOR,
66
+ ean5Checksum,
67
+ } from './addons.js';
68
+ import { decodeDataBar14Scanline } from '../databar/decoder.js';
63
69
 
64
70
  /* ------------------------------------------------------------------ *
65
71
  * Pattern matching primitives
@@ -232,6 +238,14 @@ const CODABAR_BITS = Object.fromEntries(
232
238
  Object.entries(CODABAR).map(([k, v]) => [nwToBits(v), k])
233
239
  );
234
240
  const ITF_BITS = Object.fromEntries(ITF.map((v, i) => [nwToBits(v), i]));
241
+ const CODE11_BITS = Object.fromEntries(
242
+ Object.entries(CODE11).flatMap(([ch, value]) => {
243
+ const bits = nwToBits(value);
244
+ return [[`${bits}:1`, ch], [`${bits}:2`, ch]];
245
+ })
246
+ );
247
+ const CODE11_START_BITS = nwToBits(CODE11_START_STOP);
248
+ const CODE11_CHARSET = '0123456789-';
235
249
 
236
250
  /**
237
251
  * Shortest ITF payload treated as a real read.
@@ -317,7 +331,8 @@ function decodeEANFamily(row) {
317
331
  digits.push(d.digit);
318
332
  offset = d.end;
319
333
  }
320
- if (!matchAt(row, offset, START_END_PATTERN)) return null;
334
+ const trailing = matchAt(row, offset, START_END_PATTERN);
335
+ if (!trailing) return null;
321
336
 
322
337
  const parityStr = [];
323
338
  for (let i = 0; i < 6; i++) parityStr.push((parityBits >> (5 - i)) & 1 ? 'G' : 'L');
@@ -328,9 +343,10 @@ function decodeEANFamily(row) {
328
343
  if (Number(text[12]) !== ean13CheckDigit(text.slice(0, 12))) return null;
329
344
 
330
345
  // A leading zero means this was printed as UPC-A.
331
- return first === 0
346
+ const result = first === 0
332
347
  ? { format: 'upca', text: text.slice(1) }
333
348
  : { format: 'ean13', text };
349
+ return attachEANAddon(result, row, trailing.end);
334
350
  }
335
351
 
336
352
  return null;
@@ -365,11 +381,12 @@ function decodeEAN8(row) {
365
381
  digits.push(d.digit);
366
382
  offset = d.end;
367
383
  }
368
- if (!matchAt(row, offset, START_END_PATTERN)) return null;
384
+ const trailing = matchAt(row, offset, START_END_PATTERN);
385
+ if (!trailing) return null;
369
386
 
370
387
  const text = digits.join('');
371
388
  if (Number(text[7]) !== ean13CheckDigit(text.slice(0, 7))) return null;
372
- return { format: 'ean8', text };
389
+ return attachEANAddon({ format: 'ean8', text }, row, trailing.end);
373
390
  }
374
391
 
375
392
  /**
@@ -396,7 +413,8 @@ function decodeUPCE(row) {
396
413
  // Six digits and then the end guard, in that order and nothing between. The
397
414
  // EAN readers above match their trailing guard; this one used to stop at the
398
415
  // last digit, which let it report a symbol it had never seen the end of.
399
- if (!matchAt(row, offset, UPCE_END_PATTERN)) return null;
416
+ const trailing = matchAt(row, offset, UPCE_END_PATTERN);
417
+ if (!trailing) return null;
400
418
 
401
419
  const parityStr = [];
402
420
  for (let i = 0; i < 6; i++) parityStr.push((parityBits >> (5 - i)) & 1 ? 'E' : 'O');
@@ -413,7 +431,7 @@ function decodeUPCE(row) {
413
431
  const body = digits.join('');
414
432
  if (ean13CheckDigit(upceToUpcaBody(0, body)) !== check) return null;
415
433
 
416
- return { format: 'upce', text: '0' + body + String(check) };
434
+ return attachEANAddon({ format: 'upce', text: '0' + body + String(check) }, row, trailing.end);
417
435
  }
418
436
 
419
437
  /* ------------------------------------------------------------------ *
@@ -484,6 +502,256 @@ function matchAt(row, start, pattern) {
484
502
  return { end: start + width };
485
503
  }
486
504
 
505
+ /* ------------------------------------------------------------------ *
506
+ * EAN supplements
507
+ * ------------------------------------------------------------------ */
508
+
509
+ /**
510
+ * Decode a supplement immediately following a validated EAN/UPC symbol.
511
+ * The caller supplies the end of the parent trailing guard, so a supplement
512
+ * can never be accepted as an unrelated standalone linear symbol.
513
+ *
514
+ * @param {Uint8Array} row
515
+ * @param {number} baseEnd
516
+ * @param {2|5} digitCount
517
+ * @returns {{format:'ean2'|'ean5', text:string, parity:string, checksum?:number, end:number}|null}
518
+ */
519
+ function decodeEANSupplementRow(row, baseEnd, digitCount) {
520
+ const guard = findGuard(row, baseEnd, [1, 1, 2], false);
521
+ if (!guard || guard.start - baseEnd < 4) return null;
522
+
523
+ let offset = guard.end;
524
+ let text = '';
525
+ let parity = '';
526
+
527
+ for (let i = 0; i < digitCount; i++) {
528
+ const digit = decodeEANDigit(row, offset, false);
529
+ if (!digit) return null;
530
+ text += String(digit.digit);
531
+ parity += digit.even ? 'B' : 'A';
532
+ offset = digit.end;
533
+
534
+ if (i + 1 < digitCount) {
535
+ const separator = matchAt(row, offset, [1, 1]);
536
+ if (!separator) return null;
537
+ offset = separator.end;
538
+ }
539
+ }
540
+
541
+ if (digitCount === 2) {
542
+ if (EAN2_PARITY[Number(text) % 4] !== parity) return null;
543
+ return { format: 'ean2', text, parity, end: offset };
544
+ }
545
+
546
+ const checksum = ean5Checksum(text);
547
+ if (EAN5_PARITY[checksum] !== parity) return null;
548
+ return { format: 'ean5', text, parity, checksum, end: offset };
549
+ }
550
+
551
+ /**
552
+ * Attach the longest valid supplement to a base result. EAN-5 is attempted
553
+ * first so its prefix cannot be reported as a shorter EAN-2 symbol.
554
+ *
555
+ * @param {object} base
556
+ * @param {Uint8Array} row
557
+ * @param {number} baseEnd
558
+ * @returns {object}
559
+ */
560
+ function attachEANAddon(base, row, baseEnd) {
561
+ const addon = decodeEANSupplementRow(row, baseEnd, 5)
562
+ ?? decodeEANSupplementRow(row, baseEnd, 2);
563
+ if (!addon) return base;
564
+ const { end, ...publicAddon } = addon;
565
+ void end;
566
+ return { ...base, addon: publicAddon };
567
+ }
568
+
569
+ /* ------------------------------------------------------------------ *
570
+ * Code 11 and MSI/Plessey
571
+ * ------------------------------------------------------------------ */
572
+
573
+ /** @param {number[]} counters @returns {number} */
574
+ function counterTotal(counters) {
575
+ return counters.reduce((sum, value) => sum + value, 0);
576
+ }
577
+
578
+ /**
579
+ * Apply the Code 11 C/K checksum grammar used by the writer.
580
+ *
581
+ * @param {string} encoded
582
+ * @param {boolean|undefined} requested
583
+ * @returns {string|null}
584
+ */
585
+ function finalizeCode11(encoded, requested) {
586
+ const weighted = (text, maxWeight) => {
587
+ let sum = 0;
588
+ for (let i = 0; i < text.length; i++) {
589
+ const weight = ((text.length - 1 - i) % maxWeight) + 1;
590
+ sum += weight * CODE11_CHARSET.indexOf(text[i]);
591
+ }
592
+ return sum;
593
+ };
594
+ const validC = (text) => {
595
+ if (text.length < 2) return null;
596
+ const body = text.slice(0, -1);
597
+ const expected = CODE11_CHARSET[weighted(body, 10) % 11];
598
+ return text[text.length - 1] === expected ? body : null;
599
+ };
600
+ const validCK = (text) => {
601
+ if (text.length < 12) return null;
602
+ const body = text.slice(0, -2);
603
+ const c = text[text.length - 2];
604
+ const k = text[text.length - 1];
605
+ const expectedC = CODE11_CHARSET[weighted(body, 10) % 11];
606
+ if (c !== expectedC) return null;
607
+ const expectedK = CODE11_CHARSET[weighted(body + c, 9) % 11];
608
+ return k === expectedK ? body : null;
609
+ };
610
+
611
+ if (requested === false) return encoded;
612
+ if (requested === true) {
613
+ const checked = encoded.length >= 12 ? validCK(encoded) : validC(encoded);
614
+ return checked;
615
+ }
616
+
617
+ // With no explicit option, strip checks only when the complete grammar is
618
+ // unambiguous; otherwise preserve the literal payload.
619
+ return validCK(encoded) ?? validC(encoded) ?? encoded;
620
+ }
621
+
622
+ /**
623
+ * Decode Code 11 from one binarized scanline.
624
+ *
625
+ * @param {Uint8Array} row
626
+ * @param {object} [options]
627
+ * @param {boolean} [options.checkDigit]
628
+ * @returns {{format:'code11', text:string}|null}
629
+ */
630
+ export function decodeCode11(row, options = {}) {
631
+ const counters = new Array(5).fill(0);
632
+ let start = null;
633
+ for (let i = 0; i < row.length; i++) {
634
+ if (row[i] !== 1 || (i > 0 && row[i - 1] === 1)) continue;
635
+ if (!recordPattern(row, i, counters)) continue;
636
+ if (toNarrowWidePattern(counters, 2) === CODE11_START_BITS) {
637
+ start = { position: i, end: i + counterTotal(counters), scale: counterTotal(counters) / 9 };
638
+ break;
639
+ }
640
+ }
641
+ if (!start) return null;
642
+
643
+ let offset = start.end;
644
+ while (offset < row.length && row[offset] === 0) offset++;
645
+ let encoded = '';
646
+
647
+ for (let count = 0; count < 160 && offset < row.length; count++) {
648
+ if (!recordPattern(row, offset, counters)) return null;
649
+ const width = counterTotal(counters);
650
+ const stop = toNarrowWidePattern(counters, 2);
651
+ if (stop === CODE11_START_BITS) {
652
+ if (encoded.length === 0) return null;
653
+ const stopEnd = offset + width;
654
+ let nextDark = stopEnd;
655
+ while (nextDark < row.length && row[nextDark] === 0) nextDark++;
656
+ if (nextDark !== row.length && nextDark - stopEnd < Math.max(3, Math.ceil(start.scale * 3))) {
657
+ return null;
658
+ }
659
+ const text = finalizeCode11(encoded, options.checkDigit);
660
+ return text == null ? null : { format: 'code11', text };
661
+ }
662
+
663
+ let character = null;
664
+ for (const expectedWide of [1, 2]) {
665
+ const bits = toNarrowWidePattern(counters, expectedWide);
666
+ if (bits < 0) continue;
667
+ const candidate = CODE11_BITS[`${bits}:${expectedWide}`];
668
+ if (candidate !== undefined) {
669
+ character = candidate;
670
+ break;
671
+ }
672
+ }
673
+ if (character == null) return null;
674
+ encoded += character;
675
+ if (encoded.length > 128) return null;
676
+ offset += width;
677
+ while (offset < row.length && row[offset] === 0) offset++;
678
+ }
679
+ return null;
680
+ }
681
+
682
+ /** @param {string} encoded @param {boolean|undefined} requested */
683
+ function finalizeMSI(encoded, requested) {
684
+ if (requested !== true) return encoded;
685
+ if (encoded.length < 2) return null;
686
+ const body = encoded.slice(0, -1);
687
+ let odd = '';
688
+ for (let i = body.length - 1; i >= 0; i -= 2) odd = body[i] + odd;
689
+ const doubled = String(Number(odd) * 2);
690
+ let sum = 0;
691
+ for (const ch of doubled) sum += Number(ch);
692
+ for (let i = body.length - 2; i >= 0; i -= 2) sum += Number(body[i]);
693
+ const expected = String((10 - (sum % 10)) % 10);
694
+ return encoded.endsWith(expected) ? body : null;
695
+ }
696
+
697
+ /**
698
+ * Decode MSI/Plessey from one binarized scanline.
699
+ *
700
+ * @param {Uint8Array} row
701
+ * @param {object} [options]
702
+ * @param {boolean} [options.checkDigit]
703
+ * @returns {{format:'msi', text:string}|null}
704
+ */
705
+ export function decodeMSI(row, options = {}) {
706
+ const start = findGuard(row, 0, [2, 1], false);
707
+ if (!start) return null;
708
+ const scale = (start.end - start.start) / 3;
709
+ if (!(scale >= 1)) return null;
710
+
711
+ let offset = start.end;
712
+ let encoded = '';
713
+ const bitPatterns = [
714
+ { pattern: [1, 2], bit: '0' },
715
+ { pattern: [2, 1], bit: '1' },
716
+ ];
717
+
718
+ for (let digitIndex = 0; digitIndex < 80; digitIndex++) {
719
+ const stop = matchAt(row, offset, [1, 2, 1]);
720
+ if (stop && encoded.length > 0) {
721
+ let nextDark = stop.end;
722
+ while (nextDark < row.length && row[nextDark] === 0) nextDark++;
723
+ if (nextDark === row.length ||
724
+ nextDark - stop.end >= Math.max(3, Math.ceil(scale * 3))) {
725
+ if (encoded.length < 6 && !(options.formats && options.formats.includes('msi'))) {
726
+ return null;
727
+ }
728
+ const text = finalizeMSI(encoded, options.checkDigit);
729
+ return text == null ? null : { format: 'msi', text };
730
+ }
731
+ }
732
+
733
+ let nibble = '';
734
+ for (let bitIndex = 0; bitIndex < 4; bitIndex++) {
735
+ let matched = null;
736
+ for (const candidate of bitPatterns) {
737
+ const found = matchAt(row, offset, candidate.pattern);
738
+ if (found) {
739
+ matched = { ...candidate, end: found.end };
740
+ break;
741
+ }
742
+ }
743
+ if (!matched) return null;
744
+ nibble += matched.bit;
745
+ offset = matched.end;
746
+ }
747
+
748
+ const digit = Number.parseInt(nibble, 2);
749
+ if (digit > 9) return null;
750
+ encoded += String(digit);
751
+ }
752
+ return null;
753
+ }
754
+
487
755
  /* ------------------------------------------------------------------ *
488
756
  * Code 128
489
757
  * ------------------------------------------------------------------ */
@@ -556,8 +824,11 @@ function decodeCode128(row) {
556
824
  : start.code === CODE128_START_B ? 'B' : 'C';
557
825
  let shifted = null;
558
826
  let text = '';
827
+ const fnc1AtStart = dataValues[0] === CODE128_FNC1;
828
+ const fnc1Positions = [];
559
829
 
560
- for (const value of dataValues) {
830
+ for (let dataIndex = 0; dataIndex < dataValues.length; dataIndex++) {
831
+ const value = dataValues[dataIndex];
561
832
  const active = shifted || mode;
562
833
  shifted = null;
563
834
 
@@ -565,7 +836,13 @@ function decodeCode128(row) {
565
836
  if (value === CODE128_CODE_B && mode !== 'B') { mode = 'B'; continue; }
566
837
  if (value === CODE128_CODE_C) { mode = 'C'; continue; }
567
838
  if (value === CODE128_SHIFT) { shifted = mode === 'A' ? 'B' : 'A'; continue; }
568
- if (value === CODE128_FNC1) { continue; }
839
+ if (value === CODE128_FNC1) {
840
+ if (dataIndex > 0) {
841
+ fnc1Positions.push(text.length);
842
+ text += '\x1d';
843
+ }
844
+ continue;
845
+ }
569
846
  if (value >= 96 && value <= 102) { continue; } // other function characters
570
847
 
571
848
  if (active === 'C') {
@@ -578,6 +855,16 @@ function decodeCode128(row) {
578
855
  }
579
856
 
580
857
  if (text.length === 0) return null;
858
+ if (fnc1AtStart) {
859
+ return {
860
+ format: 'gs1128',
861
+ text,
862
+ gs1: true,
863
+ symbologyIdentifier: ']C1',
864
+ fnc1AtStart: true,
865
+ fnc1Positions,
866
+ };
867
+ }
581
868
  return { format: 'code128', text };
582
869
  }
583
870
 
@@ -722,6 +1009,7 @@ function decodeCode93(row) {
722
1009
  function decodeITF(row) {
723
1010
  const start = findGuard(row, 0, [1, 1, 1, 1], false);
724
1011
  if (!start) return null;
1012
+ const startScale = (start.end - start.start) / 4;
725
1013
 
726
1014
  let offset = start.end;
727
1015
  const digits = [];
@@ -767,6 +1055,10 @@ function decodeITF(row) {
767
1055
  const stop = new Array(3).fill(0);
768
1056
  if (!recordPattern(row, offset, stop)) return null;
769
1057
  if (toNarrowWidePattern(stop, 1) !== 0b100) return null;
1058
+ const stopEnd = offset + counterTotal(stop);
1059
+ let nextDark = stopEnd;
1060
+ while (nextDark < row.length && row[nextDark] === 0) nextDark++;
1061
+ if (nextDark !== row.length && nextDark - stopEnd < Math.max(3, Math.ceil(startScale * 3))) return null;
770
1062
 
771
1063
  return { format: 'itf', text: digits.join('') };
772
1064
  }
@@ -841,6 +1133,9 @@ const DECODERS = [
841
1133
  ['ean8', decodeEAN8],
842
1134
  ['upce', decodeUPCE],
843
1135
  ['code128', decodeCode128],
1136
+ ['code11', decodeCode11],
1137
+ ['msi', decodeMSI],
1138
+ ['gs1databar14', decodeDataBar14Scanline],
844
1139
  ['code39', decodeCode39],
845
1140
  ['code93', decodeCode93],
846
1141
  ['itf', decodeITF],
@@ -860,7 +1155,16 @@ const DECODERS = [
860
1155
  export function decodeOneD(image, options = {}) {
861
1156
  const { formats = null, rows = 15, tryHarder = true } = options;
862
1157
  const enabled = formats ? new Set(formats) : null;
863
- const active = DECODERS.filter(([id]) => !enabled || enabled.has(id));
1158
+ const active = DECODERS.filter(([id]) => {
1159
+ if (!enabled) return true;
1160
+ if (enabled.has(id)) return true;
1161
+ if (id === 'ean13' || id === 'ean8' || id === 'upca' || id === 'upce') {
1162
+ return enabled.has('ean2') || enabled.has('ean5');
1163
+ }
1164
+ if (id === 'code128') return enabled.has('gs1128');
1165
+ if (id === 'gs1databar14') return enabled.has('databar') || enabled.has('gs1-databar14');
1166
+ return false;
1167
+ });
864
1168
  if (active.length === 0) return [];
865
1169
 
866
1170
  const results = [];
@@ -891,9 +1195,23 @@ export function decodeOneD(image, options = {}) {
891
1195
  result = null; // a malformed candidate is not an error
892
1196
  }
893
1197
  if (!result) continue;
894
- if (enabled && !enabled.has(result.format)) continue;
1198
+ if (enabled) {
1199
+ const addonRequested = enabled.has('ean2') || enabled.has('ean5');
1200
+ const isEANBase = result.format === 'ean13' || result.format === 'ean8' ||
1201
+ result.format === 'upca' || result.format === 'upce';
1202
+ if (isEANBase && addonRequested) {
1203
+ if (!result.addon || !enabled.has(result.addon.format)) continue;
1204
+ } else if (result.format === 'gs1128') {
1205
+ if (!enabled.has('gs1128') && !enabled.has('code128')) continue;
1206
+ } else if (result.format === 'gs1databar14') {
1207
+ if (!enabled.has('gs1databar14') && !enabled.has('databar') && !enabled.has('gs1-databar14')) continue;
1208
+ } else if (!enabled.has(result.format)) {
1209
+ continue;
1210
+ }
1211
+ }
895
1212
 
896
- const key = `${result.format}:${result.text}`;
1213
+ const addonKey = result.addon ? `:${result.addon.format}:${result.addon.text}` : '';
1214
+ const key = `${result.format}:${result.text}${addonKey}`;
897
1215
  if (seen.has(key)) continue;
898
1216
  seen.add(key);
899
1217
  results.push({ ...result, row: y });
@@ -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
@@ -23,7 +24,6 @@
23
24
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
25
  * SOFTWARE.
25
26
  *
26
- * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
27
27
  * SPDX-License-Identifier: MIT
28
28
  *
29
29
  * Original work. No code from any other barcode implementation.
@@ -464,6 +464,11 @@ export function encodeCode128(value, options = {}) {
464
464
  if (gs1) codes.push(CODE128_FNC1);
465
465
 
466
466
  while (i < value.length) {
467
+ if (gs1 && value[i] === '\x1d') {
468
+ codes.push(CODE128_FNC1);
469
+ i++;
470
+ continue;
471
+ }
467
472
  const run = digitRun(i);
468
473
  const atEnd = i + run === value.length;
469
474
  const worthC = run >= 6 || (i === 0 && run >= 4) || (atEnd && run >= 4);