@sythos/js_barcode_universal 1.5.8 → 1.5.10

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 (179) hide show
  1. package/LICENSE +5 -4
  2. package/NOTICE.md +2 -2
  3. package/README.md +61 -13
  4. package/bundle/sythos-barcode.esm.js +11658 -11244
  5. package/bundle/sythos-barcode.js +11658 -11244
  6. package/package.json +10 -3
  7. package/src/index.d.ts +1 -1
  8. package/src/index.js +508 -516
  9. package/src/js/aztec/decoder.js +208 -199
  10. package/src/js/aztec/detector.js +153 -126
  11. package/src/js/aztec/encoder.js +172 -153
  12. package/src/js/aztec/high-level.js +106 -101
  13. package/src/js/aztec/index.js +1 -12
  14. package/src/js/aztec/tables.js +106 -120
  15. package/src/js/aztecrune/decoder.js +76 -85
  16. package/src/js/aztecrune/detector.js +107 -93
  17. package/src/js/aztecrune/encoder.js +50 -68
  18. package/src/js/aztecrune/index.js +1 -16
  19. package/src/js/aztecrune/tables.js +59 -54
  20. package/src/js/compactpdf417/decoder.js +69 -74
  21. package/src/js/compactpdf417/detector.js +74 -75
  22. package/src/js/compactpdf417/encoder.js +68 -81
  23. package/src/js/compactpdf417/index.js +1 -13
  24. package/src/js/compactpdf417/tables.js +71 -80
  25. package/src/js/core/bit-buffer.js +106 -122
  26. package/src/js/core/bit-matrix.js +196 -194
  27. package/src/js/core/errors.js +13 -15
  28. package/src/js/core/galois-field.js +119 -133
  29. package/src/js/core/index.js +2 -19
  30. package/src/js/core/reed-solomon.js +195 -204
  31. package/src/js/databar/codec.js +93 -108
  32. package/src/js/databar/decoder.js +149 -140
  33. package/src/js/databar/encoder.js +47 -55
  34. package/src/js/databar/gs1.js +122 -123
  35. package/src/js/databar/index.js +4 -36
  36. package/src/js/databar/patterns.js +81 -85
  37. package/src/js/databar/tables.js +51 -52
  38. package/src/js/datamatrix/decoder.js +216 -182
  39. package/src/js/datamatrix/detector.js +161 -132
  40. package/src/js/datamatrix/encoder.js +192 -133
  41. package/src/js/datamatrix/index.js +1 -9
  42. package/src/js/datamatrix/tables.js +68 -69
  43. package/src/js/frameqr/decoder.js +102 -125
  44. package/src/js/frameqr/detector.js +101 -94
  45. package/src/js/frameqr/encoder.js +67 -80
  46. package/src/js/frameqr/index.js +1 -10
  47. package/src/js/frameqr/tables.js +145 -163
  48. package/src/js/image/binarizer.js +146 -162
  49. package/src/js/image/grid-sampler.js +63 -79
  50. package/src/js/image/index.js +0 -2
  51. package/src/js/image/luminance.js +129 -140
  52. package/src/js/image/perspective.js +112 -149
  53. package/src/js/micropdf417/compaction.js +64 -65
  54. package/src/js/micropdf417/decoder.js +106 -109
  55. package/src/js/micropdf417/detector.js +73 -70
  56. package/src/js/micropdf417/encoder.js +129 -154
  57. package/src/js/micropdf417/error-correction.js +6 -11
  58. package/src/js/micropdf417/index.js +2 -16
  59. package/src/js/micropdf417/tables.js +112 -99
  60. package/src/js/microqr/decoder.js +177 -182
  61. package/src/js/microqr/detector.js +240 -256
  62. package/src/js/microqr/encoder.js +239 -194
  63. package/src/js/microqr/index.js +0 -2
  64. package/src/js/microqr/tables.js +208 -200
  65. package/src/js/oned/addons.js +167 -191
  66. package/src/js/oned/index.js +27 -58
  67. package/src/js/oned/patterns.js +194 -213
  68. package/src/js/oned/reader.js +1003 -984
  69. package/src/js/oned/writers.js +411 -466
  70. package/src/js/pdf417/compaction.js +315 -222
  71. package/src/js/pdf417/decoder.js +71 -36
  72. package/src/js/pdf417/detector.js +377 -361
  73. package/src/js/pdf417/encoder.js +57 -46
  74. package/src/js/pdf417/error-correction.js +5 -8
  75. package/src/js/pdf417/tables.js +248 -250
  76. package/src/js/qr/decoder.js +311 -347
  77. package/src/js/qr/detector.js +378 -395
  78. package/src/js/qr/encoder.js +514 -599
  79. package/src/js/qr/index.js +0 -2
  80. package/src/js/qr/tables.js +375 -422
  81. package/src/js/render/image-data.js +57 -62
  82. package/src/js/render/index.js +21 -23
  83. package/src/js/render/options.js +82 -90
  84. package/src/js/render/png.js +144 -169
  85. package/src/js/render/svg.js +41 -44
  86. package/src/js/render/webgl.js +103 -109
  87. package/src/js/render/webgpu.js +205 -217
  88. package/src/js/rmqr/decoder.js +161 -53
  89. package/src/js/rmqr/detector.js +73 -41
  90. package/src/js/rmqr/encoder.js +253 -104
  91. package/src/js/rmqr/index.js +1 -5
  92. package/src/js/rmqr/tables.js +108 -85
  93. package/src/ts/aztec/decoder.ts +317 -0
  94. package/src/ts/aztec/detector.ts +224 -0
  95. package/src/ts/aztec/encoder.ts +257 -0
  96. package/src/ts/aztec/high-level.ts +211 -0
  97. package/src/ts/aztec/index.ts +45 -0
  98. package/src/ts/aztec/tables.ts +210 -0
  99. package/src/ts/aztecrune/decoder.ts +155 -0
  100. package/src/ts/aztecrune/detector.ts +166 -0
  101. package/src/ts/aztecrune/encoder.ts +121 -0
  102. package/src/ts/aztecrune/index.ts +49 -0
  103. package/src/ts/aztecrune/tables.ts +137 -0
  104. package/src/ts/compactpdf417/decoder.ts +128 -0
  105. package/src/ts/compactpdf417/detector.ts +139 -0
  106. package/src/ts/compactpdf417/encoder.ts +140 -0
  107. package/src/ts/compactpdf417/index.ts +17 -0
  108. package/src/ts/compactpdf417/tables.ts +173 -0
  109. package/src/ts/core/bit-buffer.ts +174 -0
  110. package/src/ts/core/bit-matrix.ts +241 -0
  111. package/src/ts/core/errors.ts +61 -0
  112. package/src/ts/core/galois-field.ts +207 -0
  113. package/src/ts/core/index.ts +56 -0
  114. package/src/ts/core/reed-solomon.ts +327 -0
  115. package/src/ts/databar/codec.ts +181 -0
  116. package/src/ts/databar/decoder.ts +215 -0
  117. package/src/ts/databar/encoder.ts +96 -0
  118. package/src/ts/databar/gs1.ts +177 -0
  119. package/src/ts/databar/index.ts +68 -0
  120. package/src/ts/databar/patterns.ts +143 -0
  121. package/src/ts/databar/tables.ts +124 -0
  122. package/src/ts/datamatrix/decoder.ts +262 -0
  123. package/src/ts/datamatrix/detector.ts +225 -0
  124. package/src/ts/datamatrix/encoder.ts +191 -0
  125. package/src/ts/datamatrix/index.ts +42 -0
  126. package/src/ts/datamatrix/tables.ts +123 -0
  127. package/src/ts/frameqr/decoder.ts +239 -0
  128. package/src/ts/frameqr/detector.ts +192 -0
  129. package/src/ts/frameqr/encoder.ts +156 -0
  130. package/src/ts/frameqr/index.ts +42 -0
  131. package/src/ts/frameqr/tables.ts +270 -0
  132. package/src/ts/image/binarizer.ts +270 -0
  133. package/src/ts/image/grid-sampler.ts +164 -0
  134. package/src/ts/image/index.ts +40 -0
  135. package/src/ts/image/luminance.ts +196 -0
  136. package/src/ts/image/perspective.ts +195 -0
  137. package/src/ts/index.d.ts +1 -1
  138. package/src/ts/index.ts +790 -0
  139. package/src/ts/micropdf417/compaction.ts +116 -0
  140. package/src/ts/micropdf417/decoder.ts +183 -0
  141. package/src/ts/micropdf417/detector.ts +149 -0
  142. package/src/ts/micropdf417/encoder.ts +209 -0
  143. package/src/ts/micropdf417/error-correction.ts +55 -0
  144. package/src/ts/micropdf417/index.ts +49 -0
  145. package/src/ts/micropdf417/tables.ts +184 -0
  146. package/src/ts/microqr/decoder.ts +245 -0
  147. package/src/ts/microqr/detector.ts +355 -0
  148. package/src/ts/microqr/encoder.ts +269 -0
  149. package/src/ts/microqr/index.ts +36 -0
  150. package/src/ts/microqr/tables.ts +316 -0
  151. package/src/ts/oned/addons.ts +420 -0
  152. package/src/ts/oned/index.ts +106 -0
  153. package/src/ts/oned/patterns.ts +384 -0
  154. package/src/ts/oned/reader.ts +1364 -0
  155. package/src/ts/oned/writers.ts +746 -0
  156. package/src/ts/pdf417/compaction.ts +298 -0
  157. package/src/ts/pdf417/decoder.ts +75 -0
  158. package/src/ts/pdf417/detector.ts +468 -0
  159. package/src/ts/pdf417/encoder.ts +91 -0
  160. package/src/ts/pdf417/error-correction.ts +47 -0
  161. package/src/ts/pdf417/index.ts +6 -0
  162. package/src/ts/pdf417/tables.ts +317 -0
  163. package/src/ts/qr/decoder.ts +575 -0
  164. package/src/ts/qr/detector.ts +630 -0
  165. package/src/ts/qr/encoder.ts +958 -0
  166. package/src/ts/qr/index.ts +44 -0
  167. package/src/ts/qr/tables.ts +737 -0
  168. package/src/ts/render/image-data.ts +125 -0
  169. package/src/ts/render/index.ts +130 -0
  170. package/src/ts/render/options.ts +160 -0
  171. package/src/ts/render/png.ts +295 -0
  172. package/src/ts/render/svg.ts +120 -0
  173. package/src/ts/render/webgl.ts +206 -0
  174. package/src/ts/render/webgpu.ts +369 -0
  175. package/src/ts/rmqr/decoder.ts +101 -0
  176. package/src/ts/rmqr/detector.ts +90 -0
  177. package/src/ts/rmqr/encoder.ts +172 -0
  178. package/src/ts/rmqr/index.ts +37 -0
  179. package/src/ts/rmqr/tables.ts +154 -0
@@ -27,7 +27,6 @@
27
27
  *
28
28
  * Original work. No code from any other barcode implementation.
29
29
  */
30
-
31
30
  /**
32
31
  * Linear barcode reading.
33
32
  *
@@ -47,29 +46,14 @@
47
46
  *
48
47
  * @module oned/reader
49
48
  */
50
-
51
49
  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
- CODE11, CODE11_START_STOP, MSI_START, MSI_STOP, MSI_BIT,
61
- } from './patterns.js';
50
+ import { EAN_L, EAN_G, EAN_R, EAN13_PARITY, UPCE_PARITY, CODE39, CODE39_CHECK_SET, CODE93, CODE93_VALUES, CODE128, CODE128_START_A, CODE128_START_B, CODE128_START_C, CODE128_STOP, CODE128_FNC1, CODE128_CODE_A, CODE128_CODE_B, CODE128_CODE_C, CODE128_SHIFT, ITF, CODABAR, CODABAR_START_STOP, CODE11, CODE11_START_STOP, } from './patterns.js';
62
51
  import { ean13CheckDigit, upceToUpcaBody } from './writers.js';
63
- import {
64
- EAN2_PARITY, EAN5_PARITY, EAN_ADDON_START, EAN_ADDON_SEPARATOR,
65
- ean5Checksum,
66
- } from './addons.js';
52
+ import { EAN2_PARITY, EAN5_PARITY, ean5Checksum, } from './addons.js';
67
53
  import { decodeDataBar14Scanline } from '../databar/decoder.js';
68
-
69
54
  /* ------------------------------------------------------------------ *
70
55
  * Pattern matching primitives
71
56
  * ------------------------------------------------------------------ */
72
-
73
57
  /**
74
58
  * Compare measured run lengths against an ideal pattern, scale-independently.
75
59
  *
@@ -84,30 +68,29 @@ import { decodeDataBar14Scanline } from '../databar/decoder.js';
84
68
  * @returns {number}
85
69
  */
