@sythos/js_barcode_universal 0.1.0 → 1.1.0
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/README.md +483 -425
- package/bundle/sythos-barcode.esm.js +3089 -1181
- package/bundle/sythos-barcode.js +3081 -1181
- package/examples/create.html +732 -730
- package/licenses/README.md +2 -0
- package/licenses/aztec-code.license +74 -0
- package/licenses/data-matrix.license +82 -0
- package/package.json +94 -88
- 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 +313 -313
- package/src/datamatrix/decoder.js +262 -0
- package/src/datamatrix/detector.js +225 -0
- package/src/datamatrix/encoder.js +191 -0
- package/src/datamatrix/index.js +42 -0
- package/src/datamatrix/tables.js +123 -0
- package/src/index.js +70 -2
|
@@ -0,0 +1,224 @@
|
|
|
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
|
+
* Aztec image detection.
|
|
33
|
+
*
|
|
34
|
+
* Aztec has no finder pattern at its outer border. Its reliable geometric
|
|
35
|
+
* anchor is instead the alternating square bull's-eye in the centre: five
|
|
36
|
+
* rings in Compact symbols, seven rings in Full symbols. The detector finds
|
|
37
|
+
* isolated central modules, verifies those rings at module centres, then
|
|
38
|
+
* samples each legal symbol dimension. The decoder is deliberately the final
|
|
39
|
+
* arbiter: its mode-message Reed--Solomon check rejects accidental concentric
|
|
40
|
+
* artwork and tells us which of the compact/full dimensions is real.
|
|
41
|
+
*
|
|
42
|
+
* Sampling uses a quadrilateral, not a cropped bitmap, so the detected
|
|
43
|
+
* rotation is corrected before decoding. The ring search covers arbitrary
|
|
44
|
+
* in-plane rotations (four-degree coarse search; at normal camera scales its
|
|
45
|
+
* positional error remains well inside a module). The optional inverse pass
|
|
46
|
+
* supports light modules on a dark field.
|
|
47
|
+
*
|
|
48
|
+
* @module aztec/detector
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
import { NotFoundError } from '../core/errors.js';
|
|
52
|
+
import { sampleQuad } from '../image/grid-sampler.js';
|
|
53
|
+
import { decodeAztec } from './decoder.js';
|
|
54
|
+
|
|
55
|
+
/** @typedef {{x:number, y:number}} Point */
|
|
56
|
+
/** @typedef {{corners: Point[], dimension: number, compact: boolean, moduleSize: number, matrix: import('../core/bit-matrix.js').BitMatrix}} Detection */
|
|
57
|
+
|
|
58
|
+
// Compact: 11 + 4 layers. Full symbols add reference-grid rows/columns every
|
|
59
|
+
// 15 modules measured from their central 14-module base, not every 15 layers.
|
|
60
|
+
const DIMENSIONS = [
|
|
61
|
+
...[1, 2, 3, 4].map((layers) => ({ compact: true, dimension: 11 + 4 * layers })),
|
|
62
|
+
...Array.from({ length: 32 }, (_, index) => {
|
|
63
|
+
const layers = index + 1;
|
|
64
|
+
return { compact: false, dimension: 15 + 4 * layers + 2 * Math.floor((2 * layers + 6) / 15) };
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
function pixel(image, x, y) {
|
|
69
|
+
const ix = Math.round(x);
|
|
70
|
+
const iy = Math.round(y);
|
|
71
|
+
return ix >= 0 && iy >= 0 && ix < image.width && iy < image.height && image.get(ix, iy);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Connected components of either polarity, retaining only plausible modules. */
|
|
75
|
+
function components(image, value) {
|
|
76
|
+
const seen = new Uint8Array(image.width * image.height);
|
|
77
|
+
const out = [];
|
|
78
|
+
const maximumArea = Math.max(4, Math.floor(image.width * image.height * 0.08));
|
|
79
|
+
for (let y = 0; y < image.height; y++) for (let x = 0; x < image.width; x++) {
|
|
80
|
+
const start = y * image.width + x;
|
|
81
|
+
if (seen[start] || image.get(x, y) !== value) continue;
|
|
82
|
+
const xs = [x];
|
|
83
|
+
const ys = [y];
|
|
84
|
+
seen[start] = 1;
|
|
85
|
+
let head = 0;
|
|
86
|
+
let minX = x; let maxX = x; let minY = y; let maxY = y;
|
|
87
|
+
while (head < xs.length) {
|
|
88
|
+
const px = xs[head]; const py = ys[head++];
|
|
89
|
+
if (px < minX) minX = px; if (px > maxX) maxX = px;
|
|
90
|
+
if (py < minY) minY = py; if (py > maxY) maxY = py;
|
|
91
|
+
for (const [nx, ny] of [[px - 1, py], [px + 1, py], [px, py - 1], [px, py + 1]]) {
|
|
92
|
+
if (nx < 0 || ny < 0 || nx >= image.width || ny >= image.height) continue;
|
|
93
|
+
const at = ny * image.width + nx;
|
|
94
|
+
if (!seen[at] && image.get(nx, ny) === value) {
|
|
95
|
+
seen[at] = 1; xs.push(nx); ys.push(ny);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const width = maxX - minX + 1;
|
|
100
|
+
const height = maxY - minY + 1;
|
|
101
|
+
const area = width * height;
|
|
102
|
+
// The central module is solid and approximately square. This filter is
|
|
103
|
+
// intentionally permissive because a rotated raster module is diamond-ish.
|
|
104
|
+
if (xs.length <= maximumArea && Math.abs(width - height) <= Math.max(1, Math.ceil(Math.max(width, height) * 0.35)) &&
|
|
105
|
+
xs.length >= area * 0.45) {
|
|
106
|
+
out.push({ x: (minX + maxX) / 2, y: (minY + maxY) / 2, width, height, pixels: xs.length });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out.sort((a, b) => b.pixels - a.pixels).slice(0, 2000);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function expectedDark(ring, inverted) {
|
|
113
|
+
return inverted ? (ring & 1) === 1 : (ring & 1) === 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Score one square bull's-eye at an angle and a candidate module pitch. */
|
|
117
|
+
function ringScore(image, centre, pitch, angle, inverted, rings) {
|
|
118
|
+
const cos = Math.cos(angle);
|
|
119
|
+
const sin = Math.sin(angle);
|
|
120
|
+
let correct = 0;
|
|
121
|
+
let total = 0;
|
|
122
|
+
for (let ring = 0; ring < rings; ring++) {
|
|
123
|
+
const wanted = expectedDark(ring, inverted);
|
|
124
|
+
for (let j = -ring; j <= ring; j++) for (let i = -ring; i <= ring; i++) {
|
|
125
|
+
if (ring && Math.abs(i) !== ring && Math.abs(j) !== ring) continue;
|
|
126
|
+
const x = centre.x + (i * cos - j * sin) * pitch;
|
|
127
|
+
const y = centre.y + (i * sin + j * cos) * pitch;
|
|
128
|
+
if (pixel(image, x, y) === wanted) correct++;
|
|
129
|
+
total++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return correct / total;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function rotateCorners(corners, turn) {
|
|
136
|
+
return corners.slice(turn).concat(corners.slice(0, turn));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function invert(matrix) {
|
|
140
|
+
const out = matrix.clone();
|
|
141
|
+
for (let y = 0; y < out.height; y++) for (let x = 0; x < out.width; x++) out.flip(x, y);
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function cornersFor(centre, pitch, angle, dimension) {
|
|
146
|
+
const half = dimension * pitch / 2;
|
|
147
|
+
const cos = Math.cos(angle);
|
|
148
|
+
const sin = Math.sin(angle);
|
|
149
|
+
const point = (x, y) => ({ x: centre.x + x * cos - y * sin, y: centre.y + x * sin + y * cos });
|
|
150
|
+
return [point(-half, -half), point(half, -half), point(half, half), point(-half, half)];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Find an Aztec symbol in a binarized image.
|
|
155
|
+
*
|
|
156
|
+
* The returned matrix is in the orientation accepted by the Aztec decoder.
|
|
157
|
+
* A valid mode message is required before a geometric candidate is returned,
|
|
158
|
+
* making false positives from decorative concentric squares very unlikely.
|
|
159
|
+
*
|
|
160
|
+
* @param {import('../core/bit-matrix.js').BitMatrix} binaryImage Set bit = dark.
|
|
161
|
+
* @returns {Detection | null}
|
|
162
|
+
*/
|
|
163
|
+
export function detectAztec(binaryImage) {
|
|
164
|
+
if (!binaryImage || !binaryImage.width || !binaryImage.height) {
|
|
165
|
+
throw new NotFoundError('detectAztec: no image supplied');
|
|
166
|
+
}
|
|
167
|
+
const candidates = [];
|
|
168
|
+
for (const inverted of [false, true]) {
|
|
169
|
+
for (const core of components(binaryImage, !inverted)) {
|
|
170
|
+
// A non-rotated one-module component directly gives its pitch. For
|
|
171
|
+
// rotated modules its bounding box grows by |sin| + |cos|, compensated
|
|
172
|
+
// below for every tested angle.
|
|
173
|
+
for (let degrees = 0; degrees < 180; degrees += 4) {
|
|
174
|
+
const angle = degrees * Math.PI / 180;
|
|
175
|
+
const scale = Math.abs(Math.cos(angle)) + Math.abs(Math.sin(angle));
|
|
176
|
+
const pitch = ((core.width + core.height) / 2) / scale;
|
|
177
|
+
if (pitch < 0.8) continue;
|
|
178
|
+
// Test Full first: its seven rings also exclude Compact candidates.
|
|
179
|
+
const fullScore = ringScore(binaryImage, core, pitch, angle, inverted, 7);
|
|
180
|
+
const rings = fullScore >= 0.88 ? 7 : 5;
|
|
181
|
+
const score = rings === 7 ? fullScore : ringScore(binaryImage, core, pitch, angle, inverted, 5);
|
|
182
|
+
if (score < 0.91) continue;
|
|
183
|
+
const symbolKinds = rings === 7 ? DIMENSIONS.filter((item) => !item.compact) : DIMENSIONS.filter((item) => item.compact);
|
|
184
|
+
for (const kind of symbolKinds) {
|
|
185
|
+
const baseCorners = cornersFor(core, pitch, angle, kind.dimension);
|
|
186
|
+
for (let turn = 0; turn < 4; turn++) {
|
|
187
|
+
const corners = rotateCorners(baseCorners, turn);
|
|
188
|
+
let matrix;
|
|
189
|
+
try { matrix = sampleQuad(binaryImage, kind.dimension, corners); } catch (e) { continue; }
|
|
190
|
+
if (inverted) matrix = invert(matrix);
|
|
191
|
+
try {
|
|
192
|
+
// The decoder verifies the mode-message ECC and exact geometry.
|
|
193
|
+
// We do not expose its result here so callers can use pure
|
|
194
|
+
// detection without treating payload decoding as an API contract.
|
|
195
|
+
decodeAztec(matrix);
|
|
196
|
+
candidates.push({ corners, dimension: kind.dimension, compact: kind.compact,
|
|
197
|
+
moduleSize: pitch, matrix, score });
|
|
198
|
+
} catch (e) { /* Not an Aztec mode message at this dimension. */ }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
candidates.sort((a, b) => b.score - a.score || b.moduleSize - a.moduleSize);
|
|
205
|
+
const best = candidates[0];
|
|
206
|
+
if (!best) return null;
|
|
207
|
+
delete best.score;
|
|
208
|
+
return best;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Detect then decode an Aztec symbol. Detection failure is a normal result for
|
|
213
|
+
* images without an Aztec code, therefore invalid candidates return null.
|
|
214
|
+
*
|
|
215
|
+
* @param {import('../core/bit-matrix.js').BitMatrix} binaryImage
|
|
216
|
+
* @returns {(import('./decoder.js').DecodeResult & {corners: Point[]}) | null}
|
|
217
|
+
*/
|
|
218
|
+
export function detectAndDecodeAztec(binaryImage) {
|
|
219
|
+
let detection;
|
|
220
|
+
try { detection = detectAztec(binaryImage); } catch (e) { return null; }
|
|
221
|
+
if (!detection) return null;
|
|
222
|
+
try { return Object.assign({ corners: detection.corners }, decodeAztec(detection.matrix)); }
|
|
223
|
+
catch (e) { return null; }
|
|
224
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
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
|
+
* Aztec encoder: high-level bits, bit stuffing, Reed-Solomon and matrix layout.
|
|
33
|
+
*
|
|
34
|
+
* `tables.js` is intentionally the source of geometry and field selection.
|
|
35
|
+
* Its `aztecLayer(layers, compact)` entries must expose `totalBits`,
|
|
36
|
+
* `totalCodewords`, `baseMatrixSize` and `symbolSize`; `fieldForLayers()` must
|
|
37
|
+
* return the matching binary field. All Aztec Reed-Solomon generators start
|
|
38
|
+
* at alpha^1, hence the explicit base `1` in both data and mode messages.
|
|
39
|
+
*
|
|
40
|
+
* @module aztec/encoder
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { BitWriter } from '../core/bit-buffer.js';
|
|
44
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
45
|
+
import { EncodeError } from '../core/errors.js';
|
|
46
|
+
import { rsEncode } from '../core/reed-solomon.js';
|
|
47
|
+
import { encodeHighLevel } from './high-level.js';
|
|
48
|
+
import { AZTEC_COMPACT_LAYERS, AZTEC_FULL_LAYERS, aztecLayer, eccCodewordsFor, fieldForLayers, fieldForWordSize, wordSizeForLayers } from './tables.js';
|
|
49
|
+
|
|
50
|
+
/** @param {BitWriter} bits @param {number} at @returns {boolean} */
|
|
51
|
+
function bitAt(bits, at) {
|
|
52
|
+
return at >= 0 && at < bits.length && ((bits.bytes[at >>> 3] >>> (7 - (at & 7))) & 1) !== 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** @param {BitWriter} bits @param {number} from @param {number} count @returns {number} */
|
|
56
|
+
function readBits(bits, from, count) {
|
|
57
|
+
let value = 0;
|
|
58
|
+
for (let i = 0; i < count; i++) value = (value << 1) | (bitAt(bits, from + i) ? 1 : 0);
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Prevent all-zero and all-one codewords except their final bit. The final
|
|
64
|
+
* bit is intentionally re-consumed after a stuffed word; it is the mechanism
|
|
65
|
+
* that makes the transform injective and reversible.
|
|
66
|
+
*
|
|
67
|
+
* @param {BitWriter} bits @param {number} wordSize @returns {BitWriter}
|
|
68
|
+
*/
|
|
69
|
+
export function stuffBits(bits, wordSize) {
|
|
70
|
+
const out = new BitWriter();
|
|
71
|
+
const reserved = (1 << wordSize) - 2;
|
|
72
|
+
for (let at = 0; at < bits.length; at += wordSize) {
|
|
73
|
+
const word = readBits(bits, at, wordSize);
|
|
74
|
+
if ((word & reserved) === reserved) {
|
|
75
|
+
out.put(word & reserved, wordSize);
|
|
76
|
+
at--;
|
|
77
|
+
} else if ((word & reserved) === 0) {
|
|
78
|
+
out.put(word | 1, wordSize);
|
|
79
|
+
at--;
|
|
80
|
+
} else {
|
|
81
|
+
out.put(word, wordSize);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add systematic Aztec Reed-Solomon parity and the leading alignment bits.
|
|
89
|
+
* @param {BitWriter} data @param {number} totalBits @param {number} wordSize
|
|
90
|
+
* @param {import('../core/galois-field.js').GaloisField} field
|
|
91
|
+
* @returns {{bits: BitWriter, dataWords: number, eccWords: number}}
|
|
92
|
+
*/
|
|
93
|
+
export function addCheckWords(data, totalBits, wordSize, field) {
|
|
94
|
+
const totalWords = Math.floor(totalBits / wordSize);
|
|
95
|
+
const dataWords = Math.ceil(data.length / wordSize);
|
|
96
|
+
if (dataWords > totalWords) throw new EncodeError('Aztec: data codewords exceed layer capacity');
|
|
97
|
+
const eccWords = totalWords - dataWords;
|
|
98
|
+
const words = new Array(dataWords);
|
|
99
|
+
for (let i = 0; i < dataWords; i++) words[i] = readBits(data, i * wordSize, wordSize);
|
|
100
|
+
const ecc = rsEncode(words, eccWords, field, 1);
|
|
101
|
+
const out = new BitWriter();
|
|
102
|
+
out.put(0, totalBits % wordSize);
|
|
103
|
+
for (const word of words) out.put(word, wordSize);
|
|
104
|
+
for (const word of ecc) out.put(word, wordSize);
|
|
105
|
+
return { bits: out, dataWords, eccWords };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @param {number} layers @param {number} dataWords @param {boolean} compact @returns {BitWriter} */
|
|
109
|
+
export function modeMessage(layers, dataWords, compact) {
|
|
110
|
+
const raw = new BitWriter();
|
|
111
|
+
if (compact) {
|
|
112
|
+
raw.put(layers - 1, 2);
|
|
113
|
+
raw.put(dataWords - 1, 6);
|
|
114
|
+
return addCheckWords(raw, 28, 4, fieldForWordSize(4)).bits;
|
|
115
|
+
}
|
|
116
|
+
raw.put(layers - 1, 5);
|
|
117
|
+
raw.put(dataWords - 1, 11);
|
|
118
|
+
return addCheckWords(raw, 40, 4, fieldForWordSize(4)).bits;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** @param {BitMatrix} matrix @param {number} center @param {number} size */
|
|
122
|
+
function drawBullsEye(matrix, center, size) {
|
|
123
|
+
for (let ring = 0; ring < size; ring += 2) {
|
|
124
|
+
for (let p = center - ring; p <= center + ring; p++) {
|
|
125
|
+
matrix.set(p, center - ring); matrix.set(p, center + ring);
|
|
126
|
+
matrix.set(center - ring, p); matrix.set(center + ring, p);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
matrix.set(center - size, center - size);
|
|
130
|
+
matrix.set(center - size + 1, center - size);
|
|
131
|
+
matrix.set(center - size, center - size + 1);
|
|
132
|
+
matrix.set(center + size, center - size);
|
|
133
|
+
matrix.set(center + size, center - size + 1);
|
|
134
|
+
matrix.set(center + size, center + size - 1);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** @param {BitMatrix} matrix @param {BitWriter} message @param {boolean} compact @param {number} center */
|
|
138
|
+
function drawModeMessage(matrix, message, compact, center) {
|
|
139
|
+
if (compact) {
|
|
140
|
+
for (let i = 0; i < 7; i++) {
|
|
141
|
+
const offset = center - 3 + i;
|
|
142
|
+
if (bitAt(message, i)) matrix.set(offset, center - 5);
|
|
143
|
+
if (bitAt(message, i + 7)) matrix.set(center + 5, offset);
|
|
144
|
+
if (bitAt(message, 20 - i)) matrix.set(offset, center + 5);
|
|
145
|
+
if (bitAt(message, 27 - i)) matrix.set(center - 5, offset);
|
|
146
|
+
}
|
|
147
|
+
} else {
|
|
148
|
+
for (let i = 0; i < 10; i++) {
|
|
149
|
+
const offset = center - 5 + i + Math.floor(i / 5);
|
|
150
|
+
if (bitAt(message, i)) matrix.set(offset, center - 7);
|
|
151
|
+
if (bitAt(message, i + 10)) matrix.set(center + 7, offset);
|
|
152
|
+
if (bitAt(message, 29 - i)) matrix.set(offset, center + 7);
|
|
153
|
+
if (bitAt(message, 39 - i)) matrix.set(center - 7, offset);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Lay low-level bits in the four-sided, inward Aztec spiral.
|
|
160
|
+
* @param {BitWriter} bits @param {{layers:number,compact:boolean,baseMatrixSize:number,symbolSize:number}} symbol
|
|
161
|
+
* @returns {BitMatrix}
|
|
162
|
+
*/
|
|
163
|
+
export function buildAztecMatrix(bits, symbol) {
|
|
164
|
+
const { layers, compact, baseMatrixSize, symbolSize } = symbol;
|
|
165
|
+
const matrix = new BitMatrix(symbolSize);
|
|
166
|
+
const alignment = new Int32Array(baseMatrixSize);
|
|
167
|
+
const center = Math.floor(symbolSize / 2);
|
|
168
|
+
|
|
169
|
+
if (compact) {
|
|
170
|
+
for (let i = 0; i < baseMatrixSize; i++) alignment[i] = i;
|
|
171
|
+
} else {
|
|
172
|
+
const originalCenter = Math.floor(baseMatrixSize / 2);
|
|
173
|
+
for (let i = 0; i < originalCenter; i++) {
|
|
174
|
+
const offset = i + Math.floor(i / 15);
|
|
175
|
+
alignment[originalCenter - i - 1] = center - offset - 1;
|
|
176
|
+
alignment[originalCenter + i] = center + offset + 1;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let bit = 0;
|
|
181
|
+
for (let layer = 0; layer < layers; layer++) {
|
|
182
|
+
const rowSize = (layers - layer) * 4 + (compact ? 9 : 12);
|
|
183
|
+
const low = layer * 2;
|
|
184
|
+
const high = baseMatrixSize - 1 - low;
|
|
185
|
+
for (let j = 0; j < rowSize; j++) {
|
|
186
|
+
const offset = j * 2;
|
|
187
|
+
for (let k = 0; k < 2; k++) {
|
|
188
|
+
if (bitAt(bits, bit + offset + k)) matrix.set(alignment[low + k], alignment[low + j]);
|
|
189
|
+
if (bitAt(bits, bit + rowSize * 2 + offset + k)) matrix.set(alignment[low + j], alignment[high - k]);
|
|
190
|
+
if (bitAt(bits, bit + rowSize * 4 + offset + k)) matrix.set(alignment[high - k], alignment[high - j]);
|
|
191
|
+
if (bitAt(bits, bit + rowSize * 6 + offset + k)) matrix.set(alignment[high - j], alignment[low + k]);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
bit += rowSize * 8;
|
|
195
|
+
}
|
|
196
|
+
if (bit !== bits.length) throw new EncodeError(`Aztec: layout consumed ${bit} of ${bits.length} bits`);
|
|
197
|
+
|
|
198
|
+
const mode = modeMessage(layers, symbol.dataWords, compact);
|
|
199
|
+
drawModeMessage(matrix, mode, compact, center);
|
|
200
|
+
drawBullsEye(matrix, center, compact ? 5 : 7);
|
|
201
|
+
|
|
202
|
+
if (!compact) {
|
|
203
|
+
for (let i = 0, offset = 0; i < Math.floor(baseMatrixSize / 2) - 1; i += 15, offset += 16) {
|
|
204
|
+
for (let p = center & 1; p < symbolSize; p += 2) {
|
|
205
|
+
matrix.set(center - offset, p); matrix.set(center + offset, p);
|
|
206
|
+
matrix.set(p, center - offset); matrix.set(p, center + offset);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return matrix;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @param {number | undefined} layers @param {boolean | undefined} compact */
|
|
214
|
+
function candidates(layers, compact) {
|
|
215
|
+
if (layers !== undefined) {
|
|
216
|
+
if (!Number.isInteger(layers) || layers < 1 || layers > 32) throw new EncodeError('Aztec: layers must be an integer 1..32');
|
|
217
|
+
if (compact === true && layers > 4) throw new EncodeError('Aztec: compact symbols support layers 1..4');
|
|
218
|
+
return [aztecLayer(layers, compact === true)];
|
|
219
|
+
}
|
|
220
|
+
if (compact === true) return AZTEC_COMPACT_LAYERS;
|
|
221
|
+
if (compact === false) return AZTEC_FULL_LAYERS;
|
|
222
|
+
return [...AZTEC_COMPACT_LAYERS, ...AZTEC_FULL_LAYERS];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Encode a UTF-8 string or bytes into an Aztec Code matrix.
|
|
227
|
+
*
|
|
228
|
+
* @param {string|ArrayBuffer|ArrayBufferView} value
|
|
229
|
+
* @param {{layers?:number,compact?:boolean,eccPercent?:number,charset?:'utf-8'}} [options]
|
|
230
|
+
* @returns {BitMatrix & {format?:string,layers?:number,compact?:boolean,eccPercent?:number,dataCodewords?:number}}
|
|
231
|
+
*/
|
|
232
|
+
export function encodeAztec(value, options = {}) {
|
|
233
|
+
const eccPercent = options.eccPercent ?? 23;
|
|
234
|
+
if (!Number.isFinite(eccPercent) || eccPercent < 5 || eccPercent > 95) {
|
|
235
|
+
throw new EncodeError('Aztec: eccPercent must be between 5 and 95');
|
|
236
|
+
}
|
|
237
|
+
const high = encodeHighLevel(value, { charset: options.charset ?? 'utf-8' });
|
|
238
|
+
for (const candidate of candidates(options.layers, options.compact)) {
|
|
239
|
+
if (!candidate) continue;
|
|
240
|
+
const wordSize = wordSizeForLayers(candidate.layers);
|
|
241
|
+
const stuffed = stuffBits(high, wordSize);
|
|
242
|
+
const dataWords = Math.ceil(stuffed.length / wordSize);
|
|
243
|
+
const eccWords = eccCodewordsFor(dataWords, eccPercent);
|
|
244
|
+
if (dataWords > candidate.maxDataCodewords || dataWords + eccWords > candidate.totalCodewords) continue;
|
|
245
|
+
const checked = addCheckWords(stuffed, candidate.totalBits, wordSize, fieldForLayers(candidate.layers));
|
|
246
|
+
// `addCheckWords` uses every remaining word as parity. This is stronger
|
|
247
|
+
// than the requested percentage, never weaker, and canonical for a chosen
|
|
248
|
+
// layer/data-word combination.
|
|
249
|
+
const symbol = { ...candidate, dataWords: checked.dataWords };
|
|
250
|
+
const matrix = buildAztecMatrix(checked.bits, symbol);
|
|
251
|
+
matrix.format = 'aztec'; matrix.layers = candidate.layers; matrix.compact = candidate.compact;
|
|
252
|
+
matrix.eccPercent = Math.round(checked.eccWords * wordSize * 100 / Math.max(1, stuffed.length));
|
|
253
|
+
matrix.dataCodewords = checked.dataWords;
|
|
254
|
+
return matrix;
|
|
255
|
+
}
|
|
256
|
+
throw new EncodeError('Aztec: payload does not fit the requested layers and error correction');
|
|
257
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
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
|
+
* Aztec high-level stream writer.
|
|
33
|
+
*
|
|
34
|
+
* The output is deliberately a `BitWriter`, rather than a byte array: Aztec's
|
|
35
|
+
* text controls and binary-shift lengths are not byte aligned. This module is
|
|
36
|
+
* also the boundary where JavaScript strings become UTF-8. Passing a byte
|
|
37
|
+
* view bypasses that conversion and preserves every octet unchanged.
|
|
38
|
+
*
|
|
39
|
+
* The initial state mandated by the symbology is UPPER. The greedy text pass
|
|
40
|
+
* uses UPPER, LOWER, DIGIT and PUNCT tables, selecting the shortest available
|
|
41
|
+
* latch at each byte. Bytes without a text-table representation are emitted
|
|
42
|
+
* through the standard B/S (binary shift) escape. B/S is available from
|
|
43
|
+
* UPPER and makes this a complete, lossless representation of UTF-8 payloads.
|
|
44
|
+
*
|
|
45
|
+
* @module aztec/high-level
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import { BitWriter } from '../core/bit-buffer.js';
|
|
49
|
+
import { EncodeError } from '../core/errors.js';
|
|
50
|
+
|
|
51
|
+
/** Aztec high-level table identifiers, exposed for decoder/API symmetry. */
|
|
52
|
+
export const HIGH_LEVEL_MODE = Object.freeze({
|
|
53
|
+
UPPER: 0,
|
|
54
|
+
LOWER: 1,
|
|
55
|
+
DIGIT: 2,
|
|
56
|
+
MIXED: 3,
|
|
57
|
+
PUNCT: 4,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** Maximum number of bytes represented by one B/S escape. */
|
|
61
|
+
export const MAX_BINARY_SHIFT = 2078;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Convert accepted public input to its encoded octets.
|
|
65
|
+
*
|
|
66
|
+
* @param {string|ArrayBuffer|ArrayBufferView} value
|
|
67
|
+
* @param {'utf-8'} [charset]
|
|
68
|
+
* @returns {Uint8Array}
|
|
69
|
+
*/
|
|
70
|
+
export function aztecBytes(value, charset = 'utf-8') {
|
|
71
|
+
if (charset !== 'utf-8') throw new EncodeError(`Aztec: unsupported charset "${charset}"`);
|
|
72
|
+
if (typeof value === 'string') return new TextEncoder().encode(value);
|
|
73
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
74
|
+
if (ArrayBuffer.isView(value)) {
|
|
75
|
+
return new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
76
|
+
}
|
|
77
|
+
throw new EncodeError('Aztec: value must be a string, ArrayBuffer, or byte view');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @param {number} byte @returns {number} UPPER-table value, or -1. */
|
|
81
|
+
function upperValue(byte) {
|
|
82
|
+
if (byte === 0x20) return 1;
|
|
83
|
+
if (byte >= 0x41 && byte <= 0x5a) return byte - 0x41 + 2;
|
|
84
|
+
return -1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Aztec's latch table, packed as `(bitCount << 16) | bits`. */
|
|
88
|
+
const LATCH = Object.freeze([
|
|
89
|
+
[0, 327708, 327710, 327709, 656318],
|
|
90
|
+
[590318, 0, 327710, 327709, 656318],
|
|
91
|
+
[262158, 590300, 0, 590301, 932798],
|
|
92
|
+
[327709, 327708, 656322, 0, 327710],
|
|
93
|
+
[327711, 656380, 656382, 656381, 0],
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
/** @param {number} byte @returns {number} */
|
|
97
|
+
function lowerValue(byte) {
|
|
98
|
+
if (byte === 0x20) return 1;
|
|
99
|
+
if (byte >= 0x61 && byte <= 0x7a) return byte - 0x61 + 2;
|
|
100
|
+
return -1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** @param {number} byte @returns {number} */
|
|
104
|
+
function digitValue(byte) {
|
|
105
|
+
if (byte === 0x20) return 1;
|
|
106
|
+
if (byte >= 0x30 && byte <= 0x39) return byte - 0x30 + 2;
|
|
107
|
+
if (byte === 0x2c) return 12;
|
|
108
|
+
if (byte === 0x2e) return 13;
|
|
109
|
+
return -1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const PUNCT = new Map([
|
|
113
|
+
[0x0d, 1], [0x21, 6], [0x22, 7], [0x23, 8], [0x24, 9], [0x25, 10],
|
|
114
|
+
[0x26, 11], [0x27, 12], [0x28, 13], [0x29, 14], [0x2a, 15], [0x2b, 16],
|
|
115
|
+
[0x2c, 17], [0x2d, 18], [0x2e, 19], [0x2f, 20], [0x3a, 21], [0x3b, 22],
|
|
116
|
+
[0x3c, 23], [0x3d, 24], [0x3e, 25], [0x3f, 26], [0x5b, 27], [0x5d, 28],
|
|
117
|
+
[0x7b, 29], [0x7d, 30],
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
/** @param {number} byte @param {number} mode @returns {number} */
|
|
121
|
+
function textValue(byte, mode) {
|
|
122
|
+
switch (mode) {
|
|
123
|
+
case HIGH_LEVEL_MODE.UPPER: return upperValue(byte);
|
|
124
|
+
case HIGH_LEVEL_MODE.LOWER: return lowerValue(byte);
|
|
125
|
+
case HIGH_LEVEL_MODE.DIGIT: return digitValue(byte);
|
|
126
|
+
case HIGH_LEVEL_MODE.PUNCT: return PUNCT.get(byte) ?? -1;
|
|
127
|
+
default: return -1;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @param {BitWriter} writer @param {number} from @param {number} to */
|
|
132
|
+
function latch(writer, from, to) {
|
|
133
|
+
if (from === to) return;
|
|
134
|
+
const packed = LATCH[from][to];
|
|
135
|
+
writer.put(packed & 0xffff, packed >>> 16);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @param {number} mode @returns {number} */
|
|
139
|
+
function characterWidth(mode) {
|
|
140
|
+
return mode === HIGH_LEVEL_MODE.DIGIT ? 4 : 5;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Write an Aztec binary-shift segment while in UPPER mode.
|
|
145
|
+
*
|
|
146
|
+
* B/S is `11111`; its five-bit length directly covers 1..31 bytes. A zero
|
|
147
|
+
* length selects the extended eleven-bit form, whose stored value is n - 31.
|
|
148
|
+
* Splitting at 2078 keeps each control representable and makes arbitrarily
|
|
149
|
+
* long byte input well-defined.
|
|
150
|
+
*
|
|
151
|
+
* @param {BitWriter} writer
|
|
152
|
+
* @param {Uint8Array} bytes
|
|
153
|
+
* @param {number} start
|
|
154
|
+
* @param {number} length
|
|
155
|
+
*/
|
|
156
|
+
export function writeBinaryShift(writer, bytes, start, length) {
|
|
157
|
+
let at = start;
|
|
158
|
+
let left = length;
|
|
159
|
+
while (left > 0) {
|
|
160
|
+
const count = Math.min(left, MAX_BINARY_SHIFT);
|
|
161
|
+
writer.put(31, 5); // UPPER B/S
|
|
162
|
+
if (count <= 31) writer.put(count, 5);
|
|
163
|
+
else {
|
|
164
|
+
writer.put(0, 5);
|
|
165
|
+
writer.put(count - 31, 11);
|
|
166
|
+
}
|
|
167
|
+
for (let i = 0; i < count; i++) writer.put(bytes[at + i], 8);
|
|
168
|
+
at += count;
|
|
169
|
+
left -= count;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Build a valid Aztec high-level bitstream.
|
|
175
|
+
*
|
|
176
|
+
* @param {string|ArrayBuffer|ArrayBufferView} value
|
|
177
|
+
* @param {{charset?: 'utf-8'}} [options]
|
|
178
|
+
* @returns {BitWriter}
|
|
179
|
+
*/
|
|
180
|
+
export function encodeHighLevel(value, options = {}) {
|
|
181
|
+
const bytes = aztecBytes(value, options.charset ?? 'utf-8');
|
|
182
|
+
const writer = new BitWriter();
|
|
183
|
+
let mode = HIGH_LEVEL_MODE.UPPER;
|
|
184
|
+
|
|
185
|
+
for (let at = 0; at < bytes.length;) {
|
|
186
|
+
let bestMode = -1;
|
|
187
|
+
let bestValue = -1;
|
|
188
|
+
let bestCost = Number.POSITIVE_INFINITY;
|
|
189
|
+
for (const candidate of [HIGH_LEVEL_MODE.UPPER, HIGH_LEVEL_MODE.LOWER, HIGH_LEVEL_MODE.DIGIT, HIGH_LEVEL_MODE.PUNCT]) {
|
|
190
|
+
const value = textValue(bytes[at], candidate);
|
|
191
|
+
if (value < 0) continue;
|
|
192
|
+
const latchCost = candidate === mode ? 0 : LATCH[mode][candidate] >>> 16;
|
|
193
|
+
const cost = latchCost + characterWidth(candidate);
|
|
194
|
+
if (cost < bestCost) { bestCost = cost; bestMode = candidate; bestValue = value; }
|
|
195
|
+
}
|
|
196
|
+
if (bestMode >= 0) {
|
|
197
|
+
latch(writer, mode, bestMode);
|
|
198
|
+
writer.put(bestValue, characterWidth(bestMode));
|
|
199
|
+
mode = bestMode;
|
|
200
|
+
at++;
|
|
201
|
+
} else {
|
|
202
|
+
// B/S is defined from UPPER; the latch is retained after the shift.
|
|
203
|
+
latch(writer, mode, HIGH_LEVEL_MODE.UPPER);
|
|
204
|
+
mode = HIGH_LEVEL_MODE.UPPER;
|
|
205
|
+
const start = at;
|
|
206
|
+
while (at < bytes.length && ![HIGH_LEVEL_MODE.UPPER, HIGH_LEVEL_MODE.LOWER, HIGH_LEVEL_MODE.DIGIT, HIGH_LEVEL_MODE.PUNCT].some((m) => textValue(bytes[at], m) >= 0)) at++;
|
|
207
|
+
writeBinaryShift(writer, bytes, start, at - start);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return writer;
|
|
211
|
+
}
|