@sythos/js_barcode_universal 1.5.13 → 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 +416 -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 +62 -6
- package/package.json +47 -3
- 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,528 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Sythos
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*
|
|
26
|
+
* SPDX-License-Identifier: MIT
|
|
27
|
+
*
|
|
28
|
+
* Original work. No code from any other barcode implementation.
|
|
29
|
+
*/
|
|
30
|
+
/** GS1 DataBar Stacked encoder, decoder and clean-raster detector. @module databar/stacked */
|
|
31
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
32
|
+
import { ChecksumError, EncodeError, FormatError } from '../core/errors.js';
|
|
33
|
+
import { decodeDataBar14GTIN, encodeDataBar14GTIN, normalizeGTIN } from './codec.js';
|
|
34
|
+
import { DATABAR14_CHECKSUM_WEIGHTS, DATABAR14_FINDERS, dataBar14CharacterWidths, dataBar14ValueForWidths, } from './patterns.js';
|
|
35
|
+
const STACKED_MODULES = 50;
|
|
36
|
+
const TOP_ROW_MINIMUM = 5;
|
|
37
|
+
const BOTTOM_ROW_MINIMUM = 7;
|
|
38
|
+
const SEPARATOR_MINIMUM = 1;
|
|
39
|
+
const HALF_ELEMENT_COUNT = 23;
|
|
40
|
+
const MAX_SYMBOL_DIMENSION = 32768;
|
|
41
|
+
function isRecord(value) {
|
|
42
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function positiveInteger(value, label) {
|
|
45
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
46
|
+
throw new EncodeError(`GS1 DataBar Stacked ${label} must be a positive integer`);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
function resolveGeometry(options) {
|
|
51
|
+
if (options === undefined)
|
|
52
|
+
options = {};
|
|
53
|
+
if (!isRecord(options))
|
|
54
|
+
throw new TypeError('GS1 DataBar Stacked options must be an object');
|
|
55
|
+
if (options.linkage !== undefined && typeof options.linkage !== 'boolean') {
|
|
56
|
+
throw new TypeError('GS1 DataBar linkage must be a boolean');
|
|
57
|
+
}
|
|
58
|
+
const hasHeight = options.height !== undefined;
|
|
59
|
+
const hasRows = options.topHeight !== undefined
|
|
60
|
+
|| options.bottomHeight !== undefined
|
|
61
|
+
|| options.separatorHeight !== undefined;
|
|
62
|
+
const hasScale = options.moduleScale !== undefined || options.scale !== undefined;
|
|
63
|
+
if (hasHeight && (hasRows || hasScale)) {
|
|
64
|
+
throw new EncodeError('GS1 DataBar Stacked height cannot be combined with row heights or moduleScale');
|
|
65
|
+
}
|
|
66
|
+
if (hasHeight) {
|
|
67
|
+
const height = positiveInteger(options.height, 'height');
|
|
68
|
+
if (height % 13 !== 0) {
|
|
69
|
+
throw new EncodeError('GS1 DataBar Stacked height must be a multiple of the 13-module profile');
|
|
70
|
+
}
|
|
71
|
+
const scale = height / 13;
|
|
72
|
+
return boundedGeometry(Object.freeze({
|
|
73
|
+
moduleScale: scale,
|
|
74
|
+
topHeight: TOP_ROW_MINIMUM * scale,
|
|
75
|
+
separatorHeight: SEPARATOR_MINIMUM * scale,
|
|
76
|
+
bottomHeight: BOTTOM_ROW_MINIMUM * scale,
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
79
|
+
if (options.moduleScale !== undefined && options.scale !== undefined
|
|
80
|
+
&& options.moduleScale !== options.scale) {
|
|
81
|
+
throw new EncodeError('GS1 DataBar Stacked moduleScale and scale disagree');
|
|
82
|
+
}
|
|
83
|
+
const scale = positiveInteger(options.moduleScale ?? options.scale ?? 1, 'moduleScale');
|
|
84
|
+
const requestedTopHeight = positiveInteger(options.topHeight ?? TOP_ROW_MINIMUM, 'topHeight');
|
|
85
|
+
const requestedSeparatorHeight = positiveInteger(options.separatorHeight ?? SEPARATOR_MINIMUM, 'separatorHeight');
|
|
86
|
+
const requestedBottomHeight = positiveInteger(options.bottomHeight ?? BOTTOM_ROW_MINIMUM, 'bottomHeight');
|
|
87
|
+
if (hasScale && hasRows) {
|
|
88
|
+
throw new EncodeError('GS1 DataBar Stacked explicit row heights cannot be combined with moduleScale');
|
|
89
|
+
}
|
|
90
|
+
const topHeight = hasRows ? requestedTopHeight : requestedTopHeight * scale;
|
|
91
|
+
const separatorHeight = hasRows ? requestedSeparatorHeight : requestedSeparatorHeight * scale;
|
|
92
|
+
const bottomHeight = hasRows ? requestedBottomHeight : requestedBottomHeight * scale;
|
|
93
|
+
if (topHeight < TOP_ROW_MINIMUM * scale
|
|
94
|
+
|| separatorHeight < SEPARATOR_MINIMUM * scale
|
|
95
|
+
|| bottomHeight < BOTTOM_ROW_MINIMUM * scale) {
|
|
96
|
+
throw new EncodeError('GS1 DataBar Stacked row heights are below their normative minima');
|
|
97
|
+
}
|
|
98
|
+
return boundedGeometry(Object.freeze({ moduleScale: scale, topHeight, separatorHeight, bottomHeight }));
|
|
99
|
+
}
|
|
100
|
+
function boundedGeometry(geometry) {
|
|
101
|
+
const width = STACKED_MODULES * geometry.moduleScale;
|
|
102
|
+
const height = geometry.topHeight + geometry.separatorHeight + geometry.bottomHeight;
|
|
103
|
+
if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height)
|
|
104
|
+
|| width > MAX_SYMBOL_DIMENSION || height > MAX_SYMBOL_DIMENSION) {
|
|
105
|
+
throw new EncodeError('GS1 DataBar Stacked dimensions exceed the safe matrix limit');
|
|
106
|
+
}
|
|
107
|
+
return geometry;
|
|
108
|
+
}
|
|
109
|
+
function strictGTIN14(value) {
|
|
110
|
+
const text = String(value);
|
|
111
|
+
if (!/^\d+$/.test(text))
|
|
112
|
+
throw new TypeError('GTIN must contain only decimal digits');
|
|
113
|
+
if (text.length !== 14) {
|
|
114
|
+
throw new RangeError('GS1 DataBar Stacked requires a 14-digit GTIN including its check digit');
|
|
115
|
+
}
|
|
116
|
+
return normalizeGTIN(text);
|
|
117
|
+
}
|
|
118
|
+
function finderIndexes(characters) {
|
|
119
|
+
const widths = characters.map((value, index) => dataBar14CharacterWidths(value, index === 0 || index === 3 ? 'outside' : 'inside'));
|
|
120
|
+
let checksum = 0;
|
|
121
|
+
const weightOffsets = [0, 8, 24, 16];
|
|
122
|
+
for (let character = 0; character < 4; character++) {
|
|
123
|
+
for (let element = 0; element < 8; element++) {
|
|
124
|
+
checksum += widths[character][element]
|
|
125
|
+
* DATABAR14_CHECKSUM_WEIGHTS[weightOffsets[character] + element];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
checksum %= 79;
|
|
129
|
+
if (checksum >= 8)
|
|
130
|
+
checksum++;
|
|
131
|
+
if (checksum >= 72)
|
|
132
|
+
checksum++;
|
|
133
|
+
return { widths, checksum, left: Math.floor(checksum / 9), right: checksum % 9 };
|
|
134
|
+
}
|
|
135
|
+
function linearWidths(compacted) {
|
|
136
|
+
const check = finderIndexes(compacted.physicalCharacters);
|
|
137
|
+
const widths = [1, 1];
|
|
138
|
+
widths.push(...check.widths[0]);
|
|
139
|
+
widths.push(...DATABAR14_FINDERS[check.left]);
|
|
140
|
+
widths.push(...check.widths[1].slice().reverse());
|
|
141
|
+
widths.push(...check.widths[2]);
|
|
142
|
+
widths.push(...DATABAR14_FINDERS[check.right].slice().reverse());
|
|
143
|
+
widths.push(...check.widths[3].slice().reverse());
|
|
144
|
+
widths.push(1, 1);
|
|
145
|
+
if (widths.length !== 46 || widths.reduce((sum, width) => sum + width, 0) !== 96) {
|
|
146
|
+
throw new EncodeError('GS1 DataBar Stacked internal element geometry is invalid');
|
|
147
|
+
}
|
|
148
|
+
return { widths, checksum: check.checksum, left: check.left, right: check.right };
|
|
149
|
+
}
|
|
150
|
+
function writeRuns(row, start, dark, widths) {
|
|
151
|
+
let cursor = start;
|
|
152
|
+
let colour = dark;
|
|
153
|
+
for (const width of widths) {
|
|
154
|
+
if (!Number.isInteger(width) || width < 1 || cursor + width > row.length) {
|
|
155
|
+
throw new EncodeError('GS1 DataBar Stacked row geometry exceeds 50 modules');
|
|
156
|
+
}
|
|
157
|
+
if (colour) {
|
|
158
|
+
for (let x = cursor; x < cursor + width; x++)
|
|
159
|
+
row[x] = true;
|
|
160
|
+
}
|
|
161
|
+
cursor += width;
|
|
162
|
+
colour = !colour;
|
|
163
|
+
}
|
|
164
|
+
return { cursor, dark: colour };
|
|
165
|
+
}
|
|
166
|
+
function rowBits(widths, half) {
|
|
167
|
+
const row = new Array(STACKED_MODULES).fill(false);
|
|
168
|
+
if (half === 'top') {
|
|
169
|
+
const written = writeRuns(row, 0, false, widths.slice(0, HALF_ELEMENT_COUNT));
|
|
170
|
+
if (written.cursor !== 48)
|
|
171
|
+
throw new EncodeError('GS1 DataBar Stacked top row is not 50 modules wide');
|
|
172
|
+
row[48] = true;
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
row[0] = true;
|
|
176
|
+
const written = writeRuns(row, 2, true, widths.slice(HALF_ELEMENT_COUNT));
|
|
177
|
+
if (written.cursor !== STACKED_MODULES)
|
|
178
|
+
throw new EncodeError('GS1 DataBar Stacked bottom row is not 50 modules wide');
|
|
179
|
+
}
|
|
180
|
+
return row;
|
|
181
|
+
}
|
|
182
|
+
function separatorBits(top, bottom) {
|
|
183
|
+
const separator = new Array(STACKED_MODULES).fill(false);
|
|
184
|
+
for (let x = 1; x < 46; x++) {
|
|
185
|
+
if (top[x] === bottom[x]) {
|
|
186
|
+
if (!top[x])
|
|
187
|
+
separator[x] = true;
|
|
188
|
+
}
|
|
189
|
+
else if (!separator[x - 1]) {
|
|
190
|
+
separator[x] = true;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// The four modules at both outside edges remain quiet in the separator.
|
|
194
|
+
for (let x = 0; x < 4; x++)
|
|
195
|
+
separator[x] = false;
|
|
196
|
+
for (let x = 46; x < STACKED_MODULES; x++)
|
|
197
|
+
separator[x] = false;
|
|
198
|
+
return separator;
|
|
199
|
+
}
|
|
200
|
+
function paintRow(matrix, y, height, bits, scale) {
|
|
201
|
+
for (let x = 0; x < bits.length; x++) {
|
|
202
|
+
if (bits[x])
|
|
203
|
+
matrix.setRegion(x * scale, y, scale, height);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Encode a checked 14-digit GTIN as GS1 DataBar Stacked.
|
|
208
|
+
*
|
|
209
|
+
* The default matrix is 50 by 13 modules: a five-module top row, one-module
|
|
210
|
+
* separator, and seven-module bottom row. `moduleScale` (or `scale`) produces
|
|
211
|
+
* an integer nearest-neighbour raster. Explicit row heights are accepted when
|
|
212
|
+
* they satisfy the same minima and are already expressed in output modules.
|
|
213
|
+
*/
|
|
214
|
+
export function encodeDataBar14Stacked(value, options = {}) {
|
|
215
|
+
const gtin = strictGTIN14(value);
|
|
216
|
+
const geometry = resolveGeometry(options);
|
|
217
|
+
const compacted = encodeDataBar14GTIN(gtin, { linkage: options.linkage });
|
|
218
|
+
const pattern = linearWidths(compacted);
|
|
219
|
+
const top = rowBits(pattern.widths, 'top');
|
|
220
|
+
const bottom = rowBits(pattern.widths, 'bottom');
|
|
221
|
+
const separator = separatorBits(top, bottom);
|
|
222
|
+
const width = STACKED_MODULES * geometry.moduleScale;
|
|
223
|
+
const matrix = new BitMatrix(width, geometry.topHeight + geometry.separatorHeight + geometry.bottomHeight);
|
|
224
|
+
paintRow(matrix, 0, geometry.topHeight, top, geometry.moduleScale);
|
|
225
|
+
paintRow(matrix, geometry.topHeight + geometry.separatorHeight, geometry.bottomHeight, bottom, geometry.moduleScale);
|
|
226
|
+
paintRow(matrix, geometry.topHeight, geometry.separatorHeight, separator, geometry.moduleScale);
|
|
227
|
+
matrix.databar = Object.freeze({
|
|
228
|
+
variant: 'stacked',
|
|
229
|
+
gtin: compacted.gtin,
|
|
230
|
+
linkage: compacted.linkage,
|
|
231
|
+
checksum: pattern.checksum,
|
|
232
|
+
rows: 2,
|
|
233
|
+
modulesPerRow: STACKED_MODULES,
|
|
234
|
+
moduleScale: geometry.moduleScale,
|
|
235
|
+
topHeight: geometry.topHeight,
|
|
236
|
+
separatorHeight: geometry.separatorHeight,
|
|
237
|
+
bottomHeight: geometry.bottomHeight,
|
|
238
|
+
});
|
|
239
|
+
return matrix;
|
|
240
|
+
}
|
|
241
|
+
function requireMatrix(matrix) {
|
|
242
|
+
if (!matrix || !Number.isInteger(matrix.width) || !Number.isInteger(matrix.height)
|
|
243
|
+
|| matrix.width < 1 || matrix.height < 1 || typeof matrix.get !== 'function') {
|
|
244
|
+
throw new TypeError('GS1 DataBar Stacked decoder expects a BitMatrix-like value');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function logicalRows(matrix) {
|
|
248
|
+
requireMatrix(matrix);
|
|
249
|
+
if (matrix.width % STACKED_MODULES !== 0) {
|
|
250
|
+
throw new FormatError('GS1 DataBar Stacked matrix width must be a positive multiple of 50 modules');
|
|
251
|
+
}
|
|
252
|
+
const scale = matrix.width / STACKED_MODULES;
|
|
253
|
+
if (!Number.isSafeInteger(scale) || scale < 1) {
|
|
254
|
+
throw new FormatError('GS1 DataBar Stacked horizontal module scale is invalid');
|
|
255
|
+
}
|
|
256
|
+
const rows = [];
|
|
257
|
+
for (let y = 0; y < matrix.height; y++) {
|
|
258
|
+
const bits = new Array(STACKED_MODULES);
|
|
259
|
+
for (let x = 0; x < STACKED_MODULES; x++) {
|
|
260
|
+
const value = Boolean(matrix.get(x * scale + Math.floor(scale / 2), y));
|
|
261
|
+
// A clean integer-scaled raster must not contain a transition inside a
|
|
262
|
+
// horizontal module. This rejects silently aliased or partial symbols.
|
|
263
|
+
for (let dx = 0; dx < scale; dx++) {
|
|
264
|
+
if (Boolean(matrix.get(x * scale + dx, y)) !== value) {
|
|
265
|
+
throw new FormatError('GS1 DataBar Stacked horizontal modules are not integer-scaled');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
bits[x] = value;
|
|
269
|
+
}
|
|
270
|
+
rows.push({ bits, scale });
|
|
271
|
+
}
|
|
272
|
+
return rows;
|
|
273
|
+
}
|
|
274
|
+
function rowKind(bits) {
|
|
275
|
+
if (!bits[0] && bits[1])
|
|
276
|
+
return 'top';
|
|
277
|
+
if (bits[0] && !bits[1])
|
|
278
|
+
return 'bottom';
|
|
279
|
+
if (!bits[0] && !bits[1] && !bits[2] && !bits[3])
|
|
280
|
+
return 'separator';
|
|
281
|
+
return 'invalid';
|
|
282
|
+
}
|
|
283
|
+
function equalBits(left, right) {
|
|
284
|
+
for (let x = 0; x < STACKED_MODULES; x++)
|
|
285
|
+
if (left[x] !== right[x])
|
|
286
|
+
return false;
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
function runs(bits) {
|
|
290
|
+
const result = [];
|
|
291
|
+
let dark = bits[0];
|
|
292
|
+
let width = 0;
|
|
293
|
+
for (const bit of bits) {
|
|
294
|
+
if (bit === dark)
|
|
295
|
+
width++;
|
|
296
|
+
else {
|
|
297
|
+
result.push({ dark, width });
|
|
298
|
+
dark = bit;
|
|
299
|
+
width = 1;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
result.push({ dark, width });
|
|
303
|
+
return result;
|
|
304
|
+
}
|
|
305
|
+
function extractWidths(rows) {
|
|
306
|
+
if (!rows.length)
|
|
307
|
+
throw new FormatError('GS1 DataBar Stacked has no rows');
|
|
308
|
+
const scale = rows[0].scale;
|
|
309
|
+
const kinds = rows.map(({ bits }) => rowKind(bits));
|
|
310
|
+
if (kinds.some((kind) => kind === 'invalid')) {
|
|
311
|
+
throw new FormatError('GS1 DataBar Stacked row guard pattern is invalid');
|
|
312
|
+
}
|
|
313
|
+
let topEnd = 0;
|
|
314
|
+
while (topEnd < kinds.length && kinds[topEnd] === 'top')
|
|
315
|
+
topEnd++;
|
|
316
|
+
let separatorEnd = topEnd;
|
|
317
|
+
while (separatorEnd < kinds.length && kinds[separatorEnd] === 'separator')
|
|
318
|
+
separatorEnd++;
|
|
319
|
+
if (topEnd < 1 || separatorEnd === topEnd || separatorEnd >= kinds.length) {
|
|
320
|
+
throw new FormatError('GS1 DataBar Stacked requires top, separator and bottom rows');
|
|
321
|
+
}
|
|
322
|
+
if (kinds.slice(separatorEnd).some((kind) => kind !== 'bottom')) {
|
|
323
|
+
throw new FormatError('GS1 DataBar Stacked rows are out of order');
|
|
324
|
+
}
|
|
325
|
+
const topHeight = topEnd;
|
|
326
|
+
const separatorHeight = separatorEnd - topEnd;
|
|
327
|
+
const bottomHeight = kinds.length - separatorEnd;
|
|
328
|
+
if (topHeight < TOP_ROW_MINIMUM * scale
|
|
329
|
+
|| separatorHeight < SEPARATOR_MINIMUM * scale
|
|
330
|
+
|| bottomHeight < BOTTOM_ROW_MINIMUM * scale) {
|
|
331
|
+
throw new FormatError('GS1 DataBar Stacked row heights are below their normative minima');
|
|
332
|
+
}
|
|
333
|
+
const top = rows[0].bits;
|
|
334
|
+
const bottom = rows[separatorEnd].bits;
|
|
335
|
+
for (let y = 1; y < topEnd; y++) {
|
|
336
|
+
if (!equalBits(top, rows[y].bits))
|
|
337
|
+
throw new FormatError('GS1 DataBar Stacked top row is inconsistent');
|
|
338
|
+
}
|
|
339
|
+
for (let y = topEnd; y < separatorEnd; y++) {
|
|
340
|
+
if (!equalBits(rows[topEnd].bits, rows[y].bits)) {
|
|
341
|
+
throw new FormatError('GS1 DataBar Stacked separator rows are inconsistent');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (let y = separatorEnd + 1; y < rows.length; y++) {
|
|
345
|
+
if (!equalBits(bottom, rows[y].bits))
|
|
346
|
+
throw new FormatError('GS1 DataBar Stacked bottom row is inconsistent');
|
|
347
|
+
}
|
|
348
|
+
const expectedSeparator = separatorBits(top, bottom);
|
|
349
|
+
if (!equalBits(expectedSeparator, rows[topEnd].bits)) {
|
|
350
|
+
throw new FormatError('GS1 DataBar Stacked separator pattern is invalid');
|
|
351
|
+
}
|
|
352
|
+
const topRuns = runs(top);
|
|
353
|
+
const bottomRuns = runs(bottom);
|
|
354
|
+
if (topRuns.length !== 25 || bottomRuns.length !== 25) {
|
|
355
|
+
throw new FormatError('GS1 DataBar Stacked element count is invalid');
|
|
356
|
+
}
|
|
357
|
+
if (topRuns[0].dark || topRuns[0].width !== 1
|
|
358
|
+
|| !topRuns[23].dark || topRuns[23].width !== 1
|
|
359
|
+
|| topRuns[24].dark || topRuns[24].width !== 1) {
|
|
360
|
+
throw new FormatError('GS1 DataBar Stacked top guard pattern is invalid');
|
|
361
|
+
}
|
|
362
|
+
if (!bottomRuns[0].dark || bottomRuns[0].width !== 1
|
|
363
|
+
|| bottomRuns[1].dark || bottomRuns[1].width !== 1
|
|
364
|
+
|| !bottomRuns[24].dark || bottomRuns[24].width !== 1) {
|
|
365
|
+
throw new FormatError('GS1 DataBar Stacked bottom guard pattern is invalid');
|
|
366
|
+
}
|
|
367
|
+
const widths = [
|
|
368
|
+
...topRuns.slice(0, HALF_ELEMENT_COUNT).map((run) => run.width),
|
|
369
|
+
...bottomRuns.slice(2).map((run) => run.width),
|
|
370
|
+
];
|
|
371
|
+
if (widths.length !== 46 || widths.reduce((sum, width) => sum + width, 0) !== 96) {
|
|
372
|
+
throw new FormatError('GS1 DataBar Stacked linear element geometry is invalid');
|
|
373
|
+
}
|
|
374
|
+
return { widths, scale, topHeight, separatorHeight, bottomHeight };
|
|
375
|
+
}
|
|
376
|
+
function finderIndex(widths) {
|
|
377
|
+
const key = widths.join(',');
|
|
378
|
+
return DATABAR14_FINDERS.findIndex((candidate) => candidate.join(',') === key);
|
|
379
|
+
}
|
|
380
|
+
function expectedChecksum(widths) {
|
|
381
|
+
const offsets = [0, 8, 24, 16];
|
|
382
|
+
let checksum = 0;
|
|
383
|
+
for (let character = 0; character < 4; character++) {
|
|
384
|
+
for (let element = 0; element < 8; element++) {
|
|
385
|
+
checksum += widths[character][element]
|
|
386
|
+
* DATABAR14_CHECKSUM_WEIGHTS[offsets[character] + element];
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
checksum %= 79;
|
|
390
|
+
if (checksum >= 8)
|
|
391
|
+
checksum++;
|
|
392
|
+
if (checksum >= 72)
|
|
393
|
+
checksum++;
|
|
394
|
+
return checksum;
|
|
395
|
+
}
|
|
396
|
+
/** Decode a clean or integer-scaled GS1 DataBar Stacked matrix. */
|
|
397
|
+
export function decodeDataBar14Stacked(matrix) {
|
|
398
|
+
const geometry = extractWidths(logicalRows(matrix));
|
|
399
|
+
const widths = geometry.widths;
|
|
400
|
+
const characters = [
|
|
401
|
+
widths.slice(2, 10),
|
|
402
|
+
widths.slice(15, 23).slice().reverse(),
|
|
403
|
+
widths.slice(23, 31),
|
|
404
|
+
widths.slice(36, 44).slice().reverse(),
|
|
405
|
+
];
|
|
406
|
+
const leftFinder = finderIndex(widths.slice(10, 15));
|
|
407
|
+
const rightFinder = finderIndex(widths.slice(31, 36).slice().reverse());
|
|
408
|
+
if (leftFinder < 0 || rightFinder < 0) {
|
|
409
|
+
throw new FormatError('GS1 DataBar Stacked finder pattern is invalid');
|
|
410
|
+
}
|
|
411
|
+
const encodedChecksum = leftFinder * 9 + rightFinder;
|
|
412
|
+
if (encodedChecksum !== expectedChecksum(characters)) {
|
|
413
|
+
throw new ChecksumError('GS1 DataBar Stacked checksum mismatch');
|
|
414
|
+
}
|
|
415
|
+
let decoded;
|
|
416
|
+
try {
|
|
417
|
+
decoded = decodeDataBar14GTIN({
|
|
418
|
+
outerLeft: dataBar14ValueForWidths(characters[0], 'outside'),
|
|
419
|
+
innerLeft: dataBar14ValueForWidths(characters[1], 'inside'),
|
|
420
|
+
outerRight: dataBar14ValueForWidths(characters[3], 'outside'),
|
|
421
|
+
innerRight: dataBar14ValueForWidths(characters[2], 'inside'),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch (error) {
|
|
425
|
+
throw new FormatError(`GS1 DataBar Stacked character widths are invalid: ${error.message}`);
|
|
426
|
+
}
|
|
427
|
+
return Object.freeze({
|
|
428
|
+
format: 'databar-stacked',
|
|
429
|
+
variant: 'stacked',
|
|
430
|
+
text: decoded.gtin,
|
|
431
|
+
gtin: decoded.gtin,
|
|
432
|
+
gs1: true,
|
|
433
|
+
linkage: decoded.linkage,
|
|
434
|
+
checksum: encodedChecksum,
|
|
435
|
+
rows: 2,
|
|
436
|
+
topHeight: geometry.topHeight,
|
|
437
|
+
separatorHeight: geometry.separatorHeight,
|
|
438
|
+
bottomHeight: geometry.bottomHeight,
|
|
439
|
+
moduleScale: geometry.scale,
|
|
440
|
+
symbologyIdentifier: ']e0',
|
|
441
|
+
elements: [{ ai: '01', value: decoded.gtin, fixed: true }],
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
function rotateClockwise(source) {
|
|
445
|
+
const out = new BitMatrix(source.height, source.width);
|
|
446
|
+
for (let y = 0; y < source.height; y++) {
|
|
447
|
+
for (let x = 0; x < source.width; x++) {
|
|
448
|
+
if (source.get(x, y))
|
|
449
|
+
out.set(source.height - 1 - y, x);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return out;
|
|
453
|
+
}
|
|
454
|
+
function crop(source, box) {
|
|
455
|
+
const out = new BitMatrix(box.width, box.height);
|
|
456
|
+
for (let y = 0; y < box.height; y++) {
|
|
457
|
+
for (let x = 0; x < box.width; x++) {
|
|
458
|
+
if (source.get(box.x + x, box.y + y))
|
|
459
|
+
out.set(x, y);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
function cornersFor(box) {
|
|
465
|
+
return [
|
|
466
|
+
{ x: box.x, y: box.y },
|
|
467
|
+
{ x: box.x + box.width, y: box.y },
|
|
468
|
+
{ x: box.x + box.width, y: box.y + box.height },
|
|
469
|
+
{ x: box.x, y: box.y + box.height },
|
|
470
|
+
];
|
|
471
|
+
}
|
|
472
|
+
function mapRotationPoint(point, source) {
|
|
473
|
+
return { x: point.y, y: source.height - point.x };
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Detect one clean, axis-aligned or quarter-turned DataBar Stacked symbol.
|
|
477
|
+
* The input may contain a light quiet zone and an integer nearest-neighbour
|
|
478
|
+
* scale. Arbitrary perspective and grayscale sampling are outside this API.
|
|
479
|
+
*/
|
|
480
|
+
export function detectDataBar14Stacked(binaryImage) {
|
|
481
|
+
if (!binaryImage?.width || !binaryImage?.height || typeof binaryImage.get !== 'function')
|
|
482
|
+
return null;
|
|
483
|
+
let oriented = binaryImage;
|
|
484
|
+
let toOriginal = (point) => ({ x: point.x, y: point.y });
|
|
485
|
+
for (let rotation = 0; rotation < 4; rotation++) {
|
|
486
|
+
const bounds = oriented.getBounds?.();
|
|
487
|
+
if (bounds && Number.isInteger(bounds.width) && Number.isInteger(bounds.height)) {
|
|
488
|
+
const scale = bounds.width / STACKED_MODULES;
|
|
489
|
+
if (Number.isInteger(scale) && scale >= 1) {
|
|
490
|
+
const candidate = crop(oriented, bounds);
|
|
491
|
+
try {
|
|
492
|
+
const result = decodeDataBar14Stacked(candidate);
|
|
493
|
+
const corners = cornersFor(bounds).map(toOriginal);
|
|
494
|
+
return {
|
|
495
|
+
...result,
|
|
496
|
+
result,
|
|
497
|
+
matrix: candidate,
|
|
498
|
+
corners,
|
|
499
|
+
rotation: rotation * 90,
|
|
500
|
+
moduleSize: scale,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// The dark bounds may belong to another one-dimensional symbol.
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const previous = oriented;
|
|
509
|
+
const previousToOriginal = toOriginal;
|
|
510
|
+
oriented = rotateClockwise(previous);
|
|
511
|
+
toOriginal = (point) => previousToOriginal(mapRotationPoint(point, previous));
|
|
512
|
+
}
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
/** Detect and decode one clean GS1 DataBar Stacked symbol, or return null. */
|
|
516
|
+
export function detectAndDecodeDataBar14Stacked(binaryImage) {
|
|
517
|
+
return detectDataBar14Stacked(binaryImage);
|
|
518
|
+
}
|
|
519
|
+
// Descriptive aliases keep the variant discoverable without changing the
|
|
520
|
+
// existing DataBar-14 API surface or its index module.
|
|
521
|
+
export const encodeDataBarStacked = encodeDataBar14Stacked;
|
|
522
|
+
export const decodeDataBarStacked = decodeDataBar14Stacked;
|
|
523
|
+
export const detectDataBarStacked = detectDataBar14Stacked;
|
|
524
|
+
export const detectAndDecodeDataBarStacked = detectAndDecodeDataBar14Stacked;
|
|
525
|
+
export const encodeGS1DataBarStacked = encodeDataBar14Stacked;
|
|
526
|
+
export const decodeGS1DataBarStacked = decodeDataBar14Stacked;
|
|
527
|
+
export const detectGS1DataBarStacked = detectDataBar14Stacked;
|
|
528
|
+
export const detectAndDecodeGS1DataBarStacked = detectAndDecodeDataBar14Stacked;
|