86
70
  export function patternVariance(counters, pattern, maxIndividual) {
87
- const n = counters.length;
88
- if (n !== pattern.length) return Infinity;
89
-
90
- let total = 0;
91
- let patternTotal = 0;
92
- for (let i = 0; i < n; i++) {
93
- total += counters[i];
94
- patternTotal += pattern[i];
95
- }
96
- if (total < patternTotal) return Infinity; // fewer pixels than modules
97
-
98
- const unit = total / patternTotal;
99
- const maxVariance = unit * maxIndividual;
100
-
101
- let variance = 0;
102
- for (let i = 0; i < n; i++) {
103
- const expected = pattern[i] * unit;
104
- const delta = Math.abs(counters[i] - expected);
105
- if (delta > maxVariance) return Infinity;
106
- variance += delta;
107
- }
108
- return variance / total;
71
+ const n = counters.length;
72
+ if (n !== pattern.length)
73
+ return Infinity;
74
+ let total = 0;
75
+ let patternTotal = 0;
76
+ for (let i = 0; i < n; i++) {
77
+ total += counters[i];
78
+ patternTotal += pattern[i];
79
+ }
80
+ if (total < patternTotal)
81
+ return Infinity; // fewer pixels than modules
82
+ const unit = total / patternTotal;
83
+ const maxVariance = unit * maxIndividual;
84
+ let variance = 0;
85
+ for (let i = 0; i < n; i++) {
86
+ const expected = pattern[i] * unit;
87
+ const delta = Math.abs(counters[i] - expected);
88
+ if (delta > maxVariance)
89
+ return Infinity;
90
+ variance += delta;
91
+ }
92
+ return variance / total;
109
93
  }
110
-
111
94
  /**
112
95
  * Measure alternating run lengths starting at `start`.
113
96
  *
@@ -117,30 +100,29 @@ export function patternVariance(counters, pattern, maxIndividual) {
117
100
  * @returns {boolean} False if the row ended before the runs were filled.
118
101
  */
