@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.
Files changed (53) hide show
  1. package/LICENSE +8 -1
  2. package/NOTICE.md +3 -0
  3. package/README.md +548 -463
  4. package/bundle/sythos-barcode.esm.js +2377 -3
  5. package/bundle/sythos-barcode.js +2365 -3
  6. package/examples/create.html +1009 -730
  7. package/licenses/README.md +35 -58
  8. package/licenses/aztec-code.license +15 -13
  9. package/licenses/codabar.license +18 -15
  10. package/licenses/code-11.license +11 -9
  11. package/licenses/code-128.license +10 -8
  12. package/licenses/code-39.license +10 -8
  13. package/licenses/code-93.license +16 -14
  14. package/licenses/data-matrix.license +15 -13
  15. package/licenses/ean-13.license +10 -8
  16. package/licenses/ean-8.license +10 -8
  17. package/licenses/frameqr.license +84 -0
  18. package/licenses/gs1-128.license +10 -8
  19. package/licenses/isbn.license +10 -8
  20. package/licenses/itf-14.license +10 -8
  21. package/licenses/itf.license +9 -7
  22. package/licenses/micro-qr.license +79 -0
  23. package/licenses/micropdf417.license +52 -67
  24. package/licenses/msi-plessey.license +13 -11
  25. package/licenses/pdf417.license +78 -37
  26. package/licenses/pharmacode.license +14 -12
  27. package/licenses/qr-code.license +9 -7
  28. package/licenses/rmqr.license +79 -0
  29. package/licenses/upc-a.license +12 -10
  30. package/licenses/upc-e.license +10 -8
  31. package/package.json +97 -88
  32. package/src/datamatrix/decoder.js +262 -262
  33. package/src/datamatrix/detector.js +225 -225
  34. package/src/datamatrix/encoder.js +191 -191
  35. package/src/datamatrix/index.js +42 -42
  36. package/src/datamatrix/tables.js +123 -123
  37. package/src/frameqr/decoder.js +239 -0
  38. package/src/frameqr/detector.js +192 -0
  39. package/src/frameqr/encoder.js +156 -0
  40. package/src/frameqr/index.js +42 -0
  41. package/src/frameqr/tables.js +270 -0
  42. package/src/index.js +91 -2
  43. package/src/microqr/decoder.js +245 -0
  44. package/src/microqr/detector.js +355 -0
  45. package/src/microqr/encoder.js +269 -0
  46. package/src/microqr/index.js +36 -0
  47. package/src/microqr/tables.js +316 -0
  48. package/src/oned/index.js +59 -59
  49. package/src/rmqr/decoder.js +101 -0
  50. package/src/rmqr/detector.js +90 -0
  51. package/src/rmqr/encoder.js +172 -0
  52. package/src/rmqr/index.js +37 -0
  53. package/src/rmqr/tables.js +154 -0
