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