@sythos/js_barcode_universal 1.5.14 → 1.5.15
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 +22 -4
- package/NOTICE.md +10 -1
- package/README.md +372 -22
- package/bundle/sythos-barcode.esm.js +22176 -11991
- package/bundle/sythos-barcode.js +22059 -11991
- package/licenses/README.md +12 -1
- package/licenses/codablockf.license +51 -0
- package/licenses/code16k.license +52 -0
- package/licenses/code25.license +58 -0
- package/licenses/code32.license +51 -0
- package/licenses/dotcode.license +66 -0
- package/licenses/gs1-composite.license +106 -0
- package/licenses/gs1-databar.license +21 -14
- package/licenses/hanxin.license +95 -0
- package/licenses/maxicode.license +62 -0
- package/licenses/postal.license +61 -0
- package/licenses/pzn.license +53 -0
- package/licenses/telepen.license +73 -0
- package/llms.txt +52 -5
- package/package.json +40 -2
- package/src/index.d.ts +111 -6
- package/src/index.js +362 -13
- package/src/js/codablockf/decoder.js +198 -0
- package/src/js/codablockf/encoder.js +97 -0
- package/src/js/codablockf/index.js +3 -0
- package/src/js/code16k/decoder.js +303 -0
- package/src/js/code16k/detector.js +187 -0
- package/src/js/code16k/encoder.js +153 -0
- package/src/js/code16k/index.js +14 -0
- package/src/js/code16k/tables.js +152 -0
- package/src/js/composite/index.js +570 -0
- package/src/js/core/detection-contract.js +185 -0
- package/src/js/core/symbol-layout.js +155 -0
- package/src/js/databar/expanded.js +968 -0
- package/src/js/databar/index.js +4 -0
- package/src/js/databar/layout.js +192 -0
- package/src/js/databar/limited.js +533 -0
- package/src/js/databar/stacked-omnidirectional.js +548 -0
- package/src/js/databar/stacked.js +528 -0
- package/src/js/dotcode/decoder.js +548 -0
- package/src/js/dotcode/detector.js +251 -0
- package/src/js/dotcode/encoder.js +453 -0
- package/src/js/dotcode/index.js +34 -0
- package/src/js/dotcode/tables.js +168 -0
- package/src/js/hanxin/decoder.js +299 -0
- package/src/js/hanxin/detector.js +122 -0
- package/src/js/hanxin/encoder.js +259 -0
- package/src/js/hanxin/index.js +16 -0
- package/src/js/hanxin/tables.js +316 -0
- package/src/js/image/height-coded.js +164 -0
- package/src/js/maxicode/decoder.js +275 -0
- package/src/js/maxicode/detector.js +118 -0
- package/src/js/maxicode/encoder.js +438 -0
- package/src/js/maxicode/index.js +34 -0
- package/src/js/maxicode/tables.js +132 -0
- package/src/js/oned/code25.js +148 -0
- package/src/js/oned/index.js +21 -3
- package/src/js/oned/postal.js +889 -0
- package/src/js/oned/reader.js +272 -7
- package/src/js/oned/telepen.js +361 -0
- package/src/js/oned/writers.js +186 -12
- package/src/js/stacked128/common.js +208 -0
- package/src/ts/codablockf/decoder.ts +212 -0
- package/src/ts/codablockf/encoder.ts +128 -0
- package/src/ts/codablockf/index.d.ts +31 -0
- package/src/ts/codablockf/index.ts +6 -0
- package/src/ts/code16k/decoder.d.ts +25 -0
- package/src/ts/code16k/decoder.ts +313 -0
- package/src/ts/code16k/detector.d.ts +21 -0
- package/src/ts/code16k/detector.ts +203 -0
- package/src/ts/code16k/encoder.d.ts +26 -0
- package/src/ts/code16k/encoder.ts +198 -0
- package/src/ts/code16k/index.d.ts +10 -0
- package/src/ts/code16k/index.ts +47 -0
- package/src/ts/code16k/tables.d.ts +54 -0
- package/src/ts/code16k/tables.ts +195 -0
- package/src/ts/composite/index.d.ts +74 -0
- package/src/ts/composite/index.ts +547 -0
- package/src/ts/core/detection-contract.d.ts +119 -0
- package/src/ts/core/detection-contract.ts +267 -0
- package/src/ts/core/symbol-layout.d.ts +108 -0
- package/src/ts/core/symbol-layout.ts +236 -0
- package/src/ts/databar/expanded.d.ts +40 -0
- package/src/ts/databar/expanded.ts +948 -0
- package/src/ts/databar/index.d.ts +48 -0
- package/src/ts/databar/index.ts +48 -0
- package/src/ts/databar/layout.d.ts +122 -0
- package/src/ts/databar/layout.ts +275 -0
- package/src/ts/databar/limited.d.ts +85 -0
- package/src/ts/databar/limited.ts +553 -0
- package/src/ts/databar/stacked-omnidirectional.d.ts +128 -0
- package/src/ts/databar/stacked-omnidirectional.ts +559 -0
- package/src/ts/databar/stacked.d.ts +96 -0
- package/src/ts/databar/stacked.ts +549 -0
- package/src/ts/dotcode/decoder.d.ts +58 -0
- package/src/ts/dotcode/decoder.ts +467 -0
- package/src/ts/dotcode/detector.d.ts +63 -0
- package/src/ts/dotcode/detector.ts +263 -0
- package/src/ts/dotcode/encoder.d.ts +69 -0
- package/src/ts/dotcode/encoder.ts +427 -0
- package/src/ts/dotcode/index.d.ts +37 -0
- package/src/ts/dotcode/index.ts +72 -0
- package/src/ts/dotcode/tables.d.ts +74 -0
- package/src/ts/dotcode/tables.ts +174 -0
- package/src/ts/hanxin/decoder.d.ts +48 -0
- package/src/ts/hanxin/decoder.ts +314 -0
- package/src/ts/hanxin/detector.d.ts +45 -0
- package/src/ts/hanxin/detector.ts +124 -0
- package/src/ts/hanxin/encoder.d.ts +41 -0
- package/src/ts/hanxin/encoder.ts +290 -0
- package/src/ts/hanxin/index.d.ts +16 -0
- package/src/ts/hanxin/index.ts +18 -0
- package/src/ts/hanxin/tables.d.ts +80 -0
- package/src/ts/hanxin/tables.ts +348 -0
- package/src/ts/image/height-coded.d.ts +101 -0
- package/src/ts/image/height-coded.ts +227 -0
- package/src/ts/index.d.ts +46 -2
- package/src/ts/index.ts +380 -13
- package/src/ts/maxicode/decoder.ts +265 -0
- package/src/ts/maxicode/detector.ts +109 -0
- package/src/ts/maxicode/encoder.ts +414 -0
- package/src/ts/maxicode/index.d.ts +96 -0
- package/src/ts/maxicode/index.ts +44 -0
- package/src/ts/maxicode/tables.ts +136 -0
- package/src/ts/oned/code25.d.ts +42 -0
- package/src/ts/oned/code25.ts +170 -0
- package/src/ts/oned/index.d.ts +7 -2
- package/src/ts/oned/index.ts +45 -3
- package/src/ts/oned/postal.d.ts +44 -0
- package/src/ts/oned/postal.ts +819 -0
- package/src/ts/oned/reader.d.ts +35 -0
- package/src/ts/oned/reader.ts +261 -7
- package/src/ts/oned/telepen.d.ts +65 -0
- package/src/ts/oned/telepen.ts +368 -0
- package/src/ts/oned/writers.d.ts +32 -0
- package/src/ts/oned/writers.ts +184 -13
- package/src/ts/stacked128/common.ts +225 -0
|
@@ -0,0 +1,948 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Sythos
|
|
7
|
+
* SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
|
|
8
|
+
*
|
|
9
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
* in the Software without restriction, including without limitation the rights
|
|
12
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
* furnished to do so, subject to the following conditions:
|
|
15
|
+
*
|
|
16
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
* copies or substantial portions of the Software.
|
|
18
|
+
*
|
|
19
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
* SOFTWARE.
|
|
26
|
+
*
|
|
27
|
+
* SPDX-License-Identifier: MIT
|
|
28
|
+
*
|
|
29
|
+
* Original work. No code from any other barcode implementation.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GS1 DataBar Expanded (RSS Expanded) writer, strict matrix reader and
|
|
34
|
+
* clean-raster detector.
|
|
35
|
+
*
|
|
36
|
+
* The implementation deliberately handles the linear Expanded symbol only.
|
|
37
|
+
* Expanded Stacked has a different row layout and remains in `stacked.ts`.
|
|
38
|
+
* Width construction is performed from the normative constrained-composition
|
|
39
|
+
* rules, so no third-party barcode table or runtime dependency is needed.
|
|
40
|
+
*
|
|
41
|
+
* @module databar/expanded
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
45
|
+
import { ChecksumError, EncodeError, FormatError } from '../core/errors.js';
|
|
46
|
+
import { createDetectionCandidate } from '../core/detection-contract.js';
|
|
47
|
+
import { dataBarWidths } from './patterns.js';
|
|
48
|
+
import {
|
|
49
|
+
GS1_SEPARATOR,
|
|
50
|
+
decodeGS1ElementString,
|
|
51
|
+
encodeGS1ElementString,
|
|
52
|
+
} from './gs1.js';
|
|
53
|
+
|
|
54
|
+
const DATA_CHARACTER_MODULES = 17;
|
|
55
|
+
const FINDER_PATTERN_MODULES = 15;
|
|
56
|
+
const MIN_HEIGHT_MODULES = 34;
|
|
57
|
+
const MIN_DATA_CHARACTERS = 4;
|
|
58
|
+
const MAX_DATA_CHARACTERS = 21;
|
|
59
|
+
const MAX_CHARACTER_VALUE = 4_191;
|
|
60
|
+
const MAX_DIMENSION = 32_768;
|
|
61
|
+
const MAX_MODULES = 16_777_216;
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Finder codes are five-run physical patterns. Codes 1, 3, 5, 7, 9 and 11
|
|
65
|
+
* are the reflected forms used when the finder is read in the opposite
|
|
66
|
+
* parity position.
|
|
67
|
+
*/
|
|
68
|
+
const FINDER_PATTERNS = Object.freeze([
|
|
69
|
+
Object.freeze([1, 8, 4, 1, 1]),
|
|
70
|
+
Object.freeze([1, 1, 4, 8, 1]),
|
|
71
|
+
Object.freeze([3, 6, 4, 1, 1]),
|
|
72
|
+
Object.freeze([1, 1, 4, 6, 3]),
|
|
73
|
+
Object.freeze([3, 4, 6, 1, 1]),
|
|
74
|
+
Object.freeze([1, 1, 6, 4, 3]),
|
|
75
|
+
Object.freeze([3, 2, 8, 1, 1]),
|
|
76
|
+
Object.freeze([1, 1, 8, 2, 3]),
|
|
77
|
+
Object.freeze([2, 6, 5, 1, 1]),
|
|
78
|
+
Object.freeze([1, 1, 5, 6, 2]),
|
|
79
|
+
Object.freeze([2, 2, 9, 1, 1]),
|
|
80
|
+
Object.freeze([1, 1, 9, 2, 2]),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
/* Finder codes for 2..11 data-character pairs, in their normative order. */
|
|
84
|
+
const FINDER_SEQUENCES = Object.freeze([
|
|
85
|
+
Object.freeze([0, 1]),
|
|
86
|
+
Object.freeze([0, 3, 2]),
|
|
87
|
+
Object.freeze([0, 5, 2, 7]),
|
|
88
|
+
Object.freeze([0, 9, 2, 7, 4]),
|
|
89
|
+
Object.freeze([0, 9, 2, 7, 6, 11]),
|
|
90
|
+
Object.freeze([0, 9, 2, 7, 8, 11, 10]),
|
|
91
|
+
Object.freeze([0, 1, 2, 3, 4, 5, 6, 7]),
|
|
92
|
+
Object.freeze([0, 1, 2, 3, 4, 5, 6, 9, 8]),
|
|
93
|
+
Object.freeze([0, 1, 2, 3, 4, 5, 6, 9, 10, 11]),
|
|
94
|
+
Object.freeze([0, 1, 2, 3, 4, 7, 6, 9, 8, 11, 10]),
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/*
|
|
98
|
+
* Expanded character groups. `oddRanks` and `evenRanks` are the number of
|
|
99
|
+
* legal constrained compositions. Data characters carry 12 information bits;
|
|
100
|
+
* the check character also uses the normative values through 4191.
|
|
101
|
+
*/
|
|
102
|
+
const GROUPS = Object.freeze([
|
|
103
|
+
Object.freeze({ gsum: 0, oddTotal: 12, evenTotal: 5, oddWidest: 7, evenWidest: 2, oddRanks: 87, evenRanks: 4 }),
|
|
104
|
+
Object.freeze({ gsum: 348, oddTotal: 10, evenTotal: 7, oddWidest: 5, evenWidest: 4, oddRanks: 52, evenRanks: 20 }),
|
|
105
|
+
Object.freeze({ gsum: 1388, oddTotal: 8, evenTotal: 9, oddWidest: 4, evenWidest: 5, oddRanks: 30, evenRanks: 52 }),
|
|
106
|
+
Object.freeze({ gsum: 2948, oddTotal: 6, evenTotal: 11, oddWidest: 3, evenWidest: 6, oddRanks: 10, evenRanks: 104 }),
|
|
107
|
+
Object.freeze({ gsum: 3988, oddTotal: 4, evenTotal: 13, oddWidest: 1, evenWidest: 8, oddRanks: 1, evenRanks: 204 }),
|
|
108
|
+
]);
|
|
109
|
+
|
|
110
|
+
/* Checksum blocks derive from the repeating power-of-three sequence. */
|
|
111
|
+
const CHECKSUM_POWERS = Object.freeze(Array.from({ length: 184 }, (_, exponent) => {
|
|
112
|
+
let value = 1;
|
|
113
|
+
for (let index = 0; index < exponent; index++) value = (value * 3) % 211;
|
|
114
|
+
return value;
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
const CHECKSUM_WEIGHT_BLOCKS = Object.freeze([
|
|
118
|
+
Object.freeze([
|
|
119
|
+
-1, -1, -1, -1, -1, -1, -1, -1,
|
|
120
|
+
...Array.from({ length: 8 }, (_, index) => CHECKSUM_POWERS[7 - index]),
|
|
121
|
+
]),
|
|
122
|
+
...Array.from({ length: 11 }, (_, index) => Object.freeze([
|
|
123
|
+
...CHECKSUM_POWERS.slice(8 + index * 16, 16 + index * 16),
|
|
124
|
+
...Array.from({ length: 8 }, (_, offset) => CHECKSUM_POWERS[23 + index * 16 - offset]),
|
|
125
|
+
])),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
const ISO646_VALUES = new Map([
|
|
129
|
+
['!', 232], ['"', 233], ['%', 234], ['&', 235], ["'", 236],
|
|
130
|
+
['(', 237], [')', 238], ['*', 239], ['+', 240], [',', 241],
|
|
131
|
+
['-', 242], ['.', 243], ['/', 244], [':', 245], [';', 246],
|
|
132
|
+
['<', 247], ['=', 248], ['>', 249], ['?', 250], ['_', 251],
|
|
133
|
+
[' ', 252],
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
function isRecord(value) {
|
|
137
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function positiveInteger(value, label) {
|
|
141
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
142
|
+
throw new EncodeError(`GS1 DataBar Expanded ${label} must be a positive integer`);
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function resolveGeometry(options, logicalWidth) {
|
|
148
|
+
if (options === undefined) options = {};
|
|
149
|
+
if (!isRecord(options)) throw new TypeError('GS1 DataBar Expanded options must be an object');
|
|
150
|
+
if (options.linkage !== undefined && typeof options.linkage !== 'boolean') {
|
|
151
|
+
throw new TypeError('GS1 DataBar linkage must be a boolean');
|
|
152
|
+
}
|
|
153
|
+
if (options.moduleScale !== undefined && options.scale !== undefined
|
|
154
|
+
&& options.moduleScale !== options.scale) {
|
|
155
|
+
throw new EncodeError('GS1 DataBar Expanded moduleScale and scale disagree');
|
|
156
|
+
}
|
|
157
|
+
const scale = positiveInteger(options.moduleScale ?? options.scale ?? 1, 'moduleScale');
|
|
158
|
+
const height = positiveInteger(options.height ?? MIN_HEIGHT_MODULES * scale, 'height');
|
|
159
|
+
if (height < MIN_HEIGHT_MODULES * scale) {
|
|
160
|
+
throw new EncodeError('GS1 DataBar Expanded height is below the normative 34-module minimum');
|
|
161
|
+
}
|
|
162
|
+
const width = logicalWidth * scale;
|
|
163
|
+
if (!Number.isSafeInteger(width) || width > MAX_DIMENSION
|
|
164
|
+
|| height > MAX_DIMENSION || width * height > MAX_MODULES) {
|
|
165
|
+
throw new EncodeError('GS1 DataBar Expanded dimensions exceed the safe matrix limit');
|
|
166
|
+
}
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
scale,
|
|
169
|
+
height,
|
|
170
|
+
width,
|
|
171
|
+
linkage: options.linkage === true,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function combinations(n, r) {
|
|
176
|
+
if (n < r || r < 0) return 0;
|
|
177
|
+
if (r === 0 || n === r) return 1;
|
|
178
|
+
const k = Math.min(r, n - r);
|
|
179
|
+
let result = 1;
|
|
180
|
+
for (let index = 1; index <= k; index++) {
|
|
181
|
+
result = result * (n - k + index) / index;
|
|
182
|
+
}
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/*
|
|
187
|
+
* Rank the same constrained composition that `dataBarWidths` un-ranks. This
|
|
188
|
+
* local inverse lets the reader reject width sequences that merely resemble a
|
|
189
|
+
* data character instead of silently normalising them.
|
|
190
|
+
*/
|
|
191
|
+
function candidateCount(remaining, bar, width, elements, maximumWidth, noNarrow, narrowMask) {
|
|
192
|
+
let count = combinations(remaining - width - 1, elements - bar - 2);
|
|
193
|
+
if (noNarrow && narrowMask === 0
|
|
194
|
+
&& remaining - width - (elements - bar - 1) >= elements - bar - 1) {
|
|
195
|
+
count -= combinations(remaining - width - (elements - bar), elements - bar - 2);
|
|
196
|
+
}
|
|
197
|
+
if (elements - bar - 1 > 1) {
|
|
198
|
+
let tooWide = 0;
|
|
199
|
+
for (let last = remaining - width - (elements - bar - 2); last > maximumWidth; last--) {
|
|
200
|
+
tooWide += combinations(remaining - width - last - 1, elements - bar - 3);
|
|
201
|
+
}
|
|
202
|
+
count -= tooWide * (elements - 1 - bar);
|
|
203
|
+
} else if (remaining - width > maximumWidth) {
|
|
204
|
+
count--;
|
|
205
|
+
}
|
|
206
|
+
return count;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function rankWidths(widths, modules, maximumWidth, noNarrow) {
|
|
210
|
+
if (!Array.isArray(widths) || widths.length !== 4
|
|
211
|
+
|| widths.some((width) => !Number.isSafeInteger(width) || width < 1 || width > maximumWidth)
|
|
212
|
+
|| widths.reduce((sum, width) => sum + width, 0) !== modules) {
|
|
213
|
+
throw new FormatError('GS1 DataBar Expanded subset widths are invalid');
|
|
214
|
+
}
|
|
215
|
+
let rank = 0;
|
|
216
|
+
let remaining = modules;
|
|
217
|
+
let narrowMask = 0;
|
|
218
|
+
for (let bar = 0; bar < 3; bar++) {
|
|
219
|
+
const selected = widths[bar];
|
|
220
|
+
for (let width = 1; width < selected; width++) {
|
|
221
|
+
if (width === 1) narrowMask |= 1 << bar;
|
|
222
|
+
rank += candidateCount(remaining, bar, width, 4, maximumWidth, noNarrow, narrowMask);
|
|
223
|
+
narrowMask &= ~(1 << bar);
|
|
224
|
+
}
|
|
225
|
+
if (selected === 1) narrowMask |= 1 << bar;
|
|
226
|
+
else narrowMask &= ~(1 << bar);
|
|
227
|
+
remaining -= selected;
|
|
228
|
+
}
|
|
229
|
+
if (widths[3] !== remaining) throw new FormatError('GS1 DataBar Expanded subset rank is not canonical');
|
|
230
|
+
return rank;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function groupForValue(value) {
|
|
234
|
+
for (let index = GROUPS.length - 1; index >= 0; index--) {
|
|
235
|
+
if (value >= GROUPS[index].gsum) return GROUPS[index];
|
|
236
|
+
}
|
|
237
|
+
throw new RangeError('GS1 DataBar Expanded character value is out of range');
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function expandedCharacterWidths(value) {
|
|
241
|
+
if (!Number.isSafeInteger(value) || value < 0 || value > MAX_CHARACTER_VALUE) {
|
|
242
|
+
throw new RangeError('GS1 DataBar Expanded character value is outside the normative range');
|
|
243
|
+
}
|
|
244
|
+
const group = groupForValue(value);
|
|
245
|
+
const offset = value - group.gsum;
|
|
246
|
+
const oddRank = Math.floor(offset / group.evenRanks);
|
|
247
|
+
const evenRank = offset % group.evenRanks;
|
|
248
|
+
if (oddRank >= group.oddRanks || evenRank >= group.evenRanks) {
|
|
249
|
+
throw new RangeError('GS1 DataBar Expanded character value is outside its group');
|
|
250
|
+
}
|
|
251
|
+
const odd = dataBarWidths(oddRank, group.oddTotal, 4, group.oddWidest, true);
|
|
252
|
+
const even = dataBarWidths(evenRank, group.evenTotal, 4, group.evenWidest, false);
|
|
253
|
+
const widths = [];
|
|
254
|
+
for (let index = 0; index < 4; index++) widths.push(odd[index], even[index]);
|
|
255
|
+
if (widths.reduce((sum, width) => sum + width, 0) !== DATA_CHARACTER_MODULES) {
|
|
256
|
+
throw new EncodeError('GS1 DataBar Expanded data character width total is invalid');
|
|
257
|
+
}
|
|
258
|
+
return widths;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function physicalDataCharacterWidths(value, characterIndex) {
|
|
262
|
+
const canonical = expandedCharacterWidths(value);
|
|
263
|
+
/* The first payload character follows the check character in reverse order. */
|
|
264
|
+
return characterIndex % 2 === 1 ? canonical.slice().reverse() : canonical;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const dataCharacterMaps = new Map();
|
|
268
|
+
const checkCharacterMap = new Map();
|
|
269
|
+
|
|
270
|
+
function getCharacterMap(characterIndex, check = false) {
|
|
271
|
+
const key = check ? 'check' : (characterIndex & 1);
|
|
272
|
+
const cache = check ? checkCharacterMap : dataCharacterMaps;
|
|
273
|
+
if (cache.has(key)) return cache.get(key);
|
|
274
|
+
const map = new Map();
|
|
275
|
+
const maximum = check ? MAX_CHARACTER_VALUE : 4095;
|
|
276
|
+
for (let value = 0; value <= maximum; value++) {
|
|
277
|
+
const widths = check ? expandedCharacterWidths(value) : physicalDataCharacterWidths(value, characterIndex);
|
|
278
|
+
map.set(widths.join(','), value);
|
|
279
|
+
}
|
|
280
|
+
cache.set(key, map);
|
|
281
|
+
return map;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function valueForWidths(widths, characterIndex, check = false) {
|
|
285
|
+
const value = getCharacterMap(characterIndex, check).get(widths.join(','));
|
|
286
|
+
if (value === undefined) throw new FormatError('GS1 DataBar Expanded character widths are invalid');
|
|
287
|
+
return value;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function finderWidths(finderCode) {
|
|
291
|
+
const pattern = FINDER_PATTERNS[finderCode];
|
|
292
|
+
if (!pattern) throw new FormatError('GS1 DataBar Expanded finder type is invalid');
|
|
293
|
+
return pattern;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function appendWidths(bits, widths, dark) {
|
|
297
|
+
let colour = dark;
|
|
298
|
+
for (const width of widths) {
|
|
299
|
+
for (let index = 0; index < width; index++) bits.push(colour);
|
|
300
|
+
colour = !colour;
|
|
301
|
+
}
|
|
302
|
+
return colour;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function bitsToValue(bits, offset, length) {
|
|
306
|
+
let value = 0;
|
|
307
|
+
for (let index = 0; index < length; index++) value = (value << 1) | (bits[offset + index] ? 1 : 0);
|
|
308
|
+
return value;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function appendBits(target, value, length) {
|
|
312
|
+
for (let shift = length - 1; shift >= 0; shift--) target.push(((value >>> shift) & 1) !== 0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function isDigit(value) {
|
|
316
|
+
return value >= '0' && value <= '9';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function isDigitOrFNC1(value) {
|
|
320
|
+
return isDigit(value) || value === GS1_SEPARATOR;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function appendNumericPair(bits, first, second) {
|
|
324
|
+
if (first === GS1_SEPARATOR) {
|
|
325
|
+
if (second !== GS1_SEPARATOR) throw new EncodeError('GS1 DataBar Expanded cannot start a field with FNC1');
|
|
326
|
+
appendBits(bits, 0, 4);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const firstValue = first === GS1_SEPARATOR ? 10 : Number(first);
|
|
330
|
+
const secondValue = second === GS1_SEPARATOR ? 10 : Number(second);
|
|
331
|
+
if (firstValue > 9 || secondValue > 10) throw new EncodeError('GS1 DataBar Expanded numeric value is invalid');
|
|
332
|
+
if (second === GS1_SEPARATOR) {
|
|
333
|
+
appendBits(bits, firstValue + 1, 4);
|
|
334
|
+
} else {
|
|
335
|
+
appendBits(bits, 8 + 11 * firstValue + secondValue, 7);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function alphaValue(value) {
|
|
340
|
+
if (isDigit(value)) return { value: Number(value) + 5, length: 5 };
|
|
341
|
+
if (value === GS1_SEPARATOR) return { value: 15, length: 5, fnc1: true };
|
|
342
|
+
if (value >= 'A' && value <= 'Z') return { value: value.charCodeAt(0) - 33, length: 6 };
|
|
343
|
+
if (value === '*') return { value: 58, length: 6 };
|
|
344
|
+
if (value >= ',' && value <= '/') return { value: value.charCodeAt(0) + 15, length: 6 };
|
|
345
|
+
return undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function isoValue(value) {
|
|
349
|
+
if (isDigit(value)) return { value: Number(value) + 5, length: 5 };
|
|
350
|
+
if (value === GS1_SEPARATOR) return { value: 15, length: 5, fnc1: true };
|
|
351
|
+
if (value >= 'A' && value <= 'Z') return { value: value.charCodeAt(0) - 1, length: 7 };
|
|
352
|
+
if (value >= 'a' && value <= 'z') return { value: value.charCodeAt(0) + 7, length: 7 };
|
|
353
|
+
const encoded = ISO646_VALUES.get(value);
|
|
354
|
+
return encoded === undefined ? undefined : { value: encoded, length: 8 };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* Encode the general-purpose field from numeric mode, as required by Expanded. */
|
|
358
|
+
function encodeGeneralPurpose(raw) {
|
|
359
|
+
const bits = [];
|
|
360
|
+
let mode = 'numeric';
|
|
361
|
+
let offset = 0;
|
|
362
|
+
while (offset < raw.length) {
|
|
363
|
+
const value = raw[offset];
|
|
364
|
+
if (mode === 'numeric') {
|
|
365
|
+
if (offset + 1 < raw.length && isDigitOrFNC1(value) && isDigitOrFNC1(raw[offset + 1])
|
|
366
|
+
&& value !== GS1_SEPARATOR && raw[offset + 1] !== GS1_SEPARATOR) {
|
|
367
|
+
appendNumericPair(bits, value, raw[offset + 1]);
|
|
368
|
+
offset += 2;
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
if (offset + 1 < raw.length && isDigit(value) && raw[offset + 1] === GS1_SEPARATOR) {
|
|
372
|
+
appendNumericPair(bits, value, raw[offset + 1]);
|
|
373
|
+
offset += 2;
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
/* A lone digit before an alphanumeric value is encoded in alpha mode. */
|
|
377
|
+
appendBits(bits, 0, 4);
|
|
378
|
+
mode = 'alpha';
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (mode === 'alpha') {
|
|
383
|
+
if (value === GS1_SEPARATOR) {
|
|
384
|
+
appendBits(bits, 15, 5);
|
|
385
|
+
mode = 'numeric';
|
|
386
|
+
offset++;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
const encoded = alphaValue(value);
|
|
390
|
+
if (encoded) {
|
|
391
|
+
appendBits(bits, encoded.value, encoded.length);
|
|
392
|
+
offset++;
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
/* Alpha-to-ISO/IEC 646 latch. */
|
|
396
|
+
appendBits(bits, 4, 5);
|
|
397
|
+
mode = 'iso646';
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (value === GS1_SEPARATOR) {
|
|
402
|
+
appendBits(bits, 15, 5);
|
|
403
|
+
mode = 'numeric';
|
|
404
|
+
offset++;
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
const encoded = isoValue(value);
|
|
408
|
+
if (!encoded) {
|
|
409
|
+
throw new EncodeError(`GS1 DataBar Expanded value contains unsupported character "${value}"`);
|
|
410
|
+
}
|
|
411
|
+
appendBits(bits, encoded.value, encoded.length);
|
|
412
|
+
offset++;
|
|
413
|
+
}
|
|
414
|
+
return Object.freeze({ bits, finalMode: mode });
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function padGeneralPurpose(bits, finalMode, dataCharacters) {
|
|
418
|
+
const output = bits.slice();
|
|
419
|
+
const headerLength = 5;
|
|
420
|
+
const targetLength = dataCharacters * 12;
|
|
421
|
+
let remaining = targetLength - headerLength - output.length;
|
|
422
|
+
if (remaining < 0) throw new EncodeError('GS1 DataBar Expanded general-purpose field exceeds the selected symbol size');
|
|
423
|
+
if (finalMode === 'numeric' && remaining >= 4) {
|
|
424
|
+
appendBits(output, 0, 4);
|
|
425
|
+
remaining -= 4;
|
|
426
|
+
}
|
|
427
|
+
const fill = [false, false, true, false, false];
|
|
428
|
+
for (let index = 0; index < remaining; index++) output.push(fill[index % fill.length]);
|
|
429
|
+
if (output.length !== targetLength - headerLength) {
|
|
430
|
+
throw new EncodeError('GS1 DataBar Expanded padding length is invalid');
|
|
431
|
+
}
|
|
432
|
+
return output;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function encodePayload(raw, linkage) {
|
|
436
|
+
const general = encodeGeneralPurpose(raw);
|
|
437
|
+
let dataCharacters = Math.max(
|
|
438
|
+
MIN_DATA_CHARACTERS,
|
|
439
|
+
Math.ceil((12 + 1 + 2 + 2 + general.bits.length) / 12),
|
|
440
|
+
);
|
|
441
|
+
if (dataCharacters > MAX_DATA_CHARACTERS) {
|
|
442
|
+
throw new EncodeError('GS1 DataBar Expanded payload is too long for the 21 data-character limit');
|
|
443
|
+
}
|
|
444
|
+
const vlf = [dataCharacters & 1, dataCharacters > 14 ? 1 : 0];
|
|
445
|
+
const generalBits = padGeneralPurpose(general.bits, general.finalMode, dataCharacters);
|
|
446
|
+
const information = [Boolean(linkage), false, false, Boolean(vlf[0]), Boolean(vlf[1]), ...generalBits];
|
|
447
|
+
if (information.length !== dataCharacters * 12) {
|
|
448
|
+
throw new EncodeError('GS1 DataBar Expanded information field has an invalid length');
|
|
449
|
+
}
|
|
450
|
+
const values = [];
|
|
451
|
+
for (let offset = 0; offset < information.length; offset += 12) {
|
|
452
|
+
values.push(bitsToValue(information, offset, 12));
|
|
453
|
+
}
|
|
454
|
+
return Object.freeze({ values, dataCharacters, information });
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function checksumWeightSequence(dataCharacters) {
|
|
458
|
+
const sequenceIndex = Math.floor((dataCharacters - 2) / 2);
|
|
459
|
+
const sequence = FINDER_SEQUENCES[sequenceIndex];
|
|
460
|
+
if (!sequence) throw new FormatError('GS1 DataBar Expanded finder sequence is invalid');
|
|
461
|
+
const weights = [];
|
|
462
|
+
for (const finderCode of sequence) {
|
|
463
|
+
const block = CHECKSUM_WEIGHT_BLOCKS[finderCode];
|
|
464
|
+
if (!block) throw new FormatError('GS1 DataBar Expanded checksum weight block is invalid');
|
|
465
|
+
weights.push(...block);
|
|
466
|
+
}
|
|
467
|
+
const payloadWeights = weights.slice(8, 8 + dataCharacters * 8);
|
|
468
|
+
if (payloadWeights.length !== dataCharacters * 8) {
|
|
469
|
+
throw new FormatError('GS1 DataBar Expanded checksum weight sequence is truncated');
|
|
470
|
+
}
|
|
471
|
+
return payloadWeights;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function checkCharacterValue(physicalWidths, dataCharacters) {
|
|
475
|
+
const weights = checksumWeightSequence(dataCharacters);
|
|
476
|
+
let checksum = 0;
|
|
477
|
+
for (let dataIndex = 0; dataIndex < physicalWidths.length; dataIndex++) {
|
|
478
|
+
for (let widthIndex = 0; widthIndex < 8; widthIndex++) {
|
|
479
|
+
checksum += physicalWidths[dataIndex][widthIndex] * weights[dataIndex * 8 + widthIndex];
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const check = 211 * (dataCharacters - 3) + (checksum % 211);
|
|
483
|
+
if (check < 0 || check > MAX_CHARACTER_VALUE) throw new EncodeError('GS1 DataBar Expanded checksum value is out of range');
|
|
484
|
+
return check;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function buildPhysicalBits(dataValues, linkage) {
|
|
488
|
+
const dataCharacters = dataValues.length;
|
|
489
|
+
const pairCount = Math.ceil((dataCharacters + 1) / 2);
|
|
490
|
+
const sequenceIndex = Math.floor((dataCharacters - 2) / 2);
|
|
491
|
+
const sequence = FINDER_SEQUENCES[sequenceIndex];
|
|
492
|
+
if (!sequence || sequence.length !== pairCount) {
|
|
493
|
+
throw new EncodeError('GS1 DataBar Expanded data-character count has no valid finder sequence');
|
|
494
|
+
}
|
|
495
|
+
const dataWidths = dataValues.map((value, index) => physicalDataCharacterWidths(value, index + 1));
|
|
496
|
+
const checksumValue = checkCharacterValue(dataWidths, dataCharacters);
|
|
497
|
+
const widths = [expandedCharacterWidths(checksumValue), ...dataWidths];
|
|
498
|
+
let dark = true;
|
|
499
|
+
const bits = [];
|
|
500
|
+
dark = appendWidths(bits, [1, 1], dark);
|
|
501
|
+
for (let index = 0; index < widths.length; index++) {
|
|
502
|
+
dark = appendWidths(bits, widths[index], dark);
|
|
503
|
+
if ((index & 1) === 0) dark = appendWidths(bits, finderWidths(sequence[index / 2]), dark);
|
|
504
|
+
}
|
|
505
|
+
appendWidths(bits, [1, 1], dark);
|
|
506
|
+
const expectedWidth = 2 + widths.length * DATA_CHARACTER_MODULES
|
|
507
|
+
+ pairCount * FINDER_PATTERN_MODULES + 2;
|
|
508
|
+
if (bits.length !== expectedWidth) throw new EncodeError('GS1 DataBar Expanded physical width is invalid');
|
|
509
|
+
return Object.freeze({ bits, widths, dataCharacters, pairCount, linkage, checksum: checksumValue });
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function paint(bits, height, scale) {
|
|
513
|
+
const matrix = new BitMatrix(bits.length * scale, height);
|
|
514
|
+
for (let x = 0; x < bits.length; x++) {
|
|
515
|
+
if (bits[x]) matrix.setRegion(x * scale, 0, scale, height);
|
|
516
|
+
}
|
|
517
|
+
return matrix;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Encode a GS1 element string as linear GS1 DataBar Expanded. */
|
|
521
|
+
export function encodeDataBarExpanded(value, options = {}) {
|
|
522
|
+
const raw = encodeGS1ElementString(value);
|
|
523
|
+
const linkage = options?.linkage === true;
|
|
524
|
+
const payload = encodePayload(raw, linkage);
|
|
525
|
+
const physical = buildPhysicalBits(payload.values, linkage);
|
|
526
|
+
const logicalWidth = physical.bits.length;
|
|
527
|
+
const geometry = resolveGeometry(options, logicalWidth);
|
|
528
|
+
const matrix = paint(physical.bits, geometry.height, geometry.scale);
|
|
529
|
+
matrix.databar = Object.freeze({
|
|
530
|
+
variant: 'expanded',
|
|
531
|
+
raw,
|
|
532
|
+
linkage,
|
|
533
|
+
dataCharacters: payload.dataCharacters,
|
|
534
|
+
pairs: physical.pairCount,
|
|
535
|
+
modules: logicalWidth,
|
|
536
|
+
moduleScale: geometry.scale,
|
|
537
|
+
height: geometry.height,
|
|
538
|
+
});
|
|
539
|
+
return matrix;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function requireMatrix(matrix) {
|
|
543
|
+
if (!matrix || !Number.isInteger(matrix.width) || !Number.isInteger(matrix.height)
|
|
544
|
+
|| matrix.width < 1 || matrix.height < 1 || typeof matrix.get !== 'function') {
|
|
545
|
+
throw new TypeError('GS1 DataBar Expanded decoder expects a BitMatrix-like value');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function runWidths(bits) {
|
|
550
|
+
if (!bits.length) return [];
|
|
551
|
+
const widths = [];
|
|
552
|
+
let colour = bits[0];
|
|
553
|
+
let width = 0;
|
|
554
|
+
for (const bit of bits) {
|
|
555
|
+
if (bit === colour) width++;
|
|
556
|
+
else {
|
|
557
|
+
widths.push(width);
|
|
558
|
+
colour = bit;
|
|
559
|
+
width = 1;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
widths.push(width);
|
|
563
|
+
return widths;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function logicalRow(matrix) {
|
|
567
|
+
requireMatrix(matrix);
|
|
568
|
+
const candidates = [];
|
|
569
|
+
for (let dataCharacters = MIN_DATA_CHARACTERS; dataCharacters <= MAX_DATA_CHARACTERS; dataCharacters++) {
|
|
570
|
+
const pairCount = Math.ceil((dataCharacters + 1) / 2);
|
|
571
|
+
const logicalWidth = 2 + (dataCharacters + 1) * DATA_CHARACTER_MODULES
|
|
572
|
+
+ pairCount * FINDER_PATTERN_MODULES + 2;
|
|
573
|
+
if (matrix.width % logicalWidth === 0) candidates.push({ dataCharacters, logicalWidth, scale: matrix.width / logicalWidth });
|
|
574
|
+
}
|
|
575
|
+
if (candidates.length === 0) throw new FormatError('GS1 DataBar Expanded matrix width is not a valid symbol width');
|
|
576
|
+
const candidate = candidates.find(({ scale }) => Number.isSafeInteger(scale) && scale >= 1);
|
|
577
|
+
if (!candidate || matrix.height < MIN_HEIGHT_MODULES * candidate.scale) {
|
|
578
|
+
throw new FormatError('GS1 DataBar Expanded matrix scale or height is invalid');
|
|
579
|
+
}
|
|
580
|
+
const { dataCharacters, logicalWidth, scale } = candidate;
|
|
581
|
+
const row = [];
|
|
582
|
+
for (let x = 0; x < logicalWidth; x++) {
|
|
583
|
+
const sample = Boolean(matrix.get(x * scale + Math.floor(scale / 2), Math.floor(matrix.height / 2)));
|
|
584
|
+
for (let dx = 0; dx < scale; dx++) {
|
|
585
|
+
if (Boolean(matrix.get(x * scale + dx, Math.floor(matrix.height / 2))) !== sample) {
|
|
586
|
+
throw new FormatError('GS1 DataBar Expanded horizontal modules are not integer-scaled');
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
row.push(sample);
|
|
590
|
+
}
|
|
591
|
+
for (let y = 0; y < matrix.height; y++) {
|
|
592
|
+
for (let x = 0; x < logicalWidth; x++) {
|
|
593
|
+
const expected = row[x];
|
|
594
|
+
for (let dx = 0; dx < scale; dx++) {
|
|
595
|
+
if (Boolean(matrix.get(x * scale + dx, y)) !== expected) {
|
|
596
|
+
throw new FormatError('GS1 DataBar Expanded rows are inconsistent');
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return { row, dataCharacters, logicalWidth, scale };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function decodePhysical(row, dataCharacters) {
|
|
605
|
+
const pairCount = Math.ceil((dataCharacters + 1) / 2);
|
|
606
|
+
const sequenceIndex = Math.floor((dataCharacters - 2) / 2);
|
|
607
|
+
const sequence = FINDER_SEQUENCES[sequenceIndex];
|
|
608
|
+
if (!sequence || sequence.length !== pairCount) throw new FormatError('GS1 DataBar Expanded finder sequence is invalid');
|
|
609
|
+
if (row.length < 4 || !row[0] || row[1]) throw new FormatError('GS1 DataBar Expanded left guard is invalid');
|
|
610
|
+
const values = [];
|
|
611
|
+
const physicalWidths = [];
|
|
612
|
+
let cursor = 2;
|
|
613
|
+
for (let index = 0; index <= dataCharacters; index++) {
|
|
614
|
+
const charBits = row.slice(cursor, cursor + DATA_CHARACTER_MODULES);
|
|
615
|
+
const widths = runWidths(charBits);
|
|
616
|
+
if (widths.length !== 8 || widths.reduce((sum, width) => sum + width, 0) !== DATA_CHARACTER_MODULES) {
|
|
617
|
+
throw new FormatError('GS1 DataBar Expanded data-character run count is invalid');
|
|
618
|
+
}
|
|
619
|
+
physicalWidths.push(widths);
|
|
620
|
+
values.push(valueForWidths(widths, index, index === 0));
|
|
621
|
+
cursor += DATA_CHARACTER_MODULES;
|
|
622
|
+
if ((index & 1) === 0) {
|
|
623
|
+
const finderBits = row.slice(cursor, cursor + FINDER_PATTERN_MODULES);
|
|
624
|
+
const observed = runWidths(finderBits);
|
|
625
|
+
const expected = finderWidths(sequence[index / 2]);
|
|
626
|
+
if (observed.length !== 5 || observed.some((width, position) => width !== expected[position])) {
|
|
627
|
+
throw new FormatError('GS1 DataBar Expanded finder pattern is invalid');
|
|
628
|
+
}
|
|
629
|
+
cursor += FINDER_PATTERN_MODULES;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const end = row.slice(cursor);
|
|
633
|
+
if (end.length !== 2 || end[0] === end[1]) throw new FormatError('GS1 DataBar Expanded right guard is invalid');
|
|
634
|
+
if (cursor + 2 !== row.length) throw new FormatError('GS1 DataBar Expanded trailing modules are invalid');
|
|
635
|
+
const weights = checksumWeightSequence(dataCharacters);
|
|
636
|
+
let checksum = 0;
|
|
637
|
+
for (let index = 1; index < values.length; index++) {
|
|
638
|
+
for (let widthIndex = 0; widthIndex < 8; widthIndex++) {
|
|
639
|
+
checksum += physicalWidths[index][widthIndex] * weights[(index - 1) * 8 + widthIndex];
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const expected = 211 * (dataCharacters - 3) + (checksum % 211);
|
|
643
|
+
if (values[0] !== expected) throw new ChecksumError('GS1 DataBar Expanded checksum mismatch');
|
|
644
|
+
return Object.freeze({ values, physicalWidths, dataCharacters, pairCount, scale: 1 });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function decodeGeneralPurpose(payload) {
|
|
648
|
+
let position = 0;
|
|
649
|
+
let mode = 'numeric';
|
|
650
|
+
let raw = '';
|
|
651
|
+
const allZero = (offset) => {
|
|
652
|
+
for (let index = offset; index < payload.length; index++) if (payload[index]) return false;
|
|
653
|
+
return true;
|
|
654
|
+
};
|
|
655
|
+
const isPaddingTail = (offset) => {
|
|
656
|
+
const fill = [false, false, true, false, false];
|
|
657
|
+
for (let phase = 0; phase < fill.length; phase++) {
|
|
658
|
+
let matches = true;
|
|
659
|
+
for (let index = offset; index < payload.length; index++) {
|
|
660
|
+
if (payload[index] !== fill[(phase + index - offset) % fill.length]) {
|
|
661
|
+
matches = false;
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (matches) return true;
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
};
|
|
669
|
+
const read = (length) => {
|
|
670
|
+
if (position + length > payload.length) throw new FormatError('GS1 DataBar Expanded general-purpose field is truncated');
|
|
671
|
+
const value = bitsToValue(payload, position, length);
|
|
672
|
+
position += length;
|
|
673
|
+
return value;
|
|
674
|
+
};
|
|
675
|
+
let guard = 0;
|
|
676
|
+
while (position < payload.length && guard++ < payload.length + 4) {
|
|
677
|
+
if (mode === 'numeric') {
|
|
678
|
+
if (position + 7 <= payload.length && !allZero(position)) {
|
|
679
|
+
const value = bitsToValue(payload, position, 7);
|
|
680
|
+
if (value >= 8 && value <= 117) {
|
|
681
|
+
const first = Math.floor((value - 8) / 11);
|
|
682
|
+
const second = (value - 8) % 11;
|
|
683
|
+
if (first > 9 || second > 9) throw new FormatError('GS1 DataBar Expanded numeric digit is invalid');
|
|
684
|
+
position += 7;
|
|
685
|
+
raw += `${first}${second}`;
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (position + 4 <= payload.length) {
|
|
690
|
+
const value = read(4);
|
|
691
|
+
if (value === 0) {
|
|
692
|
+
if (position >= payload.length || allZero(position)) break;
|
|
693
|
+
mode = 'alpha';
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
if (value >= 1 && value <= 10) {
|
|
697
|
+
raw += `${value - 1}${GS1_SEPARATOR}`;
|
|
698
|
+
mode = 'numeric';
|
|
699
|
+
continue;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
throw new FormatError('GS1 DataBar Expanded numeric block is invalid');
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (mode === 'alpha') {
|
|
706
|
+
if (position + 3 <= payload.length && bitsToValue(payload, position, 3) === 0) {
|
|
707
|
+
position += 3;
|
|
708
|
+
mode = 'numeric';
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
if (position + 5 > payload.length) {
|
|
712
|
+
if (allZero(position) || isPaddingTail(position)) break;
|
|
713
|
+
throw new FormatError('GS1 DataBar Expanded alphanumeric block is truncated');
|
|
714
|
+
}
|
|
715
|
+
const five = bitsToValue(payload, position, 5);
|
|
716
|
+
if (five === 4) {
|
|
717
|
+
position += 5;
|
|
718
|
+
mode = 'iso646';
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
if (five >= 5 && five <= 15) {
|
|
722
|
+
position += 5;
|
|
723
|
+
if (five === 15) { raw += GS1_SEPARATOR; mode = 'numeric'; }
|
|
724
|
+
else raw += String(five - 5);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
if (position + 6 > payload.length) throw new FormatError('GS1 DataBar Expanded alphanumeric character is truncated');
|
|
728
|
+
const six = read(6);
|
|
729
|
+
if (six >= 32 && six <= 57) raw += String.fromCharCode(six + 33);
|
|
730
|
+
else if (six === 58) raw += '*';
|
|
731
|
+
else if (six >= 59 && six <= 62) raw += String.fromCharCode(six - 15);
|
|
732
|
+
else throw new FormatError('GS1 DataBar Expanded alphanumeric character is invalid');
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (position + 3 <= payload.length && bitsToValue(payload, position, 3) === 0) {
|
|
737
|
+
position += 3;
|
|
738
|
+
mode = 'numeric';
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (position + 5 > payload.length) {
|
|
742
|
+
if (allZero(position) || isPaddingTail(position)) break;
|
|
743
|
+
throw new FormatError('GS1 DataBar Expanded ISO/IEC 646 block is truncated');
|
|
744
|
+
}
|
|
745
|
+
const five = bitsToValue(payload, position, 5);
|
|
746
|
+
if (five === 4) {
|
|
747
|
+
position += 5;
|
|
748
|
+
mode = 'alpha';
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (five >= 5 && five <= 15) {
|
|
752
|
+
position += 5;
|
|
753
|
+
if (five === 15) { raw += GS1_SEPARATOR; mode = 'numeric'; }
|
|
754
|
+
else raw += String(five - 5);
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
if (position + 7 <= payload.length) {
|
|
758
|
+
const seven = read(7);
|
|
759
|
+
if (seven >= 64 && seven <= 89) raw += String.fromCharCode(seven + 1);
|
|
760
|
+
else if (seven >= 90 && seven <= 115) raw += String.fromCharCode(seven + 7);
|
|
761
|
+
else {
|
|
762
|
+
if (position < 7) throw new FormatError('GS1 DataBar Expanded ISO/IEC 646 character is invalid');
|
|
763
|
+
position -= 7;
|
|
764
|
+
const eight = read(8);
|
|
765
|
+
const entry = [...ISO646_VALUES.entries()].find(([, encoded]) => encoded === eight);
|
|
766
|
+
if (!entry) throw new FormatError('GS1 DataBar Expanded ISO/IEC 646 character is invalid');
|
|
767
|
+
raw += entry[0];
|
|
768
|
+
}
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
throw new FormatError('GS1 DataBar Expanded ISO/IEC 646 block is truncated');
|
|
772
|
+
}
|
|
773
|
+
if (guard >= payload.length + 4) throw new FormatError('GS1 DataBar Expanded general-purpose field did not terminate');
|
|
774
|
+
return raw;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function gtinCheckDigit(value) {
|
|
778
|
+
let sum = 0;
|
|
779
|
+
for (let index = value.length - 1; index >= 0; index--) {
|
|
780
|
+
sum += Number(value[index]) * ((value.length - index) % 2 === 1 ? 3 : 1);
|
|
781
|
+
}
|
|
782
|
+
return String((10 - (sum % 10)) % 10);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function decodeMethodOne(bits, dataCharacters) {
|
|
786
|
+
if (bits.length < 48) throw new FormatError('GS1 DataBar Expanded method 1 information is truncated');
|
|
787
|
+
const symbolCharacters = dataCharacters + 1;
|
|
788
|
+
const expectedVlf = [symbolCharacters % 2 === 1, symbolCharacters > 14];
|
|
789
|
+
if (bits[2] !== expectedVlf[0] || bits[3] !== expectedVlf[1]) {
|
|
790
|
+
throw new FormatError('GS1 DataBar Expanded method 1 variable-length field is invalid');
|
|
791
|
+
}
|
|
792
|
+
const chunks = [bitsToValue(bits, 4, 4)];
|
|
793
|
+
for (let offset = 8; offset < 48; offset += 10) chunks.push(bitsToValue(bits, offset, 10));
|
|
794
|
+
if (chunks[0] > 9 || chunks.slice(1).some((value) => value > 999)) {
|
|
795
|
+
throw new FormatError('GS1 DataBar Expanded method 1 GTIN is invalid');
|
|
796
|
+
}
|
|
797
|
+
const gtinWithoutCheck = `${chunks[0]}${chunks.slice(1).map((value) => String(value).padStart(3, '0')).join('')}`;
|
|
798
|
+
const general = bits.length === 48 ? '' : decodeGeneralPurpose(bits.slice(48));
|
|
799
|
+
return `01${gtinWithoutCheck}${gtinCheckDigit(gtinWithoutCheck)}${general}`;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function decodeInformation(values, linkage, dataCharacters) {
|
|
803
|
+
const bits = [];
|
|
804
|
+
for (const value of values) appendBits(bits, value, 12);
|
|
805
|
+
if (bits.length < 5 || bits[0] !== linkage) {
|
|
806
|
+
throw new FormatError('GS1 DataBar Expanded information header is invalid');
|
|
807
|
+
}
|
|
808
|
+
let raw;
|
|
809
|
+
if (!bits[1] && !bits[2]) {
|
|
810
|
+
raw = decodeGeneralPurpose(bits.slice(5));
|
|
811
|
+
} else if (bits[1]) {
|
|
812
|
+
raw = decodeMethodOne(bits, dataCharacters);
|
|
813
|
+
} else {
|
|
814
|
+
throw new FormatError('GS1 DataBar Expanded information header is invalid');
|
|
815
|
+
}
|
|
816
|
+
const elements = decodeGS1ElementString(raw);
|
|
817
|
+
return Object.freeze({ raw, elements });
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/** Decode a clean or integer-scaled GS1 DataBar Expanded matrix. */
|
|
821
|
+
export function decodeDataBarExpanded(matrix) {
|
|
822
|
+
const geometry = logicalRow(matrix);
|
|
823
|
+
const decoded = decodePhysical(geometry.row, geometry.dataCharacters);
|
|
824
|
+
const linkage = (decoded.values[1] & 0x800) !== 0;
|
|
825
|
+
const information = decodeInformation(decoded.values.slice(1), linkage, decoded.dataCharacters);
|
|
826
|
+
return Object.freeze({
|
|
827
|
+
format: 'databar-expanded',
|
|
828
|
+
variant: 'expanded',
|
|
829
|
+
text: information.raw,
|
|
830
|
+
raw: information.raw,
|
|
831
|
+
gs1: true,
|
|
832
|
+
linkage,
|
|
833
|
+
checksum: decoded.values[0],
|
|
834
|
+
checksumValid: true,
|
|
835
|
+
dataCharacters: decoded.dataCharacters,
|
|
836
|
+
pairs: decoded.pairCount,
|
|
837
|
+
moduleScale: geometry.scale,
|
|
838
|
+
height: matrix.height,
|
|
839
|
+
symbologyIdentifier: ']e0',
|
|
840
|
+
elements: information.elements,
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function crop(source, box) {
|
|
845
|
+
const output = new BitMatrix(box.width, box.height);
|
|
846
|
+
for (let y = 0; y < box.height; y++) {
|
|
847
|
+
for (let x = 0; x < box.width; x++) if (source.get(box.x + x, box.y + y)) output.set(x, y);
|
|
848
|
+
}
|
|
849
|
+
return output;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function rotateClockwise(source) {
|
|
853
|
+
const output = new BitMatrix(source.height, source.width);
|
|
854
|
+
for (let y = 0; y < source.height; y++) {
|
|
855
|
+
for (let x = 0; x < source.width; x++) if (source.get(x, y)) output.set(source.height - 1 - y, x);
|
|
856
|
+
}
|
|
857
|
+
return output;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function mapPoint(point, previous) {
|
|
861
|
+
return { x: point.y, y: previous.height - point.x };
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function cornersFor(box) {
|
|
865
|
+
return [
|
|
866
|
+
{ x: box.x, y: box.y },
|
|
867
|
+
{ x: box.x + box.width, y: box.y },
|
|
868
|
+
{ x: box.x + box.width, y: box.y + box.height },
|
|
869
|
+
{ x: box.x, y: box.y + box.height },
|
|
870
|
+
];
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function canonicalCorners(points) {
|
|
874
|
+
const ordered = [...points].sort((left, right) => left.y - right.y || left.x - right.x);
|
|
875
|
+
const [topLeft, topRight] = ordered.slice(0, 2).sort((left, right) => left.x - right.x);
|
|
876
|
+
const [bottomLeft, bottomRight] = ordered.slice(2).sort((left, right) => left.x - right.x);
|
|
877
|
+
return [topLeft, topRight, bottomRight, bottomLeft];
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function validImage(image) {
|
|
881
|
+
return isRecord(image) && typeof image.get === 'function'
|
|
882
|
+
&& Number.isSafeInteger(image.width) && Number.isSafeInteger(image.height)
|
|
883
|
+
&& image.width > 0 && image.height > 0
|
|
884
|
+
&& image.width <= MAX_DIMENSION && image.height <= MAX_DIMENSION
|
|
885
|
+
&& image.width * image.height <= MAX_MODULES;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** Detect one complete, dark-on-light Expanded symbol in a clean raster. */
|
|
889
|
+
export function detectDataBarExpanded(binaryImage, options = {}) {
|
|
890
|
+
if (!validImage(binaryImage) || !isRecord(options)) return null;
|
|
891
|
+
let oriented = binaryImage;
|
|
892
|
+
let toOriginal = (point) => ({ x: point.x, y: point.y });
|
|
893
|
+
for (let rotation = 0; rotation < 4; rotation++) {
|
|
894
|
+
const bounds = oriented.getBounds?.();
|
|
895
|
+
if (bounds && bounds.width > 0 && bounds.height > 0) {
|
|
896
|
+
const candidate = crop(oriented, bounds);
|
|
897
|
+
try {
|
|
898
|
+
const result = decodeDataBarExpanded(candidate);
|
|
899
|
+
const geometry = createDetectionCandidate({
|
|
900
|
+
corners: canonicalCorners(cornersFor(bounds).map((point) => toOriginal(point))),
|
|
901
|
+
moduleSize: result.moduleScale,
|
|
902
|
+
rotation: rotation * 90,
|
|
903
|
+
matrix: candidate,
|
|
904
|
+
confidence: 1,
|
|
905
|
+
}, {
|
|
906
|
+
result,
|
|
907
|
+
quality: {
|
|
908
|
+
quietZone: bounds.x > 0 && bounds.y > 0
|
|
909
|
+
&& bounds.x + bounds.width < oriented.width
|
|
910
|
+
&& bounds.y + bounds.height < oriented.height,
|
|
911
|
+
checksum: true,
|
|
912
|
+
rows: 1,
|
|
913
|
+
consistency: 1,
|
|
914
|
+
},
|
|
915
|
+
score: 1,
|
|
916
|
+
});
|
|
917
|
+
return Object.freeze({ ...result, ...geometry });
|
|
918
|
+
} catch {
|
|
919
|
+
/* A complete dark component belonging to another symbol is ignored. */
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const previous = oriented;
|
|
923
|
+
const previousToOriginal = toOriginal;
|
|
924
|
+
oriented = rotateClockwise(previous);
|
|
925
|
+
toOriginal = (point) => previousToOriginal(mapPoint(point, previous));
|
|
926
|
+
}
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** Detect-and-decode alias matching the other DataBar modules. */
|
|
931
|
+
export const detectAndDecodeDataBarExpanded = detectDataBarExpanded;
|
|
932
|
+
|
|
933
|
+
/** Decode one complete, unscaled binary scanline. */
|
|
934
|
+
export function decodeDataBarExpandedScanline(row) {
|
|
935
|
+
if (!row || typeof row.length !== 'number' || row.length < 1) return null;
|
|
936
|
+
const matrix = new BitMatrix(row.length, MIN_HEIGHT_MODULES);
|
|
937
|
+
for (let x = 0; x < row.length; x++) if (row[x]) matrix.setRegion(x, 0, 1, MIN_HEIGHT_MODULES);
|
|
938
|
+
try {
|
|
939
|
+
return decodeDataBarExpanded(matrix);
|
|
940
|
+
} catch {
|
|
941
|
+
return null;
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
export const encodeGS1DataBarExpanded = encodeDataBarExpanded;
|
|
946
|
+
export const decodeGS1DataBarExpanded = decodeDataBarExpanded;
|
|
947
|
+
export const detectGS1DataBarExpanded = detectDataBarExpanded;
|
|
948
|
+
export const detectAndDecodeGS1DataBarExpanded = detectAndDecodeDataBarExpanded;
|