@@ -0,0 +1,156 @@
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
+ * Sythos Canvas QR profile encoder.
33
+ *
34
+ * This encoder deliberately builds on this project's QR Code implementation
35
+ * and then removes a conservatively bounded set of data modules for artwork.
36
+ * It is not an implementation of DENSO FrameQR and makes no interoperability
37
+ * claim for that proprietary format.
38
+ *
39
+ * @module frameqr/encoder
40
+ */
41
+
42
+ import { EncodeError } from '../core/errors.js';
43
+ import { encodeQR } from '../qr/encoder.js';
44
+ import {
45
+ FRAMEQR_PROFILE,
46
+ canvasModules,
47
+ normalizeCanvasSpec,
48
+ validateCanvasSpec,
49
+ } from './tables.js';
50
+
51
+ /**
52
+ * @typedef {object} FrameQrEncodeOptions
53
+ * @property {'H'} [ecc] The profile always uses QR error correction H.
54
+ * @property {number} [version] Force a QR version 1-40.
55
+ * @property {number} [mask] Force a QR mask 0-7.
56
+ * @property {'auto'|'utf-8'|'iso-8859-1'} [charset] Byte mode interpretation.
57
+ * @property {boolean} [kanji] Allow QR kanji mode.
58
+ * @property {object} [canvas] Profile artwork reservation.
59
+ */
60
+
61
+ function versionFor(matrix) {
62
+ return (matrix.width - 17) / 4;
63
+ }
64
+
65
+ function clearCanvas(matrix, modules) {
66
+ for (const [x, y] of modules) matrix.unset(x, y);
67
+ }
68
+
69
+ /**
70
+ * Encode a QR Code with a conservative artwork canvas according to the
71
+ * non-certified Sythos Canvas QR profile.
72
+ *
73
+ * The profile forces QR H error correction and rejects a canvas whenever its
74
+ * known codeword damage exceeds the per-block correction budget. When a
75
+ * version is not forced, the smallest QR version that holds both payload and
76
+ * safe canvas is selected. A decoder can reconstruct the reserved modules from
77
+ * the returned profile metadata.
78
+ *
79
+ * @param {string} text
80
+ * @param {FrameQrEncodeOptions} [options]
81
+ * @returns {import('../core/bit-matrix.js').BitMatrix}
82
+ * @throws {EncodeError} When the QR payload/options are invalid or the canvas
83
+ * cannot safely fit the selected QR version.
84
+ */
85
+ export function encodeFrameQR(text, options = {}) {
86
+ if (typeof text !== 'string') {
87
+ throw new EncodeError('Sythos Canvas QR: text must be a string');
88
+ }
89
+ if (options.ecc !== undefined && options.ecc !== 'H') {
90
+ throw new EncodeError('Sythos Canvas QR: ecc is fixed to H for this profile');
91
+ }
92
+
93
+ const qrOptions = {
94
+ mask: options.mask,
95
+ charset: options.charset,
96
+ kanji: options.kanji,
97
+ ecc: 'H',
98
+ };
99
+ for (const key of Object.keys(qrOptions)) {
100
+ if (qrOptions[key] === undefined) delete qrOptions[key];
101
+ }
102
+
103
+ const versions = options.version === undefined
104
+ ? Array.from({ length: 40 }, (_, index) => index + 1)
105
+ : [options.version];
106
+ let capacityError = null;
107
+ let unsafeAnalysis = null;
108
+ let selected = null;
109
+
110
+ for (const version of versions) {
111
+ let matrix;
112
+ try {
113
+ matrix = encodeQR(text, { ...qrOptions, version });
114
+ } catch (error) {
115
+ capacityError = error;
116
+ continue;
117
+ }
118
+
119
+ let canvas;
120
+ let analysis;
121
+ try {
122
+ canvas = normalizeCanvasSpec(matrix.width, options.canvas);
123
+ analysis = validateCanvasSpec(versionFor(matrix), canvas);
124
+ } catch (error) {
125
+ if (error instanceof EncodeError) throw error;
126
+ throw new EncodeError(`Sythos Canvas QR: invalid canvas: ${error.message}`);
127
+ }
128
+ if (!analysis.safe) {
129
+ unsafeAnalysis = analysis;
130
+ continue;
131
+ }
132
+ selected = { matrix, canvas };
133
+ break;
134
+ }
135
+
136
+ if (!selected) {
137
+ if (unsafeAnalysis) {
138
+ throw new EncodeError(
139
+ 'Sythos Canvas QR: canvas is not safe for the selected QR version; ' +
140
+ `it touches ${unsafeAnalysis.touchedCodewordCount} codewords and has ` +
141
+ `a per-block correction budget of ${unsafeAnalysis.correctionBudgetPerBlock}`
142
+ );
143
+ }
144
+ if (capacityError) throw capacityError;
145
+ throw new EncodeError('Sythos Canvas QR: unable to select a QR version');
146
+ }
147
+
148
+ const { matrix, canvas } = selected;
149
+ clearCanvas(matrix, canvasModules(matrix.width, canvas));
150
+ matrix.frameqr = {
151
+ profile: FRAMEQR_PROFILE.id,
152
+ certified: false,
153
+ canvas,
154
+ };
155
+ return matrix;
156
+ }
@@ -0,0 +1,42 @@
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
+ export { encodeFrameQR } from './encoder.js';
32
+ export { decodeFrameQR } from './decoder.js';
33
+ export { detectFrameQR, detectAndDecodeFrameQR } from './detector.js';
34
+ export {
35
+ FRAMEQR_PROFILE,
36
+ FRAMEQR_CANVAS_SHAPES,
37
+ canvasModules,
38
+ normalizeCanvasSpec,
39
+ analyzeCanvasDamage,
40
+ validateCanvasSpec,
41
+ validateFrameQrTables,
42
+ } from './tables.js';
@@ -0,0 +1,270 @@
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
+ * Structural contract for the Sythos Canvas QR profile.
33
+ *
34
+ * DENSO WAVE's public material describes FrameQR(R) as a proprietary symbol
35
+ * with a freely shaped canvas, dedicated generation/reading software and no
36
+ * compatibility with ordinary QR readers. It does not publish the bitstream,
37
+ * placement or error-correction rules required for an interoperable encoder.
38
+ * Consequently this module does not claim to implement DENSO FrameQR.
39
+ *
40
+ * The implementable profile below starts with an ISO/IEC 18004 QR Model 2
41
+ * symbol at level H and clears a bounded group of data modules. Its worst-case
42
+ * damage is calculated per Reed-Solomon block and must remain within the
43
+ * standard QR correction radius. Function modules are never canvas modules.
44
+ * This provides a deterministic, independently testable canvas QR profile, but
45
+ * it is explicitly non-certified and not a substitute for proprietary FrameQR
46
+ * generation or validation software.
47
+ *
48
+ * @module frameqr/tables
49
+ */
50
+
51
+ import {
52
+ blockLayout,
53
+ dataModuleOrder,
54
+ reservedModules,
55
+ versionSize,
56
+ } from '../qr/tables.js';
57
+
58
+ /** Public identity and compatibility boundary of the implementable profile. */
59
+ export const FRAMEQR_PROFILE = Object.freeze({
60
+ id: 'sythos-canvas-qr/1',
61
+ name: 'Sythos Canvas QR profile',
62
+ certified: false,
63
+ densoFrameQrCompatible: false,
64
+ baseSymbology: 'QR Code Model 2',
65
+ requiredEcc: 'H',
66
+ standard: 'ISO/IEC 18004 QR Code baseline',
67
+ });
68
+
69
+ /** Canvas shapes whose module membership is fully deterministic. */
70
+ export const FRAMEQR_CANVAS_SHAPES = Object.freeze(['square', 'circle', 'diamond']);
71
+
72
+ function oddAtMost(value, maximum) {
73
+ let n = Math.max(1, Math.min(maximum, Math.floor(value)));
74
+ if ((n & 1) === 0) n--;
75
+ return Math.max(1, n);
76
+ }
77
+
78
+ function assertSymbolSize(symbolSize) {
79
+ if (!Number.isInteger(symbolSize) || symbolSize < 21 || symbolSize > 177 || (symbolSize - 17) % 4 !== 0) {
80
+ throw new RangeError(`Frame QR profile: ${symbolSize} is not a QR Model 2 symbol size`);
81
+ }
82
+ }
83
+
84
+ /**
85
+ * Canonicalise a canvas request.
86
+ *
87
+ * Coordinates and dimensions are module units. Odd dimensions make the centre
88
+ * unambiguous. Only quarter turns are accepted because arbitrary-angle raster
89
+ * membership would depend on renderer-specific sampling.
90
+ *
91
+ * @param {number} symbolSize QR module width/height (21..177).
92
+ * @param {object} [canvas]
93
+ * @returns {{shape:string,centerX:number,centerY:number,width:number,height:number,angle:number}}
94
+ */
95
+ export function normalizeCanvasSpec(symbolSize, canvas = {}) {
96
+ assertSymbolSize(symbolSize);
97
+ if (canvas === null || typeof canvas !== 'object' || Array.isArray(canvas)) {
98
+ throw new TypeError('Frame QR profile: canvas must be an object');
99
+ }
100
+
101
+ const shape = String(canvas.shape ?? 'square').toLowerCase();
102
+ if (!FRAMEQR_CANVAS_SHAPES.includes(shape)) {
103
+ throw new RangeError(`Frame QR profile: unsupported canvas shape "${shape}"`);
104
+ }
105
+
106
+ const defaultSize = oddAtMost(Math.max(3, Math.round(symbolSize * 0.17)), symbolSize - 16);
107
+ const requestedSize = canvas.size;
108
+ const requestedWidth = canvas.width ?? requestedSize ?? defaultSize;
109
+ const requestedHeight = canvas.height ?? requestedSize ?? defaultSize;
110
+ const maximumDimension = symbolSize - 16;
111
+ for (const [name, value] of [['width', requestedWidth], ['height', requestedHeight]]) {
112
+ if (!Number.isFinite(Number(value)) || Number(value) < 1 || Number(value) > maximumDimension) {
113
+ throw new RangeError(
114
+ `Frame QR profile: canvas ${name} must be between 1 and ${maximumDimension} modules`
115
+ );
116
+ }
117
+ }
118
+ const width = oddAtMost(Number(requestedWidth), maximumDimension);
119
+ const height = oddAtMost(Number(requestedHeight), maximumDimension);
120
+ const centerX = Math.round(canvas.centerX ?? (symbolSize - 1) / 2);
121
+ const centerY = Math.round(canvas.centerY ?? (symbolSize - 1) / 2);
122
+ const angle = ((Number(canvas.angle ?? 0) % 360) + 360) % 360;
123
+
124
+ if (![0, 90, 180, 270].includes(angle)) {
125
+ throw new RangeError('Frame QR profile: angle must be 0, 90, 180 or 270 degrees');
126
+ }
127
+ if (centerX < 8 || centerY < 8 || centerX >= symbolSize - 8 || centerY >= symbolSize - 8) {
128
+ throw new RangeError('Frame QR profile: canvas centre must remain inside the finder-pattern boundary');
129
+ }
130
+
131
+ return { shape, centerX, centerY, width, height, angle };
132
+ }
133
+
134
+ /**
135
+ * Enumerate canvas modules, including any overlaps with QR function modules.
136
+ * A conforming encoder rejects such overlaps rather than damaging function
137
+ * patterns.
138
+ *
139
+ * @param {number} symbolSize
140
+ * @param {object} [canvas]
141
+ * @returns {Array<[number, number]>}
142
+ */
143
+ export function canvasModules(symbolSize, canvas = {}) {
144
+ const spec = normalizeCanvasSpec(symbolSize, canvas);
145
+ const rotate = spec.angle === 90 || spec.angle === 270;
146
+ const width = rotate ? spec.height : spec.width;
147
+ const height = rotate ? spec.width : spec.height;
148
+ const halfWidth = (width - 1) / 2;
149
+ const halfHeight = (height - 1) / 2;
150
+ const modules = [];
151
+
152
+ for (let dy = -halfHeight; dy <= halfHeight; dy++) {
153
+ for (let dx = -halfWidth; dx <= halfWidth; dx++) {
154
+ const nx = halfWidth === 0 ? 0 : dx / halfWidth;
155
+ const ny = halfHeight === 0 ? 0 : dy / halfHeight;
156
+ let inside;
157
+ if (spec.shape === 'circle') inside = nx * nx + ny * ny <= 1 + Number.EPSILON;
158
+ else if (spec.shape === 'diamond') inside = Math.abs(nx) + Math.abs(ny) <= 1 + Number.EPSILON;
159
+ else inside = true;
160
+ if (inside) modules.push([spec.centerX + dx, spec.centerY + dy]);
161
+ }
162
+ }
163
+ return modules;
164
+ }
165
+
166
+ /** Build the interleaved-codeword to RS-block map used by QR Model 2. */
167
+ function codewordBlockMap(layout) {
168
+ const dataCounts = new Array(layout.blockCount);
169
+ for (let block = 0; block < layout.blockCount; block++) {
170
+ dataCounts[block] = block < layout.group1Blocks
171
+ ? layout.group1DataCount
172
+ : layout.group2DataCount;
173
+ }
174
+
175
+ const map = [];
176
+ const maxData = Math.max(...dataCounts);
177
+ for (let i = 0; i < maxData; i++) {
178
+ for (let block = 0; block < layout.blockCount; block++) {
179
+ if (i < dataCounts[block]) map.push(block);
180
+ }
181
+ }
182
+ for (let i = 0; i < layout.eccPerBlock; i++) {
183
+ for (let block = 0; block < layout.blockCount; block++) map.push(block);
184
+ }
185
+ return map;
186
+ }
187
+
188
+ /**
189
+ * Calculate worst-case QR codeword damage caused by a canvas.
190
+ * A codeword is counted if any of its modules is touched. This is conservative:
191
+ * clearing an already-light module does no damage, but safety cannot depend on
192
+ * one payload or mask.
193
+ *
194
+ * @param {number} version QR version 1..40.
195
+ * @param {object} [canvas]
196
+ */
197
+ export function analyzeCanvasDamage(version, canvas = {}) {
198
+ if (!Number.isInteger(version) || version < 1 || version > 40) {
199
+ throw new RangeError(`Frame QR profile: version must be an integer 1-40, got ${version}`);
200
+ }
201
+ const symbolSize = versionSize(version);
202
+ const spec = normalizeCanvasSpec(symbolSize, canvas);
203
+ const modules = canvasModules(symbolSize, spec);
204
+ const reserved = reservedModules(version);
205
+ const reservedOverlaps = modules.filter(([x, y]) => reserved.get(x, y));
206
+
207
+ const moduleKeys = new Set(modules.map(([x, y]) => `${x},${y}`));
208
+ const order = dataModuleOrder(version);
209
+ const touchedCodewords = new Set();
210
+ for (let bit = 0; bit < order.length / 2; bit++) {
211
+ if (moduleKeys.has(`${order[bit * 2]},${order[bit * 2 + 1]}`)) touchedCodewords.add(bit >> 3);
212
+ }
213
+
214
+ const layout = blockLayout(version, 'H');
215
+ const blockMap = codewordBlockMap(layout);
216
+ const touchedByBlockSets = Array.from({ length: layout.blockCount }, () => new Set());
217
+ for (const codeword of touchedCodewords) {
218
+ const block = blockMap[codeword];
219
+ if (block !== undefined) touchedByBlockSets[block].add(codeword);
220
+ }
221
+ const touchedCodewordsByBlock = touchedByBlockSets.map((set) => set.size);
222
+ const correctionBudgetPerBlock = Math.floor(layout.eccPerBlock / 2);
223
+ const safe = reservedOverlaps.length === 0 &&
224
+ touchedCodewordsByBlock.every((count) => count <= correctionBudgetPerBlock);
225
+
226
+ return {
227
+ profile: FRAMEQR_PROFILE.id,
228
+ certified: false,
229
+ version,
230
+ symbolSize,
231
+ canvas: spec,
232
+ canvasModuleCount: modules.length,
233
+ reservedOverlaps,
234
+ touchedCodewordCount: touchedCodewords.size,
235
+ touchedCodewordsByBlock,
236
+ correctionBudgetPerBlock,
237
+ safe,
238
+ };
239
+ }
240
+
241
+ /** Validate a canvas and return the non-certifying structural analysis. */
242
+ export function validateCanvasSpec(version, canvas = {}) {
243
+ return analyzeCanvasDamage(version, canvas);
244
+ }
245
+
246
+ /** Self-check the fixed profile contract and representative QR geometries. */
247
+ export function validateFrameQrTables() {
248
+ const problems = [];
249
+ if (FRAMEQR_PROFILE.certified !== false || FRAMEQR_PROFILE.densoFrameQrCompatible !== false) {
250
+ problems.push('profile compatibility boundary must remain explicitly non-certified');
251
+ }
252
+ if (new Set(FRAMEQR_CANVAS_SHAPES).size !== FRAMEQR_CANVAS_SHAPES.length) {
253
+ problems.push('canvas shape identifiers must be unique');
254
+ }
255
+ for (const version of [1, 2, 3, 4, 7, 10, 20, 30, 40]) {
256
+ const size = versionSize(version);
257
+ const spec = normalizeCanvasSpec(size);
258
+ const modules = canvasModules(size, spec);
259
+ const unique = new Set(modules.map(([x, y]) => `${x},${y}`));
260
+ if (unique.size !== modules.length) problems.push(`v${version}: duplicate canvas modules`);
261
+ if (modules.some(([x, y]) => x < 0 || y < 0 || x >= size || y >= size)) {
262
+ problems.push(`v${version}: canvas escapes the symbol`);
263
+ }
264
+ const analysis = analyzeCanvasDamage(version, spec);
265
+ if (analysis.touchedCodewordsByBlock.length !== blockLayout(version, 'H').blockCount) {
266
+ problems.push(`v${version}: damage analysis block count mismatch`);
267
+ }
268
+ }
269
+ return problems;
270
+ }
package/src/index.js CHANGED
@@ -56,6 +56,9 @@ import * as qr from './qr/index.js';
56
56
  import * as aztec from './aztec/index.js';