119
102
  export function recordPattern(row, start, counters) {
120
- counters.fill(0);
121
- const end = row.length;
122
- if (start >= end) return false;
123
-
124
- let isDark = row[start] === 1;
125
- let index = 0;
126
- let i = start;
127
-
128
- while (i < end) {
129
- if ((row[i] === 1) === isDark) {
130
- counters[index]++;
131
- } else {
132
- index++;
133
- if (index === counters.length) break;
134
- counters[index] = 1;
135
- isDark = !isDark;
103
+ counters.fill(0);
104
+ const end = row.length;
105
+ if (start >= end)
106
+ return false;
107
+ let isDark = row[start] === 1;
108
+ let index = 0;
109
+ let i = start;
110
+ while (i < end) {
111
+ if ((row[i] === 1) === isDark) {
112
+ counters[index]++;
113
+ }
114
+ else {
115
+ index++;
116
+ if (index === counters.length)
117
+ break;
118
+ counters[index] = 1;
119
+ isDark = !isDark;
120
+ }
121
+ i++;
136
122
  }
137
- i++;
138
- }
139
-
140
- // The final run may legitimately reach the edge of the image.
141
- return index === counters.length || (index === counters.length - 1 && i === end);
123
+ // The final run may legitimately reach the edge of the image.
124
+ return index === counters.length || (index === counters.length - 1 && i === end);
142
125
  }
143
-
144
126
  /**
145
127
  * Classify run lengths into narrow and wide, for the n/w symbologies.
146
128
  *
@@ -154,98 +136,93 @@ export function recordPattern(row, start, counters) {
154
136
  * @returns {number} Bit pattern, MSB = first element wide; -1 if undecidable.
155
137
  */
156
138
  export function toNarrowWidePattern(counters, expectedWide) {
157
- const n = counters.length;
158
- let maxNarrow = 0;
159
-
160
- for (;;) {
161
- let nextNarrow = Infinity;
162
- for (let i = 0; i < n; i++) {
163
- if (counters[i] > maxNarrow && counters[i] < nextNarrow) nextNarrow = counters[i];
164
- }
165
- if (nextNarrow === Infinity) return -1;
166
- maxNarrow = nextNarrow;
167
-
168
- let wideCount = 0;
169
- let pattern = 0;
170
- let wideTotal = 0;
171
- let narrowTotal = 0;
172
- for (let i = 0; i < n; i++) {
173
- if (counters[i] > maxNarrow) {
174
- pattern |= 1 << (n - 1 - i);
175
- wideCount++;
176
- wideTotal += counters[i];
177
- } else {
178
- narrowTotal += counters[i];
179
- }
180
- }
181
-
182
- if (wideCount === expectedWide) {
183
- // Sanity: a wide element should be clearly wider than a narrow one.
184
- const narrowCount = n - wideCount;
185
- if (narrowCount === 0) return -1;
186
- const avgWide = wideTotal / wideCount;
187
- const avgNarrow = narrowTotal / narrowCount;
188
- if (avgWide < avgNarrow * 1.4) return -1;
189
- return pattern;
139
+ const n = counters.length;
140
+ let maxNarrow = 0;
141
+ for (;;) {
142
+ let nextNarrow = Infinity;
143
+ for (let i = 0; i < n; i++) {
144
+ if (counters[i] > maxNarrow && counters[i] < nextNarrow)
145
+ nextNarrow = counters[i];
146
+ }
147
+ if (nextNarrow === Infinity)
148
+ return -1;
149
+ maxNarrow = nextNarrow;
150
+ let wideCount = 0;
151
+ let pattern = 0;
152
+ let wideTotal = 0;
153
+ let narrowTotal = 0;
154
+ for (let i = 0; i < n; i++) {
155
+ if (counters[i] > maxNarrow) {
156
+ pattern |= 1 << (n - 1 - i);
157
+ wideCount++;
158
+ wideTotal += counters[i];
159
+ }
160
+ else {
161
+ narrowTotal += counters[i];
162
+ }
163
+ }
164
+ if (wideCount === expectedWide) {
165
+ // Sanity: a wide element should be clearly wider than a narrow one.
166
+ const narrowCount = n - wideCount;
167
+ if (narrowCount === 0)
168
+ return -1;
169
+ const avgWide = wideTotal / wideCount;
170
+ const avgNarrow = narrowTotal / narrowCount;
171
+ if (avgWide < avgNarrow * 1.4)
172
+ return -1;
173
+ return pattern;
174
+ }
175
+ if (wideCount < expectedWide)
176
+ return -1;
190
177
  }
191
- if (wideCount < expectedWide) return -1;
192
- }
193
178
  }
194
-
195
179
  /** Convert an 'n'/'w' pattern string to the same bit encoding. */
196
180
  function nwToBits(pattern) {
197
- let bits = 0;
198
- for (let i = 0; i < pattern.length; i++) {
199
- if (pattern[i] === 'w') bits |= 1 << (pattern.length - 1 - i);
200
- }
201
- return bits;
181
+ let bits = 0;
182
+ for (let i = 0; i < pattern.length; i++) {
183
+ if (pattern[i] === 'w')
184
+ bits |= 1 << (pattern.length - 1 - i);
185
+ }
186
+ return bits;
202
187
  }
203
-
204
188
  /** Convert a digit-width pattern string to a numeric array. */
205
189
  function widthsToArray(pattern) {
206
- return [...pattern].map(Number);
190
+ return [...pattern].map(Number);
207
191
  }
208
-
209
192
  /** Convert a module string ('0'/'1') to run lengths. */
210
193
  function modulesToRuns(modules) {
211
- const runs = [];
212
- let current = modules[0];
213
- let count = 0;
214
- for (const ch of modules) {
215
- if (ch === current) count++;
216
- else { runs.push(count); current = ch; count = 1; }
217
- }
218
- runs.push(count);
219
- return runs;
194
+ const runs = [];
195
+ let current = modules[0];
196
+ let count = 0;
197
+ for (const ch of modules) {
198
+ if (ch === current)
199
+ count++;
200
+ else {
201
+ runs.push(count);
202
+ current = ch;
203
+ count = 1;
204
+ }
205
+ }
206
+ runs.push(count);
207
+ return runs;
220
208
  }
221
-
222
209
  /* ------------------------------------------------------------------ *
223
210
  * Precomputed lookup structures
224
211
  * ------------------------------------------------------------------ */
225
-
226
212
  const EAN_L_RUNS = EAN_L.map(modulesToRuns);
227
213
  const EAN_G_RUNS = EAN_G.map(modulesToRuns);
228
214
  const EAN_R_RUNS = EAN_R.map(modulesToRuns);
229
215
  const CODE128_RUNS = CODE128.map(widthsToArray);
230
- const CODE93_RUNS = Object.fromEntries(
231
- Object.entries(CODE93).map(([k, v]) => [k, widthsToArray(v)])
232
- );
233
- const CODE39_BITS = Object.fromEntries(
234
- Object.entries(CODE39).map(([k, v]) => [nwToBits(v), k])
235
- );
236
- const CODABAR_BITS = Object.fromEntries(
237
- Object.entries(CODABAR).map(([k, v]) => [nwToBits(v), k])
238
- );
216
+ const CODE93_RUNS = Object.fromEntries(Object.entries(CODE93).map(([k, v]) => [k, widthsToArray(v)]));
217
+ const CODE39_BITS = Object.fromEntries(Object.entries(CODE39).map(([k, v]) => [nwToBits(v), k]));
218
+ const CODABAR_BITS = Object.fromEntries(Object.entries(CODABAR).map(([k, v]) => [nwToBits(v), k]));
239
219
  const ITF_BITS = Object.fromEntries(ITF.map((v, i) => [nwToBits(v), i]));
240
- const CODE11_BITS = Object.fromEntries(
241
- Object.entries(CODE11).flatMap(([ch, value]) => {
220
+ const CODE11_BITS = Object.fromEntries(Object.entries(CODE11).flatMap(([ch, value]) => {
242
221
  const bits = nwToBits(value);
243
222
  return [[`${bits}:1`, ch], [`${bits}:2`, ch]];
244
- })
245
- );
223
+ }));
246
224
  const CODE11_START_BITS = nwToBits(CODE11_START_STOP);
247
225
  const CODE11_CHARSET = '0123456789-';
248
-
249
226
  /**
250
227
  * Shortest ITF payload treated as a real read.
251
228
  *
@@ -254,15 +231,12 @@ const CODE11_CHARSET = '0123456789-';
254
231
  * pair of matching runs inside an unrelated symbol read as a valid ITF.
255
232
  */
256
233
  const MIN_ITF_DIGITS = 6;
257
-
258
234
  const START_END_PATTERN = [1, 1, 1];
259
235
  const MIDDLE_PATTERN = [1, 1, 1, 1, 1];
260
236
  const UPCE_END_PATTERN = [1, 1, 1, 1, 1, 1];
261
-
262
237
  /* ------------------------------------------------------------------ *
263
238
  * EAN / UPC
264
239
  * ------------------------------------------------------------------ */
265
-
266
240
  /**
267
241
  * Decode one EAN/UPC digit, reporting which parity set matched.
268
242
  *
@@ -272,85 +246,83 @@ const UPCE_END_PATTERN = [1, 1, 1, 1, 1, 1];
272
246
  * @returns {{digit: number, even: boolean, end: number} | null}
273
247
  */
274
248
  function decodeEANDigit(row, start, rightHand) {
275
- const counters = [0, 0, 0, 0];
276
- if (!recordPattern(row, start, counters)) return null;
277
- const width = counters[0] + counters[1] + counters[2] + counters[3];
278
-
279
- let best = null;
280
- let bestVariance = 0.48; // reject anything worse than this
281
-
282
- const consider = (runs, digit, even) => {
283
- const v = patternVariance(counters, runs, 0.7);
284
- if (v < bestVariance) {
285
- bestVariance = v;
286
- best = { digit, even, end: start + width };
287
- }
288
- };
289
-
290
- for (let d = 0; d < 10; d++) {
291
- if (rightHand) {
292
- consider(EAN_R_RUNS[d], d, false);
293
- } else {
294
- consider(EAN_L_RUNS[d], d, false);
295
- consider(EAN_G_RUNS[d], d, true);
249
+ const counters = [0, 0, 0, 0];
250
+ if (!recordPattern(row, start, counters))
251
+ return null;
252
+ const width = counters[0] + counters[1] + counters[2] + counters[3];
253
+ let best = null;
254
+ let bestVariance = 0.48; // reject anything worse than this
255
+ const consider = (runs, digit, even) => {
256
+ const v = patternVariance(counters, runs, 0.7);
257
+ if (v < bestVariance) {
258
+ bestVariance = v;
259
+ best = { digit, even, end: start + width };
260
+ }
261
+ };
262
+ for (let d = 0; d < 10; d++) {
263
+ if (rightHand) {
264
+ consider(EAN_R_RUNS[d], d, false);
265
+ }
266
+ else {
267
+ consider(EAN_L_RUNS[d], d, false);
268
+ consider(EAN_G_RUNS[d], d, true);
269
+ }
296
270
  }
297
- }
298
- return best;
271
+ return best;
299
272
  }
300
-
301
273
  /**
302
274
  * @param {Uint8Array} row
303
275
  * @returns {{format: string, text: string} | null}
304
276
  */
305
277
  function decodeEANFamily(row) {
306
- const guard = findGuard(row, 0, START_END_PATTERN, false);
307
- if (!guard) return null;
308
-
309
- let offset = guard.end;
310
- const digits = [];
311
- let parityBits = 0;
312
-
313
- // Six left-hand digits, recording parity as we go.
314
- for (let i = 0; i < 6; i++) {
315
- const d = decodeEANDigit(row, offset, false);
316
- if (!d) return null;
317
- digits.push(d.digit);
318
- if (d.even) parityBits |= 1 << (5 - i);
319
- offset = d.end;
320
- }
321
-
322
- // EAN-8 has no even-parity digits and a middle guard at a different offset;
323
- // try the 13-digit reading first, then fall back.
324
- const middle = matchAt(row, offset, MIDDLE_PATTERN);
325
- if (middle) {
326
- offset = middle.end;
278
+ const guard = findGuard(row, 0, START_END_PATTERN, false);
279
+ if (!guard)
280
+ return null;
281
+ let offset = guard.end;
282
+ const digits = [];
283
+ let parityBits = 0;
284
+ // Six left-hand digits, recording parity as we go.
327
285
  for (let i = 0; i < 6; i++) {
328
- const d = decodeEANDigit(row, offset, true);
329
- if (!d) return null;
330
- digits.push(d.digit);
331
- offset = d.end;
286
+ const d = decodeEANDigit(row, offset, false);
287
+ if (!d)
288
+ return null;
289
+ digits.push(d.digit);
290
+ if (d.even)
291
+ parityBits |= 1 << (5 - i);
292
+ offset = d.end;
332
293
  }
333
- const trailing = matchAt(row, offset, START_END_PATTERN);
334
- if (!trailing) return null;
335
-
336
- const parityStr = [];
337
- for (let i = 0; i < 6; i++) parityStr.push((parityBits >> (5 - i)) & 1 ? 'G' : 'L');
338
- const first = EAN13_PARITY.indexOf(parityStr.join(''));
339
- if (first < 0) return null;
340
-
341
- const text = String(first) + digits.join('');
342
- if (Number(text[12]) !== ean13CheckDigit(text.slice(0, 12))) return null;
343
-
344
- // A leading zero means this was printed as UPC-A.
345
- const result = first === 0
346
- ? { format: 'upca', text: text.slice(1) }
347
- : { format: 'ean13', text };
348
- return attachEANAddon(result, row, trailing.end);
349
- }
350
-
351
- return null;
294
+ // EAN-8 has no even-parity digits and a middle guard at a different offset;
295
+ // try the 13-digit reading first, then fall back.
296
+ const middle = matchAt(row, offset, MIDDLE_PATTERN);
297
+ if (middle) {
298
+ offset = middle.end;
299
+ for (let i = 0; i < 6; i++) {
300
+ const d = decodeEANDigit(row, offset, true);
301
+ if (!d)
302
+ return null;
303
+ digits.push(d.digit);
304
+ offset = d.end;
305
+ }
306
+ const trailing = matchAt(row, offset, START_END_PATTERN);
307
+ if (!trailing)
308
+ return null;
309
+ const parityStr = [];
310
+ for (let i = 0; i < 6; i++)
311
+ parityStr.push((parityBits >> (5 - i)) & 1 ? 'G' : 'L');
312
+ const first = EAN13_PARITY.indexOf(parityStr.join(''));
313
+ if (first < 0)
314
+ return null;
315
+ const text = String(first) + digits.join('');
316
+ if (Number(text[12]) !== ean13CheckDigit(text.slice(0, 12)))
317
+ return null;
318
+ // A leading zero means this was printed as UPC-A.
319
+ const result = first === 0
320
+ ? { format: 'upca', text: text.slice(1) }
321
+ : { format: 'ean13', text };
322
+ return attachEANAddon(result, row, trailing.end);
323
+ }
324
+ return null;
352
325
  }
353
-
354
326
  /**
355
327
  * EAN-8: four left digits, middle guard, four right digits.
356
328
  *
@@ -358,36 +330,37 @@ function decodeEANFamily(row) {
358
330
  * @returns {{format: string, text: string} | null}
359
331
  */
360
332
  function decodeEAN8(row) {
361
- const guard = findGuard(row, 0, START_END_PATTERN, false);
362
- if (!guard) return null;
363
-
364
- let offset = guard.end;
365
- const digits = [];
366
- for (let i = 0; i < 4; i++) {
367
- const d = decodeEANDigit(row, offset, false);
368
- if (!d || d.even) return null; // EAN-8 left digits are all odd parity
369
- digits.push(d.digit);
370
- offset = d.end;
371
- }
372
-
373
- const middle = matchAt(row, offset, MIDDLE_PATTERN);
374
- if (!middle) return null;
375
- offset = middle.end;
376
-
377
- for (let i = 0; i < 4; i++) {
378
- const d = decodeEANDigit(row, offset, true);
379
- if (!d) return null;
380
- digits.push(d.digit);
381
- offset = d.end;
382
- }
383
- const trailing = matchAt(row, offset, START_END_PATTERN);
384
- if (!trailing) return null;
385
-
386
- const text = digits.join('');
387
- if (Number(text[7]) !== ean13CheckDigit(text.slice(0, 7))) return null;
388
- return attachEANAddon({ format: 'ean8', text }, row, trailing.end);
333
+ const guard = findGuard(row, 0, START_END_PATTERN, false);
334
+ if (!guard)
335
+ return null;
336
+ let offset = guard.end;
337
+ const digits = [];
338
+ for (let i = 0; i < 4; i++) {
339
+ const d = decodeEANDigit(row, offset, false);
340
+ if (!d || d.even)
341
+ return null; // EAN-8 left digits are all odd parity
342
+ digits.push(d.digit);
343
+ offset = d.end;
344
+ }
345
+ const middle = matchAt(row, offset, MIDDLE_PATTERN);
346
+ if (!middle)
347
+ return null;
348
+ offset = middle.end;
349
+ for (let i = 0; i < 4; i++) {
350
+ const d = decodeEANDigit(row, offset, true);
351
+ if (!d)
352
+ return null;
353
+ digits.push(d.digit);
354
+ offset = d.end;
355
+ }
356
+ const trailing = matchAt(row, offset, START_END_PATTERN);
357
+ if (!trailing)
358
+ return null;
359
+ const text = digits.join('');
360
+ if (Number(text[7]) !== ean13CheckDigit(text.slice(0, 7)))
361
+ return null;
362
+ return attachEANAddon({ format: 'ean8', text }, row, trailing.end);
389
363
  }
390
-
391
364
  /**
392
365
  * UPC-E: six digits, parity-encoded, terminated by a six-element guard.
393
366
  *
@@ -395,48 +368,48 @@ function decodeEAN8(row) {
395
368
  * @returns {{format: string, text: string} | null}
396
369
  */
397
370
  function decodeUPCE(row) {
398
- const guard = findGuard(row, 0, START_END_PATTERN, false);
399
- if (!guard) return null;
400
-
401
- let offset = guard.end;
402
- const digits = [];
403
- let parityBits = 0;
404
- for (let i = 0; i < 6; i++) {
405
- const d = decodeEANDigit(row, offset, false);
406
- if (!d) return null;
407
- digits.push(d.digit);
408
- if (d.even) parityBits |= 1 << (5 - i);
409
- offset = d.end;
410
- }
411
-
412
- // Six digits and then the end guard, in that order and nothing between. The
413
- // EAN readers above match their trailing guard; this one used to stop at the
414
- // last digit, which let it report a symbol it had never seen the end of.
415
- const trailing = matchAt(row, offset, UPCE_END_PATTERN);
416
- if (!trailing) return null;
417
-
418
- const parityStr = [];
419
- for (let i = 0; i < 6; i++) parityStr.push((parityBits >> (5 - i)) & 1 ? 'E' : 'O');
420
- const check = UPCE_PARITY.indexOf(parityStr.join(''));
421
- if (check < 0) return null;
422
-
423
- // The parity pattern carries the check digit and nothing else, so on its own
424
- // it says nothing about the six digits it was read alongside: any run whose
425
- // parities happen to spell one of the ten patterns would be accepted. What
426
- // ties the two together is the check digit itself expand the body to the
427
- // UPC-A it stands for and confirm the digits produce the check digit the
428
- // parity claimed. Six digits scraped out of a neighbouring symbol pass the
429
- // parity test one time in ten and this one almost never.
430
- const body = digits.join('');
431
- if (ean13CheckDigit(upceToUpcaBody(0, body)) !== check) return null;
432
-
433
- return attachEANAddon({ format: 'upce', text: '0' + body + String(check) }, row, trailing.end);
371
+ const guard = findGuard(row, 0, START_END_PATTERN, false);
372
+ if (!guard)
373
+ return null;
374
+ let offset = guard.end;
375
+ const digits = [];
376
+ let parityBits = 0;
377
+ for (let i = 0; i < 6; i++) {
378
+ const d = decodeEANDigit(row, offset, false);
379
+ if (!d)
380
+ return null;
381
+ digits.push(d.digit);
382
+ if (d.even)
383
+ parityBits |= 1 << (5 - i);
384
+ offset = d.end;
385
+ }
386
+ // Six digits and then the end guard, in that order and nothing between. The
387
+ // EAN readers above match their trailing guard; this one used to stop at the
388
+ // last digit, which let it report a symbol it had never seen the end of.
389
+ const trailing = matchAt(row, offset, UPCE_END_PATTERN);
390
+ if (!trailing)
391
+ return null;
392
+ const parityStr = [];
393
+ for (let i = 0; i < 6; i++)
394
+ parityStr.push((parityBits >> (5 - i)) & 1 ? 'E' : 'O');
395
+ const check = UPCE_PARITY.indexOf(parityStr.join(''));
396
+ if (check < 0)
397
+ return null;
398
+ // The parity pattern carries the check digit and nothing else, so on its own
399
+ // it says nothing about the six digits it was read alongside: any run whose
400
+ // parities happen to spell one of the ten patterns would be accepted. What
401
+ // ties the two together is the check digit itself expand the body to the
402
+ // UPC-A it stands for and confirm the digits produce the check digit the
403
+ // parity claimed. Six digits scraped out of a neighbouring symbol pass the
404
+ // parity test one time in ten and this one almost never.
405
+ const body = digits.join('');
406
+ if (ean13CheckDigit(upceToUpcaBody(0, body)) !== check)
407
+ return null;
408
+ return attachEANAddon({ format: 'upce', text: '0' + body + String(check) }, row, trailing.end);
434
409
  }
435
-
436
410
  /* ------------------------------------------------------------------ *
437
411
  * Guard finding
438
412
  * ------------------------------------------------------------------ */
439
-
440
413
  /**
441
414
  * Scan forward for the first place a pattern matches, starting on a dark run.
442
415
  *
@@ -447,43 +420,43 @@ function decodeUPCE(row) {
447
420
  * @returns {{start: number, end: number} | null}
448
421
  */
449
422
  function findGuard(row, from, pattern, startsLight) {
450
- const counters = new Array(pattern.length).fill(0);
451
- const width = row.length;
452
- let index = 0;
453
- let isDark = !startsLight;
454
- let i = from;
455
-
456
- // Skip any leading run of the wrong colour.
457
- while (i < width && (row[i] === 1) !== isDark) i++;
458
-
459
- let patternStart = i;
460
- counters.fill(0);
461
-
462
- while (i < width) {
463
- if ((row[i] === 1) === isDark) {
464
- counters[index]++;
465
- } else {
466
- if (index === pattern.length - 1) {
467
- if (patternVariance(counters, pattern, 0.7) < 0.5) {
468
- return { start: patternStart, end: i };
423
+ const counters = new Array(pattern.length).fill(0);
424
+ const width = row.length;
425
+ let index = 0;
426
+ let isDark = !startsLight;
427
+ let i = from;
428
+ // Skip any leading run of the wrong colour.
429
+ while (i < width && (row[i] === 1) !== isDark)
430
+ i++;
431
+ let patternStart = i;
432
+ counters.fill(0);
433
+ while (i < width) {
434
+ if ((row[i] === 1) === isDark) {
435
+ counters[index]++;
469
436
  }
470
- // Slide the window forward by two runs and keep looking.
471
- patternStart += counters[0] + counters[1];
472
- for (let k = 2; k < pattern.length; k++) counters[k - 2] = counters[k];
473
- counters[pattern.length - 2] = 0;
474
- counters[pattern.length - 1] = 0;
475
- index--;
476
- } else {
477
- index++;
478
- }
479
- counters[index] = 1;
480
- isDark = !isDark;
437
+ else {
438
+ if (index === pattern.length - 1) {
439
+ if (patternVariance(counters, pattern, 0.7) < 0.5) {
440
+ return { start: patternStart, end: i };
441
+ }
442
+ // Slide the window forward by two runs and keep looking.
443
+ patternStart += counters[0] + counters[1];
444
+ for (let k = 2; k < pattern.length; k++)
445
+ counters[k - 2] = counters[k];
446
+ counters[pattern.length - 2] = 0;
447
+ counters[pattern.length - 1] = 0;
448
+ index--;
449
+ }
450
+ else {
451
+ index++;
452
+ }
453
+ counters[index] = 1;
454
+ isDark = !isDark;
455
+ }
456
+ i++;
481
457
  }
482
- i++;
483
- }
484
- return null;
458
+ return null;
485
459
  }
486
-
487
460
  /**
488
461
  * Match a pattern at an exact position.
489
462
  *
@@ -493,18 +466,19 @@ function findGuard(row, from, pattern, startsLight) {
493
466
  * @returns {{end: number} | null}
494
467
  */
495
468
  function matchAt(row, start, pattern) {
496
- const counters = new Array(pattern.length).fill(0);
497
- if (!recordPattern(row, start, counters)) return null;
498
- if (patternVariance(counters, pattern, 0.7) >= 0.5) return null;
499
- let width = 0;
500
- for (const c of counters) width += c;
501
- return { end: start + width };
469
+ const counters = new Array(pattern.length).fill(0);
470
+ if (!recordPattern(row, start, counters))
471
+ return null;
472
+ if (patternVariance(counters, pattern, 0.7) >= 0.5)
473
+ return null;
474
+ let width = 0;
475
+ for (const c of counters)
476
+ width += c;
477
+ return { end: start + width };
502
478
  }
503
-
504
479
  /* ------------------------------------------------------------------ *
505
480
  * EAN supplements
506
481
  * ------------------------------------------------------------------ */
507
-
508
482
  /**
509
483
  * Decode a supplement immediately following a validated EAN/UPC symbol.
510
484
  * The caller supplies the end of the parent trailing guard, so a supplement
@@ -516,37 +490,36 @@ function matchAt(row, start, pattern) {
516
490
  * @returns {{format:'ean2'|'ean5', text:string, parity:string, checksum?:number, end:number}|null}
517
491
  */
518
492
  function decodeEANSupplementRow(row, baseEnd, digitCount) {
519
- const guard = findGuard(row, baseEnd, [1, 1, 2], false);
520
- if (!guard || guard.start - baseEnd < 4) return null;
521
-
522
- let offset = guard.end;
523
- let text = '';
524
- let parity = '';
525
-
526
- for (let i = 0; i < digitCount; i++) {
527
- const digit = decodeEANDigit(row, offset, false);
528
- if (!digit) return null;
529
- text += String(digit.digit);
530
- parity += digit.even ? 'B' : 'A';
531
- offset = digit.end;
532
-
533
- if (i + 1 < digitCount) {
534
- const separator = matchAt(row, offset, [1, 1]);
535
- if (!separator) return null;
536
- offset = separator.end;
493
+ const guard = findGuard(row, baseEnd, [1, 1, 2], false);
494
+ if (!guard || guard.start - baseEnd < 4)
495
+ return null;
496
+ let offset = guard.end;
497
+ let text = '';
498
+ let parity = '';
499
+ for (let i = 0; i < digitCount; i++) {
500
+ const digit = decodeEANDigit(row, offset, false);
501
+ if (!digit)
502
+ return null;
503
+ text += String(digit.digit);
504
+ parity += digit.even ? 'B' : 'A';
505
+ offset = digit.end;
506
+ if (i + 1 < digitCount) {
507
+ const separator = matchAt(row, offset, [1, 1]);
508
+ if (!separator)
509
+ return null;
510
+ offset = separator.end;
511
+ }
512
+ }
513
+ if (digitCount === 2) {
514
+ if (EAN2_PARITY[Number(text) % 4] !== parity)
515
+ return null;
516
+ return { format: 'ean2', text, parity, end: offset };
537
517
  }
538
- }
539
-
540
- if (digitCount === 2) {
541
- if (EAN2_PARITY[Number(text) % 4] !== parity) return null;
542
- return { format: 'ean2', text, parity, end: offset };
543
- }
544
-
545
- const checksum = ean5Checksum(text);
546
- if (EAN5_PARITY[checksum] !== parity) return null;
547
- return { format: 'ean5', text, parity, checksum, end: offset };
518
+ const checksum = ean5Checksum(text);
519
+ if (EAN5_PARITY[checksum] !== parity)
520
+ return null;
521
+ return { format: 'ean5', text, parity, checksum, end: offset };
548
522
  }
549
-
550
523
  /**
551
524
  * Attach the longest valid supplement to a base result. EAN-5 is attempted
552
525
  * first so its prefix cannot be reported as a shorter EAN-2 symbol.
@@ -557,23 +530,21 @@ function decodeEANSupplementRow(row, baseEnd, digitCount) {
557
530
  * @returns {object}
558
531
  */
559
532
  function attachEANAddon(base, row, baseEnd) {
560
- const addon = decodeEANSupplementRow(row, baseEnd, 5)
561
- ?? decodeEANSupplementRow(row, baseEnd, 2);
562
- if (!addon) return base;
563
- const { end, ...publicAddon } = addon;
564
- void end;
565
- return { ...base, addon: publicAddon };
533
+ const addon = decodeEANSupplementRow(row, baseEnd, 5)
534
+ ?? decodeEANSupplementRow(row, baseEnd, 2);
535
+ if (!addon)
536
+ return base;
537
+ const { end, ...publicAddon } = addon;
538
+ void end;
539
+ return { ...base, addon: publicAddon };
566
540
  }
567
-
568
541
  /* ------------------------------------------------------------------ *
569
542
  * Code 11 and MSI/Plessey
570
543
  * ------------------------------------------------------------------ */
571
-
572
544
  /** @param {number[]} counters @returns {number} */
573
545
  function counterTotal(counters) {
574
- return counters.reduce((sum, value) => sum + value, 0);
546
+ return counters.reduce((sum, value) => sum + value, 0);
575
547
  }
576
-
577
548
  /**
578
549
  * Apply the Code 11 C/K checksum grammar used by the writer.
579
550
  *
@@ -582,42 +553,43 @@ function counterTotal(counters) {
582
553
  * @returns {string|null}
583
554
  */
584
555
  function finalizeCode11(encoded, requested) {
585
- const weighted = (text, maxWeight) => {
586
- let sum = 0;
587
- for (let i = 0; i < text.length; i++) {
588
- const weight = ((text.length - 1 - i) % maxWeight) + 1;
589
- sum += weight * CODE11_CHARSET.indexOf(text[i]);
556
+ const weighted = (text, maxWeight) => {
557
+ let sum = 0;
558
+ for (let i = 0; i < text.length; i++) {
559
+ const weight = ((text.length - 1 - i) % maxWeight) + 1;
560
+ sum += weight * CODE11_CHARSET.indexOf(text[i]);
561
+ }
562
+ return sum;
563
+ };
564
+ const validC = (text) => {
565
+ if (text.length < 2)
566
+ return null;
567
+ const body = text.slice(0, -1);
568
+ const expected = CODE11_CHARSET[weighted(body, 10) % 11];
569
+ return text[text.length - 1] === expected ? body : null;
570
+ };
571
+ const validCK = (text) => {
572
+ if (text.length < 12)
573
+ return null;
574
+ const body = text.slice(0, -2);
575
+ const c = text[text.length - 2];
576
+ const k = text[text.length - 1];
577
+ const expectedC = CODE11_CHARSET[weighted(body, 10) % 11];
578
+ if (c !== expectedC)
579
+ return null;
580
+ const expectedK = CODE11_CHARSET[weighted(body + c, 9) % 11];
581
+ return k === expectedK ? body : null;
582
+ };
583
+ if (requested === false)
584
+ return encoded;
585
+ if (requested === true) {
586
+ const checked = encoded.length >= 12 ? validCK(encoded) : validC(encoded);
587
+ return checked;
590
588
  }
591
- return sum;
592
- };
593
- const validC = (text) => {
594
- if (text.length < 2) return null;
595
- const body = text.slice(0, -1);
596
- const expected = CODE11_CHARSET[weighted(body, 10) % 11];
597
- return text[text.length - 1] === expected ? body : null;
598
- };
599
- const validCK = (text) => {
600
- if (text.length < 12) return null;
601
- const body = text.slice(0, -2);
602
- const c = text[text.length - 2];
603
- const k = text[text.length - 1];
604
- const expectedC = CODE11_CHARSET[weighted(body, 10) % 11];
605
- if (c !== expectedC) return null;
606
- const expectedK = CODE11_CHARSET[weighted(body + c, 9) % 11];
607
- return k === expectedK ? body : null;
608
- };
609
-
610
- if (requested === false) return encoded;
611
- if (requested === true) {
612
- const checked = encoded.length >= 12 ? validCK(encoded) : validC(encoded);
613
- return checked;
614
- }
615
-
616
- // With no explicit option, strip checks only when the complete grammar is
617
- // unambiguous; otherwise preserve the literal payload.
618
- return validCK(encoded) ?? validC(encoded) ?? encoded;
589
+ // With no explicit option, strip checks only when the complete grammar is
590
+ // unambiguous; otherwise preserve the literal payload.
591
+ return validCK(encoded) ?? validC(encoded) ?? encoded;
619
592
  }
620
-
621
593
  /**
622
594
  * Decode Code 11 from one binarized scanline.
623
595
  *
@@ -627,72 +599,83 @@ function finalizeCode11(encoded, requested) {
627
599
  * @returns {{format:'code11', text:string}|null}
628
600
  */
629
601
  export function decodeCode11(row, options = {}) {
630
- const counters = new Array(5).fill(0);
631
- let start = null;
632
- for (let i = 0; i < row.length; i++) {
633
- if (row[i] !== 1 || (i > 0 && row[i - 1] === 1)) continue;
634
- if (!recordPattern(row, i, counters)) continue;
635
- if (toNarrowWidePattern(counters, 2) === CODE11_START_BITS) {
636
- start = { position: i, end: i + counterTotal(counters), scale: counterTotal(counters) / 9 };
637
- break;
602
+ const counters = new Array(5).fill(0);
603
+ let start = null;
604
+ for (let i = 0; i < row.length; i++) {
605
+ if (row[i] !== 1 || (i > 0 && row[i - 1] === 1))
606
+ continue;
607
+ if (!recordPattern(row, i, counters))
608
+ continue;
609
+ if (toNarrowWidePattern(counters, 2) === CODE11_START_BITS) {
610
+ start = { position: i, end: i + counterTotal(counters), scale: counterTotal(counters) / 9 };
611
+ break;
612
+ }
638
613
  }
639
- }
640
- if (!start) return null;
641
-
642
- let offset = start.end;
643
- while (offset < row.length && row[offset] === 0) offset++;
644
- let encoded = '';
645
-
646
- for (let count = 0; count < 160 && offset < row.length; count++) {
647
- if (!recordPattern(row, offset, counters)) return null;
648
- const width = counterTotal(counters);
649
- const stop = toNarrowWidePattern(counters, 2);
650
- if (stop === CODE11_START_BITS) {
651
- if (encoded.length === 0) return null;
652
- const stopEnd = offset + width;
653
- let nextDark = stopEnd;
654
- while (nextDark < row.length && row[nextDark] === 0) nextDark++;
655
- if (nextDark !== row.length && nextDark - stopEnd < Math.max(3, Math.ceil(start.scale * 3))) {
614
+ if (!start)
656
615
  return null;
657
- }
658
- const text = finalizeCode11(encoded, options.checkDigit);
659
- return text == null ? null : { format: 'code11', text };
660
- }
661
-
662
- let character = null;
663
- for (const expectedWide of [1, 2]) {
664
- const bits = toNarrowWidePattern(counters, expectedWide);
665
- if (bits < 0) continue;
666
- const candidate = CODE11_BITS[`${bits}:${expectedWide}`];
667
- if (candidate !== undefined) {
668
- character = candidate;
669
- break;
670
- }
616
+ let offset = start.end;
617
+ while (offset < row.length && row[offset] === 0)
618
+ offset++;
619
+ let encoded = '';
620
+ for (let count = 0; count < 160 && offset < row.length; count++) {
621
+ if (!recordPattern(row, offset, counters))
622
+ return null;
623
+ const width = counterTotal(counters);
624
+ const stop = toNarrowWidePattern(counters, 2);
625
+ if (stop === CODE11_START_BITS) {
626
+ if (encoded.length === 0)
627
+ return null;
628
+ const stopEnd = offset + width;
629
+ let nextDark = stopEnd;
630
+ while (nextDark < row.length && row[nextDark] === 0)
631
+ nextDark++;
632
+ if (nextDark !== row.length && nextDark - stopEnd < Math.max(3, Math.ceil(start.scale * 3))) {
633
+ return null;
634
+ }
635
+ const text = finalizeCode11(encoded, options.checkDigit);
636
+ return text == null ? null : { format: 'code11', text };
637
+ }
638
+ let character = null;
639
+ for (const expectedWide of [1, 2]) {
640
+ const bits = toNarrowWidePattern(counters, expectedWide);
641
+ if (bits < 0)
642
+ continue;
643
+ const candidate = CODE11_BITS[`${bits}:${expectedWide}`];
644
+ if (candidate !== undefined) {
645
+ character = candidate;
646
+ break;
647
+ }
648
+ }
649
+ if (character == null)
650
+ return null;
651
+ encoded += character;
652
+ if (encoded.length > 128)
653
+ return null;
654
+ offset += width;
655
+ while (offset < row.length && row[offset] === 0)
656
+ offset++;
671
657
  }
672
- if (character == null) return null;
673
- encoded += character;
674
- if (encoded.length > 128) return null;
675
- offset += width;
676
- while (offset < row.length && row[offset] === 0) offset++;
677
- }
678
- return null;
658
+ return null;
679
659
  }
680
-
681
660
  /** @param {string} encoded @param {boolean|undefined} requested */
682
661
  function finalizeMSI(encoded, requested) {
683
- if (requested !== true) return encoded;
684
- if (encoded.length < 2) return null;
685
- const body = encoded.slice(0, -1);
686
- let odd = '';
687
- for (let i = body.length - 1; i >= 0; i -= 2) odd = body[i] + odd;
688
- const doubled = String(Number(odd) * 2);
689
- let sum = 0;
690
- for (const ch of doubled) sum += Number(ch);
691
- for (let i = body.length - 2; i >= 0; i -= 2) sum += Number(body[i]);
692
- const expected = String((10 - (sum % 10)) % 10);
693
- return encoded.endsWith(expected) ? body : null;
662
+ if (requested !== true)
663
+ return encoded;
664
+ if (encoded.length < 2)
665
+ return null;
666
+ const body = encoded.slice(0, -1);
667
+ let odd = '';
668
+ for (let i = body.length - 1; i >= 0; i -= 2)
669
+ odd = body[i] + odd;
670
+ const doubled = String(Number(odd) * 2);
671
+ let sum = 0;
672
+ for (const ch of doubled)
673
+ sum += Number(ch);
674
+ for (let i = body.length - 2; i >= 0; i -= 2)
675
+ sum += Number(body[i]);
676
+ const expected = String((10 - (sum % 10)) % 10);
677
+ return encoded.endsWith(expected) ? body : null;
694
678
  }
695
-
696
679
  /**
697
680
  * Decode MSI/Plessey from one binarized scanline.
698
681
  *
@@ -702,453 +685,480 @@ function finalizeMSI(encoded, requested) {
702
685
  * @returns {{format:'msi', text:string}|null}
703
686
  */
704
687
  export function decodeMSI(row, options = {}) {
705
- const start = findGuard(row, 0, [2, 1], false);
706
- if (!start) return null;
707
- const scale = (start.end - start.start) / 3;
708
- if (!(scale >= 1)) return null;
709
-
710
- let offset = start.end;
711
- let encoded = '';
712
- const bitPatterns = [
713
- { pattern: [1, 2], bit: '0' },
714
- { pattern: [2, 1], bit: '1' },
715
- ];
716
-
717
- for (let digitIndex = 0; digitIndex < 80; digitIndex++) {
718
- const stop = matchAt(row, offset, [1, 2, 1]);
719
- if (stop && encoded.length > 0) {
720
- let nextDark = stop.end;
721
- while (nextDark < row.length && row[nextDark] === 0) nextDark++;
722
- if (nextDark === row.length ||
723
- nextDark - stop.end >= Math.max(3, Math.ceil(scale * 3))) {
724
- if (encoded.length < 6 && !(options.formats && options.formats.includes('msi'))) {
725
- return null;
688
+ const start = findGuard(row, 0, [2, 1], false);
689
+ if (!start)
690
+ return null;
691
+ const scale = (start.end - start.start) / 3;
692
+ if (!(scale >= 1))
693
+ return null;
694
+ let offset = start.end;
695
+ let encoded = '';
696
+ const bitPatterns = [
697
+ { pattern: [1, 2], bit: '0' },
698
+ { pattern: [2, 1], bit: '1' },
699
+ ];
700
+ for (let digitIndex = 0; digitIndex < 80; digitIndex++) {
701
+ const stop = matchAt(row, offset, [1, 2, 1]);
702
+ if (stop && encoded.length > 0) {
703
+ let nextDark = stop.end;
704
+ while (nextDark < row.length && row[nextDark] === 0)
705
+ nextDark++;
706
+ if (nextDark === row.length ||
707
+ nextDark - stop.end >= Math.max(3, Math.ceil(scale * 3))) {
708
+ if (encoded.length < 6 && !(options.formats && options.formats.includes('msi'))) {
709
+ return null;
710
+ }
711
+ const text = finalizeMSI(encoded, options.checkDigit);
712
+ return text == null ? null : { format: 'msi', text };
713
+ }
726
714
  }
727
- const text = finalizeMSI(encoded, options.checkDigit);
728
- return text == null ? null : { format: 'msi', text };
729
- }
730
- }
731
-
732
- let nibble = '';
733
- for (let bitIndex = 0; bitIndex < 4; bitIndex++) {
734
- let matched = null;
735
- for (const candidate of bitPatterns) {
736
- const found = matchAt(row, offset, candidate.pattern);
737
- if (found) {
738
- matched = { ...candidate, end: found.end };
739
- break;
715
+ let nibble = '';
716
+ for (let bitIndex = 0; bitIndex < 4; bitIndex++) {
717
+ let matched = null;
718
+ for (const candidate of bitPatterns) {
719
+ const found = matchAt(row, offset, candidate.pattern);
720
+ if (found) {
721
+ matched = { ...candidate, end: found.end };
722
+ break;
723
+ }
724
+ }
725
+ if (!matched)
726
+ return null;
727
+ nibble += matched.bit;
728
+ offset = matched.end;
740
729
  }
741
- }
742
- if (!matched) return null;
743
- nibble += matched.bit;
744
- offset = matched.end;
730
+ const digit = Number.parseInt(nibble, 2);
731
+ if (digit > 9)
732
+ return null;
733
+ encoded += String(digit);
745
734
  }
746
-
747
- const digit = Number.parseInt(nibble, 2);
748
- if (digit > 9) return null;
749
- encoded += String(digit);
750
- }
751
- return null;
735
+ return null;
752
736
  }
753
-
754
737
  /* ------------------------------------------------------------------ *
755
738
  * Code 128
756
739
  * ------------------------------------------------------------------ */
757
-
758
740
  /**
759
741
  * @param {Uint8Array} row
760
742
  * @returns {{format: string, text: string} | null}
761
743
  */
762
744
  function decodeCode128(row) {
763
- // Locate whichever start symbol appears first.
764
- let start = null;
765
- for (const startCode of [CODE128_START_A, CODE128_START_B, CODE128_START_C]) {
766
- const found = findGuard(row, 0, CODE128_RUNS[startCode], false);
767
- if (found && (!start || found.start < start.found.start)) {
768
- start = { code: startCode, found };
769
- }
770
- }
771
- if (!start) return null;
772
-
773
- // Read every symbol up to the stop pattern first, and only then interpret
774
- // them. The symbol immediately before the stop is the checksum, and it is
775
- // indistinguishable from data while scanning — interpreting as we go would
776
- // append it to the text (a Code C checksum of 70 arrives as the digits
777
- // "70"). Collecting first makes dropping it exact rather than a guess.
778
- const values = [];
779
- let offset = start.found.end;
780
- const counters = new Array(6).fill(0);
781
- const stopCounters = new Array(7).fill(0);
782
-
783
- for (;;) {
784
- // The stop pattern has seven elements, so it must be tried before the
785
- // six-element symbol set or its first six would match something.
786
- if (recordPattern(row, offset, stopCounters) &&
787
- patternVariance(stopCounters, CODE128_RUNS[CODE128_STOP], 0.7) < 0.38) {
788
- break;
745
+ // Locate whichever start symbol appears first.
746
+ let start = null;
747
+ for (const startCode of [CODE128_START_A, CODE128_START_B, CODE128_START_C]) {
748
+ const found = findGuard(row, 0, CODE128_RUNS[startCode], false);
749
+ if (found && (!start || found.start < start.found.start)) {
750
+ start = { code: startCode, found };
751
+ }
789
752
  }
790
-
791
- if (!recordPattern(row, offset, counters)) return null;
792
-
793
- let best = -1;
794
- let bestVariance = 0.4;
795
- for (let c = 0; c < CODE128_RUNS.length - 1; c++) {
796
- const v = patternVariance(counters, CODE128_RUNS[c], 0.7);
797
- if (v < bestVariance) { bestVariance = v; best = c; }
753
+ if (!start)
754
+ return null;
755
+ // Read every symbol up to the stop pattern first, and only then interpret
756
+ // them. The symbol immediately before the stop is the checksum, and it is
757
+ // indistinguishable from data while scanning — interpreting as we go would
758
+ // append it to the text (a Code C checksum of 70 arrives as the digits
759
+ // "70"). Collecting first makes dropping it exact rather than a guess.
760
+ const values = [];
761
+ let offset = start.found.end;
762
+ const counters = new Array(6).fill(0);
763
+ const stopCounters = new Array(7).fill(0);
764
+ for (;;) {
765
+ // The stop pattern has seven elements, so it must be tried before the
766
+ // six-element symbol set or its first six would match something.
767
+ if (recordPattern(row, offset, stopCounters) &&
768
+ patternVariance(stopCounters, CODE128_RUNS[CODE128_STOP], 0.7) < 0.38) {
769
+ break;
770
+ }
771
+ if (!recordPattern(row, offset, counters))
772
+ return null;
773
+ let best = -1;
774
+ let bestVariance = 0.4;
775
+ for (let c = 0; c < CODE128_RUNS.length - 1; c++) {
776
+ const v = patternVariance(counters, CODE128_RUNS[c], 0.7);
777
+ if (v < bestVariance) {
778
+ bestVariance = v;
779
+ best = c;
780
+ }
781
+ }
782
+ if (best < 0)
783
+ return null;
784
+ let width = 0;
785
+ for (const c of counters)
786
+ width += c;
787
+ offset += width;
788
+ values.push(best);
789
+ if (values.length > 256)
790
+ return null; // runaway scan
798
791
  }
799
- if (best < 0) return null;
800
-
801
- let width = 0;
802
- for (const c of counters) width += c;
803
- offset += width;
804
- values.push(best);
805
-
806
- if (values.length > 256) return null; // runaway scan
807
- }
808
-
809
- // Start symbol, data, checksum, stop. Anything shorter is not a symbol.
810
- if (values.length < 2) return null;
811
-
812
- const checksum = values[values.length - 1];
813
- const dataValues = values.slice(0, -1);
814
-
815
- // Verify the checksum rather than trusting the scan. Without this a run of
816
- // noise that happens to match valid patterns decodes to plausible garbage,
817
- // which is far worse than reporting nothing.
818
- let sum = start.code;
819
- for (let i = 0; i < dataValues.length; i++) sum += dataValues[i] * (i + 1);
820
- if (sum % 103 !== checksum) return null;
821
-
822
- let mode = start.code === CODE128_START_A ? 'A'
823
- : start.code === CODE128_START_B ? 'B' : 'C';
824
- let shifted = null;
825
- let text = '';
826
- const fnc1AtStart = dataValues[0] === CODE128_FNC1;
827
- const fnc1Positions = [];
828
-
829
- for (let dataIndex = 0; dataIndex < dataValues.length; dataIndex++) {
830
- const value = dataValues[dataIndex];
831
- const active = shifted || mode;
832
- shifted = null;
833
-
834
- if (value === CODE128_CODE_A && mode !== 'A') { mode = 'A'; continue; }
835
- if (value === CODE128_CODE_B && mode !== 'B') { mode = 'B'; continue; }
836
- if (value === CODE128_CODE_C) { mode = 'C'; continue; }
837
- if (value === CODE128_SHIFT) { shifted = mode === 'A' ? 'B' : 'A'; continue; }
838
- if (value === CODE128_FNC1) {
839
- if (dataIndex > 0) {
840
- fnc1Positions.push(text.length);
841
- text += '\x1d';
842
- }
843
- continue;
792
+ // Start symbol, data, checksum, stop. Anything shorter is not a symbol.
793
+ if (values.length < 2)
794
+ return null;
795
+ const checksum = values[values.length - 1];
796
+ const dataValues = values.slice(0, -1);
797
+ // Verify the checksum rather than trusting the scan. Without this a run of
798
+ // noise that happens to match valid patterns decodes to plausible garbage,
799
+ // which is far worse than reporting nothing.
800
+ let sum = start.code;
801
+ for (let i = 0; i < dataValues.length; i++)
802
+ sum += dataValues[i] * (i + 1);
803
+ if (sum % 103 !== checksum)
804
+ return null;
805
+ let mode = start.code === CODE128_START_A ? 'A'
806
+ : start.code === CODE128_START_B ? 'B' : 'C';
807
+ let shifted = null;
808
+ let text = '';
809
+ const fnc1AtStart = dataValues[0] === CODE128_FNC1;
810
+ const fnc1Positions = [];
811
+ for (let dataIndex = 0; dataIndex < dataValues.length; dataIndex++) {
812
+ const value = dataValues[dataIndex];
813
+ const active = shifted || mode;
814
+ shifted = null;
815
+ if (value === CODE128_CODE_A && mode !== 'A') {
816
+ mode = 'A';
817
+ continue;
818
+ }
819
+ if (value === CODE128_CODE_B && mode !== 'B') {
820
+ mode = 'B';
821
+ continue;
822
+ }
823
+ if (value === CODE128_CODE_C) {
824
+ mode = 'C';
825
+ continue;
826
+ }
827
+ if (value === CODE128_SHIFT) {
828
+ shifted = mode === 'A' ? 'B' : 'A';
829
+ continue;
830
+ }
831
+ if (value === CODE128_FNC1) {
832
+ if (dataIndex > 0) {
833
+ fnc1Positions.push(text.length);
834
+ text += '\x1d';
835
+ }
836
+ continue;
837
+ }
838
+ if (value >= 96 && value <= 102) {
839
+ continue;
840
+ } // other function characters
841
+ if (active === 'C') {
842
+ text += String(value).padStart(2, '0');
843
+ }
844
+ else if (active === 'A') {
845
+ text += value < 64 ? String.fromCharCode(value + 32) : String.fromCharCode(value - 64);
846
+ }
847
+ else {
848
+ text += String.fromCharCode(value + 32);
849
+ }
844
850
  }
845
- if (value >= 96 && value <= 102) { continue; } // other function characters
846
-
847
- if (active === 'C') {
848
- text += String(value).padStart(2, '0');
849
- } else if (active === 'A') {
850
- text += value < 64 ? String.fromCharCode(value + 32) : String.fromCharCode(value - 64);
851
- } else {
852
- text += String.fromCharCode(value + 32);
851
+ if (text.length === 0)
852
+ return null;
853
+ if (fnc1AtStart) {
854
+ return {
855
+ format: 'gs1128',
856
+ text,
857
+ gs1: true,
858
+ symbologyIdentifier: ']C1',
859
+ fnc1AtStart: true,
860
+ fnc1Positions,
861
+ };
853
862
  }
854
- }
855
-
856
- if (text.length === 0) return null;
857
- if (fnc1AtStart) {
858
- return {
859
- format: 'gs1128',
860
- text,
861
- gs1: true,
862
- symbologyIdentifier: ']C1',
863
- fnc1AtStart: true,
864
- fnc1Positions,
865
- };
866
- }
867
- return { format: 'code128', text };
863
+ return { format: 'code128', text };
868
864
  }
869
-
870
865
  /* ------------------------------------------------------------------ *
871
866
  * Code 39
872
867
  * ------------------------------------------------------------------ */
873
-
874
868
  /**
875
869
  * @param {Uint8Array} row
876
870
  * @param {object} options
877
871
  * @returns {{format: string, text: string} | null}
878
872
  */
879
873
  function decodeCode39(row, options = {}) {
880
- const counters = new Array(9).fill(0);
881
- let offset = 0;
882
-
883
- // Find the '*' start character.
884
- const startBits = nwToBits(CODE39['*']);
885
- let found = false;
886
- while (offset < row.length) {
887
- while (offset < row.length && row[offset] !== 1) offset++;
888
- if (offset >= row.length) break;
889
- if (recordPattern(row, offset, counters)) {
890
- if (toNarrowWidePattern(counters, 3) === startBits) { found = true; break; }
874
+ const counters = new Array(9).fill(0);
875
+ let offset = 0;
876
+ // Find the '*' start character.
877
+ const startBits = nwToBits(CODE39['*']);
878
+ let found = false;
879
+ while (offset < row.length) {
880
+ while (offset < row.length && row[offset] !== 1)
881
+ offset++;
882
+ if (offset >= row.length)
883
+ break;
884
+ if (recordPattern(row, offset, counters)) {
885
+ if (toNarrowWidePattern(counters, 3) === startBits) {
886
+ found = true;
887
+ break;
888
+ }
889
+ }
890
+ // Advance past this dark run and the following light run.
891
+ while (offset < row.length && row[offset] === 1)
892
+ offset++;
893
+ while (offset < row.length && row[offset] === 0)
894
+ offset++;
891
895
  }
892
- // Advance past this dark run and the following light run.
893
- while (offset < row.length && row[offset] === 1) offset++;
894
- while (offset < row.length && row[offset] === 0) offset++;
895
- }
896
- if (!found) return null;
897
-
898
- let width = 0;
899
- for (const c of counters) width += c;
900
- offset += width;
901
-
902
- let text = '';
903
- for (;;) {
904
- // Skip the inter-character gap.
905
- while (offset < row.length && row[offset] === 0) offset++;
906
- if (offset >= row.length) return null;
907
- if (!recordPattern(row, offset, counters)) return null;
908
-
909
- const bits = toNarrowWidePattern(counters, 3);
910
- const ch = CODE39_BITS[bits];
911
- if (ch === undefined) return null;
912
-
913
- let w = 0;
914
- for (const c of counters) w += c;
915
- offset += w;
916
-
917
- if (ch === '*') break;
918
- text += ch;
919
- if (text.length > 80) return null;
920
- }
921
-
922
- if (text.length === 0) return null;
923
-
924
- if (options.checkDigit) {
925
- const expected = text[text.length - 1];
926
- const body = text.slice(0, -1);
927
- let sum = 0;
928
- for (const ch of body) sum += CODE39_CHECK_SET.indexOf(ch);
929
- if (CODE39_CHECK_SET[sum % 43] !== expected) return null;
930
- text = body;
931
- }
932
-
933
- return { format: 'code39', text };
896
+ if (!found)
897
+ return null;
898
+ let width = 0;
899
+ for (const c of counters)
900
+ width += c;
901
+ offset += width;
902
+ let text = '';
903
+ for (;;) {
904
+ // Skip the inter-character gap.
905
+ while (offset < row.length && row[offset] === 0)
906
+ offset++;
907
+ if (offset >= row.length)
908
+ return null;
909
+ if (!recordPattern(row, offset, counters))
910
+ return null;
911
+ const bits = toNarrowWidePattern(counters, 3);
912
+ const ch = CODE39_BITS[bits];
913
+ if (ch === undefined)
914
+ return null;
915
+ let w = 0;
916
+ for (const c of counters)
917
+ w += c;
918
+ offset += w;
919
+ if (ch === '*')
920
+ break;
921
+ text += ch;
922
+ if (text.length > 80)
923
+ return null;
924
+ }
925
+ if (text.length === 0)
926
+ return null;
927
+ if (options.checkDigit) {
928
+ const expected = text[text.length - 1];
929
+ const body = text.slice(0, -1);
930
+ let sum = 0;
931
+ for (const ch of body)
932
+ sum += CODE39_CHECK_SET.indexOf(ch);
933
+ if (CODE39_CHECK_SET[sum % 43] !== expected)
934
+ return null;
935
+ text = body;
936
+ }
937
+ return { format: 'code39', text };
934
938
  }
935
-
936
939
  /* ------------------------------------------------------------------ *
937
940
  * Code 93
938
941
  * ------------------------------------------------------------------ */
939
-
940
942
  /**
941
943
  * @param {Uint8Array} row
942
944
  * @returns {{format: string, text: string} | null}
943
945
  */
944
946
  function decodeCode93(row) {
945
- const startRuns = widthsToArray('111141');
946
- const start = findGuard(row, 0, startRuns, false);
947
- if (!start) return null;
948
-
949
- let offset = start.end;
950
- const counters = new Array(6).fill(0);
951
- const values = [];
952
-
953
- for (;;) {
954
- if (!recordPattern(row, offset, counters)) return null;
955
-
956
- let best = -1;
957
- let bestVariance = 0.38;
958
- for (let v = 0; v < CODE93_VALUES.length; v++) {
959
- const runs = CODE93_RUNS[CODE93_VALUES[v]];
960
- const variance = patternVariance(counters, runs, 0.7);
961
- if (variance < bestVariance) { bestVariance = variance; best = v; }
947
+ const startRuns = widthsToArray('111141');
948
+ const start = findGuard(row, 0, startRuns, false);
949
+ if (!start)
950
+ return null;
951
+ let offset = start.end;
952
+ const counters = new Array(6).fill(0);
953
+ const values = [];
954
+ for (;;) {
955
+ if (!recordPattern(row, offset, counters))
956
+ return null;
957
+ let best = -1;
958
+ let bestVariance = 0.38;
959
+ for (let v = 0; v < CODE93_VALUES.length; v++) {
960
+ const runs = CODE93_RUNS[CODE93_VALUES[v]];
961
+ const variance = patternVariance(counters, runs, 0.7);
962
+ if (variance < bestVariance) {
963
+ bestVariance = variance;
964
+ best = v;
965
+ }
966
+ }
967
+ const stopVariance = patternVariance(counters, startRuns, 0.7);
968
+ if (stopVariance < bestVariance)
969
+ break;
970
+ if (best < 0)
971
+ return null;
972
+ values.push(best);
973
+ let w = 0;
974
+ for (const c of counters)
975
+ w += c;
976
+ offset += w;
977
+ if (values.length > 90)
978
+ return null;
962
979
  }
963
-
964
- const stopVariance = patternVariance(counters, startRuns, 0.7);
965
- if (stopVariance < bestVariance) break;
966
- if (best < 0) return null;
967
-
968
- values.push(best);
969
- let w = 0;
970
- for (const c of counters) w += c;
971
- offset += w;
972
- if (values.length > 90) return null;
973
- }
974
-
975
- if (values.length < 3) return null;
976
-
977
- // Verify both check characters before trusting anything.
978
- const weighted = (data, maxWeight) => {
979
- let sum = 0;
980
- for (let i = 0; i < data.length; i++) {
981
- const weight = ((data.length - 1 - i) % maxWeight) + 1;
982
- sum += weight * data[i];
980
+ if (values.length < 3)
981
+ return null;
982
+ // Verify both check characters before trusting anything.
983
+ const weighted = (data, maxWeight) => {
984
+ let sum = 0;
985
+ for (let i = 0; i < data.length; i++) {
986
+ const weight = ((data.length - 1 - i) % maxWeight) + 1;
987
+ sum += weight * data[i];
988
+ }
989
+ return sum % 47;
990
+ };
991
+ const k = values.pop();
992
+ const c = values.pop();
993
+ if (weighted(values, 20) !== c)
994
+ return null;
995
+ if (weighted([...values, c], 15) !== k)
996
+ return null;
997
+ let text = '';
998
+ for (const v of values) {
999
+ const key = CODE93_VALUES[v];
1000
+ if (key.length > 1)
1001
+ return null; // shift characters not expanded here
1002
+ text += key;
983
1003
  }
984
- return sum % 47;
985
- };
986
- const k = values.pop();
987
- const c = values.pop();
988
- if (weighted(values, 20) !== c) return null;
989
- if (weighted([...values, c], 15) !== k) return null;
990
-
991
- let text = '';
992
- for (const v of values) {
993
- const key = CODE93_VALUES[v];
994
- if (key.length > 1) return null; // shift characters not expanded here
995
- text += key;
996
- }
997
- return { format: 'code93', text };
1004
+ return { format: 'code93', text };
998
1005
  }
999
-
1000
1006
  /* ------------------------------------------------------------------ *
1001
1007
  * ITF
1002
1008
  * ------------------------------------------------------------------ */
1003
-
1004
1009
  /**
1005
1010
  * @param {Uint8Array} row
1006
1011
  * @returns {{format: string, text: string} | null}
1007
1012
  */
1008
1013
  function decodeITF(row) {
1009
- const start = findGuard(row, 0, [1, 1, 1, 1], false);
1010
- if (!start) return null;
1011
- const startScale = (start.end - start.start) / 4;
1012
-
1013
- let offset = start.end;
1014
- const digits = [];
1015
- const barCounters = new Array(5).fill(0);
1016
- const spaceCounters = new Array(5).fill(0);
1017
- const pair = new Array(10).fill(0);
1018
-
1019
- for (;;) {
1020
- if (!recordPattern(row, offset, pair)) break;
1021
-
1022
- // De-interleave: even indices are bars, odd are spaces.
1023
- for (let k = 0; k < 5; k++) {
1024
- barCounters[k] = pair[k * 2];
1025
- spaceCounters[k] = pair[k * 2 + 1];
1014
+ const start = findGuard(row, 0, [1, 1, 1, 1], false);
1015
+ if (!start)
1016
+ return null;
1017
+ const startScale = (start.end - start.start) / 4;
1018
+ let offset = start.end;
1019
+ const digits = [];
1020
+ const barCounters = new Array(5).fill(0);
1021
+ const spaceCounters = new Array(5).fill(0);
1022
+ const pair = new Array(10).fill(0);
1023
+ for (;;) {
1024
+ if (!recordPattern(row, offset, pair))
1025
+ break;
1026
+ // De-interleave: even indices are bars, odd are spaces.
1027
+ for (let k = 0; k < 5; k++) {
1028
+ barCounters[k] = pair[k * 2];
1029
+ spaceCounters[k] = pair[k * 2 + 1];
1030
+ }
1031
+ const barBits = toNarrowWidePattern(barCounters, 2);
1032
+ const spaceBits = toNarrowWidePattern(spaceCounters, 2);
1033
+ const a = ITF_BITS[barBits];
1034
+ const b = ITF_BITS[spaceBits];
1035
+ if (a === undefined || b === undefined)
1036
+ break;
1037
+ digits.push(a, b);
1038
+ let w = 0;
1039
+ for (const c of pair)
1040
+ w += c;
1041
+ offset += w;
1042
+ if (digits.length > 40)
1043
+ break;
1026
1044
  }
1027
-
1028
- const barBits = toNarrowWidePattern(barCounters, 2);
1029
- const spaceBits = toNarrowWidePattern(spaceCounters, 2);
1030
- const a = ITF_BITS[barBits];
1031
- const b = ITF_BITS[spaceBits];
1032
- if (a === undefined || b === undefined) break;
1033
-
1034
- digits.push(a, b);
1035
- let w = 0;
1036
- for (const c of pair) w += c;
1037
- offset += w;
1038
- if (digits.length > 40) break;
1039
- }
1040
-
1041
- // ITF carries no mandatory checksum, so structure is the only defence
1042
- // against a false positive — and without these two checks there is none.
1043
- //
1044
- // The loop above stops as soon as a pair fails to match, which happens both
1045
- // at the genuine end of a symbol and in the middle of unrelated bars. Two
1046
- // digits scraped out of a Code 39 or UPC-A symbol matched the digit patterns
1047
- // often enough to be reported as a real ITF read, so `decode()` returned a
1048
- // phantom result alongside the true one.
1049
- if (digits.length < MIN_ITF_DIGITS) return null;
1050
-
1051
- // Require the run to end on the actual stop pattern: wide bar, narrow space,
1052
- // narrow bar. A fragment that merely ran out of matching pairs has no stop
1053
- // pattern after it and is rejected here.
1054
- const stop = new Array(3).fill(0);
1055
- if (!recordPattern(row, offset, stop)) return null;
1056
- if (toNarrowWidePattern(stop, 1) !== 0b100) return null;
1057
- const stopEnd = offset + counterTotal(stop);
1058
- let nextDark = stopEnd;
1059
- while (nextDark < row.length && row[nextDark] === 0) nextDark++;
1060
- if (nextDark !== row.length && nextDark - stopEnd < Math.max(3, Math.ceil(startScale * 3))) return null;
1061
-
1062
- return { format: 'itf', text: digits.join('') };
1045
+ // ITF carries no mandatory checksum, so structure is the only defence
1046
+ // against a false positive — and without these two checks there is none.
1047
+ //
1048
+ // The loop above stops as soon as a pair fails to match, which happens both
1049
+ // at the genuine end of a symbol and in the middle of unrelated bars. Two
1050
+ // digits scraped out of a Code 39 or UPC-A symbol matched the digit patterns
1051
+ // often enough to be reported as a real ITF read, so `decode()` returned a
1052
+ // phantom result alongside the true one.
1053
+ if (digits.length < MIN_ITF_DIGITS)
1054
+ return null;
1055
+ // Require the run to end on the actual stop pattern: wide bar, narrow space,
1056
+ // narrow bar. A fragment that merely ran out of matching pairs has no stop
1057
+ // pattern after it and is rejected here.
1058
+ const stop = new Array(3).fill(0);
1059
+ if (!recordPattern(row, offset, stop))
1060
+ return null;
1061
+ if (toNarrowWidePattern(stop, 1) !== 0b100)
1062
+ return null;
1063
+ const stopEnd = offset + counterTotal(stop);
1064
+ let nextDark = stopEnd;
1065
+ while (nextDark < row.length && row[nextDark] === 0)
1066
+ nextDark++;
1067
+ if (nextDark !== row.length && nextDark - stopEnd < Math.max(3, Math.ceil(startScale * 3)))
1068
+ return null;
1069
+ return { format: 'itf', text: digits.join('') };
1063
1070
  }
1064
-
1065
1071
  /* ------------------------------------------------------------------ *
1066
1072
  * Codabar
1067
1073
  * ------------------------------------------------------------------ */
1068
-
1069
1074
  /**
1070
1075
  * @param {Uint8Array} row
1071
1076
  * @returns {{format: string, text: string} | null}
1072
1077
  */
1073
1078
  function decodeCodabar(row) {
1074
- const counters = new Array(7).fill(0);
1075
- let offset = 0;
1076
- let startChar = null;
1077
-
1078
- while (offset < row.length) {
1079
- while (offset < row.length && row[offset] !== 1) offset++;
1080
- if (offset >= row.length) break;
1081
- if (recordPattern(row, offset, counters)) {
1082
- const bits = toNarrowWidePattern(counters, 3) >= 0
1083
- ? toNarrowWidePattern(counters, 3)
1084
- : toNarrowWidePattern(counters, 2);
1085
- const ch = CODABAR_BITS[bits];
1086
- if (ch && CODABAR_START_STOP.includes(ch)) { startChar = ch; break; }
1079
+ const counters = new Array(7).fill(0);
1080
+ let offset = 0;
1081
+ let startChar = null;
1082
+ while (offset < row.length) {
1083
+ while (offset < row.length && row[offset] !== 1)
1084
+ offset++;
1085
+ if (offset >= row.length)
1086
+ break;
1087
+ if (recordPattern(row, offset, counters)) {
1088
+ const bits = toNarrowWidePattern(counters, 3) >= 0
1089
+ ? toNarrowWidePattern(counters, 3)
1090
+ : toNarrowWidePattern(counters, 2);
1091
+ const ch = CODABAR_BITS[bits];
1092
+ if (ch && CODABAR_START_STOP.includes(ch)) {
1093
+ startChar = ch;
1094
+ break;
1095
+ }
1096
+ }
1097
+ while (offset < row.length && row[offset] === 1)
1098
+ offset++;
1099
+ while (offset < row.length && row[offset] === 0)
1100
+ offset++;
1087
1101
  }
1088
- while (offset < row.length && row[offset] === 1) offset++;
1089
- while (offset < row.length && row[offset] === 0) offset++;
1090
- }
1091
- if (!startChar) return null;
1092
-
1093
- let w = 0;
1094
- for (const c of counters) w += c;
1095
- offset += w;
1096
-
1097
- let text = '';
1098
- for (;;) {
1099
- while (offset < row.length && row[offset] === 0) offset++;
1100
- if (offset >= row.length) return null;
1101
- if (!recordPattern(row, offset, counters)) return null;
1102
-
1103
- let bits = toNarrowWidePattern(counters, 3);
1104
- let ch = CODABAR_BITS[bits];
1105
- if (ch === undefined) {
1106
- bits = toNarrowWidePattern(counters, 2);
1107
- ch = CODABAR_BITS[bits];
1102
+ if (!startChar)
1103
+ return null;
1104
+ let w = 0;
1105
+ for (const c of counters)
1106
+ w += c;
1107
+ offset += w;
1108
+ let text = '';
1109
+ for (;;) {
1110
+ while (offset < row.length && row[offset] === 0)
1111
+ offset++;
1112
+ if (offset >= row.length)
1113
+ return null;
1114
+ if (!recordPattern(row, offset, counters))
1115
+ return null;
1116
+ let bits = toNarrowWidePattern(counters, 3);
1117
+ let ch = CODABAR_BITS[bits];
1118
+ if (ch === undefined) {
1119
+ bits = toNarrowWidePattern(counters, 2);
1120
+ ch = CODABAR_BITS[bits];
1121
+ }
1122
+ if (ch === undefined)
1123
+ return null;
1124
+ let width = 0;
1125
+ for (const c of counters)
1126
+ width += c;
1127
+ offset += width;
1128
+ if (CODABAR_START_STOP.includes(ch))
1129
+ break;
1130
+ text += ch;
1131
+ if (text.length > 60)
1132
+ return null;
1108
1133
  }
1109
- if (ch === undefined) return null;
1110
-
1111
- let width = 0;
1112
- for (const c of counters) width += c;
1113
- offset += width;
1114
-
1115
- if (CODABAR_START_STOP.includes(ch)) break;
1116
- text += ch;
1117
- if (text.length > 60) return null;
1118
- }
1119
-
1120
- if (text.length === 0) return null;
1121
- return { format: 'codabar', text };
1134
+ if (text.length === 0)
1135
+ return null;
1136
+ return { format: 'codabar', text };
1122
1137
  }
1123
-
1124
1138
  /* ------------------------------------------------------------------ *
1125
1139
  * Public entry point
1126
1140
  * ------------------------------------------------------------------ */
1127
-
1128
1141
  /** Decoders in the order they are tried. */
1129
1142
  const DECODERS = [
1130
- ['ean13', decodeEANFamily],
1131
- ['upca', decodeEANFamily],
1132
- ['ean8', decodeEAN8],
1133
- ['upce', decodeUPCE],
1134
- ['code128', decodeCode128],
1135
- ['code11', decodeCode11],
1136
- ['msi', decodeMSI],
1137
- ['gs1databar14', decodeDataBar14Scanline],
1138
- ['code39', decodeCode39],
1139
- ['code93', decodeCode93],
1140
- ['itf', decodeITF],
1141
- ['codabar', decodeCodabar],
1143
+ ['ean13', decodeEANFamily],
1144
+ ['upca', decodeEANFamily],
1145
+ ['ean8', decodeEAN8],
1146
+ ['upce', decodeUPCE],
1147
+ ['code128', decodeCode128],
1148
+ ['code11', decodeCode11],
1149
+ ['msi', decodeMSI],
1150
+ ['gs1databar14', decodeDataBar14Scanline],
1151
+ ['code39', decodeCode39],
1152
+ ['code93', decodeCode93],
1153
+ ['itf', decodeITF],
1154
+ ['codabar', decodeCodabar],
1142
1155
  ];
1143
-
1144
1156
  const EAN_PARENT_FORMATS = new Set(['ean13', 'ean8', 'upca', 'upce', 'isbn']);
1145
1157
  const EAN_SUPPLEMENT_FORMATS = new Set(['ean2', 'ean5']);
1146
-
1147
1158
  /** @param {string} format @returns {boolean} */
1148
1159
  function isEANParentFormat(format) {
1149
- return format === 'ean13' || format === 'ean8' || format === 'upca' || format === 'upce';
1160
+ return format === 'ean13' || format === 'ean8' || format === 'upca' || format === 'upce';
1150
1161
  }
1151
-
1152
1162
  /**
1153
1163
  * An ISBN is printed as a Bookland EAN-13, so its decoded parent remains
1154
1164
  * `ean13` while an ISBN filter accepts only the Bookland prefixes.
@@ -1158,62 +1168,62 @@ function isEANParentFormat(format) {
1158
1168
  * @returns {boolean}
1159
1169
  */
1160
1170
  function isRequestedEANParent(result, enabled) {
1161
- if (enabled.has(result.format)) return true;
1162
- return result.format === 'ean13' && enabled.has('isbn') && /^97[89]/.test(result.text);
1171
+ if (enabled.has(result.format))
1172
+ return true;
1173
+ return result.format === 'ean13' && enabled.has('isbn') && /^97[89]/.test(result.text);
1163
1174
  }
1164
-
1165
1175
  /** @param {object} result @returns {object} */
1166
1176
  function withoutEANAddon(result) {
1167
- const { addon, ...parent } = result;
1168
- void addon;
1169
- return parent;
1177
+ const { addon, ...parent } = result;
1178
+ void addon;
1179
+ return parent;
1170
1180
  }
1171
-
1172
1181
  /** @param {Uint8Array} row @returns {{x:number, width:number, quietZone:boolean}|null} */
1173
1182
  function cameraRowGeometry(row) {
1174
- let first = 0;
1175
- while (first < row.length && row[first] === 0) first++;
1176
- if (first === row.length) return null;
1177
- let last = row.length - 1;
1178
- while (last >= 0 && row[last] === 0) last--;
1179
- return {
1180
- x: first,
1181
- width: last - first + 1,
1182
- quietZone: first >= 2 && row.length - 1 - last >= 2,
1183
- };
1183
+ let first = 0;
1184
+ while (first < row.length && row[first] === 0)
1185
+ first++;
1186
+ if (first === row.length)
1187
+ return null;
1188
+ let last = row.length - 1;
1189
+ while (last >= 0 && row[last] === 0)
1190
+ last--;
1191
+ return {
1192
+ x: first,
1193
+ width: last - first + 1,
1194
+ quietZone: first >= 2 && row.length - 1 - last >= 2,
1195
+ };
1184
1196
  }
1185
-
1186
1197
  /** @param {string} format @param {object} options @returns {boolean|null} */
1187
1198
  function checksumStatus(format, options) {
1188
- if (format === 'ean13' || format === 'ean8' || format === 'upca' || format === 'upce' ||
1189
- format === 'code93' || format === 'code128' || format === 'gs1128' ||
1190
- format === 'gs1databar14') return true;
1191
- if (format === 'code11' || format === 'msi' || format === 'code39') {
1192
- return options.profile === 'camera' || options.checkDigit === true ? true : null;
1193
- }
1194
- return null;
1199
+ if (format === 'ean13' || format === 'ean8' || format === 'upca' || format === 'upce' ||
1200
+ format === 'code93' || format === 'code128' || format === 'gs1128' ||
1201
+ format === 'gs1databar14')
1202
+ return true;
1203
+ if (format === 'code11' || format === 'msi' || format === 'code39') {
1204
+ return options.profile === 'camera' || options.checkDigit === true ? true : null;
1205
+ }
1206
+ return null;
1195
1207
  }
1196
-
1197
1208
  /** @param {object} result @param {object} geometry @param {Set<number>} rows @param {object} options @returns {object} */
1198
1209
  function cameraMetadata(result, geometry, rows, options) {
1199
- const checksum = checksumStatus(result.format, options);
1200
- const consistency = Math.min(1, rows.size / 3);
1201
- const confidence = Math.min(1, 0.4 + (geometry.quietZone ? 0.2 : 0) +
1202
- (checksum === true ? 0.2 : 0) + consistency * 0.2);
1203
- return {
1204
- ...result,
1205
- confidence,
1206
- bounds: {
1207
- x: geometry.x,
1208
- y: Math.min(...rows),
1209
- width: geometry.width,
1210
- height: Math.max(...rows) - Math.min(...rows) + 1,
1211
- },
1212
- rotation: options.cameraRotation ?? 0,
1213
- quality: { quietZone: geometry.quietZone, checksum, rows: rows.size, consistency },
1214
- };
1210
+ const checksum = checksumStatus(result.format, options);
1211
+ const consistency = Math.min(1, rows.size / 3);
1212
+ const confidence = Math.min(1, 0.4 + (geometry.quietZone ? 0.2 : 0) +
1213
+ (checksum === true ? 0.2 : 0) + consistency * 0.2);
1214
+ return {
1215
+ ...result,
1216
+ confidence,
1217
+ bounds: {
1218
+ x: geometry.x,
1219
+ y: Math.min(...rows),
1220
+ width: geometry.width,
1221
+ height: Math.max(...rows) - Math.min(...rows) + 1,
1222
+ },
1223
+ rotation: options.cameraRotation ?? 0,
1224
+ quality: { quietZone: geometry.quietZone, checksum, rows: rows.size, consistency },
1225
+ };
1215
1226
  }
1216
-
1217
1227
  /**
1218
1228
  * Read every linear symbol found in a binarized image.
1219
1229
  *
@@ -1227,129 +1237,137 @@ function cameraMetadata(result, geometry, rows, options) {
1227
1237
  * @returns {Array<{format: string, text: string, row: number}>}
1228
1238
  */
1229
1239
  export function decodeOneD(image, options = {}) {
1230
- const { formats = null, rows = 15, tryHarder = true, profile = null } = options;
1231
- const cameraProfile = profile === 'camera';
1232
- const enabled = formats ? new Set(formats) : null;
1233
- const active = DECODERS.filter(([id]) => {
1234
- if (!enabled) return true;
1235
- if (enabled.has(id)) return true;
1236
- if (id === 'ean13' || id === 'ean8' || id === 'upca' || id === 'upce') {
1237
- return enabled.has('ean2') || enabled.has('ean5') ||
1238
- (id === 'ean13' && enabled.has('isbn'));
1239
- }
1240
- if (id === 'code128') return enabled.has('gs1128');
1241
- if (id === 'gs1databar14') return enabled.has('databar') || enabled.has('gs1-databar14');
1242
- return false;
1243
- });
1244
- if (active.length === 0) return [];
1245
-
1246
- const results = [];
1247
- const seen = new Set();
1248
- const height = image.height;
1249
- const buffer = new Uint8Array(image.width);
1250
- const cameraCandidates = new Map();
1251
-
1252
- // Sample rows from the middle outward: symbols are usually centred, and the
1253
- // middle of a linear barcode is the part least likely to be clipped.
1254
- const middle = height >> 1;
1255
- const sampleRows = cameraProfile ? Math.min(height, Math.max(rows, 48)) : rows;
1256
- const step = Math.max(1, Math.round(height / sampleRows));
1257
-
1258
- for (let attempt = 0; attempt < sampleRows; attempt++) {
1259
- const delta = Math.ceil(attempt / 2) * step * (attempt % 2 === 0 ? 1 : -1);
1260
- const y = middle + delta;
1261
- if (y < 0 || y >= height) continue;
1262
-
1263
- const row = image.getRow(y, buffer);
1264
-
1265
- for (const pass of tryHarder ? [false, true] : [false]) {
1266
- const scan = pass ? Uint8Array.from(row).reverse() : row;
1267
-
1268
- for (const [id, decoder] of active) {
1269
- let result = null;
1270
- try {
1271
- // Code 11 and MSI checks are optional in their base standards, but
1272
- // a camera frame cannot safely promote their short unchecked forms.
1273
- const decoderOptions = cameraProfile && (id === 'code11' || id === 'msi')
1274
- ? { ...options, checkDigit: true }
1275
- : options;
1276
- result = decoder(scan, decoderOptions);
1277
- } catch {
1278
- result = null; // a malformed candidate is not an error
1240
+ const { formats = null, rows = 15, tryHarder = true, profile = null } = options;
1241
+ const cameraProfile = profile === 'camera';
1242
+ const enabled = formats ? new Set(formats) : null;
1243
+ const active = DECODERS.filter(([id]) => {
1244
+ if (!enabled)
1245
+ return true;
1246
+ if (enabled.has(id))
1247
+ return true;
1248
+ if (id === 'ean13' || id === 'ean8' || id === 'upca' || id === 'upce') {
1249
+ return enabled.has('ean2') || enabled.has('ean5') ||
1250
+ (id === 'ean13' && enabled.has('isbn'));
1279
1251
  }
1280
- if (!result) continue;
1281
- if (enabled) {
1282
- if (isEANParentFormat(result.format)) {
1283
- const baseRequested = [...EAN_PARENT_FORMATS].some((format) => enabled.has(format));
1284
- const parentRequested = isRequestedEANParent(result, enabled);
1285
- if (baseRequested && !parentRequested) {
1286
- continue;
1287
- }
1288
- if (!baseRequested) {
1289
- // A supplement is never an independent barcode. When it is the
1290
- // only requested format, return its validated EAN/UPC parent.
1291
- if (!result.addon || !EAN_SUPPLEMENT_FORMATS.has(result.addon.format) ||
1292
- !enabled.has(result.addon.format)) continue;
1293
- } else if (result.addon && !enabled.has(result.addon.format)) {
1294
- // Supplements are optional whenever a requested parent exists.
1295
- result = withoutEANAddon(result);
1296
- }
1297
- } else if (result.format === 'gs1128') {
1298
- if (!enabled.has('gs1128') && !enabled.has('code128')) continue;
1299
- } else if (result.format === 'gs1databar14') {
1300
- if (!enabled.has('gs1databar14') && !enabled.has('databar') && !enabled.has('gs1-databar14')) continue;
1301
- } else if (!enabled.has(result.format)) {
1252
+ if (id === 'code128')
1253
+ return enabled.has('gs1128');
1254
+ if (id === 'gs1databar14')
1255
+ return enabled.has('databar') || enabled.has('gs1-databar14');
1256
+ return false;
1257
+ });
1258
+ if (active.length === 0)
1259
+ return [];
1260
+ const results = [];
1261
+ const seen = new Set();
1262
+ const height = image.height;
1263
+ const buffer = new Uint8Array(image.width);
1264
+ const cameraCandidates = new Map();
1265
+ // Sample rows from the middle outward: symbols are usually centred, and the
1266
+ // middle of a linear barcode is the part least likely to be clipped.
1267
+ const middle = height >> 1;
1268
+ const sampleRows = cameraProfile ? Math.min(height, Math.max(rows, 48)) : rows;
1269
+ const step = Math.max(1, Math.round(height / sampleRows));
1270
+ for (let attempt = 0; attempt < sampleRows; attempt++) {
1271
+ const delta = Math.ceil(attempt / 2) * step * (attempt % 2 === 0 ? 1 : -1);
1272
+ const y = middle + delta;
1273
+ if (y < 0 || y >= height)
1302
1274
  continue;
1303
- }
1304
- }
1305
-
1306
- const addonKey = result.addon ? `:${result.addon.format}:${result.addon.text}` : '';
1307
- const key = `${result.format}:${result.text}${addonKey}`;
1308
- if (cameraProfile) {
1309
- const geometry = cameraRowGeometry(row);
1310
- // Do not promote partial row fragments from a camera frame.
1311
- if (!geometry || !geometry.quietZone) continue;
1312
- const candidate = cameraCandidates.get(key) ?? {
1313
- result,
1314
- geometry,
1315
- rows: new Set(),
1316
- rotation: ((options.cameraRotation ?? 0) + (pass ? 180 : 0)) % 360,
1317
- };
1318
- candidate.rows.add(y);
1319
- cameraCandidates.set(key, candidate);
1320
- continue;
1275
+ const row = image.getRow(y, buffer);
1276
+ for (const pass of tryHarder ? [false, true] : [false]) {
1277
+ const scan = pass ? Uint8Array.from(row).reverse() : row;
1278
+ for (const [id, decoder] of active) {
1279
+ let result = null;
1280
+ try {
1281
+ // Code 11 and MSI checks are optional in their base standards, but
1282
+ // a camera frame cannot safely promote their short unchecked forms.
1283
+ const decoderOptions = cameraProfile && (id === 'code11' || id === 'msi')
1284
+ ? { ...options, checkDigit: true }
1285
+ : options;
1286
+ result = decoder(scan, decoderOptions);
1287
+ }
1288
+ catch {
1289
+ result = null; // a malformed candidate is not an error
1290
+ }
1291
+ if (!result)
1292
+ continue;
1293
+ if (enabled) {
1294
+ if (isEANParentFormat(result.format)) {
1295
+ const baseRequested = [...EAN_PARENT_FORMATS].some((format) => enabled.has(format));
1296
+ const parentRequested = isRequestedEANParent(result, enabled);
1297
+ if (baseRequested && !parentRequested) {
1298
+ continue;
1299
+ }
1300
+ if (!baseRequested) {
1301
+ // A supplement is never an independent barcode. When it is the
1302
+ // only requested format, return its validated EAN/UPC parent.
1303
+ if (!result.addon || !EAN_SUPPLEMENT_FORMATS.has(result.addon.format) ||
1304
+ !enabled.has(result.addon.format))
1305
+ continue;
1306
+ }
1307
+ else if (result.addon && !enabled.has(result.addon.format)) {
1308
+ // Supplements are optional whenever a requested parent exists.
1309
+ result = withoutEANAddon(result);
1310
+ }
1311
+ }
1312
+ else if (result.format === 'gs1128') {
1313
+ if (!enabled.has('gs1128') && !enabled.has('code128'))
1314
+ continue;
1315
+ }
1316
+ else if (result.format === 'gs1databar14') {
1317
+ if (!enabled.has('gs1databar14') && !enabled.has('databar') && !enabled.has('gs1-databar14'))
1318
+ continue;
1319
+ }
1320
+ else if (!enabled.has(result.format)) {
1321
+ continue;
1322
+ }
1323
+ }
1324
+ const addonKey = result.addon ? `:${result.addon.format}:${result.addon.text}` : '';
1325
+ const key = `${result.format}:${result.text}${addonKey}`;
1326
+ if (cameraProfile) {
1327
+ const geometry = cameraRowGeometry(row);
1328
+ // Do not promote partial row fragments from a camera frame.
1329
+ if (!geometry || !geometry.quietZone)
1330
+ continue;
1331
+ const candidate = cameraCandidates.get(key) ?? {
1332
+ result,
1333
+ geometry,
1334
+ rows: new Set(),
1335
+ rotation: ((options.cameraRotation ?? 0) + (pass ? 180 : 0)) % 360,
1336
+ };
1337
+ candidate.rows.add(y);
1338
+ cameraCandidates.set(key, candidate);
1339
+ continue;
1340
+ }
1341
+ if (seen.has(key))
1342
+ continue;
1343
+ seen.add(key);
1344
+ results.push({ ...result, row: y });
1345
+ void id;
1346
+ }
1321
1347
  }
1322
- if (seen.has(key)) continue;
1323
- seen.add(key);
1324
- results.push({ ...result, row: y });
1325
- void id;
1326
- }
1327
1348
  }
1328
- }
1329
-
1330
- if (cameraProfile) {
1331
- for (const candidate of cameraCandidates.values()) {
1332
- // A complete symbol must survive at least two nearby scan samples. This
1333
- // rejects isolated run coincidences without imposing a payload length.
1334
- if (candidate.rows.size < 2) continue;
1335
- results.push(cameraMetadata(candidate.result, candidate.geometry, candidate.rows, {
1336
- ...options,
1337
- cameraRotation: candidate.rotation,
1338
- }));
1349
+ if (cameraProfile) {
1350
+ for (const candidate of cameraCandidates.values()) {
1351
+ // A complete symbol must survive at least two nearby scan samples. This
1352
+ // rejects isolated run coincidences without imposing a payload length.
1353
+ if (candidate.rows.size < 2)
1354
+ continue;
1355
+ results.push(cameraMetadata(candidate.result, candidate.geometry, candidate.rows, {
1356
+ ...options,
1357
+ cameraRotation: candidate.rotation,
1358
+ }));
1359
+ }
1339
1360
  }
1340
- }
1341
-
1342
- // A valid EAN/UPC parent is substantially more constrained than a generic
1343
- // narrow/wide candidate. Suppress competing interpretations of the same
1344
- // scanline, while retaining symbols detected on other rows.
1345
- const eanRows = new Set(results
1346
- .filter((result) => isEANParentFormat(result.format))
1347
- .map((result) => result.row));
1348
- return eanRows.size === 0
1349
- ? results
1350
- : results.filter((result) => !eanRows.has(result.row) || isEANParentFormat(result.format));
1361
+ // A valid EAN/UPC parent is substantially more constrained than a generic
1362
+ // narrow/wide candidate. Suppress competing interpretations of the same
1363
+ // scanline, while retaining symbols detected on other rows.
1364
+ const eanRows = new Set(results
1365
+ .filter((result) => isEANParentFormat(result.format))
1366
+ .map((result) => result.row));
1367
+ return eanRows.size === 0
1368
+ ? results
1369
+ : results.filter((result) => !eanRows.has(result.row) || isEANParentFormat(result.format));
1351
1370
  }
1352
-
1353
1371
  /**
1354
1372
  * Convenience wrapper that throws when nothing is found.
1355
1373
  *
@@ -1358,7 +1376,8 @@ export function decodeOneD(image, options = {}) {
1358
1376
  * @returns {{format: string, text: string, row: number}}
1359
1377
  */
1360
1378
  export function decodeOneDStrict(image, options) {
1361
- const results = decodeOneD(image, options);
1362
- if (results.length === 0) throw new NotFoundError('No linear barcode found');
1363
- return results[0];
1379
+ const results = decodeOneD(image, options);
1380
+ if (results.length === 0)
1381
+ throw new NotFoundError('No linear barcode found');
1382
+ return results[0];
1364
1383
  }