@sythos/js_barcode_universal 0.1.0

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 +215 -0
  2. package/NOTICE.md +106 -0
  3. package/README.md +433 -0
  4. package/bundle/sythos-barcode.esm.js +7998 -0
  5. package/bundle/sythos-barcode.js +7948 -0
  6. package/examples/create.html +731 -0
  7. package/examples/read.html +341 -0
  8. package/licenses/README.md +42 -0
  9. package/licenses/codabar.license +74 -0
  10. package/licenses/code-11.license +69 -0
  11. package/licenses/code-128.license +69 -0
  12. package/licenses/code-39.license +70 -0
  13. package/licenses/code-93.license +71 -0
  14. package/licenses/ean-13.license +70 -0
  15. package/licenses/ean-8.license +70 -0
  16. package/licenses/gs1-128.license +71 -0
  17. package/licenses/isbn.license +76 -0
  18. package/licenses/itf-14.license +69 -0
  19. package/licenses/itf.license +70 -0
  20. package/licenses/msi-plessey.license +72 -0
  21. package/licenses/pharmacode.license +71 -0
  22. package/licenses/qr-code.license +75 -0
  23. package/licenses/upc-a.license +72 -0
  24. package/licenses/upc-e.license +69 -0
  25. package/package.json +89 -0
  26. package/src/core/bit-buffer.js +174 -0
  27. package/src/core/bit-matrix.js +241 -0
  28. package/src/core/errors.js +61 -0
  29. package/src/core/galois-field.js +204 -0
  30. package/src/core/index.js +56 -0
  31. package/src/core/reed-solomon.js +313 -0
  32. package/src/image/binarizer.js +270 -0
  33. package/src/image/grid-sampler.js +164 -0
  34. package/src/image/index.js +40 -0
  35. package/src/image/luminance.js +196 -0
  36. package/src/image/perspective.js +195 -0
  37. package/src/index.js +240 -0
  38. package/src/oned/index.js +89 -0
  39. package/src/oned/patterns.js +384 -0
  40. package/src/oned/reader.js +918 -0
  41. package/src/oned/writers.js +741 -0
  42. package/src/qr/decoder.js +575 -0
  43. package/src/qr/detector.js +630 -0
  44. package/src/qr/encoder.js +958 -0
  45. package/src/qr/index.js +44 -0
  46. package/src/qr/tables.js +737 -0
  47. package/src/render/image-data.js +125 -0
  48. package/src/render/index.js +130 -0
  49. package/src/render/options.js +160 -0
  50. package/src/render/png.js +295 -0
  51. package/src/render/svg.js +120 -0
  52. package/src/render/webgl.js +206 -0
  53. package/src/render/webgpu.js +369 -0
