@sythos/js_barcode_universal 1.0.0 → 1.2.5
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 +16 -17
- package/NOTICE.md +24 -22
- package/README.md +137 -60
- package/bundle/sythos-barcode.esm.js +3380 -216
- package/bundle/sythos-barcode.js +3368 -216
- package/examples/create.html +2 -0
- package/examples/read.html +341 -341
- package/licenses/README.md +58 -29
- package/licenses/aztec-code.license +74 -0
- package/licenses/codabar.license +9 -9
- package/licenses/code-11.license +9 -9
- package/licenses/code-128.license +6 -6
- package/licenses/code-39.license +6 -6
- package/licenses/code-93.license +7 -7
- package/licenses/data-matrix.license +11 -11
- package/licenses/ean-13.license +6 -6
- package/licenses/ean-8.license +6 -6
- package/licenses/gs1-128.license +6 -6
- package/licenses/isbn.license +7 -7
- package/licenses/itf-14.license +6 -6
- package/licenses/itf.license +6 -6
- package/licenses/micropdf417.license +96 -0
- package/licenses/msi-plessey.license +7 -7
- package/licenses/pdf417.license +37 -0
- package/licenses/pharmacode.license +7 -7
- package/licenses/qr-code.license +6 -6
- package/licenses/upc-a.license +7 -7
- package/licenses/upc-e.license +6 -6
- package/package.json +13 -3
- package/src/aztec/decoder.js +317 -0
- package/src/aztec/detector.js +224 -0
- package/src/aztec/encoder.js +257 -0
- package/src/aztec/high-level.js +211 -0
- package/src/aztec/index.js +45 -0
- package/src/aztec/tables.js +210 -0
- package/src/core/galois-field.js +3 -0
- package/src/core/reed-solomon.js +64 -50
- package/src/datamatrix/decoder.js +262 -262
- package/src/datamatrix/detector.js +225 -225
- package/src/datamatrix/encoder.js +191 -191
- package/src/datamatrix/index.js +42 -42
- package/src/datamatrix/tables.js +123 -123
- package/src/index.js +113 -3
- package/src/micropdf417/compaction.js +116 -0
- package/src/micropdf417/decoder.js +183 -0
- package/src/micropdf417/detector.js +149 -0
- package/src/micropdf417/encoder.js +209 -0
- package/src/micropdf417/error-correction.js +55 -0
- package/src/micropdf417/index.js +49 -0
- package/src/micropdf417/tables.js +184 -0
- package/src/pdf417/compaction.js +298 -0
- package/src/pdf417/decoder.js +75 -0
- package/src/pdf417/detector.js +468 -0
- package/src/pdf417/encoder.js +91 -0
- package/src/pdf417/error-correction.js +47 -0
- package/src/pdf417/index.js +6 -0
- package/src/pdf417/tables.js +317 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Sythos
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*
|
|
26
|
+
* SPDX-License-Identifier: MIT
|
|
27
|
+
*
|
|
28
|
+
* Original work. No code from any other barcode implementation.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* MicroPDF417 format facts and Row Address Pattern (RAP) helpers.
|
|
33
|
+
*
|
|
34
|
+
* The tables are represented as compact, immutable data and are guarded by
|
|
35
|
+
* {@link validateMicroPdf417Tables}. They are deliberately separate from the
|
|
36
|
+
* PDF417 symbol-character table: MicroPDF417 has a fixed family of symbols and
|
|
37
|
+
* its own row-address system.
|
|
38
|
+
*
|
|
39
|
+
* Values are derived from publicly available symbology documentation and
|
|
40
|
+
* independently checked against black-box reference output. This module makes
|
|
41
|
+
* no certification or conformance claim.
|
|
42
|
+
*
|
|
43
|
+
* @module micropdf417/tables
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const variant = (id, columns, rows, eccCodewords, rapStart, rapRotation) => Object.freeze({
|
|
47
|
+
id,
|
|
48
|
+
columns,
|
|
49
|
+
rows,
|
|
50
|
+
totalCodewords: columns * rows,
|
|
51
|
+
dataCodewords: columns * rows - eccCodewords,
|
|
52
|
+
eccCodewords,
|
|
53
|
+
rapStart,
|
|
54
|
+
rapRotation,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** All 34 predefined MicroPDF417 symbol variants, in format-table order. */
|
|
58
|
+
export const MICROPDF417_VARIANTS = Object.freeze([
|
|
59
|
+
variant(1, 1, 11, 7, 1, 8), variant(2, 1, 14, 7, 8, 0),
|
|
60
|
+
variant(3, 1, 17, 7, 36, 0), variant(4, 1, 20, 8, 19, 0),
|
|
61
|
+
variant(5, 1, 24, 8, 9, 8), variant(6, 1, 28, 8, 25, 8),
|
|
62
|
+
variant(7, 2, 8, 8, 1, 0), variant(8, 2, 11, 9, 1, 8),
|
|
63
|
+
variant(9, 2, 14, 9, 8, 0), variant(10, 2, 17, 10, 36, 0),
|
|
64
|
+
variant(11, 2, 20, 11, 19, 0), variant(12, 2, 23, 13, 9, 8),
|
|
65
|
+
variant(13, 2, 26, 15, 27, 8),
|
|
66
|
+
variant(14, 3, 6, 12, 1, 0), variant(15, 3, 8, 14, 7, 0),
|
|
67
|
+
variant(16, 3, 10, 16, 15, 0), variant(17, 3, 12, 18, 25, 0),
|
|
68
|
+
variant(18, 3, 15, 21, 37, 0), variant(19, 3, 20, 26, 1, 16),
|
|
69
|
+
variant(20, 3, 26, 32, 1, 8), variant(21, 3, 32, 38, 21, 8),
|
|
70
|
+
variant(22, 3, 38, 44, 15, 16), variant(23, 3, 44, 50, 1, 24),
|
|
71
|
+
variant(24, 4, 4, 8, 47, 24), variant(25, 4, 6, 12, 1, 0),
|
|
72
|
+
variant(26, 4, 8, 14, 7, 0), variant(27, 4, 10, 16, 15, 0),
|
|
73
|
+
variant(28, 4, 12, 18, 25, 0), variant(29, 4, 15, 21, 37, 0),
|
|
74
|
+
variant(30, 4, 20, 26, 1, 16), variant(31, 4, 26, 32, 1, 8),
|
|
75
|
+
variant(32, 4, 32, 38, 21, 8), variant(33, 4, 38, 44, 15, 16),
|
|
76
|
+
variant(34, 4, 44, 50, 1, 24),
|
|
77
|
+
]);
|
|
78
|
+
|
|
79
|
+
const byId = new Map(MICROPDF417_VARIANTS.map((entry) => [entry.id, entry]));
|
|
80
|
+
|
|
81
|
+
// Six run widths, ordered bar-space-bar-space-bar-space. A RAP is ten
|
|
82
|
+
// modules wide; the right RAP has one additional one-module stop bar when
|
|
83
|
+
// rendered. Keeping runs rather than bitmap literals makes each invariant
|
|
84
|
+
// inspectable and avoids a rendering-specific representation here.
|
|
85
|
+
const SIDE_RAP_RUNS = Object.freeze([
|
|
86
|
+
'221311', '311311', '312211', '222211', '213211', '214111', '223111', '313111',
|
|
87
|
+
'322111', '412111', '421111', '331111', '241111', '232111', '231211', '321211',
|
|
88
|
+
'411211', '411121', '411112', '321112', '312112', '311212', '311221', '311131',
|
|
89
|
+
'311122', '311113', '221113', '221122', '221131', '221221', '222121', '312121',
|
|
90
|
+
'321121', '231121', '231112', '222112', '213112', '212212', '212221', '212131',
|
|
91
|
+
'212122', '212113', '211213', '211123', '211132', '211141', '211231', '211222',
|
|
92
|
+
'211312', '211321', '211411', '212311',
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const CENTER_RAP_RUNS = Object.freeze([
|
|
96
|
+
'112231', '121231', '122131', '131131', '131221', '132121', '141121', '141211',
|
|
97
|
+
'142111', '133111', '132211', '131311', '122311', '123211', '124111', '115111',
|
|
98
|
+
'114211', '114121', '123121', '123112', '122212', '122221', '121321', '121411',
|
|
99
|
+
'112411', '113311', '113221', '113212', '113122', '122122', '131122', '131113',
|
|
100
|
+
'122113', '113113', '112213', '112222', '112312', '112321', '111421', '111331',
|
|
101
|
+
'111322', '111232', '111223', '111133', '111124', '111214', '112114', '121114',
|
|
102
|
+
'121123', '121132', '112132', '112141',
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
/** @param {number} value @param {number} offset @returns {number} */
|
|
106
|
+
export function microPdf417NextRap(value, offset = 1) {
|
|
107
|
+
if (!Number.isInteger(value) || value < 1 || value > 52) throw new RangeError('MicroPDF417: RAP number must be in 1..52');
|
|
108
|
+
if (!Number.isInteger(offset)) throw new RangeError('MicroPDF417: RAP offset must be an integer');
|
|
109
|
+
return ((value - 1 + offset) % 52 + 52) % 52 + 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @param {number} id @returns {Readonly<typeof MICROPDF417_VARIANTS[number]>} */
|
|
113
|
+
export function microPdf417VariantByNumber(id) {
|
|
114
|
+
const entry = byId.get(id);
|
|
115
|
+
if (!entry) throw new RangeError('MicroPDF417: variant must be an integer in 1..34');
|
|
116
|
+
return entry;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Return the smallest data-region candidate that fits `codewords`.
|
|
121
|
+
* Ties are resolved by width, then height, so selection is deterministic.
|
|
122
|
+
*/
|
|
123
|
+
export function microPdf417VariantForCapacity(codewords) {
|
|
124
|
+
if (!Number.isInteger(codewords) || codewords < 1) throw new RangeError('MicroPDF417: codeword capacity must be a positive integer');
|
|
125
|
+
const candidates = MICROPDF417_VARIANTS.filter((entry) => entry.dataCodewords >= codewords);
|
|
126
|
+
if (!candidates.length) throw new RangeError('MicroPDF417: payload exceeds the largest symbol data region');
|
|
127
|
+
return candidates.slice().sort((a, b) => a.totalCodewords - b.totalCodewords || a.columns - b.columns || a.rows - b.rows)[0];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Return the six bar/space run widths for a numbered side or center RAP. */
|
|
131
|
+
export function microPdf417RapSequence(number, kind = 'side') {
|
|
132
|
+
if (!Number.isInteger(number) || number < 1 || number > 52) throw new RangeError('MicroPDF417: RAP number must be in 1..52');
|
|
133
|
+
if (kind === 'side') return SIDE_RAP_RUNS[number - 1];
|
|
134
|
+
if (kind === 'center') return CENTER_RAP_RUNS[number - 1];
|
|
135
|
+
throw new RangeError('MicroPDF417: RAP kind must be side or center');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve all row-address data for a zero-based row within a variant.
|
|
140
|
+
* @returns {{left: number, center: number|null, right: number, cluster: 0|3|6}}
|
|
141
|
+
*/
|
|
142
|
+
export function microPdf417RowAddress(entry, row) {
|
|
143
|
+
if (!entry || !Number.isInteger(entry.columns) || !Number.isInteger(entry.rows)) throw new TypeError('MicroPDF417: a variant entry is required');
|
|
144
|
+
if (!Number.isInteger(row) || row < 0 || row >= entry.rows) throw new RangeError(`MicroPDF417: row must be in 0..${entry.rows - 1}`);
|
|
145
|
+
const left = microPdf417NextRap(entry.rapStart, row);
|
|
146
|
+
const cluster = /** @type {0|3|6} */ (((left - 1) % 3) * 3);
|
|
147
|
+
if (entry.columns < 3) return { left, center: null, right: microPdf417NextRap(left, entry.rapRotation), cluster };
|
|
148
|
+
const center = microPdf417NextRap(left, entry.rapRotation);
|
|
149
|
+
return { left, center, right: microPdf417NextRap(center, entry.rapRotation), cluster };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const validRuns = (runs) => runs.length === 6 && /^[1-9]{6}$/.test(runs) && [...runs].reduce((sum, digit) => sum + Number(digit), 0) === 10;
|
|
153
|
+
const oneEdgeShift = (from, to) => [...from].reduce((sum, digit, index) => sum + Math.abs(Number(digit) - Number(to[index])), 0) === 2;
|
|
154
|
+
|
|
155
|
+
/** Return any table-invariant failures; an empty result means the table is coherent. */
|
|
156
|
+
export function validateMicroPdf417Tables() {
|
|
157
|
+
const issues = [];
|
|
158
|
+
if (MICROPDF417_VARIANTS.length !== 34) issues.push('expected 34 variants');
|
|
159
|
+
const ids = new Set();
|
|
160
|
+
const formats = new Set();
|
|
161
|
+
for (const entry of MICROPDF417_VARIANTS) {
|
|
162
|
+
if (ids.has(entry.id)) issues.push(`duplicate variant ${entry.id}`); ids.add(entry.id);
|
|
163
|
+
const format = `${entry.columns}x${entry.rows}`;
|
|
164
|
+
if (formats.has(format)) issues.push(`duplicate format ${format}`); formats.add(format);
|
|
165
|
+
if (entry.totalCodewords !== entry.columns * entry.rows) issues.push(`${format}: total codeword geometry mismatch`);
|
|
166
|
+
if (entry.dataCodewords + entry.eccCodewords !== entry.totalCodewords) issues.push(`${format}: data/ECC capacity mismatch`);
|
|
167
|
+
if (entry.eccCodewords < 7 || entry.eccCodewords > 50) issues.push(`${format}: invalid ECC length`);
|
|
168
|
+
if (entry.rapStart < 1 || entry.rapStart > 52 || entry.rapRotation < 0 || entry.rapRotation > 51) issues.push(`${format}: invalid RAP assignment`);
|
|
169
|
+
for (let row = 0; row < entry.rows; row++) {
|
|
170
|
+
const address = microPdf417RowAddress(entry, row);
|
|
171
|
+
if (address.cluster !== ((address.left - 1) % 3) * 3) issues.push(`${format}: cluster mismatch at row ${row}`);
|
|
172
|
+
if ((entry.columns < 3) !== (address.center === null)) issues.push(`${format}: center RAP layout mismatch`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const [kind, runs] of [['side', SIDE_RAP_RUNS], ['center', CENTER_RAP_RUNS]]) {
|
|
176
|
+
if (runs.length !== 52) issues.push(`${kind}: expected 52 RAPs`);
|
|
177
|
+
if (new Set(runs).size !== runs.length) issues.push(`${kind}: duplicate RAP`);
|
|
178
|
+
for (let i = 0; i < runs.length; i++) {
|
|
179
|
+
if (!validRuns(runs[i])) issues.push(`${kind}: invalid RAP ${i + 1}`);
|
|
180
|
+
if (runs.length && !oneEdgeShift(runs[i], runs[(i + 1) % runs.length])) issues.push(`${kind}: RAP ${i + 1} is not adjacent to its successor`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return issues;
|
|
184
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Sythos
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*
|
|
26
|
+
* SPDX-License-Identifier: MIT
|
|
27
|
+
*
|
|
28
|
+
* Original work. No code from any other barcode implementation.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** PDF417 high-level text, byte and numeric compaction. @module pdf417/compaction */
|
|
32
|
+
|
|
33
|
+
import { EncodeError, FormatError } from '../core/errors.js';
|
|
34
|
+
|
|
35
|
+
const ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ ';
|
|
36
|
+
const LOWER = 'abcdefghijklmnopqrstuvwxyz ';
|
|
37
|
+
const MIXED = '0123456789&\r\t,:#-.$/+%*=^';
|
|
38
|
+
const PUNCT = ';<>@[\\]_`~!\r\t,:\n-.$/"|*()?{}\'';
|
|
39
|
+
|
|
40
|
+
function packBase30(values) {
|
|
41
|
+
const out = [];
|
|
42
|
+
for (let i = 0; i < values.length; i += 2) out.push(values[i] * 30 + (i + 1 < values.length ? values[i + 1] : 29));
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Compact a value using the PDF417 Text Compaction alphabet. */
|
|
47
|
+
export function compactPdf417Text(value) {
|
|
48
|
+
if (typeof value !== 'string') throw new EncodeError('PDF417 text: value must be a string');
|
|
49
|
+
const values = [];
|
|
50
|
+
let submode = 'alpha';
|
|
51
|
+
for (const character of value) {
|
|
52
|
+
const inAlpha = ALPHA.indexOf(character), inLower = LOWER.indexOf(character);
|
|
53
|
+
const inMixed = MIXED.indexOf(character), inPunct = PUNCT.indexOf(character);
|
|
54
|
+
if (submode === 'alpha') {
|
|
55
|
+
if (inAlpha >= 0) values.push(inAlpha);
|
|
56
|
+
else if (inLower >= 0) { values.push(27, inLower); submode = 'lower'; }
|
|
57
|
+
else if (inMixed >= 0 || character === ' ') { values.push(28, character === ' ' ? 26 : inMixed); submode = 'mixed'; }
|
|
58
|
+
else if (inPunct >= 0) values.push(29, inPunct);
|
|
59
|
+
else throw new EncodeError(`PDF417 text: unsupported character ${JSON.stringify(character)}`);
|
|
60
|
+
} else if (submode === 'lower') {
|
|
61
|
+
if (inLower >= 0) values.push(inLower);
|
|
62
|
+
else if (inAlpha >= 0) values.push(27, inAlpha);
|
|
63
|
+
else if (inMixed >= 0 || character === ' ') { values.push(28, character === ' ' ? 26 : inMixed); submode = 'mixed'; }
|
|
64
|
+
else if (inPunct >= 0) values.push(29, inPunct);
|
|
65
|
+
else throw new EncodeError(`PDF417 text: unsupported character ${JSON.stringify(character)}`);
|
|
66
|
+
} else {
|
|
67
|
+
if (inMixed >= 0) values.push(inMixed);
|
|
68
|
+
else if (character === ' ') values.push(26);
|
|
69
|
+
else if (inAlpha >= 0) { values.push(28); submode = 'alpha'; values.push(inAlpha); }
|
|
70
|
+
else if (inLower >= 0) { values.push(27); submode = 'lower'; values.push(inLower); }
|
|
71
|
+
else if (inPunct >= 0) values.push(29, inPunct);
|
|
72
|
+
else throw new EncodeError(`PDF417 text: unsupported character ${JSON.stringify(character)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return packBase30(values);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function asBytes(value) {
|
|
79
|
+
if (value instanceof Uint8Array) return value;
|
|
80
|
+
if (ArrayBuffer.isView(value)) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
81
|
+
if (typeof value === 'string') return new TextEncoder().encode(value);
|
|
82
|
+
throw new EncodeError('PDF417 byte: value must be text or a byte array');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Compact bytes using latch 924 for exact six-byte blocks and 901 otherwise. */
|
|
86
|
+
export function compactPdf417Bytes(value) {
|
|
87
|
+
const bytes = asBytes(value);
|
|
88
|
+
const utf8 = typeof value === 'string' && /[^\x00-\x7f]/.test(value);
|
|
89
|
+
const out = utf8 ? [927, 26] : [];
|
|
90
|
+
out.push(bytes.length > 0 && bytes.length % 6 === 0 ? 924 : 901);
|
|
91
|
+
let at = 0;
|
|
92
|
+
while (at + 6 <= bytes.length) {
|
|
93
|
+
let number = 0n;
|
|
94
|
+
for (let i = 0; i < 6; i++) number = (number << 8n) | BigInt(bytes[at++]);
|
|
95
|
+
const group = new Array(5);
|
|
96
|
+
for (let i = 4; i >= 0; i--) { group[i] = Number(number % 900n); number /= 900n; }
|
|
97
|
+
out.push(...group);
|
|
98
|
+
}
|
|
99
|
+
while (at < bytes.length) out.push(bytes[at++]);
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Compact decimal digits using latch 902 and groups of at most 44 digits. */
|
|
104
|
+
export function compactPdf417Numeric(value) {
|
|
105
|
+
if (typeof value !== 'string' || !/^\d+$/.test(value)) throw new EncodeError('PDF417 numeric: value must contain decimal digits only');
|
|
106
|
+
const out = [902];
|
|
107
|
+
for (let at = 0; at < value.length; at += 44) {
|
|
108
|
+
let number = BigInt(`1${value.slice(at, at + 44)}`);
|
|
109
|
+
const group = [];
|
|
110
|
+
do { group.unshift(Number(number % 900n)); number /= 900n; } while (number > 0n);
|
|
111
|
+
out.push(...group);
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Compact a single value, selecting text, numeric or byte mode. */
|
|
117
|
+
export function compactPdf417(value, options = {}) {
|
|
118
|
+
const mode = options.compaction ?? 'auto';
|
|
119
|
+
if (mode === 'text') return compactPdf417Text(value);
|
|
120
|
+
if (mode === 'byte') return compactPdf417Bytes(value);
|
|
121
|
+
if (mode === 'numeric') return compactPdf417Numeric(value);
|
|
122
|
+
if (mode !== 'auto') throw new EncodeError(`PDF417: unsupported compaction mode ${JSON.stringify(mode)}`);
|
|
123
|
+
if (typeof value === 'string' && /^\d{13,}$/.test(value)) return compactPdf417Numeric(value);
|
|
124
|
+
if (typeof value === 'string') {
|
|
125
|
+
try { return compactPdf417Text(value); } catch (error) { if (!(error instanceof EncodeError)) throw error; }
|
|
126
|
+
}
|
|
127
|
+
return compactPdf417Bytes(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function assertCodeword(codeword) {
|
|
131
|
+
if (!Number.isInteger(codeword) || codeword < 0 || codeword > 928) throw new FormatError('PDF417: codeword is outside 0..928');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function decodeUtf8(bytes, eci) {
|
|
135
|
+
if (eci === 3) return Array.from(bytes, (byte) => String.fromCharCode(byte)).join('');
|
|
136
|
+
if (eci !== 26) throw new FormatError(`PDF417 ECI: unsupported assignment number ${eci}`);
|
|
137
|
+
try { return new TextDecoder('utf-8', { fatal: true }).decode(new Uint8Array(bytes)); }
|
|
138
|
+
catch { throw new FormatError('PDF417 byte: invalid UTF-8 sequence'); }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function decodeByteSegment(codewords, at, eci, sixOnly = false) {
|
|
142
|
+
const values = [];
|
|
143
|
+
while (at < codewords.length && codewords[at] < 900) values.push(codewords[at++]);
|
|
144
|
+
if (sixOnly && values.length % 5) throw new FormatError('PDF417 byte: 924 segment must contain complete six-byte groups');
|
|
145
|
+
const bytes = [];
|
|
146
|
+
// In 901 mode an encoder can use five terminal literal codewords. The
|
|
147
|
+
// unambiguous groups are therefore the ones followed by another codeword;
|
|
148
|
+
// 924 is available whenever a segment consists exclusively of six-byte groups.
|
|
149
|
+
const groupCount = sixOnly ? values.length / 5 : Math.max(0, Math.floor((values.length - 1) / 5));
|
|
150
|
+
for (let groupAt = 0; groupAt < groupCount * 5; groupAt += 5) {
|
|
151
|
+
let number = 0n;
|
|
152
|
+
for (let i = 0; i < 5; i++) number = number * 900n + BigInt(values[groupAt + i]);
|
|
153
|
+
const group = new Uint8Array(6);
|
|
154
|
+
for (let i = 5; i >= 0; i--) { group[i] = Number(number & 255n); number >>= 8n; }
|
|
155
|
+
if (number !== 0n) throw new FormatError('PDF417 byte: base-900 group exceeds six bytes');
|
|
156
|
+
bytes.push(...group);
|
|
157
|
+
}
|
|
158
|
+
for (let i = groupCount * 5; i < values.length; i++) {
|
|
159
|
+
if (values[i] > 255) throw new FormatError('PDF417 byte: literal tail is outside 0..255');
|
|
160
|
+
bytes.push(values[i]);
|
|
161
|
+
}
|
|
162
|
+
return { at, text: decodeUtf8(bytes, eci), bytes: Uint8Array.from(bytes) };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function decodeTextSegment(codewords, at, eci) {
|
|
166
|
+
let mode = 'alpha';
|
|
167
|
+
let output = '';
|
|
168
|
+
let shift = null;
|
|
169
|
+
let shiftedBytes = [];
|
|
170
|
+
const bytes = [];
|
|
171
|
+
const flushShiftedBytes = () => {
|
|
172
|
+
if (shiftedBytes.length) {
|
|
173
|
+
output += decodeUtf8(shiftedBytes, eci);
|
|
174
|
+
bytes.push(...shiftedBytes);
|
|
175
|
+
shiftedBytes = [];
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const emit = (alphabet, value) => {
|
|
179
|
+
if (value < 0 || value >= alphabet.length) throw new FormatError('PDF417 text: invalid submode value');
|
|
180
|
+
output += alphabet[value];
|
|
181
|
+
};
|
|
182
|
+
const process = (value) => {
|
|
183
|
+
if (shift) { emit(shift === 'alpha' ? ALPHA : PUNCT, value); shift = null; return; }
|
|
184
|
+
if (mode === 'alpha') {
|
|
185
|
+
if (value < 26) emit(ALPHA, value);
|
|
186
|
+
else if (value === 26) output += ' ';
|
|
187
|
+
else if (value === 27) mode = 'lower';
|
|
188
|
+
else if (value === 28) mode = 'mixed';
|
|
189
|
+
else if (value === 29) shift = 'punct';
|
|
190
|
+
} else if (mode === 'lower') {
|
|
191
|
+
if (value < 26) emit(LOWER, value);
|
|
192
|
+
else if (value === 26) output += ' ';
|
|
193
|
+
else if (value === 27) shift = 'alpha';
|
|
194
|
+
else if (value === 28) mode = 'mixed';
|
|
195
|
+
else if (value === 29) shift = 'punct';
|
|
196
|
+
} else if (mode === 'mixed') {
|
|
197
|
+
if (value < 25) emit(MIXED, value);
|
|
198
|
+
else if (value === 25) mode = 'punct';
|
|
199
|
+
else if (value === 26) output += ' ';
|
|
200
|
+
else if (value === 27) mode = 'lower';
|
|
201
|
+
else if (value === 28) mode = 'alpha';
|
|
202
|
+
else if (value === 29) shift = 'punct';
|
|
203
|
+
} else {
|
|
204
|
+
if (value < 29) emit(PUNCT, value);
|
|
205
|
+
else if (value === 29) mode = 'alpha';
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
while (at < codewords.length) {
|
|
209
|
+
const codeword = codewords[at];
|
|
210
|
+
if (codeword >= 900 && codeword !== 913) break;
|
|
211
|
+
at++;
|
|
212
|
+
if (codeword === 913) {
|
|
213
|
+
if (at >= codewords.length || codewords[at] > 255) throw new FormatError('PDF417 text: invalid byte shift');
|
|
214
|
+
shiftedBytes.push(codewords[at++]);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
flushShiftedBytes();
|
|
218
|
+
process(Math.floor(codeword / 30));
|
|
219
|
+
process(codeword % 30);
|
|
220
|
+
}
|
|
221
|
+
flushShiftedBytes();
|
|
222
|
+
return { at, text: output, bytes: Uint8Array.from(bytes) };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function decodeNumericSegment(codewords, at) {
|
|
226
|
+
let output = '';
|
|
227
|
+
while (at < codewords.length && codewords[at] < 900) {
|
|
228
|
+
const end = Math.min(at + 15, codewords.length);
|
|
229
|
+
let number = 0n;
|
|
230
|
+
for (; at < end && codewords[at] < 900; at++) number = number * 900n + BigInt(codewords[at]);
|
|
231
|
+
const decimal = number.toString();
|
|
232
|
+
if (!decimal.startsWith('1')) throw new FormatError('PDF417 numeric: missing leading sentinel');
|
|
233
|
+
output += decimal.slice(1);
|
|
234
|
+
}
|
|
235
|
+
return { at, text: output, bytes: new Uint8Array(0) };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Decode PDF417 compaction while preserving raw Byte Compaction and byte-shift
|
|
240
|
+
* payloads. Text and Numeric Compaction do not manufacture bytes: their text
|
|
241
|
+
* is available on each segment, while `bytes` contains only octets carried by
|
|
242
|
+
* modes that encode octets explicitly.
|
|
243
|
+
*/
|
|
244
|
+
export function decodePdf417CompactionDetailed(codewords) {
|
|
245
|
+
if (!Array.isArray(codewords) && !ArrayBuffer.isView(codewords)) throw new FormatError('PDF417: codewords must be an array');
|
|
246
|
+
for (const codeword of codewords) assertCodeword(codeword);
|
|
247
|
+
let at = 0;
|
|
248
|
+
// ISO/IEC 8859-1 is the PDF417 default; UTF-8 is selected explicitly with ECI 26.
|
|
249
|
+
let eci = 3;
|
|
250
|
+
let output = '';
|
|
251
|
+
const bytes = [];
|
|
252
|
+
const segments = [];
|
|
253
|
+
while (at < codewords.length) {
|
|
254
|
+
const codeword = codewords[at];
|
|
255
|
+
if (codeword < 900 || codeword === 900 || codeword === 913) {
|
|
256
|
+
const start = at;
|
|
257
|
+
const latch = codeword === 900 ? codeword : null;
|
|
258
|
+
if (latch !== null) at++;
|
|
259
|
+
const segment = decodeTextSegment(codewords, at, eci);
|
|
260
|
+
at = segment.at;
|
|
261
|
+
output += segment.text;
|
|
262
|
+
bytes.push(...segment.bytes);
|
|
263
|
+
if (segment.text.length || segment.bytes.length) segments.push({ mode: 'text', text: segment.text, bytes: segment.bytes, eci, latch, codewordStart: start, codewordEnd: at });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const start = at;
|
|
267
|
+
at++;
|
|
268
|
+
if (codeword === 901) {
|
|
269
|
+
const segment = decodeByteSegment(codewords, at, eci);
|
|
270
|
+
at = segment.at;
|
|
271
|
+
output += segment.text;
|
|
272
|
+
bytes.push(...segment.bytes);
|
|
273
|
+
segments.push({ mode: 'byte', text: segment.text, bytes: segment.bytes, eci, latch: codeword, codewordStart: start, codewordEnd: at });
|
|
274
|
+
} else if (codeword === 924) {
|
|
275
|
+
const segment = decodeByteSegment(codewords, at, eci, true);
|
|
276
|
+
at = segment.at;
|
|
277
|
+
output += segment.text;
|
|
278
|
+
bytes.push(...segment.bytes);
|
|
279
|
+
segments.push({ mode: 'byte', text: segment.text, bytes: segment.bytes, eci, latch: codeword, codewordStart: start, codewordEnd: at });
|
|
280
|
+
} else if (codeword === 902) {
|
|
281
|
+
const segment = decodeNumericSegment(codewords, at);
|
|
282
|
+
at = segment.at;
|
|
283
|
+
output += segment.text;
|
|
284
|
+
segments.push({ mode: 'numeric', text: segment.text, bytes: segment.bytes, eci, latch: codeword, codewordStart: start, codewordEnd: at });
|
|
285
|
+
} else if (codeword === 927) {
|
|
286
|
+
if (at >= codewords.length || codewords[at] > 899) throw new FormatError('PDF417 ECI: missing assignment number');
|
|
287
|
+
eci = codewords[at++];
|
|
288
|
+
} else {
|
|
289
|
+
throw new FormatError(`PDF417: unsupported compaction codeword ${codeword}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return { text: output, bytes: Uint8Array.from(bytes), segments };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Decode PDF417 Text, Byte, Numeric and UTF-8 ECI compaction segments in source order. */
|
|
296
|
+
export function decodePdf417Compaction(codewords) {
|
|
297
|
+
return decodePdf417CompactionDetailed(codewords).text;
|
|
298
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Sythos Barcode Suite
|
|
3
|
+
*
|
|
4
|
+
* MIT License
|
|
5
|
+
*
|
|
6
|
+
* Copyright (c) 2026 Sythos
|
|
7
|
+
*
|
|
8
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
9
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
10
|
+
* in the Software without restriction, including without limitation the rights
|
|
11
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
12
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
13
|
+
* furnished to do so, subject to the following conditions:
|
|
14
|
+
*
|
|
15
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
16
|
+
* copies or substantial portions of the Software.
|
|
17
|
+
*
|
|
18
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
19
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
20
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
21
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
22
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
23
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
24
|
+
* SOFTWARE.
|
|
25
|
+
*
|
|
26
|
+
* SPDX-License-Identifier: MIT
|
|
27
|
+
*
|
|
28
|
+
* Original work. No code from any other barcode implementation.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { FormatError } from '../core/errors.js';
|
|
32
|
+
import { decodePdf417CompactionDetailed } from './compaction.js';
|
|
33
|
+
import { pdf417CorrectErrors, pdf417EccLength } from './error-correction.js';
|
|
34
|
+
import { pdf417CodewordForPattern } from './tables.js';
|
|
35
|
+
|
|
36
|
+
const START = '11111111010101000';
|
|
37
|
+
const STOP = '111111101000101001';
|
|
38
|
+
function bits(matrix, y, x, width) { let value = 0; for (let i = 0; i < width; i++) value = (value << 1) | (matrix.get(x + i, y) ? 1 : 0); return value; }
|
|
39
|
+
function indicators(row, rows, cols, level) { const group = Math.floor(row / 3), y = Math.floor((rows - 1) / 3), z = level * 3 + (rows - 1) % 3, v = cols - 1; return row % 3 === 0 ? [30 * group + y, 30 * group + v] : row % 3 === 1 ? [30 * group + z, 30 * group + y] : [30 * group + v, 30 * group + z]; }
|
|
40
|
+
|
|
41
|
+
export function decodePDF417(matrix, options = {}) {
|
|
42
|
+
if (!matrix?.width || !matrix?.height || (matrix.width - 69) % 17) throw new FormatError('PDF417: invalid matrix dimensions');
|
|
43
|
+
const cols = (matrix.width - 69) / 17, rowHeight = options.rowHeight ?? matrix.pdf417?.rowHeight ?? 3;
|
|
44
|
+
if (!Number.isInteger(rowHeight) || matrix.height % rowHeight) throw new FormatError('PDF417: invalid row height');
|
|
45
|
+
const rows = matrix.height / rowHeight;
|
|
46
|
+
if (rows < 3 || rows > 90 || cols < 1 || cols > 30) throw new FormatError('PDF417: dimensions outside the standard range');
|
|
47
|
+
const all = [];
|
|
48
|
+
const erasures = [];
|
|
49
|
+
for (let row = 0; row < rows; row++) {
|
|
50
|
+
const y = row * rowHeight, cluster = (row % 3) * 3;
|
|
51
|
+
if (bits(matrix, y, 0, 17).toString(2).padStart(17, '0') !== START || bits(matrix, y, matrix.width - 18, 18).toString(2).padStart(18, '0') !== STOP) throw new FormatError('PDF417: missing start or stop pattern');
|
|
52
|
+
const read = (x) => { const result = pdf417CodewordForPattern(bits(matrix, y, x, 17)); if (!result || result.cluster !== cluster) throw new FormatError('PDF417: invalid codeword pattern'); return result.codeword; };
|
|
53
|
+
const left = read(17), right = read(34 + cols * 17);
|
|
54
|
+
let matched = false; for (let level = 0; level <= 8; level++) { const expected = indicators(row, rows, cols, level); if (left === expected[0] && right === expected[1]) { matched = true; break; } }
|
|
55
|
+
if (!matched) throw new FormatError('PDF417: row indicator mismatch');
|
|
56
|
+
for (let col = 0; col < cols; col++) {
|
|
57
|
+
try { all.push(read(34 + col * 17)); }
|
|
58
|
+
catch {
|
|
59
|
+
erasures.push(all.length);
|
|
60
|
+
all.push(0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
let level = -1; for (let candidate = 0; candidate <= 8; candidate++) if (all.length > pdf417EccLength(candidate)) { level = candidate; break; }
|
|
65
|
+
// The row indicators determine the level uniquely across the symbol.
|
|
66
|
+
for (let candidate = 0; candidate <= 8; candidate++) {
|
|
67
|
+
let ok = true; for (let row = 0; row < rows; row++) { const y = row * rowHeight, cluster = (row % 3) * 3, expected = indicators(row, rows, cols, candidate); const left = pdf417CodewordForPattern(bits(matrix, y, 17, 17)); const right = pdf417CodewordForPattern(bits(matrix, y, 34 + cols * 17, 17)); if (!left || !right || left.cluster !== cluster || right.cluster !== cluster || left.codeword !== expected[0] || right.codeword !== expected[1]) { ok = false; break; } } if (ok) { level = candidate; break; }
|
|
68
|
+
}
|
|
69
|
+
if (level < 0) throw new FormatError('PDF417: could not determine error correction level');
|
|
70
|
+
const corrected = all.slice(); const corrections = pdf417CorrectErrors(corrected, level, erasures);
|
|
71
|
+
const length = corrected[0]; if (length < 1 || length > corrected.length - pdf417EccLength(level)) throw new FormatError('PDF417: invalid symbol length descriptor');
|
|
72
|
+
const payload = corrected.slice(1, length);
|
|
73
|
+
const decoded = decodePdf417CompactionDetailed(payload);
|
|
74
|
+
return { ...decoded, codewords: corrected, rows, columns: cols, eccLevel: level, corrections };
|
|
75
|
+
}
|