@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,198 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Sythos Barcode Suite — Codablock-F reader
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
* Copyright (c) 2026 Sythos
|
|
6
|
+
* SPDX-License-Identifier: MIT
|
|
7
|
+
*
|
|
8
|
+
* Original work. No code from any other barcode implementation.
|
|
9
|
+
*/
|
|
10
|
+
import { FormatError } from '../core/errors.js';
|
|
11
|
+
import { code128Checksum, code128CodewordsModules, code128ValueFromModules, decodeCode128Values, sampleModules, scanStackedRows, } from '../stacked128/common.js';
|
|
12
|
+
import { codablockFChecks } from './encoder.js';
|
|
13
|
+
const START_A = 103;
|
|
14
|
+
const CODE_B = 100;
|
|
15
|
+
const STOP = 106;
|
|
16
|
+
const PAD = 103;
|
|
17
|
+
const MIN_COLUMNS = 4;
|
|
18
|
+
const MAX_COLUMNS = 62;
|
|
19
|
+
function parseRow(row, start, moduleSize, columns) {
|
|
20
|
+
const count = columns + 4;
|
|
21
|
+
const moduleCount = 11 * count + 13;
|
|
22
|
+
const modules = sampleModules(row, start, moduleSize, moduleCount);
|
|
23
|
+
if (!modules)
|
|
24
|
+
return null;
|
|
25
|
+
let offset = 0;
|
|
26
|
+
const startValue = code128ValueFromModules(modules, offset);
|
|
27
|
+
if (startValue !== START_A)
|
|
28
|
+
return null;
|
|
29
|
+
offset += 11;
|
|
30
|
+
const selector = code128ValueFromModules(modules, offset);
|
|
31
|
+
if (selector !== CODE_B)
|
|
32
|
+
return null;
|
|
33
|
+
offset += 11;
|
|
34
|
+
const indicator = code128ValueFromModules(modules, offset);
|
|
35
|
+
if (indicator === null || indicator < 1 || indicator > 44)
|
|
36
|
+
return null;
|
|
37
|
+
offset += 11;
|
|
38
|
+
const data = [];
|
|
39
|
+
for (let i = 0; i < columns; i++) {
|
|
40
|
+
const value = code128ValueFromModules(modules, offset);
|
|
41
|
+
if (value === null || value === STOP)
|
|
42
|
+
return null;
|
|
43
|
+
data.push(value);
|
|
44
|
+
offset += 11;
|
|
45
|
+
}
|
|
46
|
+
const rowCheck = code128ValueFromModules(modules, offset);
|
|
47
|
+
if (rowCheck === null || rowCheck >= 103)
|
|
48
|
+
return null;
|
|
49
|
+
const withoutRowCheck = [START_A, CODE_B, indicator, ...data];
|
|
50
|
+
if (code128Checksum(withoutRowCheck) !== rowCheck)
|
|
51
|
+
return null;
|
|
52
|
+
offset += 11;
|
|
53
|
+
const stop = modules.slice(offset, offset + 13);
|
|
54
|
+
if (stop !== code128CodewordsModules([STOP]))
|
|
55
|
+
return null;
|
|
56
|
+
return { indicator, data };
|
|
57
|
+
}
|
|
58
|
+
function rowBits(matrix, y) {
|
|
59
|
+
let bits = '';
|
|
60
|
+
for (let x = 0; x < matrix.width; x++)
|
|
61
|
+
bits += matrix.get(x, y) ? '1' : '0';
|
|
62
|
+
return bits;
|
|
63
|
+
}
|
|
64
|
+
function allDark(matrix, y) {
|
|
65
|
+
if (y < 0 || y >= matrix.height)
|
|
66
|
+
return false;
|
|
67
|
+
for (let x = 0; x < matrix.width; x++)
|
|
68
|
+
if (!matrix.get(x, y))
|
|
69
|
+
return false;
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
function decodeRows(rowsByNumber, rows, columns, moduleSize) {
|
|
73
|
+
if (rowsByNumber.length !== rows || rows < 2 || rows > 44)
|
|
74
|
+
return null;
|
|
75
|
+
const stream = rowsByNumber.flatMap((entry) => entry.data);
|
|
76
|
+
if (stream.length < 2)
|
|
77
|
+
return null;
|
|
78
|
+
const checks = codablockFChecks(stream.slice(0, -2));
|
|
79
|
+
if (stream[stream.length - 2] !== checks[0] || stream[stream.length - 1] !== checks[1])
|
|
80
|
+
return null;
|
|
81
|
+
const body = stream.slice(0, -2);
|
|
82
|
+
while (body.length > 0 && body[body.length - 1] === PAD)
|
|
83
|
+
body.pop();
|
|
84
|
+
const decoded = decodeCode128Values(104, body);
|
|
85
|
+
if (!decoded)
|
|
86
|
+
return null;
|
|
87
|
+
return {
|
|
88
|
+
format: 'codablockf',
|
|
89
|
+
text: decoded.text,
|
|
90
|
+
rows,
|
|
91
|
+
columns,
|
|
92
|
+
moduleSize,
|
|
93
|
+
checksum: true,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/** Validate an encoder-shaped matrix row by row, including every row-height pixel. */
|
|
97
|
+
function decodeCanonical(matrix, metadata) {
|
|
98
|
+
const { rows, columns, rowHeight, separatorHeight } = metadata;
|
|
99
|
+
if (!Number.isInteger(rows) || rows < 2 || rows > 44 ||
|
|
100
|
+
!Number.isInteger(columns) || columns < MIN_COLUMNS || columns > MAX_COLUMNS)
|
|
101
|
+
return null;
|
|
102
|
+
const expectedWidth = 11 * (columns + 4) + 13;
|
|
103
|
+
const expectedHeight = 2 + rows * rowHeight + (rows - 1) * separatorHeight;
|
|
104
|
+
if (matrix.width !== expectedWidth || matrix.height !== expectedHeight)
|
|
105
|
+
return null;
|
|
106
|
+
if (!allDark(matrix, 0) || !allDark(matrix, matrix.height - 1))
|
|
107
|
+
return null;
|
|
108
|
+
const parsedRows = [];
|
|
109
|
+
for (let row = 0; row < rows; row++) {
|
|
110
|
+
const y = 1 + row * (rowHeight + separatorHeight);
|
|
111
|
+
const source = rowBits(matrix, y);
|
|
112
|
+
const parsed = parseRow(Uint8Array.from(source, (bit) => bit === '1' ? 1 : 0), 0, 1, columns);
|
|
113
|
+
if (!parsed || parsed.indicator !== (row === 0 ? rows : row))
|
|
114
|
+
return null;
|
|
115
|
+
for (let offset = 1; offset < rowHeight; offset++) {
|
|
116
|
+
if (rowBits(matrix, y + offset) !== source)
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
parsedRows.push(parsed);
|
|
120
|
+
if (row < rows - 1) {
|
|
121
|
+
for (let offset = 0; offset < separatorHeight; offset++) {
|
|
122
|
+
if (!allDark(matrix, y + rowHeight + offset))
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return decodeRows(parsedRows, rows, columns, 1);
|
|
128
|
+
}
|
|
129
|
+
function assemble(image) {
|
|
130
|
+
const minimumModules = 11 * (MIN_COLUMNS + 4) + 13;
|
|
131
|
+
const candidates = scanStackedRows(image, minimumModules, (row, start, moduleSize) => {
|
|
132
|
+
for (let columns = MIN_COLUMNS; columns <= MAX_COLUMNS; columns++) {
|
|
133
|
+
const parsed = parseRow(row, start, moduleSize, columns);
|
|
134
|
+
if (parsed)
|
|
135
|
+
return { columns, ...parsed };
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}, 2200);
|
|
139
|
+
const groups = new Map();
|
|
140
|
+
for (const candidate of candidates) {
|
|
141
|
+
const signature = `${candidate.x}:${candidate.moduleSize}`;
|
|
142
|
+
const group = groups.get(signature) ?? new Map();
|
|
143
|
+
const current = group.get(candidate.parsed.indicator);
|
|
144
|
+
// Rows are repeated vertically; keeping one exact candidate per indicator
|
|
145
|
+
// prevents rowHeight from multiplying the work without losing a symbol.
|
|
146
|
+
if (!current)
|
|
147
|
+
group.set(candidate.parsed.indicator, {
|
|
148
|
+
columns: candidate.parsed.columns,
|
|
149
|
+
parsed: candidate.parsed,
|
|
150
|
+
moduleSize: candidate.moduleSize,
|
|
151
|
+
});
|
|
152
|
+
groups.set(signature, group);
|
|
153
|
+
}
|
|
154
|
+
for (const group of groups.values()) {
|
|
155
|
+
for (const [rows, first] of group) {
|
|
156
|
+
if (rows < 2 || rows > 44)
|
|
157
|
+
continue;
|
|
158
|
+
const columns = first.parsed.columns;
|
|
159
|
+
const rowsByNumber = [];
|
|
160
|
+
let complete = true;
|
|
161
|
+
for (let row = 1; row < rows; row++) {
|
|
162
|
+
const found = group.get(row);
|
|
163
|
+
if (!found || found.columns !== columns) {
|
|
164
|
+
complete = false;
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
rowsByNumber.push(found.parsed);
|
|
168
|
+
}
|
|
169
|
+
if (!complete)
|
|
170
|
+
continue;
|
|
171
|
+
rowsByNumber.unshift(first.parsed);
|
|
172
|
+
const decoded = decodeRows(rowsByNumber, rows, columns, first.moduleSize);
|
|
173
|
+
if (decoded)
|
|
174
|
+
return decoded;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
/** Decode a Codablock-F matrix or throw when its structure is invalid. */
|
|
180
|
+
export function decodeCodablockF(matrix) {
|
|
181
|
+
const metadata = matrix.codablockf;
|
|
182
|
+
if (metadata) {
|
|
183
|
+
const canonical = decodeCanonical(matrix, metadata);
|
|
184
|
+
if (canonical)
|
|
185
|
+
return canonical;
|
|
186
|
+
throw new FormatError('Codablock-F: invalid rows, checks or Code 128 framing');
|
|
187
|
+
}
|
|
188
|
+
const result = assemble(matrix);
|
|
189
|
+
if (!result)
|
|
190
|
+
throw new FormatError('Codablock-F: invalid rows, checks or Code 128 framing');
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
/** Locate and decode one Codablock-F symbol in a binarized raster. */
|
|
194
|
+
export function detectCodablockF(image) {
|
|
195
|
+
return assemble(image);
|
|
196
|
+
}
|
|
197
|
+
/** Alias used by the generic image decoder. */
|
|
198
|
+
export const detectAndDecodeCodablockF = detectCodablockF;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Sythos Barcode Suite — Codablock-F writer
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
* Copyright (c) 2026 Sythos
|
|
6
|
+
* SPDX-License-Identifier: MIT
|
|
7
|
+
*
|
|
8
|
+
* Original work. No code from any other barcode implementation.
|
|
9
|
+
*/
|
|
10
|
+
import { EncodeError } from '../core/errors.js';
|
|
11
|
+
import { code128DataCodewords } from '../oned/writers.js';
|
|
12
|
+
import { code128Checksum, code128CodewordsModules, renderStackedRows, } from '../stacked128/common.js';
|
|
13
|
+
const START_A = 103;
|
|
14
|
+
const CODE_B = 100;
|
|
15
|
+
const PAD = 103;
|
|
16
|
+
const MIN_ROWS = 2;
|
|
17
|
+
const MAX_ROWS = 44;
|
|
18
|
+
const MIN_COLUMNS = 4;
|
|
19
|
+
const MAX_COLUMNS = 62;
|
|
20
|
+
/** Weighted modulo-86 K1/K2 checks used by the complete Codablock stream. */
|
|
21
|
+
export function codablockFChecks(values) {
|
|
22
|
+
let k1 = 0;
|
|
23
|
+
let k2 = 0;
|
|
24
|
+
for (let i = 0; i < values.length; i++) {
|
|
25
|
+
k1 = (k1 + (i + 1) * values[i]) % 86;
|
|
26
|
+
k2 = (k2 + (i + 2) * values[i]) % 86;
|
|
27
|
+
}
|
|
28
|
+
k2 = (k2 + k1) % 86;
|
|
29
|
+
return [k1, k2];
|
|
30
|
+
}
|
|
31
|
+
function validateDimensions(rows, columns) {
|
|
32
|
+
if (rows !== undefined && (!Number.isInteger(rows) || rows < MIN_ROWS || rows > MAX_ROWS)) {
|
|
33
|
+
throw new EncodeError(`Codablock-F: rows must be an integer in ${MIN_ROWS}..${MAX_ROWS}`);
|
|
34
|
+
}
|
|
35
|
+
if (columns !== undefined && (!Number.isInteger(columns) || columns < MIN_COLUMNS || columns > MAX_COLUMNS)) {
|
|
36
|
+
throw new EncodeError(`Codablock-F: columns must be an integer in ${MIN_COLUMNS}..${MAX_COLUMNS}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function chooseDimensions(length, rowsOption, columnsOption) {
|
|
40
|
+
validateDimensions(rowsOption, columnsOption);
|
|
41
|
+
const candidates = [];
|
|
42
|
+
for (let rows = rowsOption ?? MIN_ROWS; rows <= (rowsOption ?? MAX_ROWS); rows++) {
|
|
43
|
+
for (let columns = columnsOption ?? MIN_COLUMNS; columns <= (columnsOption ?? MAX_COLUMNS); columns++) {
|
|
44
|
+
if (rows * columns - 2 < length)
|
|
45
|
+
continue;
|
|
46
|
+
// Keep the default reasonably compact while favouring a readable width.
|
|
47
|
+
const ratio = (columns + 4) / rows;
|
|
48
|
+
const score = (rows * columns - 2 - length) * 10 + Math.abs(ratio - 2.5);
|
|
49
|
+
candidates.push({ rows, columns, score });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (candidates.length === 0) {
|
|
53
|
+
throw new EncodeError('Codablock-F: payload does not fit the requested rows and columns');
|
|
54
|
+
}
|
|
55
|
+
candidates.sort((a, b) => a.score - b.score || a.rows - b.rows || a.columns - b.columns);
|
|
56
|
+
return candidates[0];
|
|
57
|
+
}
|
|
58
|
+
/** Encode an ASCII payload as a conservative, standards-shaped Codablock-F symbol. */
|
|
59
|
+
export function encodeCodablockF(value, options = {}) {
|
|
60
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
61
|
+
throw new EncodeError('Codablock-F: payload must be a non-empty ASCII string');
|
|
62
|
+
}
|
|
63
|
+
const dataStream = code128DataCodewords(value, { startSet: 'B' });
|
|
64
|
+
const { rows, columns } = chooseDimensions(dataStream.values.length, options.rows, options.columns);
|
|
65
|
+
const rowHeight = options.rowHeight ?? 3;
|
|
66
|
+
const separatorHeight = options.separatorHeight ?? 1;
|
|
67
|
+
if (!Number.isInteger(rowHeight) || rowHeight < 1 || rowHeight > 128) {
|
|
68
|
+
throw new EncodeError('Codablock-F: rowHeight must be an integer in 1..128');
|
|
69
|
+
}
|
|
70
|
+
if (!Number.isInteger(separatorHeight) || separatorHeight < 1 || separatorHeight > 16) {
|
|
71
|
+
throw new EncodeError('Codablock-F: separatorHeight must be an integer in 1..16');
|
|
72
|
+
}
|
|
73
|
+
const slotCount = rows * columns;
|
|
74
|
+
const body = dataStream.values.slice();
|
|
75
|
+
while (body.length < slotCount - 2)
|
|
76
|
+
body.push(PAD);
|
|
77
|
+
const checks = codablockFChecks(body);
|
|
78
|
+
body.push(checks[0], checks[1]);
|
|
79
|
+
const rowModules = [];
|
|
80
|
+
for (let row = 0; row < rows; row++) {
|
|
81
|
+
const indicator = row === 0 ? rows : row;
|
|
82
|
+
const data = body.slice(row * columns, (row + 1) * columns);
|
|
83
|
+
const withoutRowCheck = [START_A, CODE_B, indicator, ...data];
|
|
84
|
+
const rowCheck = code128Checksum(withoutRowCheck);
|
|
85
|
+
const codewords = [...withoutRowCheck, rowCheck, 106];
|
|
86
|
+
rowModules.push(code128CodewordsModules(codewords));
|
|
87
|
+
}
|
|
88
|
+
const matrix = renderStackedRows(rowModules, rowHeight, separatorHeight);
|
|
89
|
+
matrix.codablockf = {
|
|
90
|
+
rows,
|
|
91
|
+
columns,
|
|
92
|
+
rowHeight,
|
|
93
|
+
separatorHeight,
|
|
94
|
+
checks,
|
|
95
|
+
};
|
|
96
|
+
return matrix;
|
|
97
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
* Copyright (c) 2026 Sythos
|
|
6
|
+
* SPDX-License-Identifier: MIT
|
|
7
|
+
*
|
|
8
|
+
* Original work. No code from any other barcode implementation.
|
|
9
|
+
*/
|
|
10
|
+
/** Code 16K decoder for sampled, axis-aligned module matrices. @module code16k/decoder */
|
|
11
|
+
import { FormatError } from '../core/errors.js';
|
|
12
|
+
import { CODE16K_CODEWORD_WIDTH, CODE16K_MAX_ROWS, CODE16K_MIN_ROWS, CODE16K_PAD, CODE16K_SYMBOLS_PER_ROW, code16kModeInfo, code16kStartModules, code16kStopModules, } from './tables.js';
|
|
13
|
+
import { code128ValueFromModules, } from '../stacked128/common.js';
|
|
14
|
+
function equalAt(source, offset, expected) {
|
|
15
|
+
return source.slice(offset, offset + expected.length) === expected;
|
|
16
|
+
}
|
|
17
|
+
/** Decode one complete 70-module Code 16K row. */
|
|
18
|
+
export function decodeCode16KRow(modules, expectedRow = undefined) {
|
|
19
|
+
if (typeof modules !== 'string' || modules.length !== 70)
|
|
20
|
+
return null;
|
|
21
|
+
const candidates = expectedRow === undefined
|
|
22
|
+
? Array.from({ length: CODE16K_MAX_ROWS }, (_, index) => index)
|
|
23
|
+
: [expectedRow];
|
|
24
|
+
for (const row of candidates) {
|
|
25
|
+
if (!Number.isInteger(row) || row < 0 || row >= CODE16K_MAX_ROWS)
|
|
26
|
+
continue;
|
|
27
|
+
if (!equalAt(modules, 0, code16kStartModules(row)))
|
|
28
|
+
continue;
|
|
29
|
+
if (modules[7] !== '1')
|
|
30
|
+
continue;
|
|
31
|
+
if (!equalAt(modules, 63, code16kStopModules(row)))
|
|
32
|
+
continue;
|
|
33
|
+
const values = [];
|
|
34
|
+
let valid = true;
|
|
35
|
+
for (let column = 0; column < CODE16K_SYMBOLS_PER_ROW; column++) {
|
|
36
|
+
const value = code128ValueFromModules(modules, 8 + column * CODE16K_CODEWORD_WIDTH);
|
|
37
|
+
if (value === null) {
|
|
38
|
+
valid = false;
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
values.push(value);
|
|
42
|
+
}
|
|
43
|
+
if (valid)
|
|
44
|
+
return { row, values };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function separatorIsDark(matrix, y) {
|
|
49
|
+
if (y < 0 || y >= matrix.height)
|
|
50
|
+
return false;
|
|
51
|
+
for (let x = 0; x < matrix.width; x++)
|
|
52
|
+
if (!matrix.get(x, y))
|
|
53
|
+
return false;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
function readRow(matrix, y) {
|
|
57
|
+
if (y < 0 || y >= matrix.height)
|
|
58
|
+
return null;
|
|
59
|
+
let modules = '';
|
|
60
|
+
for (let x = 0; x < matrix.width; x++)
|
|
61
|
+
modules += matrix.get(x, y) ? '1' : '0';
|
|
62
|
+
return decodeCode16KRow(modules);
|
|
63
|
+
}
|
|
64
|
+
function rowIsConsistent(matrix, y, rowHeight, expected) {
|
|
65
|
+
for (let offset = 0; offset < rowHeight; offset++) {
|
|
66
|
+
const parsed = readRow(matrix, y + offset);
|
|
67
|
+
if (!parsed || parsed.row !== expected.row ||
|
|
68
|
+
parsed.values.some((value, index) => value !== expected.values[index]))
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
function code128StartForMode(mode) {
|
|
74
|
+
if (mode === 0)
|
|
75
|
+
return 103;
|
|
76
|
+
if (mode === 1 || mode === 3)
|
|
77
|
+
return 104;
|
|
78
|
+
if (mode === 2 || mode === 4)
|
|
79
|
+
return 105;
|
|
80
|
+
return -1;
|
|
81
|
+
}
|
|
82
|
+
function checksumOne(values) {
|
|
83
|
+
let sum = 0;
|
|
84
|
+
for (let index = 0; index < values.length; index++)
|
|
85
|
+
sum += (index + 2) * values[index];
|
|
86
|
+
return sum % 107;
|
|
87
|
+
}
|
|
88
|
+
function checksumTwo(valuesThroughC1) {
|
|
89
|
+
let sum = 0;
|
|
90
|
+
for (let index = 0; index < valuesThroughC1.length; index++) {
|
|
91
|
+
sum += (index + 1) * valuesThroughC1[index];
|
|
92
|
+
}
|
|
93
|
+
return sum % 107;
|
|
94
|
+
}
|
|
95
|
+
function decodeRows(rows, rowHeight, separatorHeight) {
|
|
96
|
+
if (rows.length < CODE16K_MIN_ROWS || rows.length > CODE16K_MAX_ROWS) {
|
|
97
|
+
throw new FormatError('Code 16K: row count must be in 2..16');
|
|
98
|
+
}
|
|
99
|
+
const first = rows[0];
|
|
100
|
+
if (first.row !== 0 || first.values.length !== CODE16K_SYMBOLS_PER_ROW) {
|
|
101
|
+
throw new FormatError('Code 16K: first row is missing or malformed');
|
|
102
|
+
}
|
|
103
|
+
const symbol = first.values[0];
|
|
104
|
+
if (!Number.isInteger(symbol) || symbol < 0 || symbol > 104) {
|
|
105
|
+
throw new FormatError('Code 16K: invalid S symbol');
|
|
106
|
+
}
|
|
107
|
+
const rowCount = Math.floor(symbol / 7) + 2;
|
|
108
|
+
const mode = symbol % 7;
|
|
109
|
+
if (rowCount !== rows.length || rowCount < CODE16K_MIN_ROWS || rowCount > CODE16K_MAX_ROWS) {
|
|
110
|
+
throw new FormatError('Code 16K: S symbol does not agree with the row count');
|
|
111
|
+
}
|
|
112
|
+
if (mode === 5 || mode === 6) {
|
|
113
|
+
throw new FormatError('Code 16K: shift modes 5 and 6 are not supported by this decoder');
|
|
114
|
+
}
|
|
115
|
+
const expectedRows = Array.from({ length: rowCount }, (_, index) => index);
|
|
116
|
+
for (let index = 0; index < rowCount; index++) {
|
|
117
|
+
if (rows[index].row !== expectedRows[index] || rows[index].values.length !== CODE16K_SYMBOLS_PER_ROW) {
|
|
118
|
+
throw new FormatError('Code 16K: rows are missing or out of order');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const codewords = rows.flatMap((entry) => entry.values);
|
|
122
|
+
if (codewords.length !== rowCount * CODE16K_SYMBOLS_PER_ROW) {
|
|
123
|
+
throw new FormatError('Code 16K: invalid codeword count');
|
|
124
|
+
}
|
|
125
|
+
const c1 = codewords[codewords.length - 2];
|
|
126
|
+
const c2 = codewords[codewords.length - 1];
|
|
127
|
+
const beforeChecks = codewords.slice(0, -2);
|
|
128
|
+
if (checksumOne(beforeChecks) !== c1 || checksumTwo([...beforeChecks, c1]) !== c2) {
|
|
129
|
+
throw new FormatError('Code 16K: check character mismatch');
|
|
130
|
+
}
|
|
131
|
+
const data = codewords.slice(1, -2);
|
|
132
|
+
while (data.length && data[data.length - 1] === CODE16K_PAD)
|
|
133
|
+
data.pop();
|
|
134
|
+
if (!data.length)
|
|
135
|
+
throw new FormatError('Code 16K: data region is empty');
|
|
136
|
+
const expectedStart = code128StartForMode(mode);
|
|
137
|
+
if (expectedStart < 0)
|
|
138
|
+
throw new FormatError('Code 16K: unsupported mode');
|
|
139
|
+
const info = code16kModeInfo(mode, mode === 3 || mode === 4);
|
|
140
|
+
const firstIsFnc1 = data[0] === 102;
|
|
141
|
+
if (info.gs1 !== firstIsFnc1) {
|
|
142
|
+
throw new FormatError('Code 16K: mode and leading FNC1 do not agree');
|
|
143
|
+
}
|
|
144
|
+
// Keep Code 16K aligned with ordinary Code 128 A/B/C decoding while the row
|
|
145
|
+
// framing and checks remain format-specific.
|
|
146
|
+
const decoded = decodeCode128ValuesLocal(expectedStart, data);
|
|
147
|
+
if (!decoded)
|
|
148
|
+
throw new FormatError('Code 16K: invalid Code 128 data stream');
|
|
149
|
+
return {
|
|
150
|
+
format: 'code16k',
|
|
151
|
+
text: decoded.text,
|
|
152
|
+
rows: rowCount,
|
|
153
|
+
columns: CODE16K_SYMBOLS_PER_ROW,
|
|
154
|
+
rowHeight,
|
|
155
|
+
separatorHeight,
|
|
156
|
+
mode,
|
|
157
|
+
codewords,
|
|
158
|
+
checksum: true,
|
|
159
|
+
symbologyIdentifier: info.gs1 ? ']K1' : ']K0',
|
|
160
|
+
...(info.gs1 ? { gs1: true } : {}),
|
|
161
|
+
...(decoded.fnc1AtStart ? { fnc1AtStart: true } : {}),
|
|
162
|
+
...(decoded.fnc1Positions?.length ? { fnc1Positions: decoded.fnc1Positions } : {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/* Keep the decoder self-contained if this module is consumed without the full common declaration graph. */
|
|
166
|
+
function decodeCode128ValuesLocal(startCode, dataValues) {
|
|
167
|
+
if (![103, 104, 105].includes(startCode) || !dataValues.length)
|
|
168
|
+
return null;
|
|
169
|
+
let mode = startCode === 103 ? 'A' : startCode === 104 ? 'B' : 'C';
|
|
170
|
+
let shifted = null;
|
|
171
|
+
let text = '';
|
|
172
|
+
const fnc1AtStart = dataValues[0] === 102;
|
|
173
|
+
const fnc1Positions = [];
|
|
174
|
+
for (let index = 0; index < dataValues.length; index++) {
|
|
175
|
+
const value = dataValues[index];
|
|
176
|
+
const active = shifted ?? mode;
|
|
177
|
+
shifted = null;
|
|
178
|
+
if (value === 101 && mode !== 'A') {
|
|
179
|
+
mode = 'A';
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (value === 100 && mode !== 'B') {
|
|
183
|
+
mode = 'B';
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (value === 99) {
|
|
187
|
+
mode = 'C';
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (value === 98) {
|
|
191
|
+
shifted = mode === 'A' ? 'B' : 'A';
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (value === 102) {
|
|
195
|
+
if (index > 0) {
|
|
196
|
+
fnc1Positions.push(text.length);
|
|
197
|
+
text += '\x1d';
|
|
198
|
+
}
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
if (value >= 96 && value <= 102)
|
|
202
|
+
continue;
|
|
203
|
+
if (active === 'C') {
|
|
204
|
+
if (value > 99)
|
|
205
|
+
return null;
|
|
206
|
+
text += String(value).padStart(2, '0');
|
|
207
|
+
}
|
|
208
|
+
else if (active === 'A') {
|
|
209
|
+
if (value > 95)
|
|
210
|
+
return null;
|
|
211
|
+
text += value < 64 ? String.fromCharCode(value + 32) : String.fromCharCode(value - 64);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
if (value > 95)
|
|
215
|
+
return null;
|
|
216
|
+
text += String.fromCharCode(value + 32);
|
|
217
|
+
}
|
|
218
|
+
if (text.length > 100000)
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
return text.length ? { text, ...(fnc1AtStart ? { fnc1AtStart: true } : {}), fnc1Positions } : null;
|
|
222
|
+
}
|
|
223
|
+
function candidateGeometry(matrix, options) {
|
|
224
|
+
const metadata = matrix.code16k;
|
|
225
|
+
const requestedRows = options.rows ?? metadata?.rows;
|
|
226
|
+
const requestedRowHeight = options.rowHeight ?? metadata?.rowHeight;
|
|
227
|
+
const requestedSeparatorHeight = options.separatorHeight ?? metadata?.separatorHeight;
|
|
228
|
+
const candidates = [];
|
|
229
|
+
for (const outer of [true, false]) {
|
|
230
|
+
const innerHeight = matrix.height - (outer ? 2 : 0);
|
|
231
|
+
for (let rows = CODE16K_MIN_ROWS; rows <= CODE16K_MAX_ROWS; rows++) {
|
|
232
|
+
if (requestedRows !== undefined && rows !== requestedRows)
|
|
233
|
+
continue;
|
|
234
|
+
for (let separatorHeight = requestedSeparatorHeight ?? 1; separatorHeight <= (requestedSeparatorHeight ?? 16); separatorHeight++) {
|
|
235
|
+
if (!Number.isInteger(separatorHeight) || separatorHeight < 1 || separatorHeight > 16)
|
|
236
|
+
continue;
|
|
237
|
+
const numerator = innerHeight - (rows - 1) * separatorHeight;
|
|
238
|
+
if (numerator < rows || numerator % rows !== 0)
|
|
239
|
+
continue;
|
|
240
|
+
const rowHeight = numerator / rows;
|
|
241
|
+
if (rowHeight < 1 || rowHeight > 128)
|
|
242
|
+
continue;
|
|
243
|
+
if (requestedRowHeight !== undefined && rowHeight !== requestedRowHeight)
|
|
244
|
+
continue;
|
|
245
|
+
candidates.push({ rows, rowHeight, separatorHeight, outer });
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return candidates;
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Decode a complete, axis-aligned Code 16K module matrix.
|
|
253
|
+
*
|
|
254
|
+
* The decoder re-checks row geometry, row identifiers, both modulo-107 check
|
|
255
|
+
* characters and the Code 128 data stream. Metadata attached by the encoder
|
|
256
|
+
* is used only as a geometry hint and never substitutes for validation.
|
|
257
|
+
*/
|
|
258
|
+
export function decodeCode16K(matrix, options = {}) {
|
|
259
|
+
if (!matrix?.width || !matrix?.height || typeof matrix.get !== 'function') {
|
|
260
|
+
throw new FormatError('Code 16K: matrix with width, height and get() is required');
|
|
261
|
+
}
|
|
262
|
+
if (matrix.width !== 70)
|
|
263
|
+
throw new FormatError('Code 16K: matrix width must be 70 modules');
|
|
264
|
+
for (const geometry of candidateGeometry(matrix, options)) {
|
|
265
|
+
const first = readRow(matrix, geometry.outer ? 1 : 0);
|
|
266
|
+
if (!first || first.row !== 0)
|
|
267
|
+
continue;
|
|
268
|
+
const encodedRows = Math.floor(first.values[0] / 7) + 2;
|
|
269
|
+
if (encodedRows !== geometry.rows)
|
|
270
|
+
continue;
|
|
271
|
+
const rows = [];
|
|
272
|
+
let valid = true;
|
|
273
|
+
for (let row = 0; row < geometry.rows; row++) {
|
|
274
|
+
const y = (geometry.outer ? 1 : 0) + row * (geometry.rowHeight + geometry.separatorHeight);
|
|
275
|
+
const parsed = readRow(matrix, y);
|
|
276
|
+
if (!parsed || parsed.row !== row) {
|
|
277
|
+
valid = false;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
if (!rowIsConsistent(matrix, y, geometry.rowHeight, parsed)) {
|
|
281
|
+
valid = false;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
rows.push(parsed);
|
|
285
|
+
if (row < geometry.rows - 1 && !separatorIsDark(matrix, y + geometry.rowHeight)) {
|
|
286
|
+
valid = false;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!valid)
|
|
291
|
+
continue;
|
|
292
|
+
if (geometry.outer && (!separatorIsDark(matrix, 0) ||
|
|
293
|
+
!separatorIsDark(matrix, matrix.height - 1)))
|
|
294
|
+
continue;
|
|
295
|
+
try {
|
|
296
|
+
return decodeRows(rows, geometry.rowHeight, geometry.separatorHeight);
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
// Try another legal geometry before rejecting the matrix.
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
throw new FormatError('Code 16K: no valid row geometry or checksum found');
|
|
303
|
+
}
|