@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.
- package/LICENSE +5 -4
- package/NOTICE.md +2 -2
- package/README.md +61 -13
- package/bundle/sythos-barcode.esm.js +11658 -11244
- package/bundle/sythos-barcode.js +11658 -11244
- package/package.json +10 -3
- package/src/index.d.ts +1 -1
- package/src/index.js +508 -516
- package/src/js/aztec/decoder.js +208 -199
- package/src/js/aztec/detector.js +153 -126
- package/src/js/aztec/encoder.js +172 -153
- package/src/js/aztec/high-level.js +106 -101
- package/src/js/aztec/index.js +1 -12
- package/src/js/aztec/tables.js +106 -120
- package/src/js/aztecrune/decoder.js +76 -85
- package/src/js/aztecrune/detector.js +107 -93
- package/src/js/aztecrune/encoder.js +50 -68
- package/src/js/aztecrune/index.js +1 -16
- package/src/js/aztecrune/tables.js +59 -54
- package/src/js/compactpdf417/decoder.js +69 -74
- package/src/js/compactpdf417/detector.js +74 -75
- package/src/js/compactpdf417/encoder.js +68 -81
- package/src/js/compactpdf417/index.js +1 -13
- package/src/js/compactpdf417/tables.js +71 -80
- package/src/js/core/bit-buffer.js +106 -122
- package/src/js/core/bit-matrix.js +196 -194
- package/src/js/core/errors.js +13 -15
- package/src/js/core/galois-field.js +119 -133
- package/src/js/core/index.js +2 -19
- package/src/js/core/reed-solomon.js +195 -204
- package/src/js/databar/codec.js +93 -108
- package/src/js/databar/decoder.js +149 -140
- package/src/js/databar/encoder.js +47 -55
- package/src/js/databar/gs1.js +122 -123
- package/src/js/databar/index.js +4 -36
- package/src/js/databar/patterns.js +81 -85
- package/src/js/databar/tables.js +51 -52
- package/src/js/datamatrix/decoder.js +216 -182
- package/src/js/datamatrix/detector.js +161 -132
- package/src/js/datamatrix/encoder.js +192 -133
- package/src/js/datamatrix/index.js +1 -9
- package/src/js/datamatrix/tables.js +68 -69
- package/src/js/frameqr/decoder.js +102 -125
- package/src/js/frameqr/detector.js +101 -94
- package/src/js/frameqr/encoder.js +67 -80
- package/src/js/frameqr/index.js +1 -10
- package/src/js/frameqr/tables.js +145 -163
- package/src/js/image/binarizer.js +146 -162
- package/src/js/image/grid-sampler.js +63 -79
- package/src/js/image/index.js +0 -2
- package/src/js/image/luminance.js +129 -140
- package/src/js/image/perspective.js +112 -149
- package/src/js/micropdf417/compaction.js +64 -65
- package/src/js/micropdf417/decoder.js +106 -109
- package/src/js/micropdf417/detector.js +73 -70
- package/src/js/micropdf417/encoder.js +129 -154
- package/src/js/micropdf417/error-correction.js +6 -11
- package/src/js/micropdf417/index.js +2 -16
- package/src/js/micropdf417/tables.js +112 -99
- package/src/js/microqr/decoder.js +177 -182
- package/src/js/microqr/detector.js +240 -256
- package/src/js/microqr/encoder.js +239 -194
- package/src/js/microqr/index.js +0 -2
- package/src/js/microqr/tables.js +208 -200
- package/src/js/oned/addons.js +167 -191
- package/src/js/oned/index.js +27 -58
- package/src/js/oned/patterns.js +194 -213
- package/src/js/oned/reader.js +1003 -984
- package/src/js/oned/writers.js +411 -466
- package/src/js/pdf417/compaction.js +315 -222
- package/src/js/pdf417/decoder.js +71 -36
- package/src/js/pdf417/detector.js +377 -361
- package/src/js/pdf417/encoder.js +57 -46
- package/src/js/pdf417/error-correction.js +5 -8
- package/src/js/pdf417/tables.js +248 -250
- package/src/js/qr/decoder.js +311 -347
- package/src/js/qr/detector.js +378 -395
- package/src/js/qr/encoder.js +514 -599
- package/src/js/qr/index.js +0 -2
- package/src/js/qr/tables.js +375 -422
- package/src/js/render/image-data.js +57 -62
- package/src/js/render/index.js +21 -23
- package/src/js/render/options.js +82 -90
- package/src/js/render/png.js +144 -169
- package/src/js/render/svg.js +41 -44
- package/src/js/render/webgl.js +103 -109
- package/src/js/render/webgpu.js +205 -217
- package/src/js/rmqr/decoder.js +161 -53
- package/src/js/rmqr/detector.js +73 -41
- package/src/js/rmqr/encoder.js +253 -104
- package/src/js/rmqr/index.js +1 -5
- package/src/js/rmqr/tables.js +108 -85
- package/src/ts/aztec/decoder.ts +317 -0
- package/src/ts/aztec/detector.ts +224 -0
- package/src/ts/aztec/encoder.ts +257 -0
- package/src/ts/aztec/high-level.ts +211 -0
- package/src/ts/aztec/index.ts +45 -0
- package/src/ts/aztec/tables.ts +210 -0
- package/src/ts/aztecrune/decoder.ts +155 -0
- package/src/ts/aztecrune/detector.ts +166 -0
- package/src/ts/aztecrune/encoder.ts +121 -0
- package/src/ts/aztecrune/index.ts +49 -0
- package/src/ts/aztecrune/tables.ts +137 -0
- package/src/ts/compactpdf417/decoder.ts +128 -0
- package/src/ts/compactpdf417/detector.ts +139 -0
- package/src/ts/compactpdf417/encoder.ts +140 -0
- package/src/ts/compactpdf417/index.ts +17 -0
- package/src/ts/compactpdf417/tables.ts +173 -0
- package/src/ts/core/bit-buffer.ts +174 -0
- package/src/ts/core/bit-matrix.ts +241 -0
- package/src/ts/core/errors.ts +61 -0
- package/src/ts/core/galois-field.ts +207 -0
- package/src/ts/core/index.ts +56 -0
- package/src/ts/core/reed-solomon.ts +327 -0
- package/src/ts/databar/codec.ts +181 -0
- package/src/ts/databar/decoder.ts +215 -0
- package/src/ts/databar/encoder.ts +96 -0
- package/src/ts/databar/gs1.ts +177 -0
- package/src/ts/databar/index.ts +68 -0
- package/src/ts/databar/patterns.ts +143 -0
- package/src/ts/databar/tables.ts +124 -0
- package/src/ts/datamatrix/decoder.ts +262 -0
- package/src/ts/datamatrix/detector.ts +225 -0
- package/src/ts/datamatrix/encoder.ts +191 -0
- package/src/ts/datamatrix/index.ts +42 -0
- package/src/ts/datamatrix/tables.ts +123 -0
- package/src/ts/frameqr/decoder.ts +239 -0
- package/src/ts/frameqr/detector.ts +192 -0
- package/src/ts/frameqr/encoder.ts +156 -0
- package/src/ts/frameqr/index.ts +42 -0
- package/src/ts/frameqr/tables.ts +270 -0
- package/src/ts/image/binarizer.ts +270 -0
- package/src/ts/image/grid-sampler.ts +164 -0
- package/src/ts/image/index.ts +40 -0
- package/src/ts/image/luminance.ts +196 -0
- package/src/ts/image/perspective.ts +195 -0
- package/src/ts/index.d.ts +1 -1
- package/src/ts/index.ts +790 -0
- package/src/ts/micropdf417/compaction.ts +116 -0
- package/src/ts/micropdf417/decoder.ts +183 -0
- package/src/ts/micropdf417/detector.ts +149 -0
- package/src/ts/micropdf417/encoder.ts +209 -0
- package/src/ts/micropdf417/error-correction.ts +55 -0
- package/src/ts/micropdf417/index.ts +49 -0
- package/src/ts/micropdf417/tables.ts +184 -0
- package/src/ts/microqr/decoder.ts +245 -0
- package/src/ts/microqr/detector.ts +355 -0
- package/src/ts/microqr/encoder.ts +269 -0
- package/src/ts/microqr/index.ts +36 -0
- package/src/ts/microqr/tables.ts +316 -0
- package/src/ts/oned/addons.ts +420 -0
- package/src/ts/oned/index.ts +106 -0
- package/src/ts/oned/patterns.ts +384 -0
- package/src/ts/oned/reader.ts +1364 -0
- package/src/ts/oned/writers.ts +746 -0
- package/src/ts/pdf417/compaction.ts +298 -0
- package/src/ts/pdf417/decoder.ts +75 -0
- package/src/ts/pdf417/detector.ts +468 -0
- package/src/ts/pdf417/encoder.ts +91 -0
- package/src/ts/pdf417/error-correction.ts +47 -0
- package/src/ts/pdf417/index.ts +6 -0
- package/src/ts/pdf417/tables.ts +317 -0
- package/src/ts/qr/decoder.ts +575 -0
- package/src/ts/qr/detector.ts +630 -0
- package/src/ts/qr/encoder.ts +958 -0
- package/src/ts/qr/index.ts +44 -0
- package/src/ts/qr/tables.ts +737 -0
- package/src/ts/render/image-data.ts +125 -0
- package/src/ts/render/index.ts +130 -0
- package/src/ts/render/options.ts +160 -0
- package/src/ts/render/png.ts +295 -0
- package/src/ts/render/svg.ts +120 -0
- package/src/ts/render/webgl.ts +206 -0
- package/src/ts/render/webgpu.ts +369 -0
- package/src/ts/rmqr/decoder.ts +101 -0
- package/src/ts/rmqr/detector.ts +90 -0
- package/src/ts/rmqr/encoder.ts +172 -0
- package/src/ts/rmqr/index.ts +37 -0
- package/src/ts/rmqr/tables.ts +154 -0
package/src/js/oned/reader.js
CHANGED
|
@@ -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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
|
|
541
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
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
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
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
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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 (
|
|
846
|
-
|
|
847
|
-
if (
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
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
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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 (
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
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
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
(id === 'ean13'
|
|
1239
|
-
|
|
1240
|
-
|
|
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 (
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
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
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
.
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
}
|