@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,361 @@
|
|
|
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
|
+
* Telepen Alpha and Telepen Numeric.
|
|
32
|
+
*
|
|
33
|
+
* Telepen does not assign an arbitrary glyph table to each character. It emits
|
|
34
|
+
* the seven-bit ASCII value with an even parity bit, least-significant bit
|
|
35
|
+
* first, and maps the resulting bit stream to narrow/wide bar-space pairs.
|
|
36
|
+
* Keeping that mapping algorithmic makes the implementation auditable and
|
|
37
|
+
* avoids shipping a copied third-party pattern table.
|
|
38
|
+
*
|
|
39
|
+
* @module oned/telepen
|
|
40
|
+
*/
|
|
41
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
42
|
+
import { EncodeError } from '../core/errors.js';
|
|
43
|
+
export const TELEPEN_START_VALUE = 0x5f;
|
|
44
|
+
export const TELEPEN_STOP_VALUE = 0x7a;
|
|
45
|
+
export const TELEPEN_MAX_LENGTH = 500;
|
|
46
|
+
/** @param {number} value @returns {string} Run widths for one 16-module glyph. */
|
|
47
|
+
export function telepenPattern(value) {
|
|
48
|
+
if (!Number.isInteger(value) || value < 0 || value > 127) {
|
|
49
|
+
throw new RangeError(`Telepen character value must be in 0..127, got ${value}`);
|
|
50
|
+
}
|
|
51
|
+
// Telepen carries seven-bit ASCII plus an even parity bit in the eighth
|
|
52
|
+
// position. The stream is transmitted least-significant bit first.
|
|
53
|
+
const bits = new Array(8).fill(0);
|
|
54
|
+
let ones = 0;
|
|
55
|
+
for (let bit = 0; bit < 7; bit++) {
|
|
56
|
+
bits[bit] = (value >>> bit) & 1;
|
|
57
|
+
ones += bits[bit];
|
|
58
|
+
}
|
|
59
|
+
bits[7] = ones & 1;
|
|
60
|
+
// The four legal bar/space pairs encode the bit stream without changing the
|
|
61
|
+
// fixed 16-module character width:
|
|
62
|
+
// 1 -> narrow bar, narrow space (11)
|
|
63
|
+
// 00 -> wide bar, narrow space (31)
|
|
64
|
+
// 010 -> wide bar, wide space (33)
|
|
65
|
+
// 01/10 edges -> narrow bar, wide space (13)
|
|
66
|
+
let widths = '';
|
|
67
|
+
let index = 0;
|
|
68
|
+
while (index < bits.length) {
|
|
69
|
+
if (bits[index] === 1) {
|
|
70
|
+
widths += '11';
|
|
71
|
+
index++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
let end = index + 1;
|
|
75
|
+
while (end < bits.length && bits[end] === 1)
|
|
76
|
+
end++;
|
|
77
|
+
if (end === index + 1) {
|
|
78
|
+
widths += '31';
|
|
79
|
+
index += 2;
|
|
80
|
+
}
|
|
81
|
+
else if (end === index + 2) {
|
|
82
|
+
widths += '33';
|
|
83
|
+
index += 3;
|
|
84
|
+
}
|
|
85
|
+
else if (end < bits.length) {
|
|
86
|
+
// A block 0 1* 0 longer than 010 has 01 and 10 edges. Any
|
|
87
|
+
// one-bits between those edges remain single-bit pairs.
|
|
88
|
+
widths += '13';
|
|
89
|
+
widths += '11'.repeat(Math.max(0, end - index - 3));
|
|
90
|
+
widths += '13';
|
|
91
|
+
index = end + 1;
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// A final 0 1* block has no trailing zero. The final light element is
|
|
95
|
+
// implied by the following character or quiet zone.
|
|
96
|
+
widths += '13';
|
|
97
|
+
widths += '11'.repeat(Math.max(0, end - index - 2));
|
|
98
|
+
index = end;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return widths;
|
|
102
|
+
}
|
|
103
|
+
/** @param {string} widths @returns {number} */
|
|
104
|
+
function widthTotal(widths) {
|
|
105
|
+
let total = 0;
|
|
106
|
+
for (const width of widths)
|
|
107
|
+
total += Number(width);
|
|
108
|
+
return total;
|
|
109
|
+
}
|
|
110
|
+
/** @param {string} widths @returns {BitMatrix} */
|
|
111
|
+
function widthsToMatrix(widths) {
|
|
112
|
+
const matrix = new BitMatrix(widthTotal(widths), 1);
|
|
113
|
+
let dark = true;
|
|
114
|
+
let x = 0;
|
|
115
|
+
for (const width of widths) {
|
|
116
|
+
const count = Number(width);
|
|
117
|
+
if (dark)
|
|
118
|
+
matrix.setRegion(x, 0, count, 1);
|
|
119
|
+
x += count;
|
|
120
|
+
dark = !dark;
|
|
121
|
+
}
|
|
122
|
+
return matrix;
|
|
123
|
+
}
|
|
124
|
+
/** @param {string} value @returns {number} */
|
|
125
|
+
function telepenChecksum(value) {
|
|
126
|
+
let sum = 0;
|
|
127
|
+
for (const character of value)
|
|
128
|
+
sum = (sum + character.charCodeAt(0)) % 127;
|
|
129
|
+
return (127 - sum) % 127;
|
|
130
|
+
}
|
|
131
|
+
/** @param {string} value @returns {number[]} */
|
|
132
|
+
function numericGlyphs(value) {
|
|
133
|
+
if (value.length % 2 !== 0) {
|
|
134
|
+
throw new EncodeError('Telepen Numeric: payload must contain an even number of characters');
|
|
135
|
+
}
|
|
136
|
+
const glyphs = [];
|
|
137
|
+
for (let index = 0; index < value.length; index += 2) {
|
|
138
|
+
const first = value[index];
|
|
139
|
+
const second = value[index + 1];
|
|
140
|
+
if (!/[0-9]/.test(first) || !/[0-9X]/.test(second)) {
|
|
141
|
+
throw new EncodeError('Telepen Numeric: pairs must contain digits, with X allowed only in the second position');
|
|
142
|
+
}
|
|
143
|
+
const firstDigit = Number(first);
|
|
144
|
+
glyphs.push(second === 'X'
|
|
145
|
+
? firstDigit + 17
|
|
146
|
+
: firstDigit * 10 + Number(second) + 27);
|
|
147
|
+
}
|
|
148
|
+
return glyphs;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Encode Telepen Alpha (full seven-bit ASCII) or Telepen Numeric.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} value
|
|
154
|
+
* @param {object} [options]
|
|
155
|
+
* @param {boolean} [options.numeric] Use two-digit numeric compaction.
|
|
156
|
+
* @returns {BitMatrix}
|
|
157
|
+
*/
|
|
158
|
+
export function encodeTelepen(value, options = {}) {
|
|
159
|
+
const text = String(value);
|
|
160
|
+
const numeric = options.numeric === true || options.mode === 'numeric' || options.telepenMode === 'numeric';
|
|
161
|
+
if (text.length > TELEPEN_MAX_LENGTH) {
|
|
162
|
+
throw new EncodeError(`Telepen: payload is limited to ${TELEPEN_MAX_LENGTH} characters`);
|
|
163
|
+
}
|
|
164
|
+
const glyphs = [TELEPEN_START_VALUE];
|
|
165
|
+
if (numeric) {
|
|
166
|
+
const compacted = numericGlyphs(text);
|
|
167
|
+
glyphs.push(...compacted);
|
|
168
|
+
const checksumValue = compacted.reduce((sum, glyph) => (sum + glyph) % 127, 0);
|
|
169
|
+
glyphs.push((127 - checksumValue) % 127);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
for (const character of text) {
|
|
173
|
+
const code = character.charCodeAt(0);
|
|
174
|
+
if (code > 127) {
|
|
175
|
+
throw new EncodeError(`Telepen: character U+${code.toString(16).toUpperCase()} is outside ASCII`);
|
|
176
|
+
}
|
|
177
|
+
glyphs.push(code);
|
|
178
|
+
}
|
|
179
|
+
glyphs.push(telepenChecksum(text));
|
|
180
|
+
}
|
|
181
|
+
glyphs.push(TELEPEN_STOP_VALUE);
|
|
182
|
+
return widthsToMatrix(glyphs.map(telepenPattern).join(''));
|
|
183
|
+
}
|
|
184
|
+
/** Encode Telepen Numeric explicitly. @param {string} value @returns {BitMatrix} */
|
|
185
|
+
export function encodeTelepenNumeric(value) {
|
|
186
|
+
return encodeTelepen(value, { numeric: true });
|
|
187
|
+
}
|
|
188
|
+
const TELEPEN_RUNS = Array.from({ length: 128 }, (_, value) => telepenPattern(value).split('').map(Number));
|
|
189
|
+
const START_RUNS = TELEPEN_RUNS[TELEPEN_START_VALUE];
|
|
190
|
+
const STOP_RUNS = TELEPEN_RUNS[TELEPEN_STOP_VALUE];
|
|
191
|
+
/** @param {Uint8Array} row @param {number} start @param {number} count */
|
|
192
|
+
function measureRuns(row, start, count) {
|
|
193
|
+
if (start < 0 || start >= row.length || row[start] !== 1)
|
|
194
|
+
return null;
|
|
195
|
+
const counters = new Array(count).fill(0);
|
|
196
|
+
let run = 0;
|
|
197
|
+
let dark = true;
|
|
198
|
+
let index = start;
|
|
199
|
+
while (index < row.length) {
|
|
200
|
+
const pixelDark = row[index] === 1;
|
|
201
|
+
if (pixelDark === dark) {
|
|
202
|
+
counters[run]++;
|
|
203
|
+
index++;
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
run++;
|
|
207
|
+
if (run === count)
|
|
208
|
+
break;
|
|
209
|
+
dark = !dark;
|
|
210
|
+
counters[run] = 1;
|
|
211
|
+
index++;
|
|
212
|
+
}
|
|
213
|
+
if (run < count - 1 || counters[count - 1] === 0)
|
|
214
|
+
return null;
|
|
215
|
+
return { counters, end: index };
|
|
216
|
+
}
|
|
217
|
+
/** @param {number[]} counters @param {number[]} expected @param {number} [ignoredTail] */
|
|
218
|
+
function runVariance(counters, expected, ignoredTail = 0) {
|
|
219
|
+
const total = counters.reduce((sum, width) => sum + width, 0);
|
|
220
|
+
const expectedTotal = expected.reduce((sum, width) => sum + width, 0);
|
|
221
|
+
const compared = expected.length - ignoredTail;
|
|
222
|
+
const comparedTotal = expected
|
|
223
|
+
.slice(0, compared)
|
|
224
|
+
.reduce((sum, width) => sum + width, 0);
|
|
225
|
+
const measuredCompared = counters
|
|
226
|
+
.slice(0, compared)
|
|
227
|
+
.reduce((sum, width) => sum + width, 0);
|
|
228
|
+
if (total <= 0 || expectedTotal <= 0 || measuredCompared <= 0)
|
|
229
|
+
return Infinity;
|
|
230
|
+
// For normal glyphs all sixteen modules participate. Stop's final light
|
|
231
|
+
// run includes the quiet zone, so its first eleven runs establish scale.
|
|
232
|
+
const unit = ignoredTail > 0 ? measuredCompared / comparedTotal : total / expectedTotal;
|
|
233
|
+
const limit = unit * 0.85;
|
|
234
|
+
let variance = 0;
|
|
235
|
+
for (let index = 0; index < compared; index++) {
|
|
236
|
+
const delta = Math.abs(counters[index] - expected[index] * unit);
|
|
237
|
+
if (delta > limit)
|
|
238
|
+
return Infinity;
|
|
239
|
+
variance += delta;
|
|
240
|
+
}
|
|
241
|
+
return variance / Math.max(1, measuredCompared);
|
|
242
|
+
}
|
|
243
|
+
/** @param {Uint8Array} row @param {number} start @param {number[]} expected */
|
|
244
|
+
function matchGlyph(row, start, expected) {
|
|
245
|
+
const measured = measureRuns(row, start, expected.length);
|
|
246
|
+
if (!measured)
|
|
247
|
+
return null;
|
|
248
|
+
const score = runVariance(measured.counters, expected);
|
|
249
|
+
if (!Number.isFinite(score) || measured.end >= row.length)
|
|
250
|
+
return null;
|
|
251
|
+
return { score, end: measured.end };
|
|
252
|
+
}
|
|
253
|
+
/** @param {Uint8Array} row @param {number} start */
|
|
254
|
+
function matchStop(row, start) {
|
|
255
|
+
const measured = measureRuns(row, start, STOP_RUNS.length);
|
|
256
|
+
if (!measured)
|
|
257
|
+
return null;
|
|
258
|
+
const score = runVariance(measured.counters, STOP_RUNS, 1);
|
|
259
|
+
if (!Number.isFinite(score))
|
|
260
|
+
return null;
|
|
261
|
+
for (let index = measured.end; index < row.length; index++) {
|
|
262
|
+
if (row[index] === 1)
|
|
263
|
+
return { score, end: measured.end, terminal: false };
|
|
264
|
+
}
|
|
265
|
+
return { score, end: measured.end, terminal: true };
|
|
266
|
+
}
|
|
267
|
+
/** @param {Uint8Array} row @param {number} start @param {number[]} expected */
|
|
268
|
+
function findStart(row, start, expected) {
|
|
269
|
+
for (let index = Math.max(0, start); index < row.length; index++) {
|
|
270
|
+
if (row[index] !== 1 || (index > 0 && row[index - 1] === 1))
|
|
271
|
+
continue;
|
|
272
|
+
const measured = measureRuns(row, index, expected.length);
|
|
273
|
+
if (!measured)
|
|
274
|
+
continue;
|
|
275
|
+
const score = runVariance(measured.counters, expected);
|
|
276
|
+
if (Number.isFinite(score) && measured.end < row.length) {
|
|
277
|
+
return { end: measured.end, score };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
/** @param {number[]} glyphs @returns {boolean} */
|
|
283
|
+
function validChecksum(glyphs) {
|
|
284
|
+
if (glyphs.length < 1)
|
|
285
|
+
return false;
|
|
286
|
+
const checksum = glyphs[glyphs.length - 1];
|
|
287
|
+
const payload = glyphs.slice(0, -1);
|
|
288
|
+
const sum = payload.reduce((total, value) => (total + value) % 127, 0);
|
|
289
|
+
return checksum === (127 - sum) % 127;
|
|
290
|
+
}
|
|
291
|
+
/** @param {number[]} glyphs @returns {string|null} */
|
|
292
|
+
function decodeNumericGlyphs(glyphs) {
|
|
293
|
+
let text = '';
|
|
294
|
+
for (const glyph of glyphs) {
|
|
295
|
+
if (glyph >= 17 && glyph <= 26) {
|
|
296
|
+
text += `${glyph - 17}X`;
|
|
297
|
+
}
|
|
298
|
+
else if (glyph >= 27 && glyph <= 126) {
|
|
299
|
+
text += String(glyph - 27).padStart(2, '0');
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return text;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Decode a Telepen scanline. The row must be binarized (1 = dark).
|
|
309
|
+
*
|
|
310
|
+
* @param {Uint8Array} row
|
|
311
|
+
* @param {object} [options]
|
|
312
|
+
* @param {boolean} [options.numeric] Decode two-digit numeric glyphs.
|
|
313
|
+
* @returns {{format:'telepen'|'telepennumeric', text:string, mode:'ascii'|'numeric'}|null}
|
|
314
|
+
*/
|
|
315
|
+
export function decodeTelepen(row, options = {}) {
|
|
316
|
+
const numeric = options.numeric === true || options.mode === 'numeric' || options.telepenMode === 'numeric';
|
|
317
|
+
const start = findStart(row, 0, START_RUNS);
|
|
318
|
+
if (!start)
|
|
319
|
+
return null;
|
|
320
|
+
const glyphs = [];
|
|
321
|
+
let offset = start.end;
|
|
322
|
+
for (let count = 0; count <= TELEPEN_MAX_LENGTH + 1; count++) {
|
|
323
|
+
const stop = matchStop(row, offset);
|
|
324
|
+
if (stop?.terminal) {
|
|
325
|
+
if (!validChecksum(glyphs))
|
|
326
|
+
return null;
|
|
327
|
+
glyphs.pop();
|
|
328
|
+
if (numeric) {
|
|
329
|
+
const text = decodeNumericGlyphs(glyphs);
|
|
330
|
+
return text === null ? null : { format: 'telepennumeric', text, mode: 'numeric' };
|
|
331
|
+
}
|
|
332
|
+
return {
|
|
333
|
+
format: 'telepen',
|
|
334
|
+
text: glyphs.map((value) => String.fromCharCode(value)).join(''),
|
|
335
|
+
mode: 'ascii',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const candidates = [];
|
|
339
|
+
for (let value = 0; value < TELEPEN_RUNS.length; value++) {
|
|
340
|
+
const match = matchGlyph(row, offset, TELEPEN_RUNS[value]);
|
|
341
|
+
if (match)
|
|
342
|
+
candidates.push({ value, ...match });
|
|
343
|
+
}
|
|
344
|
+
if (candidates.length === 0)
|
|
345
|
+
return null;
|
|
346
|
+
candidates.sort((a, b) => a.score - b.score);
|
|
347
|
+
const best = candidates[0];
|
|
348
|
+
const second = candidates[1];
|
|
349
|
+
// A close tie is an ambiguous optical measurement. Returning nothing is
|
|
350
|
+
// safer than guessing a character that merely has a similar run profile.
|
|
351
|
+
if (second && second.score - best.score < 0.035)
|
|
352
|
+
return null;
|
|
353
|
+
glyphs.push(best.value);
|
|
354
|
+
offset = best.end;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
/** Decode Telepen Numeric explicitly. @param {Uint8Array} row */
|
|
359
|
+
export function decodeTelepenNumeric(row) {
|
|
360
|
+
return decodeTelepen(row, { numeric: true });
|
|
361
|
+
}
|
package/src/js/oned/writers.js
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
*/
|
|
40
40
|
import { BitMatrix } from '../core/bit-matrix.js';
|
|
41
41
|
import { EncodeError } from '../core/errors.js';
|
|
42
|
-
import { EAN_L, EAN_G, EAN_R, EAN13_PARITY, UPCE_PARITY, EAN_START_END, EAN_MIDDLE, UPCE_END, CODE39, CODE39_CHECK_SET, CODE39_EXTENDED, CODE93, CODE93_VALUES, CODE93_START_STOP, CODE128, CODE128_START_B, CODE128_START_C, CODE128_STOP, CODE128_FNC1, CODE128_CODE_A, CODE128_CODE_B, CODE128_CODE_C, ITF, CODABAR, CODABAR_START_STOP, CODE11, CODE11_START_STOP, MSI_BIT, MSI_START, MSI_STOP, } from './patterns.js';
|
|
42
|
+
import { EAN_L, EAN_G, EAN_R, EAN13_PARITY, UPCE_PARITY, EAN_START_END, EAN_MIDDLE, UPCE_END, CODE39, CODE39_CHECK_SET, CODE39_EXTENDED, CODE93, CODE93_VALUES, CODE93_START_STOP, CODE128, CODE128_START_A, CODE128_START_B, CODE128_START_C, CODE128_STOP, CODE128_FNC1, CODE128_CODE_A, CODE128_CODE_B, CODE128_CODE_C, ITF, CODABAR, CODABAR_START_STOP, CODE11, CODE11_START_STOP, MSI_BIT, MSI_START, MSI_STOP, } from './patterns.js';
|
|
43
43
|
/* ------------------------------------------------------------------ *
|
|
44
44
|
* Shared helpers
|
|
45
45
|
* ------------------------------------------------------------------ */
|
|
@@ -344,6 +344,148 @@ export function encodeCode39(value, options = {}) {
|
|
|
344
344
|
const parts = ['*', ...payload, '*'].map((ch) => expandNarrowWide(CODE39[ch], wideRatio));
|
|
345
345
|
return toMatrix(parts.join('0'));
|
|
346
346
|
}
|
|
347
|
+
/* ------------------------------------------------------------------ *
|
|
348
|
+
* Code 32 / PZN
|
|
349
|
+
* ------------------------------------------------------------------ */
|
|
350
|
+
/** Code 32's six-character alphabet after its four skipped vowels. */
|
|
351
|
+
const CODE32_ALPHABET = '0123456789ABCDEFGHIJKLMNOPQRSTUV';
|
|
352
|
+
/**
|
|
353
|
+
* Convert a Code 32 base-32 digit to the printed character set. The standard
|
|
354
|
+
* skips A, E, I and O so the human-readable text cannot be mistaken for a
|
|
355
|
+
* vowel-heavy word. The successive threshold shifts are intentionally
|
|
356
|
+
* expressed as rules rather than copied lookup data.
|
|
357
|
+
*/
|
|
358
|
+
function code32PrintCharacter(character) {
|
|
359
|
+
let code = character.charCodeAt(0);
|
|
360
|
+
for (const vowel of 'AEIO') {
|
|
361
|
+
if (code >= vowel.charCodeAt(0))
|
|
362
|
+
code++;
|
|
363
|
+
}
|
|
364
|
+
return String.fromCharCode(code);
|
|
365
|
+
}
|
|
366
|
+
const CODE32_PRINT_ALPHABET = [...CODE32_ALPHABET].map(code32PrintCharacter).join('');
|
|
367
|
+
const CODE32_DECODE = new Map([...CODE32_PRINT_ALPHABET].map((ch, index) => [ch, index]));
|
|
368
|
+
/**
|
|
369
|
+
* Italian Code 32 (Italian Pharmacode) check digit.
|
|
370
|
+
*
|
|
371
|
+
* @param {string} value The eight-digit body, without the check digit.
|
|
372
|
+
* @returns {number}
|
|
373
|
+
*/
|
|
374
|
+
export function code32CheckDigit(value) {
|
|
375
|
+
let sum = 0;
|
|
376
|
+
for (let i = 0; i < 8; i++) {
|
|
377
|
+
let digit = Number(value[i]);
|
|
378
|
+
if (i % 2 === 1) {
|
|
379
|
+
digit *= 2;
|
|
380
|
+
if (digit > 9)
|
|
381
|
+
digit -= 9;
|
|
382
|
+
}
|
|
383
|
+
sum += digit;
|
|
384
|
+
}
|
|
385
|
+
return sum % 10;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Encode the Italian Code 32 pharmaceutical identifier through Code 39.
|
|
389
|
+
* Accepts the eight-digit body or the same body followed by its check digit.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} value Eight or nine digits.
|
|
392
|
+
* @returns {BitMatrix}
|
|
393
|
+
*/
|
|
394
|
+
export function encodeCode32(value) {
|
|
395
|
+
const digits = String(value);
|
|
396
|
+
if (!/^\d{8,9}$/.test(digits)) {
|
|
397
|
+
throw new EncodeError('Code 32: payload must contain 8 or 9 digits');
|
|
398
|
+
}
|
|
399
|
+
const body = digits.slice(0, 8);
|
|
400
|
+
const check = code32CheckDigit(body);
|
|
401
|
+
if (digits.length === 9 && Number(digits[8]) !== check) {
|
|
402
|
+
throw new EncodeError(`Code 32: invalid check digit ${digits[8]}, expected ${check}`);
|
|
403
|
+
}
|
|
404
|
+
const complete = body + String(check);
|
|
405
|
+
let base32 = BigInt(complete).toString(32).toUpperCase().padStart(6, '0');
|
|
406
|
+
base32 = [...base32].map(code32PrintCharacter).join('');
|
|
407
|
+
return encodeCode39(base32);
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Decode the Code 39 payload of an Italian Code 32 symbol.
|
|
411
|
+
*
|
|
412
|
+
* This helper is deliberately kept separate from the image reader so it can
|
|
413
|
+
* validate the numeric/check-digit grammar without introducing a module cycle.
|
|
414
|
+
*
|
|
415
|
+
* @param {string} text Code 39 payload, excluding its asterisks.
|
|
416
|
+
* @returns {{text:string, checkDigit:number}|null}
|
|
417
|
+
*/
|
|
418
|
+
export function decodeCode32Payload(text) {
|
|
419
|
+
if (text.length !== 6)
|
|
420
|
+
return null;
|
|
421
|
+
let value = 0n;
|
|
422
|
+
for (const ch of text) {
|
|
423
|
+
const digit = CODE32_DECODE.get(ch);
|
|
424
|
+
if (digit === undefined)
|
|
425
|
+
return null;
|
|
426
|
+
value = value * 32n + BigInt(digit);
|
|
427
|
+
}
|
|
428
|
+
const complete = value.toString(10).padStart(9, '0');
|
|
429
|
+
if (complete.length !== 9)
|
|
430
|
+
return null;
|
|
431
|
+
const body = complete.slice(0, 8);
|
|
432
|
+
const checkDigit = code32CheckDigit(body);
|
|
433
|
+
if (Number(complete[8]) !== checkDigit)
|
|
434
|
+
return null;
|
|
435
|
+
return { text: body, checkDigit };
|
|
436
|
+
}
|
|
437
|
+
function pznCheckDigit(body, pzn8) {
|
|
438
|
+
let sum = 0;
|
|
439
|
+
const offset = pzn8 ? 1 : 2;
|
|
440
|
+
for (let i = 0; i < body.length; i++)
|
|
441
|
+
sum += Number(body[i]) * (i + offset);
|
|
442
|
+
const check = sum % 11;
|
|
443
|
+
return check === 10 ? null : check;
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Encode a Pharmazentralnummer (PZN-7 or PZN-8) through Code 39.
|
|
447
|
+
*
|
|
448
|
+
* @param {string} value Six/seven digits for PZN-7, or seven/eight for PZN-8.
|
|
449
|
+
* @param {object} [options]
|
|
450
|
+
* @param {boolean} [options.pzn8=false] Select the modern eight-digit body.
|
|
451
|
+
* @param {'pzn7'|'pzn8'} [options.variant] Alias for `pzn8`.
|
|
452
|
+
* @returns {BitMatrix}
|
|
453
|
+
*/
|
|
454
|
+
export function encodePZN(value, options = {}) {
|
|
455
|
+
const pzn8 = options.pzn8 === true || options.variant === 'pzn8';
|
|
456
|
+
const digits = String(value);
|
|
457
|
+
const bodyLength = pzn8 ? 7 : 6;
|
|
458
|
+
if (!new RegExp(`^\\d{${bodyLength},${bodyLength + 1}}$`).test(digits)) {
|
|
459
|
+
throw new EncodeError(`PZN-${pzn8 ? 8 : 7}: payload must contain ${bodyLength} or ${bodyLength + 1} digits`);
|
|
460
|
+
}
|
|
461
|
+
const body = digits.slice(0, bodyLength);
|
|
462
|
+
const check = pznCheckDigit(body, pzn8);
|
|
463
|
+
if (check === null)
|
|
464
|
+
throw new EncodeError('PZN: input sequence produces the reserved check value 10');
|
|
465
|
+
if (digits.length === bodyLength + 1 && Number(digits[bodyLength]) !== check) {
|
|
466
|
+
throw new EncodeError(`PZN: invalid check digit ${digits[bodyLength]}, expected ${check}`);
|
|
467
|
+
}
|
|
468
|
+
return encodeCode39(`-${body}${check}`);
|
|
469
|
+
}
|
|
470
|
+
/**
|
|
471
|
+
* Decode the Code 39 payload of a PZN symbol.
|
|
472
|
+
*
|
|
473
|
+
* @param {string} text Code 39 payload, excluding its asterisks.
|
|
474
|
+
* @returns {{text:string, variant:'pzn7'|'pzn8', checkDigit:number}|null}
|
|
475
|
+
*/
|
|
476
|
+
export function decodePZNPayload(text) {
|
|
477
|
+
if (!/^-\d+$/.test(text))
|
|
478
|
+
return null;
|
|
479
|
+
const digits = text.slice(1);
|
|
480
|
+
const pzn8 = digits.length === 8;
|
|
481
|
+
if (digits.length !== 7 && !pzn8)
|
|
482
|
+
return null;
|
|
483
|
+
const body = digits.slice(0, -1);
|
|
484
|
+
const checkDigit = pznCheckDigit(body, pzn8);
|
|
485
|
+
if (checkDigit === null || Number(digits.at(-1)) !== checkDigit)
|
|
486
|
+
return null;
|
|
487
|
+
return { text: body, variant: pzn8 ? 'pzn8' : 'pzn7', checkDigit };
|
|
488
|
+
}
|
|
347
489
|
/* ------------------------------------------------------------------ *
|
|
348
490
|
* Code 93
|
|
349
491
|
* ------------------------------------------------------------------ */
|
|
@@ -386,20 +528,19 @@ export function encodeCode93(value) {
|
|
|
386
528
|
/** Set B maps printable ASCII starting at space to symbol value 0. */
|
|
387
529
|
const CODE128_B_OFFSET = 32;
|
|
388
530
|
/**
|
|
389
|
-
* Code 128
|
|
531
|
+
* Build the Code 128 data stream without the checksum and stop symbol.
|
|
390
532
|
*
|
|
391
|
-
*
|
|
392
|
-
*
|
|
393
|
-
* the
|
|
394
|
-
*
|
|
533
|
+
* Stacked Code 128 symbologies use the same A/B/C data alphabet but provide
|
|
534
|
+
* their own row framing and checks. Keeping the tokeniser here makes those
|
|
535
|
+
* writers use the exact same ASCII and numeric rules as the ordinary Code 128
|
|
536
|
+
* writer. `startSet` is an optional explicit starting set for a stacked row.
|
|
395
537
|
*
|
|
396
538
|
* @param {string} value
|
|
397
|
-
* @param {
|
|
398
|
-
* @
|
|
399
|
-
* @returns {BitMatrix}
|
|
539
|
+
* @param {{gs1?: boolean, startSet?: 'A'|'B'|'C'}} [options]
|
|
540
|
+
* @returns {{start: number, values: number[], mode: 'A'|'B'|'C'}}
|
|
400
541
|
*/
|
|
401
|
-
export function
|
|
402
|
-
const { gs1 = false } = options;
|
|
542
|
+
export function code128DataCodewords(value, options = {}) {
|
|
543
|
+
const { gs1 = false, startSet = null } = options;
|
|
403
544
|
for (const ch of value) {
|
|
404
545
|
if (ch.charCodeAt(0) > 127) {
|
|
405
546
|
throw new EncodeError(`Code 128: '${ch}' is outside ASCII`);
|
|
@@ -416,7 +557,22 @@ export function encodeCode128(value, options = {}) {
|
|
|
416
557
|
let mode;
|
|
417
558
|
let i = 0;
|
|
418
559
|
const startRun = digitRun(0);
|
|
419
|
-
if (
|
|
560
|
+
if (startSet === 'A') {
|
|
561
|
+
codes.push(CODE128_START_A);
|
|
562
|
+
mode = 'A';
|
|
563
|
+
}
|
|
564
|
+
else if (startSet === 'C') {
|
|
565
|
+
if (startRun < 2 || startRun % 2 !== 0) {
|
|
566
|
+
throw new EncodeError('Code 128: set C requires an even leading digit run');
|
|
567
|
+
}
|
|
568
|
+
codes.push(CODE128_START_C);
|
|
569
|
+
mode = 'C';
|
|
570
|
+
}
|
|
571
|
+
else if (startSet === 'B') {
|
|
572
|
+
codes.push(CODE128_START_B);
|
|
573
|
+
mode = 'B';
|
|
574
|
+
}
|
|
575
|
+
else if (startRun >= 4 && startRun % 2 === 0) {
|
|
420
576
|
codes.push(CODE128_START_C);
|
|
421
577
|
mode = 'C';
|
|
422
578
|
}
|
|
@@ -470,6 +626,24 @@ export function encodeCode128(value, options = {}) {
|
|
|
470
626
|
}
|
|
471
627
|
i++;
|
|
472
628
|
}
|
|
629
|
+
return { start: codes[0], values: codes.slice(1), mode };
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Code 128, with automatic code-set selection.
|
|
633
|
+
*
|
|
634
|
+
* The heuristic: switch into set C when enough consecutive digits are present
|
|
635
|
+
* to repay the switch symbol — four at the start or end of the payload, six in
|
|
636
|
+
* the middle, since C packs two digits per symbol. An encoder that never
|
|
637
|
+
* switches produces a valid but needlessly wide symbol.
|
|
638
|
+
*
|
|
639
|
+
* @param {string} value
|
|
640
|
+
* @param {object} [options]
|
|
641
|
+
* @param {boolean} [options.gs1] Emit a leading FNC1, making this GS1-128.
|
|
642
|
+
* @returns {BitMatrix}
|
|
643
|
+
*/
|
|
644
|
+
export function encodeCode128(value, options = {}) {
|
|
645
|
+
const data = code128DataCodewords(value, options);
|
|
646
|
+
const codes = [data.start, ...data.values];
|
|
473
647
|
// Checksum: the start value plus each symbol weighted by its position.
|
|
474
648
|
let sum = codes[0];
|
|
475
649
|
for (let k = 1; k < codes.length; k++)
|