57
57
  import * as pdf417 from './pdf417/index.js';
58
58
  import * as micropdf417 from './micropdf417/index.js';
59
+ import * as microqr from './microqr/index.js';
60
+ import * as rmqr from './rmqr/index.js';
61
+ import * as frameqr from './frameqr/index.js';
59
62
 
60
63
  export { BitMatrix };
61
64
  export {
@@ -78,6 +81,11 @@ export { encodePDF417, decodePDF417, detectPDF417, detectAndDecodePDF417 } from
78
81
  export {
79
82
  encodeMicroPDF417, decodeMicroPDF417, detectMicroPDF417, detectAndDecodeMicroPDF417,
80
83
  } from './micropdf417/index.js';
84
+ export { encodeMicroQR, decodeMicroQR, detectMicroQR, detectAndDecodeMicroQR } from './microqr/index.js';
85
+ export { encodeRMQR, decodeRMQR, detectRMQR, detectAndDecodeRMQR } from './rmqr/index.js';
86
+ export {
87
+ encodeFrameQR, decodeFrameQR, detectFrameQR, detectAndDecodeFrameQR,
88
+ } from './frameqr/index.js';
81
89
 
82
90
  /**
83
91
  * @typedef {object} FormatInfo
@@ -113,6 +121,12 @@ const pdf417CanEncode = typeof pdf417.encodePDF417 === 'function';
113
121
  const pdf417CanDecode = typeof pdf417.detectAndDecodePDF417 === 'function';
114
122
  const microPdf417CanEncode = typeof micropdf417.encodeMicroPDF417 === 'function';
115
123
  const microPdf417CanDecode = typeof micropdf417.detectAndDecodeMicroPDF417 === 'function';
124
+ const microQrCanEncode = typeof microqr.encodeMicroQR === 'function';
125
+ const microQrCanDecode = typeof microqr.detectAndDecodeMicroQR === 'function';
126
+ const rmqrCanEncode = typeof rmqr.encodeRMQR === 'function';
127
+ const rmqrCanDecode = typeof rmqr.detectAndDecodeRMQR === 'function';
128
+ const frameQrCanEncode = typeof frameqr.encodeFrameQR === 'function';
129
+ const frameQrCanDecode = typeof frameqr.detectAndDecodeFrameQR === 'function';
116
130
 
117
131
  /**
118
132
  * Every format this build supports.
@@ -168,6 +182,27 @@ export function listFormats() {
168
182
  canRead: microPdf417CanDecode,
169
183
  kind: /** @type {'2D'} */ ('2D'),
170
184
  });