@@ -0,0 +1,918 @@
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
+ * Linear barcode reading.
33
+ *
34
+ * A linear symbol carries no vertical information, so reading one is a 1D
35
+ * signal problem: take a horizontal slice, measure the run lengths of dark and
36
+ * light, and match those against the symbology's patterns.
37
+ *
38
+ * Two consequences shape everything here:
39
+ *
40
+ * - **Scan many rows, not one.** Any single row can be crossed by a fold, a
41
+ * glare highlight or a printing void. Rows are sampled across the height and
42
+ * the first that decodes cleanly wins.
43
+ * - **Match ratios, not absolute widths.** The scale is unknown and varies
44
+ * across the image under perspective, so patterns are compared after
45
+ * normalising by total width. This is what makes a symbol readable at any
46
+ * size without being told the module width.
47
+ *
48
+ * @module oned/reader
49
+ */
50
+
51
+ import { NotFoundError } from '../core/errors.js';
52
+ import {
53
+ EAN_L, EAN_G, EAN_R, EAN13_PARITY, UPCE_PARITY,
54
+ CODE39, CODE39_CHECK_SET,
55
+ CODE93, CODE93_VALUES,
56
+ CODE128, CODE128_START_A, CODE128_START_B, CODE128_START_C,
57
+ CODE128_STOP, CODE128_FNC1, CODE128_CODE_A, CODE128_CODE_B, CODE128_CODE_C,
58
+ CODE128_SHIFT,
59
+ ITF, CODABAR, CODABAR_START_STOP,
60
+ } from './patterns.js';
61
+ import { ean13CheckDigit, upceToUpcaBody } from './writers.js';
62
+
63
+ /* ------------------------------------------------------------------ *
64
+ * Pattern matching primitives
65
+ * ------------------------------------------------------------------ */
66
+
67
+ /**
68
+ * Compare measured run lengths against an ideal pattern, scale-independently.
69
+ *
70
+ * Returns a normalised mismatch score, or Infinity when any single element is
71
+ * further out of proportion than `maxIndividual` allows. Rejecting on the
72
+ * worst element as well as the total is what stops a run of noise whose widths
73
+ * happen to average out from being accepted as a character.
74
+ *
75
+ * @param {number[]} counters Measured widths, in pixels.
76
+ * @param {number[]} pattern Ideal widths, in modules.
77
+ * @param {number} maxIndividual Tolerance per element, as a fraction of a module.
78
+ * @returns {number}
79
+ */
80
+ export function patternVariance(counters, pattern, maxIndividual) {
81
+ const n = counters.length;
82
+ if (n !== pattern.length) return Infinity;
83
+
84
+ let total = 0;
85
+ let patternTotal = 0;
86
+ for (let i = 0; i < n; i++) {
87
+ total += counters[i];
88
+ patternTotal += pattern[i];
89
+ }
90
+ if (total < patternTotal) return Infinity; // fewer pixels than modules
91
+
92
+ const unit = total / patternTotal;
93
+ const maxVariance = unit * maxIndividual;
94
+
95
+ let variance = 0;
96
+ for (let i = 0; i < n; i++) {
97
+ const expected = pattern[i] * unit;
98
+ const delta = Math.abs(counters[i] - expected);
99
+ if (delta > maxVariance) return Infinity;
100
+ variance += delta;
101
+ }
102
+ return variance / total;
103
+ }
104
+
105
+ /**
106
+ * Measure alternating run lengths starting at `start`.
107
+ *
108
+ * @param {Uint8Array} row One byte per pixel, 1 = dark.
109
+ * @param {number} start
110
+ * @param {number[]} counters Filled in place; its length sets how many runs to read.
111
+ * @returns {boolean} False if the row ended before the runs were filled.
112
+ */
113
+ export function recordPattern(row, start, counters) {
114
+ counters.fill(0);
115
+ const end = row.length;
116
+ if (start >= end) return false;
117
+
118
+ let isDark = row[start] === 1;
119
+ let index = 0;
120
+ let i = start;
121
+
122
+ while (i < end) {
123
+ if ((row[i] === 1) === isDark) {
124
+ counters[index]++;
125
+ } else {
126
+ index++;
127
+ if (index === counters.length) break;
128
+ counters[index] = 1;
129
+ isDark = !isDark;
130
+ }
131
+ i++;
132
+ }
133
+
134
+ // The final run may legitimately reach the edge of the image.
135
+ return index === counters.length || (index === counters.length - 1 && i === end);
136
+ }
137
+
138
+ /**
139
+ * Classify run lengths into narrow and wide, for the n/w symbologies.
140
+ *
141
+ * The wide:narrow ratio is not fixed by these formats — it is anywhere from
142
+ * 2:1 to 3:1 and varies with the printer — so the split has to be discovered
143
+ * from the data. Candidate thresholds are tried from the smallest counter
144
+ * upward until exactly the expected number of wide elements falls out.
145
+ *
146
+ * @param {number[]} counters
147
+ * @param {number} expectedWide How many elements must be wide.
148
+ * @returns {number} Bit pattern, MSB = first element wide; -1 if undecidable.
149
+ */
150
+ export function toNarrowWidePattern(counters, expectedWide) {
151
+ const n = counters.length;
152
+ let maxNarrow = 0;
153
+
154
+ for (;;) {
155
+ let nextNarrow = Infinity;
156
+ for (let i = 0; i < n; i++) {
157
+ if (counters[i] > maxNarrow && counters[i] < nextNarrow) nextNarrow = counters[i];
158
+ }
159
+ if (nextNarrow === Infinity) return -1;
160
+ maxNarrow = nextNarrow;
161
+
162
+ let wideCount = 0;
163
+ let pattern = 0;
164
+ let wideTotal = 0;
165
+ let narrowTotal = 0;
166
+ for (let i = 0; i < n; i++) {
167
+ if (counters[i] > maxNarrow) {
168
+ pattern |= 1 << (n - 1 - i);
169
+ wideCount++;
170
+ wideTotal += counters[i];
171
+ } else {
172
+ narrowTotal += counters[i];
173
+ }
174
+ }
175
+
176
+ if (wideCount === expectedWide) {
177
+ // Sanity: a wide element should be clearly wider than a narrow one.
178
+ const narrowCount = n - wideCount;
179
+ if (narrowCount === 0) return -1;
180
+ const avgWide = wideTotal / wideCount;
181
+ const avgNarrow = narrowTotal / narrowCount;
182
+ if (avgWide < avgNarrow * 1.4) return -1;
183
+ return pattern;
184
+ }
185
+ if (wideCount < expectedWide) return -1;
186
+ }
187
+ }
188
+
189
+ /** Convert an 'n'/'w' pattern string to the same bit encoding. */
190
+ function nwToBits(pattern) {
191
+ let bits = 0;
192
+ for (let i = 0; i < pattern.length; i++) {
193
+ if (pattern[i] === 'w') bits |= 1 << (pattern.length - 1 - i);
194
+ }
195
+ return bits;
196
+ }
197
+
198
+ /** Convert a digit-width pattern string to a numeric array. */
199
+ function widthsToArray(pattern) {
200
+ return [...pattern].map(Number);
201
+ }
202
+
203
+ /** Convert a module string ('0'/'1') to run lengths. */
204
+ function modulesToRuns(modules) {
205
+ const runs = [];
206
+ let current = modules[0];
207
+ let count = 0;
208
+ for (const ch of modules) {
209
+ if (ch === current) count++;
210
+ else { runs.push(count); current = ch; count = 1; }
211
+ }
212
+ runs.push(count);
213
+ return runs;
214
+ }
215
+
216
+ /* ------------------------------------------------------------------ *
217
+ * Precomputed lookup structures
218
+ * ------------------------------------------------------------------ */
219
+
220
+ const EAN_L_RUNS = EAN_L.map(modulesToRuns);
221
+ const EAN_G_RUNS = EAN_G.map(modulesToRuns);
222
+ const EAN_R_RUNS = EAN_R.map(modulesToRuns);
223
+ const CODE128_RUNS = CODE128.map(widthsToArray);
224
+ const CODE93_RUNS = Object.fromEntries(
225
+ Object.entries(CODE93).map(([k, v]) => [k, widthsToArray(v)])
226
+ );
227
+ const CODE39_BITS = Object.fromEntries(
228
+ Object.entries(CODE39).map(([k, v]) => [nwToBits(v), k])
229
+ );
230
+ const CODABAR_BITS = Object.fromEntries(
231
+ Object.entries(CODABAR).map(([k, v]) => [nwToBits(v), k])
232
+ );
233
+ const ITF_BITS = Object.fromEntries(ITF.map((v, i) => [nwToBits(v), i]));
234
+
235
+ /**
236
+ * Shortest ITF payload treated as a real read.
237
+ *
238
+ * ITF is always an even number of digits and real-world payloads are at least
239
+ * six (ITF-6, ITF-14 and the GS1 variants). Accepting two digits meant any
240
+ * pair of matching runs inside an unrelated symbol read as a valid ITF.
241
+ */
242
+ const MIN_ITF_DIGITS = 6;
243
+
244
+ const START_END_PATTERN = [1, 1, 1];
245
+ const MIDDLE_PATTERN = [1, 1, 1, 1, 1];
246
+ const UPCE_END_PATTERN = [1, 1, 1, 1, 1, 1];
247
+
248
+ /* ------------------------------------------------------------------ *
249
+ * EAN / UPC
250
+ * ------------------------------------------------------------------ */
251
+
252
+ /**
253
+ * Decode one EAN/UPC digit, reporting which parity set matched.
254
+ *
255
+ * @param {Uint8Array} row
256
+ * @param {number} start
257
+ * @param {boolean} rightHand True to match only the R set.
258
+ * @returns {{digit: number, even: boolean, end: number} | null}
259
+ */
260
+ function decodeEANDigit(row, start, rightHand) {
261
+ const counters = [0, 0, 0, 0];
262
+ if (!recordPattern(row, start, counters)) return null;
263
+ const width = counters[0] + counters[1] + counters[2] + counters[3];
264
+
265
+ let best = null;
266
+ let bestVariance = 0.48; // reject anything worse than this
267
+
268
+ const consider = (runs, digit, even) => {
269
+ const v = patternVariance(counters, runs, 0.7);
270
+ if (v < bestVariance) {
271
+ bestVariance = v;
272
+ best = { digit, even, end: start + width };
273
+ }
274
+ };
275
+
276
+ for (let d = 0; d < 10; d++) {
277
+ if (rightHand) {
278
+ consider(EAN_R_RUNS[d], d, false);
279
+ } else {
280
+ consider(EAN_L_RUNS[d], d, false);
281
+ consider(EAN_G_RUNS[d], d, true);
282
+ }
283
+ }
284
+ return best;
285
+ }
286
+
287
+ /**
288
+ * @param {Uint8Array} row
289
+ * @returns {{format: string, text: string} | null}
290
+ */
291
+ function decodeEANFamily(row) {
292
+ const guard = findGuard(row, 0, START_END_PATTERN, false);
293
+ if (!guard) return null;
294
+
295
+ let offset = guard.end;
296
+ const digits = [];
297
+ let parityBits = 0;
298
+
299
+ // Six left-hand digits, recording parity as we go.
300
+ for (let i = 0; i < 6; i++) {
301
+ const d = decodeEANDigit(row, offset, false);
302
+ if (!d) return null;
303
+ digits.push(d.digit);
304
+ if (d.even) parityBits |= 1 << (5 - i);
305
+ offset = d.end;
306
+ }
307
+
308
+ // EAN-8 has no even-parity digits and a middle guard at a different offset;
309
+ // try the 13-digit reading first, then fall back.
310
+ const middle = matchAt(row, offset, MIDDLE_PATTERN);
311
+ if (middle) {
312
+ offset = middle.end;
313
+ for (let i = 0; i < 6; i++) {
314
+ const d = decodeEANDigit(row, offset, true);
315
+ if (!d) return null;
316
+ digits.push(d.digit);
317
+ offset = d.end;
318
+ }
319
+ if (!matchAt(row, offset, START_END_PATTERN)) return null;
320
+
321
+ const parityStr = [];
322
+ for (let i = 0; i < 6; i++) parityStr.push((parityBits >> (5 - i)) & 1 ? 'G' : 'L');
323
+ const first = EAN13_PARITY.indexOf(parityStr.join(''));
324
+ if (first < 0) return null;
325
+
326
+ const text = String(first) + digits.join('');
327
+ if (Number(text[12]) !== ean13CheckDigit(text.slice(0, 12))) return null;
328
+
329
+ // A leading zero means this was printed as UPC-A.
330
+ return first === 0
331
+ ? { format: 'upca', text: text.slice(1) }
332
+ : { format: 'ean13', text };
333
+ }
334
+
335
+ return null;
336
+ }
337
+
338
+ /**
339
+ * EAN-8: four left digits, middle guard, four right digits.
340
+ *
341
+ * @param {Uint8Array} row
342
+ * @returns {{format: string, text: string} | null}
343
+ */
344
+ function decodeEAN8(row) {
345
+ const guard = findGuard(row, 0, START_END_PATTERN, false);
346
+ if (!guard) return null;
347
+
348
+ let offset = guard.end;
349
+ const digits = [];
350
+ for (let i = 0; i < 4; i++) {
351
+ const d = decodeEANDigit(row, offset, false);
352
+ if (!d || d.even) return null; // EAN-8 left digits are all odd parity
353
+ digits.push(d.digit);
354
+ offset = d.end;
355
+ }
356
+
357
+ const middle = matchAt(row, offset, MIDDLE_PATTERN);
358
+ if (!middle) return null;
359
+ offset = middle.end;
360
+
361
+ for (let i = 0; i < 4; i++) {
362
+ const d = decodeEANDigit(row, offset, true);
363
+ if (!d) return null;
364
+ digits.push(d.digit);
365
+ offset = d.end;
366
+ }
367
+ if (!matchAt(row, offset, START_END_PATTERN)) return null;
368
+
369
+ const text = digits.join('');
370
+ if (Number(text[7]) !== ean13CheckDigit(text.slice(0, 7))) return null;
371
+ return { format: 'ean8', text };
372
+ }
373
+
374
+ /**
375
+ * UPC-E: six digits, parity-encoded, terminated by a six-element guard.
376
+ *
377
+ * @param {Uint8Array} row
378
+ * @returns {{format: string, text: string} | null}
379
+ */
380
+ function decodeUPCE(row) {
381
+ const guard = findGuard(row, 0, START_END_PATTERN, false);
382
+ if (!guard) return null;
383
+
384
+ let offset = guard.end;
385
+ const digits = [];
386
+ let parityBits = 0;
387
+ for (let i = 0; i < 6; i++) {
388
+ const d = decodeEANDigit(row, offset, false);
389
+ if (!d) return null;
390
+ digits.push(d.digit);
391
+ if (d.even) parityBits |= 1 << (5 - i);
392
+ offset = d.end;
393
+ }
394
+
395
+ // Six digits and then the end guard, in that order and nothing between. The
396
+ // EAN readers above match their trailing guard; this one used to stop at the
397
+ // last digit, which let it report a symbol it had never seen the end of.
398
+ if (!matchAt(row, offset, UPCE_END_PATTERN)) return null;
399
+
400
+ const parityStr = [];
401
+ for (let i = 0; i < 6; i++) parityStr.push((parityBits >> (5 - i)) & 1 ? 'E' : 'O');
402
+ const check = UPCE_PARITY.indexOf(parityStr.join(''));
403
+ if (check < 0) return null;
404
+
405
+ // The parity pattern carries the check digit and nothing else, so on its own
406
+ // it says nothing about the six digits it was read alongside: any run whose
407
+ // parities happen to spell one of the ten patterns would be accepted. What
408
+ // ties the two together is the check digit itself — expand the body to the
409
+ // UPC-A it stands for and confirm the digits produce the check digit the
410
+ // parity claimed. Six digits scraped out of a neighbouring symbol pass the
411
+ // parity test one time in ten and this one almost never.
412
+ const body = digits.join('');
413
+ if (ean13CheckDigit(upceToUpcaBody(0, body)) !== check) return null;
414
+
415
+ return { format: 'upce', text: '0' + body + String(check) };
416
+ }
417
+
418
+ /* ------------------------------------------------------------------ *
419
+ * Guard finding
420
+ * ------------------------------------------------------------------ */
421
+
422
+ /**
423
+ * Scan forward for the first place a pattern matches, starting on a dark run.
424
+ *
425
+ * @param {Uint8Array} row
426
+ * @param {number} from
427
+ * @param {number[]} pattern
428
+ * @param {boolean} startsLight
429
+ * @returns {{start: number, end: number} | null}
430
+ */
431
+ function findGuard(row, from, pattern, startsLight) {
432
+ const counters = new Array(pattern.length).fill(0);
433
+ const width = row.length;
434
+ let index = 0;
435
+ let isDark = !startsLight;
436
+ let i = from;
437
+
438
+ // Skip any leading run of the wrong colour.
439
+ while (i < width && (row[i] === 1) !== isDark) i++;
440
+
441
+ let patternStart = i;
442
+ counters.fill(0);
443
+
444
+ while (i < width) {
445
+ if ((row[i] === 1) === isDark) {
446
+ counters[index]++;
447
+ } else {
448
+ if (index === pattern.length - 1) {
449
+ if (patternVariance(counters, pattern, 0.7) < 0.5) {
450
+ return { start: patternStart, end: i };
451
+ }
452
+ // Slide the window forward by two runs and keep looking.
453
+ patternStart += counters[0] + counters[1];
454
+ for (let k = 2; k < pattern.length; k++) counters[k - 2] = counters[k];
455
+ counters[pattern.length - 2] = 0;
456
+ counters[pattern.length - 1] = 0;
457
+ index--;
458
+ } else {
459
+ index++;
460
+ }
461
+ counters[index] = 1;
462
+ isDark = !isDark;
463
+ }
464
+ i++;
465
+ }
466
+ return null;
467
+ }
468
+
469
+ /**
470
+ * Match a pattern at an exact position.
471
+ *
472
+ * @param {Uint8Array} row
473
+ * @param {number} start
474
+ * @param {number[]} pattern
475
+ * @returns {{end: number} | null}
476
+ */
477
+ function matchAt(row, start, pattern) {
478
+ const counters = new Array(pattern.length).fill(0);
479
+ if (!recordPattern(row, start, counters)) return null;
480
+ if (patternVariance(counters, pattern, 0.7) >= 0.5) return null;
481
+ let width = 0;
482
+ for (const c of counters) width += c;
483
+ return { end: start + width };
484
+ }
485
+
486
+ /* ------------------------------------------------------------------ *
487
+ * Code 128
488
+ * ------------------------------------------------------------------ */
489
+
490
+ /**
491
+ * @param {Uint8Array} row
492
+ * @returns {{format: string, text: string} | null}
493
+ */
494
+ function decodeCode128(row) {
495
+ // Locate whichever start symbol appears first.
496
+ let start = null;
497
+ for (const startCode of [CODE128_START_A, CODE128_START_B, CODE128_START_C]) {
498
+ const found = findGuard(row, 0, CODE128_RUNS[startCode], false);
499
+ if (found && (!start || found.start < start.found.start)) {
500
+ start = { code: startCode, found };
501
+ }
502
+ }
503
+ if (!start) return null;
504
+
505
+ // Read every symbol up to the stop pattern first, and only then interpret
506
+ // them. The symbol immediately before the stop is the checksum, and it is
507
+ // indistinguishable from data while scanning — interpreting as we go would
508
+ // append it to the text (a Code C checksum of 70 arrives as the digits
509
+ // "70"). Collecting first makes dropping it exact rather than a guess.
510
+ const values = [];
511
+ let offset = start.found.end;
512
+ const counters = new Array(6).fill(0);
513
+ const stopCounters = new Array(7).fill(0);
514
+
515
+ for (;;) {
516
+ // The stop pattern has seven elements, so it must be tried before the
517
+ // six-element symbol set or its first six would match something.
518
+ if (recordPattern(row, offset, stopCounters) &&
519
+ patternVariance(stopCounters, CODE128_RUNS[CODE128_STOP], 0.7) < 0.38) {
520
+ break;
521
+ }
522
+
523
+ if (!recordPattern(row, offset, counters)) return null;
524
+
525
+ let best = -1;
526
+ let bestVariance = 0.4;
527
+ for (let c = 0; c < CODE128_RUNS.length - 1; c++) {
528
+ const v = patternVariance(counters, CODE128_RUNS[c], 0.7);
529
+ if (v < bestVariance) { bestVariance = v; best = c; }
530
+ }
531
+ if (best < 0) return null;
532
+
533
+ let width = 0;
534
+ for (const c of counters) width += c;
535
+ offset += width;
536
+ values.push(best);
537
+
538
+ if (values.length > 256) return null; // runaway scan
539
+ }
540
+
541
+ // Start symbol, data, checksum, stop. Anything shorter is not a symbol.
542
+ if (values.length < 2) return null;
543
+
544
+ const checksum = values[values.length - 1];
545
+ const dataValues = values.slice(0, -1);
546
+
547
+ // Verify the checksum rather than trusting the scan. Without this a run of
548
+ // noise that happens to match valid patterns decodes to plausible garbage,
549
+ // which is far worse than reporting nothing.
550
+ let sum = start.code;
551
+ for (let i = 0; i < dataValues.length; i++) sum += dataValues[i] * (i + 1);
552
+ if (sum % 103 !== checksum) return null;
553
+
554
+ let mode = start.code === CODE128_START_A ? 'A'
555
+ : start.code === CODE128_START_B ? 'B' : 'C';
556
+ let shifted = null;
557
+ let text = '';
558
+
559
+ for (const value of dataValues) {
560
+ const active = shifted || mode;
561
+ shifted = null;
562
+
563
+ if (value === CODE128_CODE_A && mode !== 'A') { mode = 'A'; continue; }
564
+ if (value === CODE128_CODE_B && mode !== 'B') { mode = 'B'; continue; }
565
+ if (value === CODE128_CODE_C) { mode = 'C'; continue; }
566
+ if (value === CODE128_SHIFT) { shifted = mode === 'A' ? 'B' : 'A'; continue; }
567
+ if (value === CODE128_FNC1) { continue; }
568
+ if (value >= 96 && value <= 102) { continue; } // other function characters
569
+
570
+ if (active === 'C') {
571
+ text += String(value).padStart(2, '0');
572
+ } else if (active === 'A') {
573
+ text += value < 64 ? String.fromCharCode(value + 32) : String.fromCharCode(value - 64);
574
+ } else {
575
+ text += String.fromCharCode(value + 32);
576
+ }
577
+ }
578
+
579
+ if (text.length === 0) return null;
580
+ return { format: 'code128', text };
581
+ }
582
+
583
+ /* ------------------------------------------------------------------ *
584
+ * Code 39
585
+ * ------------------------------------------------------------------ */
586
+
587
+ /**
588
+ * @param {Uint8Array} row
589
+ * @param {object} options
590
+ * @returns {{format: string, text: string} | null}
591
+ */
592
+ function decodeCode39(row, options = {}) {
593
+ const counters = new Array(9).fill(0);
594
+ let offset = 0;
595
+
596
+ // Find the '*' start character.
597
+ const startBits = nwToBits(CODE39['*']);
598
+ let found = false;
599
+ while (offset < row.length) {
600
+ while (offset < row.length && row[offset] !== 1) offset++;
601
+ if (offset >= row.length) break;
602
+ if (recordPattern(row, offset, counters)) {
603
+ if (toNarrowWidePattern(counters, 3) === startBits) { found = true; break; }
604
+ }
605
+ // Advance past this dark run and the following light run.
606
+ while (offset < row.length && row[offset] === 1) offset++;
607
+ while (offset < row.length && row[offset] === 0) offset++;
608
+ }
609
+ if (!found) return null;
610
+
611
+ let width = 0;
612
+ for (const c of counters) width += c;
613
+ offset += width;
614
+
615
+ let text = '';
616
+ for (;;) {
617
+ // Skip the inter-character gap.
618
+ while (offset < row.length && row[offset] === 0) offset++;
619
+ if (offset >= row.length) return null;
620
+ if (!recordPattern(row, offset, counters)) return null;
621
+
622
+ const bits = toNarrowWidePattern(counters, 3);
623
+ const ch = CODE39_BITS[bits];
624
+ if (ch === undefined) return null;
625
+
626
+ let w = 0;
627
+ for (const c of counters) w += c;
628
+ offset += w;
629
+
630
+ if (ch === '*') break;
631
+ text += ch;
632
+ if (text.length > 80) return null;
633
+ }
634
+
635
+ if (text.length === 0) return null;
636
+
637
+ if (options.checkDigit) {
638
+ const expected = text[text.length - 1];
639
+ const body = text.slice(0, -1);
640
+ let sum = 0;
641
+ for (const ch of body) sum += CODE39_CHECK_SET.indexOf(ch);
642
+ if (CODE39_CHECK_SET[sum % 43] !== expected) return null;
643
+ text = body;
644
+ }
645
+
646
+ return { format: 'code39', text };
647
+ }
648
+
649
+ /* ------------------------------------------------------------------ *
650
+ * Code 93
651
+ * ------------------------------------------------------------------ */
652
+
653
+ /**
654
+ * @param {Uint8Array} row
655
+ * @returns {{format: string, text: string} | null}
656
+ */
657
+ function decodeCode93(row) {
658
+ const startRuns = widthsToArray('111141');
659
+ const start = findGuard(row, 0, startRuns, false);
660
+ if (!start) return null;
661
+
662
+ let offset = start.end;
663
+ const counters = new Array(6).fill(0);
664
+ const values = [];
665
+
666
+ for (;;) {
667
+ if (!recordPattern(row, offset, counters)) return null;
668
+
669
+ let best = -1;
670
+ let bestVariance = 0.38;
671
+ for (let v = 0; v < CODE93_VALUES.length; v++) {
672
+ const runs = CODE93_RUNS[CODE93_VALUES[v]];
673
+ const variance = patternVariance(counters, runs, 0.7);
674
+ if (variance < bestVariance) { bestVariance = variance; best = v; }
675
+ }
676
+
677
+ const stopVariance = patternVariance(counters, startRuns, 0.7);
678
+ if (stopVariance < bestVariance) break;
679
+ if (best < 0) return null;
680
+
681
+ values.push(best);
682
+ let w = 0;
683
+ for (const c of counters) w += c;
684
+ offset += w;
685
+ if (values.length > 90) return null;
686
+ }
687
+
688
+ if (values.length < 3) return null;
689
+
690
+ // Verify both check characters before trusting anything.
691
+ const weighted = (data, maxWeight) => {
692
+ let sum = 0;
693
+ for (let i = 0; i < data.length; i++) {
694
+ const weight = ((data.length - 1 - i) % maxWeight) + 1;
695
+ sum += weight * data[i];
696
+ }
697
+ return sum % 47;
698
+ };
699
+ const k = values.pop();
700
+ const c = values.pop();
701
+ if (weighted(values, 20) !== c) return null;
702
+ if (weighted([...values, c], 15) !== k) return null;
703
+
704
+ let text = '';
705
+ for (const v of values) {
706
+ const key = CODE93_VALUES[v];
707
+ if (key.length > 1) return null; // shift characters not expanded here
708
+ text += key;
709
+ }
710
+ return { format: 'code93', text };
711
+ }
712
+
713
+ /* ------------------------------------------------------------------ *
714
+ * ITF
715
+ * ------------------------------------------------------------------ */
716
+
717
+ /**
718
+ * @param {Uint8Array} row
719
+ * @returns {{format: string, text: string} | null}
720
+ */
721
+ function decodeITF(row) {
722
+ const start = findGuard(row, 0, [1, 1, 1, 1], false);
723
+ if (!start) return null;
724
+
725
+ let offset = start.end;
726
+ const digits = [];
727
+ const barCounters = new Array(5).fill(0);
728
+ const spaceCounters = new Array(5).fill(0);
729
+ const pair = new Array(10).fill(0);
730
+
731
+ for (;;) {
732
+ if (!recordPattern(row, offset, pair)) break;
733
+
734
+ // De-interleave: even indices are bars, odd are spaces.
735
+ for (let k = 0; k < 5; k++) {
736
+ barCounters[k] = pair[k * 2];
737
+ spaceCounters[k] = pair[k * 2 + 1];
738
+ }
739
+
740
+ const barBits = toNarrowWidePattern(barCounters, 2);
741
+ const spaceBits = toNarrowWidePattern(spaceCounters, 2);
742
+ const a = ITF_BITS[barBits];
743
+ const b = ITF_BITS[spaceBits];
744
+ if (a === undefined || b === undefined) break;
745
+
746
+ digits.push(a, b);
747
+ let w = 0;
748
+ for (const c of pair) w += c;
749
+ offset += w;
750
+ if (digits.length > 40) break;
751
+ }
752
+
753
+ // ITF carries no mandatory checksum, so structure is the only defence
754
+ // against a false positive — and without these two checks there is none.
755
+ //
756
+ // The loop above stops as soon as a pair fails to match, which happens both
757
+ // at the genuine end of a symbol and in the middle of unrelated bars. Two
758
+ // digits scraped out of a Code 39 or UPC-A symbol matched the digit patterns
759
+ // often enough to be reported as a real ITF read, so `decode()` returned a
760
+ // phantom result alongside the true one.
761
+ if (digits.length < MIN_ITF_DIGITS) return null;
762
+
763
+ // Require the run to end on the actual stop pattern: wide bar, narrow space,
764
+ // narrow bar. A fragment that merely ran out of matching pairs has no stop
765
+ // pattern after it and is rejected here.
766
+ const stop = new Array(3).fill(0);
767
+ if (!recordPattern(row, offset, stop)) return null;
768
+ if (toNarrowWidePattern(stop, 1) !== 0b100) return null;
769
+
770
+ return { format: 'itf', text: digits.join('') };
771
+ }
772
+
773
+ /* ------------------------------------------------------------------ *
774
+ * Codabar
775
+ * ------------------------------------------------------------------ */
776
+
777
+ /**
778
+ * @param {Uint8Array} row
779
+ * @returns {{format: string, text: string} | null}
780
+ */
781
+ function decodeCodabar(row) {
782
+ const counters = new Array(7).fill(0);
783
+ let offset = 0;
784
+ let startChar = null;
785
+
786
+ while (offset < row.length) {
787
+ while (offset < row.length && row[offset] !== 1) offset++;
788
+ if (offset >= row.length) break;
789
+ if (recordPattern(row, offset, counters)) {
790
+ const bits = toNarrowWidePattern(counters, 3) >= 0
791
+ ? toNarrowWidePattern(counters, 3)
792
+ : toNarrowWidePattern(counters, 2);
793
+ const ch = CODABAR_BITS[bits];
794
+ if (ch && CODABAR_START_STOP.includes(ch)) { startChar = ch; break; }
795
+ }
796
+ while (offset < row.length && row[offset] === 1) offset++;
797
+ while (offset < row.length && row[offset] === 0) offset++;
798
+ }
799
+ if (!startChar) return null;
800
+
801
+ let w = 0;
802
+ for (const c of counters) w += c;
803
+ offset += w;
804
+
805
+ let text = '';
806
+ for (;;) {
807
+ while (offset < row.length && row[offset] === 0) offset++;
808
+ if (offset >= row.length) return null;
809
+ if (!recordPattern(row, offset, counters)) return null;
810
+
811
+ let bits = toNarrowWidePattern(counters, 3);
812
+ let ch = CODABAR_BITS[bits];
813
+ if (ch === undefined) {
814
+ bits = toNarrowWidePattern(counters, 2);
815
+ ch = CODABAR_BITS[bits];
816
+ }
817
+ if (ch === undefined) return null;
818
+
819
+ let width = 0;
820
+ for (const c of counters) width += c;
821
+ offset += width;
822
+
823
+ if (CODABAR_START_STOP.includes(ch)) break;
824
+ text += ch;
825
+ if (text.length > 60) return null;
826
+ }
827
+
828
+ if (text.length === 0) return null;
829
+ return { format: 'codabar', text };
830
+ }
831
+
832
+ /* ------------------------------------------------------------------ *
833
+ * Public entry point
834
+ * ------------------------------------------------------------------ */
835
+
836
+ /** Decoders in the order they are tried. */
837
+ const DECODERS = [
838
+ ['ean13', decodeEANFamily],
839
+ ['upca', decodeEANFamily],
840
+ ['ean8', decodeEAN8],
841
+ ['upce', decodeUPCE],
842
+ ['code128', decodeCode128],
843
+ ['code39', decodeCode39],
844
+ ['code93', decodeCode93],
845
+ ['itf', decodeITF],
846
+ ['codabar', decodeCodabar],
847
+ ];
848
+
849
+ /**
850
+ * Read every linear symbol found in a binarized image.
851
+ *
852
+ * @param {import('../core/bit-matrix.js').BitMatrix} image Binarized; set bit = dark.
853
+ * @param {object} [options]
854
+ * @param {string[]} [options.formats] Restrict to these format ids.
855
+ * @param {number} [options.rows] How many horizontal slices to try.
856
+ * @param {boolean} [options.tryHarder] Also scan reversed rows, for mirrored symbols.
857
+ * @returns {Array<{format: string, text: string, row: number}>}
858
+ */
859
+ export function decodeOneD(image, options = {}) {
860
+ const { formats = null, rows = 15, tryHarder = true } = options;
861
+ const enabled = formats ? new Set(formats) : null;
862
+ const active = DECODERS.filter(([id]) => !enabled || enabled.has(id));
863
+ if (active.length === 0) return [];
864
+
865
+ const results = [];
866
+ const seen = new Set();
867
+ const height = image.height;
868
+ const buffer = new Uint8Array(image.width);
869
+
870
+ // Sample rows from the middle outward: symbols are usually centred, and the
871
+ // middle of a linear barcode is the part least likely to be clipped.
872
+ const middle = height >> 1;
873
+ const step = Math.max(1, Math.round(height / rows));
874
+
875
+ for (let attempt = 0; attempt < rows; attempt++) {
876
+ const delta = Math.ceil(attempt / 2) * step * (attempt % 2 === 0 ? 1 : -1);
877
+ const y = middle + delta;
878
+ if (y < 0 || y >= height) continue;
879
+
880
+ const row = image.getRow(y, buffer);
881
+
882
+ for (const pass of tryHarder ? [false, true] : [false]) {
883
+ const scan = pass ? Uint8Array.from(row).reverse() : row;
884
+
885
+ for (const [id, decoder] of active) {
886
+ let result = null;
887
+ try {
888
+ result = decoder(scan, options);
889
+ } catch {
890
+ result = null; // a malformed candidate is not an error
891
+ }
892
+ if (!result) continue;
893
+ if (enabled && !enabled.has(result.format)) continue;
894
+
895
+ const key = `${result.format}:${result.text}`;
896
+ if (seen.has(key)) continue;
897
+ seen.add(key);
898
+ results.push({ ...result, row: y });
899
+ void id;
900
+ }
901
+ }
902
+ }
903
+
904
+ return results;
905
+ }
906
+
907
+ /**
908
+ * Convenience wrapper that throws when nothing is found.
909
+ *
910
+ * @param {import('../core/bit-matrix.js').BitMatrix} image
911
+ * @param {object} [options]
912
+ * @returns {{format: string, text: string, row: number}}
913
+ */
914
+ export function decodeOneDStrict(image, options) {
915
+ const results = decodeOneD(image, options);
916
+ if (results.length === 0) throw new NotFoundError('No linear barcode found');
917
+ return results[0];
918
+ }