@sythos/js_barcode_universal 1.1.0 → 1.3.1
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 +24 -18
- package/NOTICE.md +27 -22
- package/README.md +596 -460
- package/bundle/sythos-barcode.esm.js +4606 -228
- package/bundle/sythos-barcode.js +4586 -228
- package/examples/create.html +1011 -731
- package/licenses/README.md +23 -18
- package/licenses/aztec-code.license +9 -7
- package/licenses/codabar.license +16 -13
- package/licenses/code-11.license +11 -9
- package/licenses/code-128.license +8 -6
- package/licenses/code-39.license +8 -6
- package/licenses/code-93.license +13 -11
- package/licenses/data-matrix.license +11 -9
- package/licenses/ean-13.license +7 -5
- package/licenses/ean-8.license +7 -5
- package/licenses/frameqr.license +84 -0
- package/licenses/gs1-128.license +7 -5
- package/licenses/isbn.license +8 -6
- package/licenses/itf-14.license +7 -5
- package/licenses/itf.license +7 -5
- package/licenses/micro-qr.license +79 -0
- package/licenses/micropdf417.license +81 -0
- package/licenses/msi-plessey.license +12 -10
- package/licenses/pdf417.license +78 -0
- package/licenses/pharmacode.license +13 -11
- package/licenses/qr-code.license +7 -5
- package/licenses/rmqr.license +79 -0
- package/licenses/upc-a.license +9 -7
- package/licenses/upc-e.license +7 -5
- package/package.json +104 -88
- package/src/core/reed-solomon.js +326 -312
- 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/frameqr/decoder.js +239 -0
- package/src/frameqr/detector.js +192 -0
- package/src/frameqr/encoder.js +156 -0
- package/src/frameqr/index.js +42 -0
- package/src/frameqr/tables.js +270 -0
- package/src/index.js +166 -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/microqr/decoder.js +245 -0
- package/src/microqr/detector.js +355 -0
- package/src/microqr/encoder.js +269 -0
- package/src/microqr/index.js +36 -0
- package/src/microqr/tables.js +316 -0
- package/src/oned/index.js +59 -59
- 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
- package/src/rmqr/decoder.js +101 -0
- package/src/rmqr/detector.js +90 -0
- package/src/rmqr/encoder.js +172 -0
- package/src/rmqr/index.js +37 -0
- package/src/rmqr/tables.js +154 -0
|
@@ -0,0 +1,468 @@
|
|
|
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 { BitMatrix } from '../core/bit-matrix.js';
|
|
32
|
+
import { sampleGrid, sampleGridVoting } from '../image/grid-sampler.js';
|
|
33
|
+
import { PerspectiveTransform } from '../image/perspective.js';
|
|
34
|
+
import { decodePDF417 } from './decoder.js';
|
|
35
|
+
|
|
36
|
+
const START_PATTERN = [8, 1, 1, 1, 1, 1, 1, 3];
|
|
37
|
+
const STOP_PATTERN = [7, 1, 1, 3, 1, 1, 1, 2, 1];
|
|
38
|
+
const SCAN_ANGLES = [0, -4, 4, -8, 8, -14, 14, -22, 22, -32, 32]
|
|
39
|
+
.map((degrees) => degrees * Math.PI / 180);
|
|
40
|
+
|
|
41
|
+
function rotateClockwise(source) {
|
|
42
|
+
const rotated = new BitMatrix(source.height, source.width);
|
|
43
|
+
for (let y = 0; y < source.height; y++) for (let x = 0; x < source.width; x++) {
|
|
44
|
+
if (source.get(x, y)) rotated.set(source.height - 1 - y, x);
|
|
45
|
+
}
|
|
46
|
+
return rotated;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scanGeometry(angle) {
|
|
50
|
+
return {
|
|
51
|
+
along: { x: Math.cos(angle), y: Math.sin(angle) },
|
|
52
|
+
across: { x: -Math.sin(angle), y: Math.cos(angle) },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function projectionRange(image, vector) {
|
|
57
|
+
const points = [
|
|
58
|
+
{ x: 0, y: 0 }, { x: image.width - 1, y: 0 },
|
|
59
|
+
{ x: image.width - 1, y: image.height - 1 }, { x: 0, y: image.height - 1 },
|
|
60
|
+
];
|
|
61
|
+
const values = points.map((point) => point.x * vector.x + point.y * vector.y);
|
|
62
|
+
return { min: Math.min(...values), max: Math.max(...values) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function lineRange(image, geometry, across) {
|
|
66
|
+
const { along, across: normal } = geometry;
|
|
67
|
+
let min = -Infinity, max = Infinity;
|
|
68
|
+
const constrain = (low, high, step, offset) => {
|
|
69
|
+
if (Math.abs(step) < 1e-9) return offset >= low && offset <= high;
|
|
70
|
+
const first = (low - offset) / step, second = (high - offset) / step;
|
|
71
|
+
min = Math.max(min, Math.min(first, second));
|
|
72
|
+
max = Math.min(max, Math.max(first, second));
|
|
73
|
+
return min <= max;
|
|
74
|
+
};
|
|
75
|
+
if (!constrain(0, image.width - 1, along.x, normal.x * across) ||
|
|
76
|
+
!constrain(0, image.height - 1, along.y, normal.y * across)) return null;
|
|
77
|
+
return { min: Math.ceil(min), max: Math.floor(max) };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function runsInLine(image, geometry, across) {
|
|
81
|
+
const range = lineRange(image, geometry, across);
|
|
82
|
+
if (!range || range.max < range.min) return [];
|
|
83
|
+
const { along, across: normal } = geometry;
|
|
84
|
+
const runs = [];
|
|
85
|
+
const valueAt = (position) => image.get(
|
|
86
|
+
Math.round(along.x * position + normal.x * across),
|
|
87
|
+
Math.round(along.y * position + normal.y * across),
|
|
88
|
+
);
|
|
89
|
+
let dark = valueAt(range.min), start = range.min;
|
|
90
|
+
for (let position = range.min + 1; position <= range.max + 1; position++) {
|
|
91
|
+
const value = position <= range.max ? valueAt(position) : !dark;
|
|
92
|
+
if (value !== dark) {
|
|
93
|
+
runs.push({ dark, start, end: position, length: position - start });
|
|
94
|
+
start = position; dark = value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return runs;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function removeSinglePixelSpecks(runs) {
|
|
101
|
+
const clean = runs.map((run) => ({ ...run }));
|
|
102
|
+
for (let index = 1; index < clean.length - 1;) {
|
|
103
|
+
if (clean[index].length > 1) { index++; continue; }
|
|
104
|
+
const merged = {
|
|
105
|
+
dark: clean[index - 1].dark,
|
|
106
|
+
start: clean[index - 1].start,
|
|
107
|
+
end: clean[index + 1].end,
|
|
108
|
+
length: clean[index - 1].length + clean[index].length + clean[index + 1].length,
|
|
109
|
+
};
|
|
110
|
+
clean.splice(index - 1, 3, merged);
|
|
111
|
+
index = Math.max(1, index - 2);
|
|
112
|
+
}
|
|
113
|
+
return clean;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function matchPattern(runs, at, expected) {
|
|
117
|
+
if (at + expected.length > runs.length || !runs[at].dark) return null;
|
|
118
|
+
let observed = 0, modules = 0;
|
|
119
|
+
for (let i = 0; i < expected.length; i++) { observed += runs[at + i].length; modules += expected[i]; }
|
|
120
|
+
const scale = observed / modules;
|
|
121
|
+
if (scale < 0.65) return null;
|
|
122
|
+
let error = 0;
|
|
123
|
+
for (let i = 0; i < expected.length; i++) {
|
|
124
|
+
const delta = Math.abs(runs[at + i].length - expected[i] * scale);
|
|
125
|
+
if (delta > Math.max(1.15, scale * 0.62)) return null;
|
|
126
|
+
error += delta / scale;
|
|
127
|
+
}
|
|
128
|
+
if (error / expected.length > 0.48) return null;
|
|
129
|
+
return { start: runs[at].start, end: runs[at + expected.length - 1].end, scale, error, at };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function quietPenalty(runs, start, stop) {
|
|
133
|
+
const before = start.at > 0 ? runs[start.at - 1].length / start.scale : 0;
|
|
134
|
+
const afterIndex = stop.at + STOP_PATTERN.length;
|
|
135
|
+
const after = afterIndex < runs.length ? runs[afterIndex].length / stop.scale : 0;
|
|
136
|
+
return Math.max(0, 2 - before) + Math.max(0, 2 - after);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function patternPairs(runs) {
|
|
140
|
+
const starts = [], stops = [];
|
|
141
|
+
for (let at = 0; at < runs.length; at++) {
|
|
142
|
+
const start = matchPattern(runs, at, START_PATTERN); if (start) starts.push(start);
|
|
143
|
+
const stop = matchPattern(runs, at, STOP_PATTERN); if (stop) stops.push(stop);
|
|
144
|
+
}
|
|
145
|
+
const pairs = [];
|
|
146
|
+
for (const start of starts) for (const stop of stops) {
|
|
147
|
+
if (stop.start <= start.end) continue;
|
|
148
|
+
const measured = stop.end - start.start;
|
|
149
|
+
const localScale = Math.sqrt(start.scale * stop.scale);
|
|
150
|
+
const roughColumns = Math.round((measured / localScale - 69) / 17);
|
|
151
|
+
for (let columns = Math.max(1, roughColumns - 2); columns <= Math.min(30, roughColumns + 2); columns++) {
|
|
152
|
+
const width = 69 + columns * 17;
|
|
153
|
+
const globalScale = measured / width;
|
|
154
|
+
const ratio = Math.max(start.scale, stop.scale, globalScale) /
|
|
155
|
+
Math.min(start.scale, stop.scale, globalScale);
|
|
156
|
+
if (ratio > 1.85) continue;
|
|
157
|
+
const scaleError = Math.abs(Math.log(start.scale / globalScale)) +
|
|
158
|
+
Math.abs(Math.log(stop.scale / globalScale));
|
|
159
|
+
pairs.push({ start, stop, columns, width, scale: globalScale,
|
|
160
|
+
startScale: start.scale, stopScale: stop.scale,
|
|
161
|
+
error: start.error + stop.error + scaleError * 4 + quietPenalty(runs, start, stop) * 0.15 });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
pairs.sort((a, b) => a.error - b.error);
|
|
165
|
+
const used = new Set();
|
|
166
|
+
return pairs.filter((pair) => {
|
|
167
|
+
if (used.has(pair.columns)) return false;
|
|
168
|
+
used.add(pair.columns);
|
|
169
|
+
return used.size <= 3;
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function pointAt(geometry, along, across) {
|
|
174
|
+
return {
|
|
175
|
+
x: geometry.along.x * along + geometry.across.x * across,
|
|
176
|
+
y: geometry.along.y * along + geometry.across.y * across,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function scanHits(image, angle) {
|
|
181
|
+
const geometry = scanGeometry(angle);
|
|
182
|
+
const range = projectionRange(image, geometry.across);
|
|
183
|
+
const hits = [];
|
|
184
|
+
for (let across = Math.ceil(range.min); across <= Math.floor(range.max); across++) {
|
|
185
|
+
const raw = runsInLine(image, geometry, across);
|
|
186
|
+
let pairs = patternPairs(raw);
|
|
187
|
+
if (!pairs.length) pairs = patternPairs(removeSinglePixelSpecks(raw));
|
|
188
|
+
for (const pair of pairs) {
|
|
189
|
+
hits.push({ across, left: pair.start.start, right: pair.stop.end,
|
|
190
|
+
leftPoint: pointAt(geometry, pair.start.start, across),
|
|
191
|
+
rightPoint: pointAt(geometry, pair.stop.end, across),
|
|
192
|
+
scale: pair.scale, startScale: pair.startScale, stopScale: pair.stopScale,
|
|
193
|
+
columns: pair.columns, width: pair.width, error: pair.error,
|
|
194
|
+
geometry });
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return hits;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function fittedLine(hits, key) {
|
|
201
|
+
const meanX = hits.reduce((sum, hit) => sum + hit.across, 0) / hits.length;
|
|
202
|
+
const meanY = hits.reduce((sum, hit) => sum + hit[key], 0) / hits.length;
|
|
203
|
+
let covariance = 0, variance = 0;
|
|
204
|
+
for (const hit of hits) {
|
|
205
|
+
const delta = hit.across - meanX;
|
|
206
|
+
covariance += delta * (hit[key] - meanY);
|
|
207
|
+
variance += delta * delta;
|
|
208
|
+
}
|
|
209
|
+
const slope = variance ? covariance / variance : 0;
|
|
210
|
+
return { slope, intercept: meanY - slope * meanX };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function lineValue(line, at) {
|
|
214
|
+
return line.intercept + line.slope * at;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function finderExtent(image, hits, edgeKey, scaleKey, centreModules, fallback) {
|
|
218
|
+
const geometry = hits[0].geometry;
|
|
219
|
+
const edge = fittedLine(hits, edgeKey), scale = fittedLine(hits, scaleKey);
|
|
220
|
+
const span = fallback.bottom - fallback.top;
|
|
221
|
+
const projection = projectionRange(image, geometry.across);
|
|
222
|
+
const margin = Math.max(6, span * 0.35, hits[0].scale * 4);
|
|
223
|
+
const start = Math.max(Math.ceil(projection.min), Math.floor(fallback.top - margin));
|
|
224
|
+
const end = Math.min(Math.floor(projection.max), Math.ceil(fallback.bottom + margin));
|
|
225
|
+
const values = [];
|
|
226
|
+
for (let across = start; across <= end; across++) {
|
|
227
|
+
const localScale = Math.max(0.5, lineValue(scale, across));
|
|
228
|
+
const centre = lineValue(edge, across) + localScale * centreModules;
|
|
229
|
+
let dark = 0;
|
|
230
|
+
for (const offset of [-0.75, 0, 0.75]) {
|
|
231
|
+
const point = pointAt(geometry, centre + localScale * offset, across);
|
|
232
|
+
if (image.get(Math.round(point.x), Math.round(point.y))) dark++;
|
|
233
|
+
}
|
|
234
|
+
values.push({ across, dark: dark >= 2 });
|
|
235
|
+
}
|
|
236
|
+
const segments = [];
|
|
237
|
+
let first = null, lastDark = null, gap = 0;
|
|
238
|
+
for (const value of values) {
|
|
239
|
+
if (value.dark) {
|
|
240
|
+
if (first === null) first = value.across;
|
|
241
|
+
lastDark = value.across; gap = 0;
|
|
242
|
+
} else if (first !== null && ++gap > 2) {
|
|
243
|
+
segments.push({ top: first, bottom: lastDark + 1 });
|
|
244
|
+
first = null; lastDark = null; gap = 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (first !== null) segments.push({ top: first, bottom: lastDark + 1 });
|
|
248
|
+
let best = null, bestScore = -Infinity;
|
|
249
|
+
for (const segment of segments) {
|
|
250
|
+
const overlap = Math.max(0, Math.min(segment.bottom, fallback.bottom) - Math.max(segment.top, fallback.top));
|
|
251
|
+
const score = overlap * 4 + (segment.bottom - segment.top) -
|
|
252
|
+
Math.abs((segment.top + segment.bottom) / 2 - (fallback.top + fallback.bottom) / 2);
|
|
253
|
+
if (overlap >= span * 0.55 && score > bestScore) { best = segment; bestScore = score; }
|
|
254
|
+
}
|
|
255
|
+
return best ?? fallback;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function intersectBoundary(leftAcross, rightAcross, leftCentre, rightCentre, leftEdge, rightEdge) {
|
|
259
|
+
const delta = rightCentre - leftCentre;
|
|
260
|
+
if (Math.abs(delta) < 1e-6) return { left: leftAcross, right: rightAcross };
|
|
261
|
+
const slope = (rightAcross - leftAcross) / delta;
|
|
262
|
+
const intercept = leftAcross - slope * leftCentre;
|
|
263
|
+
const atEdge = (edge, fallback) => {
|
|
264
|
+
const denominator = 1 - slope * edge.slope;
|
|
265
|
+
return Math.abs(denominator) < 1e-6 ? fallback :
|
|
266
|
+
(slope * edge.intercept + intercept) / denominator;
|
|
267
|
+
};
|
|
268
|
+
return { left: atEdge(leftEdge, leftAcross), right: atEdge(rightEdge, rightAcross) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function groupHits(hits, image) {
|
|
272
|
+
const groups = [];
|
|
273
|
+
for (const hit of hits.sort((a, b) => a.across - b.across || a.error - b.error)) {
|
|
274
|
+
let best = null, bestDistance = Infinity;
|
|
275
|
+
for (const group of groups) {
|
|
276
|
+
const last = group.hits[group.hits.length - 1];
|
|
277
|
+
const gap = hit.across - last.across;
|
|
278
|
+
if (hit.columns !== last.columns || gap <= 0 || gap > Math.max(5, hit.scale * 3)) continue;
|
|
279
|
+
const scaleRatio = Math.max(hit.scale, last.scale) / Math.min(hit.scale, last.scale);
|
|
280
|
+
if (scaleRatio > 1.45) continue;
|
|
281
|
+
const tolerance = Math.max(5, hit.scale * 4 + gap);
|
|
282
|
+
const distance = Math.abs(hit.left - last.left) + Math.abs(hit.right - last.right);
|
|
283
|
+
if (distance > tolerance * 2 || distance >= bestDistance) continue;
|
|
284
|
+
best = group; bestDistance = distance;
|
|
285
|
+
}
|
|
286
|
+
if (best) best.hits.push(hit); else groups.push({ hits: [hit] });
|
|
287
|
+
}
|
|
288
|
+
const candidates = [];
|
|
289
|
+
for (const group of groups) {
|
|
290
|
+
const rows = group.hits;
|
|
291
|
+
const scale = rows.reduce((sum, hit) => sum + hit.scale, 0) / rows.length;
|
|
292
|
+
const top = rows[0].across, bottom = rows[rows.length - 1].across + 1;
|
|
293
|
+
const span = bottom - top;
|
|
294
|
+
if (rows.length < 4 || span < Math.max(6, scale * 4) || rows.length / span < 0.28) continue;
|
|
295
|
+
const geometry = rows[0].geometry;
|
|
296
|
+
const left = fittedLine(rows, 'left'), right = fittedLine(rows, 'right');
|
|
297
|
+
const startScale = fittedLine(rows, 'startScale'), stopScale = fittedLine(rows, 'stopScale');
|
|
298
|
+
const fallback = { top, bottom };
|
|
299
|
+
const leftExtent = image ? finderExtent(image, rows, 'left', 'startScale', 4, fallback) : fallback;
|
|
300
|
+
const rightExtent = image ? finderExtent(image, rows, 'right', 'stopScale', -14.5, fallback) : fallback;
|
|
301
|
+
const boundary = (leftAcross, rightAcross) => intersectBoundary(
|
|
302
|
+
leftAcross,
|
|
303
|
+
rightAcross,
|
|
304
|
+
lineValue(left, leftAcross) + lineValue(startScale, leftAcross) * 4,
|
|
305
|
+
lineValue(right, rightAcross) - lineValue(stopScale, rightAcross) * 14.5,
|
|
306
|
+
left,
|
|
307
|
+
right,
|
|
308
|
+
);
|
|
309
|
+
const topBoundary = boundary(leftExtent.top, rightExtent.top);
|
|
310
|
+
const bottomBoundary = boundary(leftExtent.bottom, rightExtent.bottom);
|
|
311
|
+
candidates.push({
|
|
312
|
+
width: rows[0].width, scale,
|
|
313
|
+
corners: [
|
|
314
|
+
pointAt(geometry, lineValue(left, topBoundary.left), topBoundary.left),
|
|
315
|
+
pointAt(geometry, lineValue(right, topBoundary.right), topBoundary.right),
|
|
316
|
+
pointAt(geometry, lineValue(right, bottomBoundary.right), bottomBoundary.right),
|
|
317
|
+
pointAt(geometry, lineValue(left, bottomBoundary.left), bottomBoundary.left),
|
|
318
|
+
],
|
|
319
|
+
score: rows.length / span * 8 + Math.log2(rows.length + 1) * 2 -
|
|
320
|
+
rows.reduce((sum, hit) => sum + hit.error, 0) / rows.length,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return candidates.sort((a, b) => b.score - a.score);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function validQuadrilateral(value) {
|
|
327
|
+
return Array.isArray(value) && value.length === 4 &&
|
|
328
|
+
value.every((point) => Number.isFinite(point?.x) && Number.isFinite(point?.y));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function manualCandidate(corners) {
|
|
332
|
+
const horizontal = (Math.hypot(corners[1].x - corners[0].x, corners[1].y - corners[0].y) +
|
|
333
|
+
Math.hypot(corners[2].x - corners[3].x, corners[2].y - corners[3].y)) / 2;
|
|
334
|
+
const vertical = (Math.hypot(corners[3].x - corners[0].x, corners[3].y - corners[0].y) +
|
|
335
|
+
Math.hypot(corners[2].x - corners[1].x, corners[2].y - corners[1].y)) / 2;
|
|
336
|
+
const out = [];
|
|
337
|
+
for (let columns = 1; columns <= 30; columns++) {
|
|
338
|
+
const width = 69 + columns * 17, scale = horizontal / width;
|
|
339
|
+
if (scale < 0.65) continue;
|
|
340
|
+
const moduleHeight = vertical / scale;
|
|
341
|
+
let plausibility = Infinity;
|
|
342
|
+
for (let rowHeight = 3; rowHeight <= 12; rowHeight++) {
|
|
343
|
+
const rows = Math.round(moduleHeight / rowHeight);
|
|
344
|
+
if (rows >= 3 && rows <= 90) plausibility = Math.min(plausibility,
|
|
345
|
+
Math.abs(moduleHeight - rows * rowHeight) / rowHeight);
|
|
346
|
+
}
|
|
347
|
+
out.push({ width, scale, corners, score: Number.isFinite(plausibility) ? 2 - plausibility : 0 });
|
|
348
|
+
}
|
|
349
|
+
return out.sort((a, b) => b.score - a.score);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function edgeLength(first, second) {
|
|
353
|
+
return Math.hypot(second.x - first.x, second.y - first.y);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function rowCandidates(candidate, options) {
|
|
357
|
+
const vertical = (edgeLength(candidate.corners[0], candidate.corners[3]) +
|
|
358
|
+
edgeLength(candidate.corners[1], candidate.corners[2])) / 2;
|
|
359
|
+
const allowedHeights = Number.isInteger(options.rowHeight) ? [options.rowHeight] :
|
|
360
|
+
Array.from({ length: 10 }, (_, index) => index + 3);
|
|
361
|
+
const rows = [];
|
|
362
|
+
for (let value = 3; value <= 90; value++) {
|
|
363
|
+
let score = Infinity;
|
|
364
|
+
for (const rowHeight of allowedHeights) {
|
|
365
|
+
score = Math.min(score, Math.abs(Math.log(vertical / (candidate.scale * value * rowHeight))));
|
|
366
|
+
}
|
|
367
|
+
rows.push({ value, score });
|
|
368
|
+
}
|
|
369
|
+
return rows.sort((a, b) => a.score - b.score).map((entry) => entry.value);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function shiftedCandidate(candidate, amount) {
|
|
373
|
+
if (!amount) return candidate;
|
|
374
|
+
const topDx = candidate.corners[3].x - candidate.corners[0].x;
|
|
375
|
+
const topDy = candidate.corners[3].y - candidate.corners[0].y;
|
|
376
|
+
const bottomDx = candidate.corners[2].x - candidate.corners[1].x;
|
|
377
|
+
const bottomDy = candidate.corners[2].y - candidate.corners[1].y;
|
|
378
|
+
const topLength = Math.hypot(topDx, topDy) || 1;
|
|
379
|
+
const bottomLength = Math.hypot(bottomDx, bottomDy) || 1;
|
|
380
|
+
return { ...candidate, corners: [
|
|
381
|
+
{ x: candidate.corners[0].x - topDx / topLength * amount, y: candidate.corners[0].y - topDy / topLength * amount },
|
|
382
|
+
{ x: candidate.corners[1].x - bottomDx / bottomLength * amount, y: candidate.corners[1].y - bottomDy / bottomLength * amount },
|
|
383
|
+
{ x: candidate.corners[2].x + bottomDx / bottomLength * amount, y: candidate.corners[2].y + bottomDy / bottomLength * amount },
|
|
384
|
+
{ x: candidate.corners[3].x + topDx / topLength * amount, y: candidate.corners[3].y + topDy / topLength * amount },
|
|
385
|
+
] };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function canonicalRows(matrix) {
|
|
389
|
+
const rowHeight = 3;
|
|
390
|
+
const out = new BitMatrix(matrix.width, matrix.height * rowHeight);
|
|
391
|
+
for (let y = 0; y < matrix.height; y++) for (let x = 0; x < matrix.width; x++) {
|
|
392
|
+
if (matrix.get(x, y)) out.setRegion(x, y * rowHeight, 1, rowHeight);
|
|
393
|
+
}
|
|
394
|
+
return out;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function sampleCandidate(image, candidate, options) {
|
|
398
|
+
for (const edgeShift of [0, candidate.scale * 0.35, -candidate.scale * 0.25]) {
|
|
399
|
+
const geometry = shiftedCandidate(candidate, edgeShift);
|
|
400
|
+
for (const rows of rowCandidates(geometry, options)) {
|
|
401
|
+
const transform = PerspectiveTransform.quadToQuad(0, 0, geometry.width, 0, geometry.width, rows, 0, rows,
|
|
402
|
+
geometry.corners[0].x, geometry.corners[0].y, geometry.corners[1].x, geometry.corners[1].y,
|
|
403
|
+
geometry.corners[2].x, geometry.corners[2].y, geometry.corners[3].x, geometry.corners[3].y);
|
|
404
|
+
for (const voting of [false, true]) {
|
|
405
|
+
let matrix;
|
|
406
|
+
try { matrix = voting ? sampleGridVoting(image, geometry.width, rows, transform) : sampleGrid(image, geometry.width, rows, transform); }
|
|
407
|
+
catch { continue; }
|
|
408
|
+
try {
|
|
409
|
+
const result = decodePDF417(matrix, { ...options, rowHeight: 1 });
|
|
410
|
+
return { matrix: canonicalRows(matrix), result, corners: geometry.corners };
|
|
411
|
+
}
|
|
412
|
+
catch { /* Try the next geometry. */ }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function automaticCandidates(image) {
|
|
420
|
+
const out = [];
|
|
421
|
+
for (const angle of SCAN_ANGLES) {
|
|
422
|
+
out.push(...groupHits(scanHits(image, angle), image));
|
|
423
|
+
if (out.length) break;
|
|
424
|
+
}
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function detectInOrientation(image, options, supplied) {
|
|
429
|
+
for (const candidate of [...supplied, ...automaticCandidates(image)]) {
|
|
430
|
+
const decoded = sampleCandidate(image, candidate, options);
|
|
431
|
+
if (decoded) return { candidate, decoded };
|
|
432
|
+
}
|
|
433
|
+
if (supplied.length) return null;
|
|
434
|
+
for (const angle of SCAN_ANGLES.slice(1)) {
|
|
435
|
+
const candidates = groupHits(scanHits(image, angle), image);
|
|
436
|
+
for (const candidate of candidates) {
|
|
437
|
+
const decoded = sampleCandidate(image, candidate, options);
|
|
438
|
+
if (decoded) return { candidate, decoded };
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/*
|
|
445
|
+
* Locate a binarized raster symbol from its repeated start and stop patterns.
|
|
446
|
+
* The detector estimates a projective quadrilateral; grayscale binarization
|
|
447
|
+
* remains the caller's responsibility.
|
|
448
|
+
*/
|
|
449
|
+
export function detectPDF417(binaryImage, options = {}) {
|
|
450
|
+
if (!binaryImage?.width || !binaryImage?.height || typeof binaryImage.get !== 'function') return null;
|
|
451
|
+
let oriented = binaryImage;
|
|
452
|
+
let toOriginal = (point) => ({ x: point.x, y: point.y });
|
|
453
|
+
for (let turns = 0; turns < 4; turns++) {
|
|
454
|
+
const supplied = turns === 0 && validQuadrilateral(options.quadrilateral)
|
|
455
|
+
? manualCandidate(options.quadrilateral.map(({ x, y }) => ({ x, y }))) : [];
|
|
456
|
+
const found = detectInOrientation(oriented, options, supplied);
|
|
457
|
+
if (found) {
|
|
458
|
+
return { matrix: found.decoded.matrix, rotation: turns * 90,
|
|
459
|
+
corners: found.decoded.corners.map(toOriginal), ...found.decoded.result };
|
|
460
|
+
}
|
|
461
|
+
const previous = oriented, previousToOriginal = toOriginal;
|
|
462
|
+
oriented = rotateClockwise(previous);
|
|
463
|
+
toOriginal = (point) => previousToOriginal({ x: point.y, y: previous.height - point.x });
|
|
464
|
+
}
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function detectAndDecodePDF417(binaryImage, options = {}) { return detectPDF417(binaryImage, options); }
|
|
@@ -0,0 +1,91 @@
|
|
|
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 { BitMatrix } from '../core/bit-matrix.js';
|
|
32
|
+
import { EncodeError } from '../core/errors.js';
|
|
33
|
+
import { compactPdf417 } from './compaction.js';
|
|
34
|
+
import { pdf417ErrorCorrection, pdf417EccLength } from './error-correction.js';
|
|
35
|
+
import { pdf417PatternForCodeword } from './tables.js';
|
|
36
|
+
|
|
37
|
+
const START = '81111113';
|
|
38
|
+
const STOP = '711311121';
|
|
39
|
+
|
|
40
|
+
function append(matrix, y, x, sequence, height) {
|
|
41
|
+
let dark = true;
|
|
42
|
+
for (const digit of sequence) {
|
|
43
|
+
const width = digit.charCodeAt(0) - 48;
|
|
44
|
+
if (dark) matrix.setRegion(x, y, width, height);
|
|
45
|
+
x += width; dark = !dark;
|
|
46
|
+
}
|
|
47
|
+
return x;
|
|
48
|
+
}
|
|
49
|
+
function patternSequence(pattern) { return pattern.toString(2).padStart(17, '0').replace(/0+|1+/g, (run) => String(run.length)); }
|
|
50
|
+
function indicators(row, rows, cols, level) {
|
|
51
|
+
const group = Math.floor(row / 3), y = Math.floor((rows - 1) / 3), z = level * 3 + (rows - 1) % 3, v = cols - 1;
|
|
52
|
+
if (row % 3 === 0) return [30 * group + y, 30 * group + v];
|
|
53
|
+
if (row % 3 === 1) return [30 * group + z, 30 * group + y];
|
|
54
|
+
return [30 * group + v, 30 * group + z];
|
|
55
|
+
}
|
|
56
|
+
function dimensions(needed, level, options) {
|
|
57
|
+
for (const [name, value, min, max] of [['rows', options.rows, 3, 90], ['columns', options.columns, 1, 30]]) {
|
|
58
|
+
if (value !== undefined && (!Number.isInteger(value) || value < min || value > max)) throw new EncodeError(`PDF417: ${name} must be an integer in ${min}..${max}`);
|
|
59
|
+
}
|
|
60
|
+
if (options.aspectRatio !== undefined && (!Number.isFinite(options.aspectRatio) || options.aspectRatio <= 0)) throw new EncodeError('PDF417: aspectRatio must be positive');
|
|
61
|
+
const ecc = pdf417EccLength(level); let best = null;
|
|
62
|
+
for (let rows = options.rows ?? 3; rows <= (options.rows ?? 90); rows++) for (let cols = options.columns ?? 1; cols <= (options.columns ?? 30); cols++) {
|
|
63
|
+
if (rows < 3 || rows * cols > 928 || rows * cols - ecc < needed) continue;
|
|
64
|
+
const ratio = (69 + cols * 17) / (rows * (options.rowHeight ?? 3));
|
|
65
|
+
const score = (rows * cols - ecc - needed) * 10 + Math.abs(ratio - (options.aspectRatio ?? 3));
|
|
66
|
+
if (!best || score < best.score) best = { rows, cols, score };
|
|
67
|
+
}
|
|
68
|
+
if (!best) throw new EncodeError('PDF417: payload does not fit the requested dimensions and error correction level');
|
|
69
|
+
return best;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function encodePDF417(value, options = {}) {
|
|
73
|
+
const level = options.eccLevel ?? 2, rowHeight = options.rowHeight ?? 3;
|
|
74
|
+
if (!Number.isInteger(rowHeight) || rowHeight < 3) throw new EncodeError('PDF417: rowHeight must be an integer of at least 3');
|
|
75
|
+
const payload = compactPdf417(value, { compaction: options.compaction });
|
|
76
|
+
const { rows, cols } = dimensions(payload.length + 1, level, { ...options, rowHeight });
|
|
77
|
+
const eccLength = pdf417EccLength(level), dataLength = rows * cols - eccLength;
|
|
78
|
+
const data = [dataLength, ...payload]; while (data.length < dataLength) data.push(900);
|
|
79
|
+
const codewords = data.concat(pdf417ErrorCorrection(data, level));
|
|
80
|
+
const matrix = new BitMatrix(69 + cols * 17, rows * rowHeight);
|
|
81
|
+
for (let row = 0; row < rows; row++) {
|
|
82
|
+
const y = row * rowHeight, cluster = (row % 3) * 3, [left, right] = indicators(row, rows, cols, level);
|
|
83
|
+
let x = append(matrix, y, 0, START, rowHeight);
|
|
84
|
+
x = append(matrix, y, x, patternSequence(pdf417PatternForCodeword(left, cluster)), rowHeight);
|
|
85
|
+
for (let col = 0; col < cols; col++) x = append(matrix, y, x, patternSequence(pdf417PatternForCodeword(codewords[row * cols + col], cluster)), rowHeight);
|
|
86
|
+
x = append(matrix, y, x, patternSequence(pdf417PatternForCodeword(right, cluster)), rowHeight);
|
|
87
|
+
append(matrix, y, x, STOP, rowHeight);
|
|
88
|
+
}
|
|
89
|
+
matrix.pdf417 = { rows, columns: cols, eccLevel: level, rowHeight, codewords };
|
|
90
|
+
return matrix;
|
|
91
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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 { EncodeError } from '../core/errors.js';
|
|
32
|
+
import { GF929 } from '../core/galois-field.js';
|
|
33
|
+
import { rsDecode, rsEncode } from '../core/reed-solomon.js';
|
|
34
|
+
|
|
35
|
+
export function pdf417EccLength(level) {
|
|
36
|
+
if (!Number.isInteger(level) || level < 0 || level > 8) throw new EncodeError('PDF417: error correction level must be in 0..8');
|
|
37
|
+
return 1 << (level + 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function pdf417ErrorCorrection(data, level) {
|
|
41
|
+
return rsEncode(data, pdf417EccLength(level), GF929, 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Correct PDF417 codewords, optionally marking unreadable codewords as erasures. */
|
|
45
|
+
export function pdf417CorrectErrors(codewords, level, erasures = []) {
|
|
46
|
+
return rsDecode(codewords, pdf417EccLength(level), GF929, 1, erasures);
|
|
47
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { compactPdf417, compactPdf417Text, compactPdf417Bytes, compactPdf417Numeric, decodePdf417Compaction, decodePdf417CompactionDetailed } from './compaction.js';
|
|
2
|
+
export { encodePDF417 } from './encoder.js';
|
|
3
|
+
export { decodePDF417 } from './decoder.js';
|
|
4
|
+
export { detectPDF417, detectAndDecodePDF417 } from './detector.js';
|
|
5
|
+
export { pdf417EccLength, pdf417ErrorCorrection, pdf417CorrectErrors } from './error-correction.js';
|
|
6
|
+
export { PDF417_PATTERN_TABLE, PDF417_CLUSTER_NUMBERS, pdf417PatternForCodeword, pdf417CodewordForPattern } from './tables.js';
|