@sythos/js_barcode_universal 1.2.5 → 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 +8 -1
- package/NOTICE.md +3 -0
- package/README.md +548 -463
- package/bundle/sythos-barcode.esm.js +2377 -3
- package/bundle/sythos-barcode.js +2365 -3
- package/examples/create.html +1009 -730
- package/licenses/README.md +35 -58
- package/licenses/aztec-code.license +15 -13
- package/licenses/codabar.license +18 -15
- package/licenses/code-11.license +11 -9
- package/licenses/code-128.license +10 -8
- package/licenses/code-39.license +10 -8
- package/licenses/code-93.license +16 -14
- package/licenses/data-matrix.license +15 -13
- package/licenses/ean-13.license +10 -8
- package/licenses/ean-8.license +10 -8
- package/licenses/frameqr.license +84 -0
- package/licenses/gs1-128.license +10 -8
- package/licenses/isbn.license +10 -8
- package/licenses/itf-14.license +10 -8
- package/licenses/itf.license +9 -7
- package/licenses/micro-qr.license +79 -0
- package/licenses/micropdf417.license +52 -67
- package/licenses/msi-plessey.license +13 -11
- package/licenses/pdf417.license +78 -37
- package/licenses/pharmacode.license +14 -12
- package/licenses/qr-code.license +9 -7
- package/licenses/rmqr.license +79 -0
- package/licenses/upc-a.license +12 -10
- package/licenses/upc-e.license +10 -8
- package/package.json +97 -88
- 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 +91 -2
- 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/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,269 @@
|
|
|
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 { BitWriter } from '../core/bit-buffer.js';
|
|
33
|
+
import { EncodeError } from '../core/errors.js';
|
|
34
|
+
import { GF256_QR } from '../core/galois-field.js';
|
|
35
|
+
import { rsEncode } from '../core/reed-solomon.js';
|
|
36
|
+
import {
|
|
37
|
+
MICROQR_VERSIONS, microQrBlockLayout, microQrDataModuleOrder,
|
|
38
|
+
microQrFormatInfo, microQrFormatInfoPositions, microQrMaskBit,
|
|
39
|
+
microQrSymbolNumber, microQrVersionSize,
|
|
40
|
+
} from './tables.js';
|
|
41
|
+
|
|
42
|
+
export const MICROQR_ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:';
|
|
43
|
+
const MODE = { numeric: 0, alphanumeric: 1, byte: 2, kanji: 3 };
|
|
44
|
+
const MODE_MIN_VERSION = { numeric: 1, alphanumeric: 2, byte: 3, kanji: 3 };
|
|
45
|
+
const COUNT_BITS = {
|
|
46
|
+
numeric: [0, 3, 4, 5, 6], alphanumeric: [0, 0, 3, 4, 5],
|
|
47
|
+
byte: [0, 0, 0, 4, 5], kanji: [0, 0, 0, 3, 4],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function parseVersion(value) {
|
|
51
|
+
if (value == null) return null;
|
|
52
|
+
const match = /^M?([1-4])$/i.exec(String(value));
|
|
53
|
+
if (!match) throw new EncodeError(`Micro QR: version must be M1-M4, got ${value}`);
|
|
54
|
+
return Number(match[1]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function versionNumber(version) {
|
|
58
|
+
return typeof version === 'number' ? version : Number(String(version).slice(1));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function latin1Bytes(text) {
|
|
62
|
+
const bytes = new Uint8Array(text.length);
|
|
63
|
+
for (let i = 0; i < text.length; i++) {
|
|
64
|
+
const cp = text.charCodeAt(i);
|
|
65
|
+
if (cp > 0xff) throw new EncodeError('Micro QR: byte mode supports ISO-8859-1 only (ECI is unavailable)');
|
|
66
|
+
bytes[i] = cp;
|
|
67
|
+
}
|
|
68
|
+
return bytes;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let sjisReverseMap;
|
|
72
|
+
|
|
73
|
+
function sjisToThirteenBits(sjis) {
|
|
74
|
+
const trail = sjis & 0xff;
|
|
75
|
+
if (trail < 0x40 || trail === 0x7f || trail > 0xfc) return -1;
|
|
76
|
+
let adjusted;
|
|
77
|
+
if (sjis >= 0x8140 && sjis <= 0x9ffc) adjusted = sjis - 0x8140;
|
|
78
|
+
else if (sjis >= 0xe040 && sjis <= 0xebbf) adjusted = sjis - 0xc140;
|
|
79
|
+
else return -1;
|
|
80
|
+
const packed = (adjusted >>> 8) * 0xc0 + (adjusted & 0xff);
|
|
81
|
+
return packed <= 0x1fff ? packed : -1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getSjisReverseMap() {
|
|
85
|
+
if (sjisReverseMap !== undefined) return sjisReverseMap;
|
|
86
|
+
let decoder;
|
|
87
|
+
try {
|
|
88
|
+
decoder = new TextDecoder('shift_jis', { fatal: true });
|
|
89
|
+
if (decoder.decode(new Uint8Array([0x82, 0xa0])) !== 'あ') return (sjisReverseMap = null);
|
|
90
|
+
} catch {
|
|
91
|
+
return (sjisReverseMap = null);
|
|
92
|
+
}
|
|
93
|
+
const result = new Map();
|
|
94
|
+
const bytes = new Uint8Array(2);
|
|
95
|
+
for (const [start, end] of [[0x8140, 0x9ffc], [0xe040, 0xebbf]]) {
|
|
96
|
+
for (let value = start; value <= end; value++) {
|
|
97
|
+
if (sjisToThirteenBits(value) < 0) continue;
|
|
98
|
+
bytes[0] = value >>> 8;
|
|
99
|
+
bytes[1] = value & 0xff;
|
|
100
|
+
let character;
|
|
101
|
+
try { character = decoder.decode(bytes); } catch { continue; }
|
|
102
|
+
if (Array.from(character).length === 1 && !result.has(character)) result.set(character, value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
sjisReverseMap = result;
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function kanjiValues(text) {
|
|
110
|
+
const reverse = getSjisReverseMap();
|
|
111
|
+
if (!reverse) return null;
|
|
112
|
+
const values = [];
|
|
113
|
+
for (const character of text) {
|
|
114
|
+
const sjis = reverse.get(character);
|
|
115
|
+
if (sjis == null) return null;
|
|
116
|
+
values.push(sjisToThirteenBits(sjis));
|
|
117
|
+
}
|
|
118
|
+
return values;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function chooseMode(text, forced) {
|
|
122
|
+
const mode = forced == null ? (/^\d+$/.test(text) ? 'numeric' :
|
|
123
|
+
[...text].every((ch) => MICROQR_ALPHANUMERIC.includes(ch)) ? 'alphanumeric' :
|
|
124
|
+
kanjiValues(text) ? 'kanji' : 'byte') :
|
|
125
|
+
String(forced).toLowerCase();
|
|
126
|
+
if (!(mode in MODE)) throw new EncodeError(`Micro QR: unsupported mode "${forced}"`);
|
|
127
|
+
if (mode === 'numeric' && !/^\d+$/.test(text)) throw new EncodeError('Micro QR: numeric mode accepts digits only');
|
|
128
|
+
if (mode === 'alphanumeric' && ![...text].every((ch) => MICROQR_ALPHANUMERIC.includes(ch))) {
|
|
129
|
+
throw new EncodeError('Micro QR: alphanumeric mode contains an unsupported character');
|
|
130
|
+
}
|
|
131
|
+
if (mode === 'kanji' && !kanjiValues(text)) {
|
|
132
|
+
throw new EncodeError('Micro QR: kanji mode requires characters representable in the QR Shift_JIS ranges');
|
|
133
|
+
}
|
|
134
|
+
return mode;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function encodePayload(text, mode) {
|
|
138
|
+
const w = new BitWriter();
|
|
139
|
+
if (mode === 'numeric') {
|
|
140
|
+
for (let i = 0; i < text.length; i += 3) {
|
|
141
|
+
const n = Math.min(3, text.length - i);
|
|
142
|
+
w.put(Number(text.slice(i, i + n)), n === 3 ? 10 : n === 2 ? 7 : 4);
|
|
143
|
+
}
|
|
144
|
+
} else if (mode === 'alphanumeric') {
|
|
145
|
+
let i = 0;
|
|
146
|
+
for (; i + 1 < text.length; i += 2) {
|
|
147
|
+
w.put(MICROQR_ALPHANUMERIC.indexOf(text[i]) * 45 + MICROQR_ALPHANUMERIC.indexOf(text[i + 1]), 11);
|
|
148
|
+
}
|
|
149
|
+
if (i < text.length) w.put(MICROQR_ALPHANUMERIC.indexOf(text[i]), 6);
|
|
150
|
+
} else if (mode === 'byte') {
|
|
151
|
+
w.putBytes(latin1Bytes(text));
|
|
152
|
+
} else {
|
|
153
|
+
for (const value of kanjiValues(text)) w.put(value, 13);
|
|
154
|
+
}
|
|
155
|
+
return w;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function getBit(writer, index) {
|
|
159
|
+
return ((writer.bytes[index >>> 3] >>> (7 - (index & 7))) & 1) === 1;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function writeData(text, mode, version, layout) {
|
|
163
|
+
const numericVersion = versionNumber(version);
|
|
164
|
+
const payload = encodePayload(text, mode);
|
|
165
|
+
const writer = new BitWriter();
|
|
166
|
+
if (numericVersion > 1) writer.put(MODE[mode], numericVersion - 1);
|
|
167
|
+
const count = mode === 'byte' ? latin1Bytes(text).length : [...text].length;
|
|
168
|
+
const countWidth = COUNT_BITS[mode][numericVersion];
|
|
169
|
+
if (countWidth === 0 || count >= 2 ** countWidth) return null;
|
|
170
|
+
writer.put(count, countWidth);
|
|
171
|
+
for (let i = 0; i < payload.length; i++) writer.putBit(getBit(payload, i));
|
|
172
|
+
if (writer.length > layout.dataBits) return null;
|
|
173
|
+
for (let i = 0, n = Math.min(2 * numericVersion + 1, layout.dataBits - writer.length); i < n; i++) writer.putBit(false);
|
|
174
|
+
if (numericVersion !== 1 && numericVersion !== 3) {
|
|
175
|
+
while ((writer.length & 7) && writer.length < layout.dataBits) writer.putBit(false);
|
|
176
|
+
}
|
|
177
|
+
if (numericVersion === 1 || numericVersion === 3) {
|
|
178
|
+
while (writer.length < layout.dataBits) writer.putBit(false);
|
|
179
|
+
return writer;
|
|
180
|
+
}
|
|
181
|
+
let pad = 0;
|
|
182
|
+
while (writer.length + 8 <= layout.dataBits) writer.put(pad++ & 1 ? 0x11 : 0xec, 8);
|
|
183
|
+
while (writer.length < layout.dataBits) writer.putBit(false);
|
|
184
|
+
return writer;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function finalMessage(dataWriter, layout) {
|
|
188
|
+
const bytes = Array.from(dataWriter.toBytes());
|
|
189
|
+
if (layout.shortDataCodewordBits === 4) bytes[bytes.length - 1] &= 0xf0;
|
|
190
|
+
const ecc = rsEncode(bytes, layout.eccCodewords, GF256_QR, 0);
|
|
191
|
+
const out = [];
|
|
192
|
+
const full = layout.shortDataCodewordBits === 4 ? bytes.length - 1 : bytes.length;
|
|
193
|
+
for (let i = 0; i < full; i++) for (let b = 7; b >= 0; b--) out.push((bytes[i] >>> b) & 1);
|
|
194
|
+
if (layout.shortDataCodewordBits === 4) for (let b = 7; b >= 4; b--) out.push((bytes[bytes.length - 1] >>> b) & 1);
|
|
195
|
+
for (const value of ecc) for (let b = 7; b >= 0; b--) out.push((value >>> b) & 1);
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function drawFunctions(matrix) {
|
|
200
|
+
const size = matrix.width;
|
|
201
|
+
for (let y = 0; y < 7; y++) for (let x = 0; x < 7; x++) {
|
|
202
|
+
const ring = x === 0 || x === 6 || y === 0 || y === 6;
|
|
203
|
+
const core = x >= 2 && x <= 4 && y >= 2 && y <= 4;
|
|
204
|
+
matrix.setValue(x, y, ring || core);
|
|
205
|
+
}
|
|
206
|
+
for (let i = 0; i < 8; i++) { matrix.unset(7, i); matrix.unset(i, 7); }
|
|
207
|
+
for (let i = 8; i < size; i++) if ((i & 1) === 0) { matrix.set(i, 0); matrix.set(0, i); }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function microMaskScore(matrix) {
|
|
211
|
+
let right = 0, bottom = 0;
|
|
212
|
+
for (let i = 1; i < matrix.width; i++) {
|
|
213
|
+
if (matrix.get(matrix.width - 1, i)) right++;
|
|
214
|
+
if (matrix.get(i, matrix.height - 1)) bottom++;
|
|
215
|
+
}
|
|
216
|
+
return Math.min(right, bottom) * 16 + Math.max(right, bottom);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildMatrix(version, ecc, mask, bits) {
|
|
220
|
+
const matrix = new BitMatrix(microQrVersionSize(version));
|
|
221
|
+
drawFunctions(matrix);
|
|
222
|
+
const order = microQrDataModuleOrder(version);
|
|
223
|
+
for (let i = 0; i < bits.length; i++) {
|
|
224
|
+
const x = order[i * 2], y = order[i * 2 + 1];
|
|
225
|
+
matrix.setValue(x, y, (bits[i] === 1) !== microQrMaskBit(mask, x, y));
|
|
226
|
+
}
|
|
227
|
+
const format = microQrFormatInfo(microQrSymbolNumber(version, ecc), mask);
|
|
228
|
+
const positions = microQrFormatInfoPositions(matrix.width);
|
|
229
|
+
for (let i = 0; i < 15; i++) matrix.setValue(positions[i][0], positions[i][1], ((format >>> i) & 1) === 1);
|
|
230
|
+
return matrix;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function encodeMicroQR(text, options = {}) {
|
|
234
|
+
text = String(text);
|
|
235
|
+
if (!text) throw new EncodeError('Micro QR: payload must not be empty');
|
|
236
|
+
if (options.eci != null || options.gs1 === true) throw new EncodeError('Micro QR: ECI and GS1/FNC1 are unavailable');
|
|
237
|
+
const mode = chooseMode(text, options.mode);
|
|
238
|
+
const wantedVersion = parseVersion(options.version);
|
|
239
|
+
const wantedEcc = options.ecc == null ? null : String(options.ecc).toUpperCase();
|
|
240
|
+
if (wantedEcc === 'H') throw new EncodeError('Micro QR: error correction level H is unavailable');
|
|
241
|
+
if (options.mask != null && (!Number.isInteger(options.mask) || options.mask < 0 || options.mask > 3)) {
|
|
242
|
+
throw new EncodeError(`Micro QR: mask must be an integer 0-3, got ${options.mask}`);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let selected;
|
|
246
|
+
for (const version of MICROQR_VERSIONS) {
|
|
247
|
+
if (wantedVersion != null && version !== wantedVersion) continue;
|
|
248
|
+
const numericVersion = versionNumber(version);
|
|
249
|
+
if (numericVersion < MODE_MIN_VERSION[mode]) continue;
|
|
250
|
+
const levels = numericVersion === 1 ? ['DETECT'] : numericVersion < 4 ? ['L', 'M'] : ['L', 'M', 'Q'];
|
|
251
|
+
for (const ecc of levels) {
|
|
252
|
+
if (wantedEcc != null && ecc !== wantedEcc) continue;
|
|
253
|
+
const layout = microQrBlockLayout(version, ecc);
|
|
254
|
+
const data = writeData(text, mode, version, layout);
|
|
255
|
+
if (data) { selected = { version, ecc, layout, data }; break; }
|
|
256
|
+
}
|
|
257
|
+
if (selected) break;
|
|
258
|
+
}
|
|
259
|
+
if (!selected) throw new EncodeError('Micro QR: payload does not fit the requested version/error level');
|
|
260
|
+
const bits = finalMessage(selected.data, selected.layout);
|
|
261
|
+
if (options.mask != null) return buildMatrix(selected.version, selected.ecc, options.mask, bits);
|
|
262
|
+
let best, score = -1;
|
|
263
|
+
for (let mask = 0; mask < 4; mask++) {
|
|
264
|
+
const candidate = buildMatrix(selected.version, selected.ecc, mask, bits);
|
|
265
|
+
const candidateScore = microMaskScore(candidate);
|
|
266
|
+
if (candidateScore > score) { score = candidateScore; best = candidate; }
|
|
267
|
+
}
|
|
268
|
+
return best;
|
|
269
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
+
/** Micro QR Code M1-M4 public module surface. @module microqr */
|
|
32
|
+
|
|
33
|
+
export { encodeMicroQR } from './encoder.js';
|
|
34
|
+
export { decodeMicroQR } from './decoder.js';
|
|
35
|
+
export { detectMicroQR, detectAndDecodeMicroQR } from './detector.js';
|
|
36
|
+
export { validateMicroQrTables } from './tables.js';
|
|
@@ -0,0 +1,316 @@
|
|
|
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
|
+
* Micro QR Code structural facts and geometry.
|
|
33
|
+
*
|
|
34
|
+
* Only the irreducible public symbology values are tabulated. Grid capacity,
|
|
35
|
+
* reserved areas and placement order are derived independently and checked by
|
|
36
|
+
* {@link validateMicroQrTables}. M1 and M3 have a four-bit final data symbol
|
|
37
|
+
* character; `dataBits` therefore is authoritative and must not be inferred as
|
|
38
|
+
* `dataCodewords * 8` for those versions.
|
|
39
|
+
*
|
|
40
|
+
* @module microqr/tables
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
44
|
+
|
|
45
|
+
/** Numeric version identifiers used by the encoder selection loop. */
|
|
46
|
+
export const MICROQR_VERSIONS = Object.freeze([1, 2, 3, 4]);
|
|
47
|
+
export const MICROQR_VERSION_NAMES = Object.freeze(['M1', 'M2', 'M3', 'M4']);
|
|
48
|
+
export const MICROQR_ECC_LEVELS = Object.freeze(['DETECT', 'L', 'M', 'Q']);
|
|
49
|
+
export const MICROQR_FORMAT_MASK = 0x4445;
|
|
50
|
+
export const MICROQR_FORMAT_GENERATOR = 0x537;
|
|
51
|
+
|
|
52
|
+
const symbol = (version, ecc, symbolNumber, totalCodewords, dataCodewords, dataBits, eccCodewords) => Object.freeze({
|
|
53
|
+
version,
|
|
54
|
+
ecc,
|
|
55
|
+
symbolNumber,
|
|
56
|
+
size: 9 + Number(version.slice(1)) * 2,
|
|
57
|
+
totalCodewords,
|
|
58
|
+
dataCodewords,
|
|
59
|
+
dataBits,
|
|
60
|
+
eccCodewords,
|
|
61
|
+
blockCount: 1,
|
|
62
|
+
shortDataCodewordBits: dataBits % 8 || 8,
|
|
63
|
+
remainderBits: 0,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
/** The eight legal Micro QR version/error-correction combinations. */
|
|
67
|
+
export const MICROQR_SYMBOLS = Object.freeze([
|
|
68
|
+
symbol('M1', 'DETECT', 0, 5, 3, 20, 2),
|
|
69
|
+
symbol('M2', 'L', 1, 10, 5, 40, 5),
|
|
70
|
+
symbol('M2', 'M', 2, 10, 4, 32, 6),
|
|
71
|
+
symbol('M3', 'L', 3, 17, 11, 84, 6),
|
|
72
|
+
symbol('M3', 'M', 4, 17, 9, 68, 8),
|
|
73
|
+
symbol('M4', 'L', 5, 24, 16, 128, 8),
|
|
74
|
+
symbol('M4', 'M', 6, 24, 14, 112, 10),
|
|
75
|
+
symbol('M4', 'Q', 7, 24, 10, 80, 14),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const symbolByKey = new Map(MICROQR_SYMBOLS.map((entry) => [`${entry.version}:${entry.ecc}`, entry]));
|
|
79
|
+
const symbolByNumber = new Map(MICROQR_SYMBOLS.map((entry) => [entry.symbolNumber, entry]));
|
|
80
|
+
|
|
81
|
+
function canonicalVersion(version) {
|
|
82
|
+
const result = typeof version === 'number' ? `M${version}` : String(version).toUpperCase();
|
|
83
|
+
if (!MICROQR_VERSION_NAMES.includes(result)) throw new RangeError(`Micro QR: version must be M1-M4, got ${version}`);
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** @param {string|number} version @returns {number} */
|
|
88
|
+
export function microQrVersionSize(version) {
|
|
89
|
+
return 9 + Number(canonicalVersion(version).slice(1)) * 2;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Resolve the format symbol number for a legal version/ECC pair. */
|
|
93
|
+
export function microQrSymbolNumber(version, ecc) {
|
|
94
|
+
return microQrBlockLayout(version, ecc).symbolNumber;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Resolve the immutable single-block layout for a legal version/ECC pair. */
|
|
98
|
+
export function microQrBlockLayout(version, ecc) {
|
|
99
|
+
const canonical = canonicalVersion(version);
|
|
100
|
+
const level = ecc == null && canonical === 'M1' ? 'DETECT' : String(ecc).toUpperCase();
|
|
101
|
+
const entry = symbolByKey.get(`${canonical}:${level}`);
|
|
102
|
+
if (!entry) throw new RangeError(`Micro QR: error correction level ${ecc} is not valid for ${canonical}`);
|
|
103
|
+
return entry;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** @returns {number} Usable message bits, including mode/count overhead. */
|
|
107
|
+
export function microQrDataCapacityBits(version, ecc) {
|
|
108
|
+
return microQrBlockLayout(version, ecc).dataBits;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Encode the five format data bits with BCH(15,5), then apply the Micro QR
|
|
113
|
+
* format mask. `symbolNumber` occupies the high three data bits and `mask`
|
|
114
|
+
* the low two.
|
|
115
|
+
*/
|
|
116
|
+
export function microQrFormatInfo(symbolNumber, mask, maybeMask) {
|
|
117
|
+
// Public convenience overload: (version, ecc, mask).
|
|
118
|
+
if (arguments.length === 3) {
|
|
119
|
+
symbolNumber = microQrSymbolNumber(symbolNumber, mask);
|
|
120
|
+
mask = maybeMask;
|
|
121
|
+
}
|
|
122
|
+
if (!Number.isInteger(symbolNumber) || symbolNumber < 0 || symbolNumber > 7) {
|
|
123
|
+
throw new RangeError('Micro QR: symbol number must be an integer in 0..7');
|
|
124
|
+
}
|
|
125
|
+
if (!Number.isInteger(mask) || mask < 0 || mask > 3) {
|
|
126
|
+
throw new RangeError('Micro QR: mask must be an integer in 0..3');
|
|
127
|
+
}
|
|
128
|
+
const data = (symbolNumber << 2) | mask;
|
|
129
|
+
let remainder = data << 10;
|
|
130
|
+
for (let bit = 14; bit >= 10; bit--) {
|
|
131
|
+
if ((remainder & (1 << bit)) !== 0) remainder ^= MICROQR_FORMAT_GENERATOR << (bit - 10);
|
|
132
|
+
}
|
|
133
|
+
return ((data << 10) | remainder) ^ MICROQR_FORMAT_MASK;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function hammingDistance(a, b) {
|
|
137
|
+
let bits = (a ^ b) & 0x7fff;
|
|
138
|
+
let count = 0;
|
|
139
|
+
while (bits) { bits &= bits - 1; count++; }
|
|
140
|
+
return count;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Decode/correct a 15-bit format word. Returns null beyond three errors. */
|
|
144
|
+
export function microQrDecodeFormatInfo(bits) {
|
|
145
|
+
if (!Number.isInteger(bits) || bits < 0 || bits > 0x7fff) {
|
|
146
|
+
throw new RangeError('Micro QR: format information must be a 15-bit integer');
|
|
147
|
+
}
|
|
148
|
+
let best = null;
|
|
149
|
+
let bestDistance = 16;
|
|
150
|
+
for (const entry of MICROQR_SYMBOLS) for (let mask = 0; mask < 4; mask++) {
|
|
151
|
+
const expected = microQrFormatInfo(entry.symbolNumber, mask);
|
|
152
|
+
const distance = hammingDistance(bits, expected);
|
|
153
|
+
if (distance < bestDistance) {
|
|
154
|
+
bestDistance = distance;
|
|
155
|
+
best = { version: entry.version, ecc: entry.ecc, symbolNumber: entry.symbolNumber, mask, correctedBits: distance, bits: expected };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return bestDistance <= 3 ? best : null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format modules in bit-number order, least significant bit first. Bits 0..7
|
|
163
|
+
* run down column 8; bits 8..14 continue right-to-left along row 8.
|
|
164
|
+
* Position (8,8) is shared by the two arms and appears once.
|
|
165
|
+
*/
|
|
166
|
+
export function microQrFormatInfoPositions(sizeOrVersion) {
|
|
167
|
+
const size = typeof sizeOrVersion === 'number' && sizeOrVersion >= 11
|
|
168
|
+
? sizeOrVersion
|
|
169
|
+
: microQrVersionSize(sizeOrVersion);
|
|
170
|
+
if (![11, 13, 15, 17].includes(size)) throw new RangeError(`Micro QR: invalid symbol size ${size}`);
|
|
171
|
+
const positions = [];
|
|
172
|
+
for (let y = 1; y <= 8; y++) positions.push([8, y]);
|
|
173
|
+
for (let x = 7; x >= 1; x--) positions.push([x, 8]);
|
|
174
|
+
return positions;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const reservedCache = new Map();
|
|
178
|
+
const functionCache = new Map();
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Fixed dark function modules before format information is written. Light
|
|
182
|
+
* separator and light timing modules remain unset; use
|
|
183
|
+
* {@link microQrReservedModules} to distinguish them from payload modules.
|
|
184
|
+
*/
|
|
185
|
+
export function microQrFunctionModules(version) {
|
|
186
|
+
const canonical = canonicalVersion(version);
|
|
187
|
+
const cached = functionCache.get(canonical);
|
|
188
|
+
if (cached) return cached;
|
|
189
|
+
const size = microQrVersionSize(canonical);
|
|
190
|
+
const matrix = new BitMatrix(size, size);
|
|
191
|
+
for (let y = 0; y < 7; y++) for (let x = 0; x < 7; x++) {
|
|
192
|
+
const outer = x === 0 || x === 6 || y === 0 || y === 6;
|
|
193
|
+
const centre = x >= 2 && x <= 4 && y >= 2 && y <= 4;
|
|
194
|
+
if (outer || centre) matrix.set(x, y);
|
|
195
|
+
}
|
|
196
|
+
for (let coordinate = 8; coordinate < size; coordinate += 2) {
|
|
197
|
+
matrix.set(coordinate, 0);
|
|
198
|
+
matrix.set(0, coordinate);
|
|
199
|
+
}
|
|
200
|
+
functionCache.set(canonical, matrix);
|
|
201
|
+
return matrix;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Shared immutable-in-use map of finder, separator, timing and format modules. */
|
|
205
|
+
export function microQrReservedModules(version) {
|
|
206
|
+
const canonical = canonicalVersion(version);
|
|
207
|
+
const cached = reservedCache.get(canonical);
|
|
208
|
+
if (cached) return cached;
|
|
209
|
+
const size = microQrVersionSize(canonical);
|
|
210
|
+
const matrix = new BitMatrix(size, size);
|
|
211
|
+
matrix.setRegion(0, 0, 8, 8); // 7x7 finder plus inner separator
|
|
212
|
+
for (let coordinate = 8; coordinate < size; coordinate++) {
|
|
213
|
+
matrix.set(coordinate, 0); // horizontal timing
|
|
214
|
+
matrix.set(0, coordinate); // vertical timing
|
|
215
|
+
}
|
|
216
|
+
for (const [x, y] of microQrFormatInfoPositions(size)) matrix.set(x, y);
|
|
217
|
+
reservedCache.set(canonical, matrix);
|
|
218
|
+
return matrix;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Number of modules carrying data or error-correction bits. */
|
|
222
|
+
export function microQrFreeModuleCount(version) {
|
|
223
|
+
const size = microQrVersionSize(version);
|
|
224
|
+
const reserved = microQrReservedModules(version);
|
|
225
|
+
let count = 0;
|
|
226
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
227
|
+
if (!reserved.get(x, y)) count++;
|
|
228
|
+
}
|
|
229
|
+
return count;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const orderCache = new Map();
|
|
233
|
+
|
|
234
|
+
/** Payload module coordinates as interleaved x,y pairs, MSB-first stream order. */
|
|
235
|
+
export function microQrDataModuleOrder(version) {
|
|
236
|
+
const canonical = canonicalVersion(version);
|
|
237
|
+
const cached = orderCache.get(canonical);
|
|
238
|
+
if (cached) return cached;
|
|
239
|
+
const size = microQrVersionSize(canonical);
|
|
240
|
+
const reserved = microQrReservedModules(canonical);
|
|
241
|
+
const order = new Int32Array(microQrFreeModuleCount(canonical) * 2);
|
|
242
|
+
let offset = 0;
|
|
243
|
+
let upward = true;
|
|
244
|
+
for (let column = size - 1; column > 0; column -= 2) {
|
|
245
|
+
for (let rowOffset = 0; rowOffset < size; rowOffset++) {
|
|
246
|
+
const y = upward ? size - 1 - rowOffset : rowOffset;
|
|
247
|
+
for (let side = 0; side < 2; side++) {
|
|
248
|
+
const x = column - side;
|
|
249
|
+
if (reserved.get(x, y)) continue;
|
|
250
|
+
order[offset++] = x;
|
|
251
|
+
order[offset++] = y;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
upward = !upward;
|
|
255
|
+
}
|
|
256
|
+
orderCache.set(canonical, order);
|
|
257
|
+
return order;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The four Micro QR data-mask predicates. */
|
|
261
|
+
export function microQrMaskBit(mask, x, y) {
|
|
262
|
+
switch (mask) {
|
|
263
|
+
case 0: return (y & 1) === 0;
|
|
264
|
+
case 1: return (((y >> 1) + Math.floor(x / 3)) & 1) === 0;
|
|
265
|
+
case 2: return ((((y * x) & 1) + ((y * x) % 3)) & 1) === 0;
|
|
266
|
+
case 3: return ((((y + x) & 1) + ((y * x) % 3)) & 1) === 0;
|
|
267
|
+
default: throw new RangeError(`Micro QR: mask must be an integer in 0..3, got ${mask}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Return all internal table/geometry invariant failures. */
|
|
272
|
+
export function validateMicroQrTables() {
|
|
273
|
+
const issues = [];
|
|
274
|
+
if (MICROQR_SYMBOLS.length !== 8) issues.push('expected eight symbol/ECC combinations');
|
|
275
|
+
const numbers = new Set();
|
|
276
|
+
for (const entry of MICROQR_SYMBOLS) {
|
|
277
|
+
const tag = `${entry.version}-${entry.ecc}`;
|
|
278
|
+
if (numbers.has(entry.symbolNumber)) issues.push(`${tag}: duplicate symbol number`);
|
|
279
|
+
numbers.add(entry.symbolNumber);
|
|
280
|
+
if (entry.blockCount !== 1) issues.push(`${tag}: Micro QR must use one block`);
|
|
281
|
+
if (entry.dataBits + entry.eccCodewords * 8 !== microQrFreeModuleCount(entry.version)) {
|
|
282
|
+
issues.push(`${tag}: data/ECC bits do not fill the encoding region`);
|
|
283
|
+
}
|
|
284
|
+
if (entry.totalCodewords !== entry.dataCodewords + entry.eccCodewords) issues.push(`${tag}: codeword count mismatch`);
|
|
285
|
+
if (entry.dataBits !== (entry.dataCodewords - 1) * 8 + entry.shortDataCodewordBits) issues.push(`${tag}: final data codeword mismatch`);
|
|
286
|
+
if (entry.shortDataCodewordBits !== (entry.version === 'M1' || entry.version === 'M3' ? 4 : 8)) issues.push(`${tag}: wrong final data codeword width`);
|
|
287
|
+
}
|
|
288
|
+
for (const version of MICROQR_VERSIONS) {
|
|
289
|
+
const size = microQrVersionSize(version);
|
|
290
|
+
const positions = microQrFormatInfoPositions(size);
|
|
291
|
+
const unique = new Set(positions.map(([x, y]) => `${x},${y}`));
|
|
292
|
+
if (positions.length !== 15 || unique.size !== 15) issues.push(`${version}: format positions must be 15 unique modules`);
|
|
293
|
+
const order = microQrDataModuleOrder(version);
|
|
294
|
+
const orderUnique = new Set();
|
|
295
|
+
for (let i = 0; i < order.length; i += 2) {
|
|
296
|
+
const x = order[i], y = order[i + 1];
|
|
297
|
+
if (microQrReservedModules(version).get(x, y)) issues.push(`${version}: placement enters reserved module ${x},${y}`);
|
|
298
|
+
orderUnique.add(`${x},${y}`);
|
|
299
|
+
}
|
|
300
|
+
if (orderUnique.size * 2 !== order.length) issues.push(`${version}: placement repeats a module`);
|
|
301
|
+
if (order.length !== microQrFreeModuleCount(version) * 2) issues.push(`${version}: placement does not cover encoding region`);
|
|
302
|
+
const functions = microQrFunctionModules(version);
|
|
303
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) {
|
|
304
|
+
if (functions.get(x, y) && !microQrReservedModules(version).get(x, y)) {
|
|
305
|
+
issues.push(`${version}: dark function module ${x},${y} is not reserved`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const entry of MICROQR_SYMBOLS) for (let mask = 0; mask < 4; mask++) {
|
|
310
|
+
const decoded = microQrDecodeFormatInfo(microQrFormatInfo(entry.symbolNumber, mask));
|
|
311
|
+
if (!decoded || decoded.symbolNumber !== entry.symbolNumber || decoded.mask !== mask || decoded.correctedBits !== 0) {
|
|
312
|
+
issues.push(`${entry.version}-${entry.ecc}: format round-trip failed for mask ${mask}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return issues;
|
|
316
|
+
}
|