185
+ formats.push({
186
+ id: 'microqr',
187
+ label: 'Micro QR Code',
188
+ canWrite: microQrCanEncode,
189
+ canRead: microQrCanDecode,
190
+ kind: /** @type {'2D'} */ ('2D'),
191
+ });
192
+ formats.push({
193
+ id: 'rmqr',
194
+ label: 'rMQR Code',
195
+ canWrite: rmqrCanEncode,
196
+ canRead: rmqrCanDecode,
197
+ kind: /** @type {'2D'} */ ('2D'),
198
+ });
199
+ formats.push({
200
+ id: 'frameqr',
201
+ label: 'Sythos Canvas QR profile',
202
+ canWrite: frameQrCanEncode,
203
+ canRead: frameQrCanDecode,
204
+ kind: /** @type {'2D'} */ ('2D'),
205
+ });
171
206
 
172
207
  return formats;
173
208
  }
@@ -198,6 +233,14 @@ export function listFormats() {
198
233
  * @param {'auto'|'text'|'byte'|'numeric'} [options.compaction] PDF417 compaction mode.
199
234
  * @param {number} [options.eci] MicroPDF417 byte-compaction ECI assignment (3 or 26).
200
235
  * @param {number} [options.aspectRatio] Preferred MicroPDF417 symbol aspect ratio.
236
+ * @param {object} [options.canvas] Sythos Canvas QR artwork reservation.
237
+ * @param {'square'|'circle'|'diamond'} [options.canvas.shape] Canvas shape.
238
+ * @param {number} [options.canvas.size] Odd canvas size in QR modules.
239
+ * @param {number} [options.canvas.width] Canvas width in QR modules.
240
+ * @param {number} [options.canvas.height] Canvas height in QR modules.
241
+ * @param {number} [options.canvas.centerX] Canvas centre X in QR modules.
242
+ * @param {number} [options.canvas.centerY] Canvas centre Y in QR modules.
243
+ * @param {0|90|180|270} [options.canvas.angle] Canvas quarter-turn.
201
244
  * @returns {BitMatrix}
202
245
  */
203
246
  export function encode(text, options = {}) {
@@ -219,10 +262,19 @@ export function encode(text, options = {}) {
219
262
  if (format === 'micropdf417' || format === 'micro-pdf417' || format === 'micro-pdf-417') {
220
263
  return micropdf417.encodeMicroPDF417(value, options);
221
264
  }
265
+ if (format === 'microqr' || format === 'micro-qr') {
266
+ return microqr.encodeMicroQR(value, options);
267
+ }
268
+ if (format === 'rmqr' || format === 'r-mqr' || format === 'rectangular-micro-qr') {
269
+ return rmqr.encodeRMQR(value, options);
270
+ }
271
+ if (format === 'frameqr' || format === 'frame-qr' || format === 'canvas-qr') {
272
+ return frameqr.encodeFrameQR(value, options);
273
+ }
222
274
 
223
275
  const entry = ONED_FORMATS[format];
224
276
  if (!entry) {
225
- const known = [...Object.keys(ONED_FORMATS), 'qr', 'datamatrix', 'aztec', 'pdf417', 'micropdf417'].join(', ');
277
+ const known = [...Object.keys(ONED_FORMATS), 'qr', 'datamatrix', 'aztec', 'pdf417', 'micropdf417', 'microqr', 'rmqr', 'frameqr'].join(', ');
226
278
  throw new EncodeError(`Unknown format "${format}". Known formats: ${known}`);
227
279
  }
228
280
  return entry.encode(value, options);
@@ -245,6 +297,9 @@ export function encode(text, options = {}) {
245
297
  * @property {number} [rowHeight] PDF417 row height in modules.
246
298
  * @property {number} [variant] MicroPDF417 predefined variant number.
247
299
  * @property {number} [eccCodewords] MicroPDF417 fixed error-correction codewords.
300
+ * @property {string} [profile] Sythos Canvas QR profile identifier.
301
+ * @property {boolean} [certified] Whether the profile is certified by its originator.
302
+ * @property {object} [canvas] Canvas reservation metadata for the Sythos profile.
248
303
  */
249
304
 
250
305
  /**
@@ -259,6 +314,8 @@ export function encode(text, options = {}) {
259
314
  * @param {string[]} [options.formats] Restrict to these format ids.
260
315
  * @param {boolean} [options.tryHarder] Retry inverted and rotated. Default true.
261
316
  * @param {'global'|'hybrid'|'auto'} [options.binarizer]
317
+ * @param {object} [options.frameqr] Sythos Canvas QR detector options when
318
+ * the profile marker is not preserved through image rendering.
262
319
  * @returns {DecodeResult[]}
263
320
  */
264
321
  export function decode(image, options = {}) {
@@ -269,6 +326,9 @@ export function decode(image, options = {}) {
269
326
  const wantAztec = !want || want.has('aztec') || want.has('aztec-code');
270
327
  const wantPDF417 = !want || want.has('pdf417') || want.has('pdf-417');
271
328
  const wantMicroPDF417 = !want || want.has('micropdf417') || want.has('micro-pdf417') || want.has('micro-pdf-417');
329
+ const wantMicroQR = !want || want.has('microqr') || want.has('micro-qr');
330
+ const wantRMQR = !want || want.has('rmqr') || want.has('r-mqr') || want.has('rectangular-micro-qr');
331
+ const wantFrameQR = !want || want.has('frameqr') || want.has('frame-qr') || want.has('canvas-qr');
272
332
  const wantOneD = !want || [...want].some((f) => f in ONED_FORMATS);
273
333
 
274
334
  const source = LuminanceSource.fromImageData(image);
@@ -345,6 +405,35 @@ export function decode(image, options = {}) {
345
405
  }
346
406
  }
347
407
 
408
+ if (wantMicroQR && microQrCanDecode) {
409
+ try {
410
+ for (const found of microqr.detectAndDecodeMicroQR(bits)) {
411
+ results.push({ ...found, format: 'microqr' });
412
+ }
413
+ } catch {
414
+ /* no Micro QR in this pass */
415
+ }
416
+ }
417
+
418
+ if (wantRMQR && rmqrCanDecode) {
419
+ try {
420
+ const found = rmqr.detectAndDecodeRMQR(bits);
421
+ if (found) results.push({ ...found, format: 'rmqr' });
422
+ } catch {
423
+ /* no rMQR in this pass */
424
+ }
425
+ }
426
+
427
+ if (wantFrameQR && frameQrCanDecode) {
428
+ try {
429
+ for (const found of frameqr.detectAndDecodeFrameQR(bits, options.frameqr ?? {})) {
430
+ results.push({ ...found, format: 'frameqr' });
431
+ }
432
+ } catch {
433
+ /* no Sythos Canvas QR profile in this pass */
434
+ }
435
+ }
436
+
348
437
  if (wantOneD) {
349
438
  const oneDFormats = want ? [...want].filter((f) => f in ONED_FORMATS) : null;
350
439
  for (const found of decodeOneD(bits, { formats: oneDFormats, tryHarder })) {
@@ -379,4 +468,4 @@ export function decodeStrict(image, options) {
379
468
  }
380
469
 
381
470
  /** Library version, matching package.json. */
382
- export const VERSION = '1.2.5';
471
+ export const VERSION = '1.3.1';