@sythos/js_barcode_universal 1.2.5 → 1.4.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 (124) hide show
  1. package/AI_USAGE.md +33 -0
  2. package/LICENSE +11 -2
  3. package/NOTICE.md +10 -4
  4. package/README.md +601 -465
  5. package/bundle/sythos-barcode.esm.js +4669 -74
  6. package/bundle/sythos-barcode.js +4575 -72
  7. package/examples/create.html +1010 -730
  8. package/examples/read.html +1 -0
  9. package/licenses/README.md +40 -58
  10. package/licenses/aztec-code.license +15 -13
  11. package/licenses/aztec-rune.license +76 -0
  12. package/licenses/codabar.license +18 -15
  13. package/licenses/code-11.license +11 -9
  14. package/licenses/code-128.license +10 -8
  15. package/licenses/code-39.license +10 -8
  16. package/licenses/code-93.license +16 -14
  17. package/licenses/compact-pdf417.license +76 -0
  18. package/licenses/data-matrix.license +15 -13
  19. package/licenses/ean-13.license +10 -8
  20. package/licenses/ean-2.license +74 -0
  21. package/licenses/ean-5.license +74 -0
  22. package/licenses/ean-8.license +10 -8
  23. package/licenses/frameqr.license +84 -0
  24. package/licenses/gs1-128.license +10 -8
  25. package/licenses/gs1-databar.license +81 -0
  26. package/licenses/isbn.license +10 -8
  27. package/licenses/itf-14.license +10 -8
  28. package/licenses/itf.license +9 -7
  29. package/licenses/micro-qr.license +79 -0
  30. package/licenses/micropdf417.license +52 -67
  31. package/licenses/msi-plessey.license +13 -11
  32. package/licenses/pdf417.license +78 -37
  33. package/licenses/pharmacode.license +14 -12
  34. package/licenses/qr-code.license +9 -7
  35. package/licenses/rmqr.license +79 -0
  36. package/licenses/upc-a.license +12 -10
  37. package/licenses/upc-e.license +10 -8
  38. package/package.json +109 -88
  39. package/src/aztec/decoder.js +1 -0
  40. package/src/aztec/detector.js +1 -0
  41. package/src/aztec/encoder.js +1 -0
  42. package/src/aztec/high-level.js +1 -0
  43. package/src/aztec/index.js +1 -0
  44. package/src/aztec/tables.js +1 -0
  45. package/src/aztecrune/decoder.js +156 -0
  46. package/src/aztecrune/detector.js +167 -0
  47. package/src/aztecrune/encoder.js +122 -0
  48. package/src/aztecrune/index.js +50 -0
  49. package/src/aztecrune/tables.js +138 -0
  50. package/src/compactpdf417/decoder.js +129 -0
  51. package/src/compactpdf417/detector.js +140 -0
  52. package/src/compactpdf417/encoder.js +141 -0
  53. package/src/compactpdf417/index.js +22 -0
  54. package/src/compactpdf417/tables.js +174 -0
  55. package/src/core/bit-buffer.js +1 -0
  56. package/src/core/bit-matrix.js +1 -0
  57. package/src/core/errors.js +1 -0
  58. package/src/core/galois-field.js +1 -0
  59. package/src/core/index.js +1 -0
  60. package/src/core/reed-solomon.js +1 -0
  61. package/src/databar/codec.js +182 -0
  62. package/src/databar/decoder.js +115 -0
  63. package/src/databar/encoder.js +97 -0
  64. package/src/databar/gs1.js +178 -0
  65. package/src/databar/index.js +69 -0
  66. package/src/databar/patterns.js +144 -0
  67. package/src/databar/tables.js +125 -0
  68. package/src/datamatrix/decoder.js +263 -262
  69. package/src/datamatrix/detector.js +226 -225
  70. package/src/datamatrix/encoder.js +192 -191
  71. package/src/datamatrix/index.js +43 -42
  72. package/src/datamatrix/tables.js +124 -123
  73. package/src/frameqr/decoder.js +240 -0
  74. package/src/frameqr/detector.js +193 -0
  75. package/src/frameqr/encoder.js +157 -0
  76. package/src/frameqr/index.js +43 -0
  77. package/src/frameqr/tables.js +271 -0
  78. package/src/image/binarizer.js +1 -0
  79. package/src/image/grid-sampler.js +1 -0
  80. package/src/image/index.js +1 -0
  81. package/src/image/luminance.js +1 -0
  82. package/src/image/perspective.js +1 -0
  83. package/src/index.js +155 -2
  84. package/src/micropdf417/compaction.js +1 -0
  85. package/src/micropdf417/decoder.js +1 -0
  86. package/src/micropdf417/detector.js +1 -0
  87. package/src/micropdf417/encoder.js +1 -0
  88. package/src/micropdf417/error-correction.js +1 -0
  89. package/src/micropdf417/index.js +1 -0
  90. package/src/micropdf417/tables.js +1 -0
  91. package/src/microqr/decoder.js +246 -0
  92. package/src/microqr/detector.js +356 -0
  93. package/src/microqr/encoder.js +270 -0
  94. package/src/microqr/index.js +37 -0
  95. package/src/microqr/tables.js +317 -0
  96. package/src/oned/addons.js +421 -0
  97. package/src/oned/index.js +77 -59
  98. package/src/oned/patterns.js +1 -0
  99. package/src/oned/reader.js +1 -0
  100. package/src/oned/writers.js +1 -0
  101. package/src/pdf417/compaction.js +1 -0
  102. package/src/pdf417/decoder.js +1 -0
  103. package/src/pdf417/detector.js +1 -0
  104. package/src/pdf417/encoder.js +1 -0
  105. package/src/pdf417/error-correction.js +1 -0
  106. package/src/pdf417/index.js +5 -0
  107. package/src/pdf417/tables.js +1 -0
  108. package/src/qr/decoder.js +1 -0
  109. package/src/qr/detector.js +1 -0
  110. package/src/qr/encoder.js +1 -0
  111. package/src/qr/index.js +1 -0
  112. package/src/qr/tables.js +1 -0
  113. package/src/render/image-data.js +1 -0
  114. package/src/render/index.js +1 -0
  115. package/src/render/options.js +1 -0
  116. package/src/render/png.js +1 -0
  117. package/src/render/svg.js +1 -0
  118. package/src/render/webgl.js +1 -0
  119. package/src/render/webgpu.js +1 -0
  120. package/src/rmqr/decoder.js +102 -0
  121. package/src/rmqr/detector.js +91 -0
  122. package/src/rmqr/encoder.js +173 -0
  123. package/src/rmqr/index.js +38 -0
  124. package/src/rmqr/tables.js +155 -0
@@ -0,0 +1,193 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ * of this software and associated documentation files (the "Software"), to deal
11
+ * in the Software without restriction, including without limitation the rights
12
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ * copies of the Software, and to permit persons to whom the Software is
14
+ * furnished to do so, subject to the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be included in all
17
+ * copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ * SOFTWARE.
26
+ *
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+
32
+ /**
33
+ * Detector for the non-certified FrameQR Code profile.
34
+ *
35
+ * The profile deliberately reuses QR Model 2 geometry. Finder localisation and
36
+ * projective sampling therefore use the QR detector; the additional profile
37
+ * check is the light canvas signature. A sampled image cannot carry the
38
+ * encoder's in-memory marker, so the detector reconstructs the expected
39
+ * metadata, verifies that every reserved canvas module is light, and only then
40
+ * opts into the profile decoder. This rejects ordinary QR symbols whose centre
41
+ * merely happens to contain a plausible payload.
42
+ *
43
+ * Detection is verified for clean binarized rasters, integer scaling, quiet
44
+ * zones, and in-plane quarter turns. Arbitrary photographic perspective is not
45
+ * claimed by this module.
46
+ *
47
+ * @module frameqr/detector
48
+ */
49
+
50
+ import { NotFoundError } from '../core/errors.js';
51
+ import { sampleQuad } from '../image/grid-sampler.js';
52
+ import { detectQR } from '../qr/detector.js';
53
+ import { decodeFrameQR } from './decoder.js';
54
+ import {
55
+ FRAMEQR_PROFILE,
56
+ canvasModules,
57
+ normalizeCanvasSpec,
58
+ } from './tables.js';
59
+
60
+ /** @typedef {{x:number, y:number}} Point */
61
+
62
+ /** @typedef {object} FrameQRDetection
63
+ * @property {Point[]} corners Outer corners in reading order.
64
+ * @property {number} dimension QR modules per side.
65
+ * @property {number} version QR Model 2 version.
66
+ * @property {number} moduleSize Estimated pixels per module.
67
+ * @property {number} rotation Clockwise in-plane orientation in degrees.
68
+ * @property {BitMatrix} matrix Rectified profile matrix.
69
+ * @property {object} canvas Normalized canvas specification.
70
+ * @property {string} profile Profile identifier.
71
+ * @property {false} certified Always false for this implementation.
72
+ */
73
+
74
+ function orientationDegrees(corners) {
75
+ const [tl, tr] = corners;
76
+ const angle = Math.atan2(tr.y - tl.y, tr.x - tl.x) * 180 / Math.PI;
77
+ return ((Math.round(angle / 90) * 90) % 360 + 360) % 360;
78
+ }
79
+ /**
80
+ * Return the number of canvas modules that are dark in a sampled matrix.
81
+ *
82
+ * Encoded profile symbols clear the whole reserved canvas. A non-zero count
83
+ * means either a normal QR symbol or a raster whose canvas was overwritten by
84
+ * artwork; both are rejected because decoding that image would be speculative.
85
+ */
86
+ function canvasDarkCount(matrix, canvas) {
87
+ let dark = 0;
88
+ for (const [x, y] of canvasModules(matrix.width, canvas)) {
89
+ if (matrix.get(x, y)) dark++;
90
+ }
91
+ return dark;
92
+ }
93
+
94
+ function sameCandidate(left, right) {
95
+ if (left.dimension !== right.dimension) return false;
96
+ const a = left.corners[0];
97
+ const b = right.corners[0];
98
+ return Math.hypot(a.x - b.x, a.y - b.y) <= Math.max(left.moduleSize, right.moduleSize) * 2;
99
+ }
100
+
101
+ /**
102
+ * Detect FrameQR Code symbols in a binarized raster.
103
+ *
104
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage Set bit = dark.
105
+ * @param {object} [options]
106
+ * @param {object} [options.canvas] Explicit canvas metadata for non-default
107
+ * shapes/size. Without it, the profile's canonical centered square is used.
108
+ * @param {boolean} [options.voting=false] Use majority sampling per module.
109
+ * @returns {FrameQRDetection[]} Best candidate first; empty when no verified
110
+ * profile signature is found.
111
+ */
112
+ export function detectFrameQR(binaryImage, options = {}) {
113
+ if (!binaryImage || !binaryImage.width || !binaryImage.height) {
114
+ throw new NotFoundError('detectFrameQR: no image supplied');
115
+ }
116
+
117
+ let candidates;
118
+ try { candidates = detectQR(binaryImage); } catch { return []; }
119
+ const detections = [];
120
+
121
+ for (const candidate of candidates) {
122
+ // QR detector dimensions are already constrained to legal Model 2 sizes.
123
+ let canvas;
124
+ try { canvas = normalizeCanvasSpec(candidate.dimension, options.canvas); }
125
+ catch { continue; }
126
+
127
+ for (const voting of [Boolean(options.voting), !Boolean(options.voting)]) {
128
+ let matrix;
129
+ try { matrix = sampleQuad(binaryImage, candidate.dimension, candidate.corners, voting); }
130
+ catch { continue; }
131
+
132
+ // The signature check is intentionally strict. It prevents an ordinary
133
+ // QR symbol from being relabelled as Canvas QR by the decoder's explicit
134
+ // allowUnmarked escape hatch.
135
+ if (canvasDarkCount(matrix, canvas) !== 0) continue;
136
+
137
+ const marked = matrix;
138
+ marked.frameqr = {
139
+ profile: FRAMEQR_PROFILE.id,
140
+ certified: false,
141
+ canvas,
142
+ };
143
+ let decoded;
144
+ try {
145
+ decoded = decodeFrameQR(marked, {
146
+ profile: FRAMEQR_PROFILE.id,
147
+ canvas,
148
+ allowUnmarked: true,
149
+ });
150
+ } catch { continue; }
151
+
152
+ const detection = {
153
+ corners: candidate.corners,
154
+ dimension: candidate.dimension,
155
+ version: candidate.version,
156
+ moduleSize: candidate.moduleSize,
157
+ rotation: orientationDegrees(candidate.corners),
158
+ matrix: marked,
159
+ canvas,
160
+ profile: FRAMEQR_PROFILE.id,
161
+ certified: false,
162
+ result: decoded,
163
+ };
164
+ if (!detections.some((entry) => sameCandidate(entry, detection))) detections.push(detection);
165
+ break;
166
+ }
167
+ }
168
+
169
+ detections.sort((a, b) => b.moduleSize - a.moduleSize);
170
+ return detections;
171
+ }
172
+
173
+ /**
174
+ * Detect and decode all verified Canvas QR symbols in one call.
175
+ *
176
+ * @param {import('../core/bit-matrix.js').BitMatrix} binaryImage
177
+ * @param {object} [options]
178
+ * @returns {Array<object>}
179
+ */
180
+ export function detectAndDecodeFrameQR(binaryImage, options = {}) {
181
+ let detections;
182
+ try { detections = detectFrameQR(binaryImage, options); } catch { return []; }
183
+ const results = [];
184
+ const seen = new Set();
185
+ for (const detection of detections) {
186
+ const result = detection.result;
187
+ const key = `${result.version}|${result.text}`;
188
+ if (seen.has(key)) continue;
189
+ seen.add(key);
190
+ results.push({ ...result, corners: detection.corners, rotation: detection.rotation });
191
+ }
192
+ return results;
193
+ }
@@ -0,0 +1,157 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ * of this software and associated documentation files (the "Software"), to deal
11
+ * in the Software without restriction, including without limitation the rights
12
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ * copies of the Software, and to permit persons to whom the Software is
14
+ * furnished to do so, subject to the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be included in all
17
+ * copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ * SOFTWARE.
26
+ *
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+
32
+ /**
33
+ * FrameQR Code encoder.
34
+ *
35
+ * This encoder deliberately builds on this project's QR Code implementation
36
+ * and then removes a conservatively bounded set of data modules for artwork.
37
+ * It is not an implementation of DENSO FrameQR and makes no interoperability
38
+ * claim for that proprietary format.
39
+ *
40
+ * @module frameqr/encoder
41
+ */
42
+
43
+ import { EncodeError } from '../core/errors.js';
44
+ import { encodeQR } from '../qr/encoder.js';
45
+ import {
46
+ FRAMEQR_PROFILE,
47
+ canvasModules,
48
+ normalizeCanvasSpec,
49
+ validateCanvasSpec,
50
+ } from './tables.js';
51
+
52
+ /**
53
+ * @typedef {object} FrameQrEncodeOptions
54
+ * @property {'H'} [ecc] The profile always uses QR error correction H.
55
+ * @property {number} [version] Force a QR version 1-40.
56
+ * @property {number} [mask] Force a QR mask 0-7.
57
+ * @property {'auto'|'utf-8'|'iso-8859-1'} [charset] Byte mode interpretation.
58
+ * @property {boolean} [kanji] Allow QR kanji mode.
59
+ * @property {object} [canvas] Profile artwork reservation.
60
+ */
61
+
62
+ function versionFor(matrix) {
63
+ return (matrix.width - 17) / 4;
64
+ }
65
+
66
+ function clearCanvas(matrix, modules) {
67
+ for (const [x, y] of modules) matrix.unset(x, y);
68
+ }
69
+
70
+ /**
71
+ * Encode a QR Code with a conservative artwork canvas according to the
72
+ * non-certified FrameQR Code profile.
73
+ *
74
+ * The profile forces QR H error correction and rejects a canvas whenever its
75
+ * known codeword damage exceeds the per-block correction budget. When a
76
+ * version is not forced, the smallest QR version that holds both payload and
77
+ * safe canvas is selected. A decoder can reconstruct the reserved modules from
78
+ * the returned profile metadata.
79
+ *
80
+ * @param {string} text
81
+ * @param {FrameQrEncodeOptions} [options]
82
+ * @returns {import('../core/bit-matrix.js').BitMatrix}
83
+ * @throws {EncodeError} When the QR payload/options are invalid or the canvas
84
+ * cannot safely fit the selected QR version.
85
+ */
86
+ export function encodeFrameQR(text, options = {}) {
87
+ if (typeof text !== 'string') {
88
+ throw new EncodeError('FrameQR Code: text must be a string');
89
+ }
90
+ if (options.ecc !== undefined && options.ecc !== 'H') {
91
+ throw new EncodeError('FrameQR Code: ecc is fixed to H for this profile');
92
+ }
93
+
94
+ const qrOptions = {
95
+ mask: options.mask,
96
+ charset: options.charset,
97
+ kanji: options.kanji,
98
+ ecc: 'H',
99
+ };
100
+ for (const key of Object.keys(qrOptions)) {
101
+ if (qrOptions[key] === undefined) delete qrOptions[key];
102
+ }
103
+
104
+ const versions = options.version === undefined
105
+ ? Array.from({ length: 40 }, (_, index) => index + 1)
106
+ : [options.version];
107
+ let capacityError = null;
108
+ let unsafeAnalysis = null;
109
+ let selected = null;
110
+
111
+ for (const version of versions) {
112
+ let matrix;
113
+ try {
114
+ matrix = encodeQR(text, { ...qrOptions, version });
115
+ } catch (error) {
116
+ capacityError = error;
117
+ continue;
118
+ }
119
+
120
+ let canvas;
121
+ let analysis;
122
+ try {
123
+ canvas = normalizeCanvasSpec(matrix.width, options.canvas);
124
+ analysis = validateCanvasSpec(versionFor(matrix), canvas);
125
+ } catch (error) {
126
+ if (error instanceof EncodeError) throw error;
127
+ throw new EncodeError(`FrameQR Code: invalid canvas: ${error.message}`);
128
+ }
129
+ if (!analysis.safe) {
130
+ unsafeAnalysis = analysis;
131
+ continue;
132
+ }
133
+ selected = { matrix, canvas };
134
+ break;
135
+ }
136
+
137
+ if (!selected) {
138
+ if (unsafeAnalysis) {
139
+ throw new EncodeError(
140
+ 'FrameQR Code: canvas is not safe for the selected QR version; ' +
141
+ `it touches ${unsafeAnalysis.touchedCodewordCount} codewords and has ` +
142
+ `a per-block correction budget of ${unsafeAnalysis.correctionBudgetPerBlock}`
143
+ );
144
+ }
145
+ if (capacityError) throw capacityError;
146
+ throw new EncodeError('FrameQR Code: unable to select a QR version');
147
+ }
148
+
149
+ const { matrix, canvas } = selected;
150
+ clearCanvas(matrix, canvasModules(matrix.width, canvas));
151
+ matrix.frameqr = {
152
+ profile: FRAMEQR_PROFILE.id,
153
+ certified: false,
154
+ canvas,
155
+ };
156
+ return matrix;
157
+ }
@@ -0,0 +1,43 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ * of this software and associated documentation files (the "Software"), to deal
11
+ * in the Software without restriction, including without limitation the rights
12
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ * copies of the Software, and to permit persons to whom the Software is
14
+ * furnished to do so, subject to the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be included in all
17
+ * copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ * SOFTWARE.
26
+ *
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+
32
+ export { encodeFrameQR } from './encoder.js';
33
+ export { decodeFrameQR } from './decoder.js';
34
+ export { detectFrameQR, detectAndDecodeFrameQR } from './detector.js';
35
+ export {
36
+ FRAMEQR_PROFILE,
37
+ FRAMEQR_CANVAS_SHAPES,
38
+ canvasModules,
39
+ normalizeCanvasSpec,
40
+ analyzeCanvasDamage,
41
+ validateCanvasSpec,
42
+ validateFrameQrTables,
43
+ } from './tables.js';
@@ -0,0 +1,271 @@
1
+ /*!
2
+ * Sythos Barcode Suite
3
+ *
4
+ * MIT License
5
+ *
6
+ * Copyright (c) 2026 Sythos
7
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
8
+ *
9
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ * of this software and associated documentation files (the "Software"), to deal
11
+ * in the Software without restriction, including without limitation the rights
12
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ * copies of the Software, and to permit persons to whom the Software is
14
+ * furnished to do so, subject to the following conditions:
15
+ *
16
+ * The above copyright notice and this permission notice shall be included in all
17
+ * copies or substantial portions of the Software.
18
+ *
19
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ * SOFTWARE.
26
+ *
27
+ * SPDX-License-Identifier: MIT
28
+ *
29
+ * Original work. No code from any other barcode implementation.
30
+ */
31
+
32
+ /**
33
+ * Structural contract for FrameQR Code.
34
+ *
35
+ * DENSO WAVE's public material describes FrameQR(R) as a proprietary symbol
36
+ * with a freely shaped canvas, dedicated generation/reading software and no
37
+ * compatibility with ordinary QR readers. It does not publish the bitstream,
38
+ * placement or error-correction rules required for an interoperable encoder.
39
+ * Consequently this module does not claim to implement DENSO FrameQR.
40
+ *
41
+ * The implementable profile below starts with an ISO/IEC 18004 QR Model 2
42
+ * symbol at level H and clears a bounded group of data modules. Its worst-case
43
+ * damage is calculated per Reed-Solomon block and must remain within the
44
+ * standard QR correction radius. Function modules are never canvas modules.
45
+ * This provides a deterministic, independently testable FrameQR Code profile, but
46
+ * it is explicitly non-certified and not a substitute for proprietary FrameQR
47
+ * generation or validation software.
48
+ *
49
+ * @module frameqr/tables
50
+ */
51
+
52
+ import {
53
+ blockLayout,
54
+ dataModuleOrder,
55
+ reservedModules,
56
+ versionSize,
57
+ } from '../qr/tables.js';
58
+
59
+ /** Public identity and compatibility boundary of the implementable profile. */
60
+ export const FRAMEQR_PROFILE = Object.freeze({
61
+ id: 'sythos-canvas-qr/1',
62
+ name: 'FrameQR Code',
63
+ certified: false,
64
+ densoFrameQrCompatible: false,
65
+ baseSymbology: 'QR Code Model 2',
66
+ requiredEcc: 'H',
67
+ standard: 'ISO/IEC 18004 QR Code baseline',
68
+ });
69
+
70
+ /** Canvas shapes whose module membership is fully deterministic. */
71
+ export const FRAMEQR_CANVAS_SHAPES = Object.freeze(['square', 'circle', 'diamond']);
72
+
73
+ function oddAtMost(value, maximum) {
74
+ let n = Math.max(1, Math.min(maximum, Math.floor(value)));
75
+ if ((n & 1) === 0) n--;
76
+ return Math.max(1, n);
77
+ }
78
+
79
+ function assertSymbolSize(symbolSize) {
80
+ if (!Number.isInteger(symbolSize) || symbolSize < 21 || symbolSize > 177 || (symbolSize - 17) % 4 !== 0) {
81
+ throw new RangeError(`FrameQR Code: ${symbolSize} is not a QR Model 2 symbol size`);
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Canonicalise a canvas request.
87
+ *
88
+ * Coordinates and dimensions are module units. Odd dimensions make the centre
89
+ * unambiguous. Only quarter turns are accepted because arbitrary-angle raster
90
+ * membership would depend on renderer-specific sampling.
91
+ *
92
+ * @param {number} symbolSize QR module width/height (21..177).
93
+ * @param {object} [canvas]
94
+ * @returns {{shape:string,centerX:number,centerY:number,width:number,height:number,angle:number}}
95
+ */
96
+ export function normalizeCanvasSpec(symbolSize, canvas = {}) {
97
+ assertSymbolSize(symbolSize);
98
+ if (canvas === null || typeof canvas !== 'object' || Array.isArray(canvas)) {
99
+ throw new TypeError('FrameQR Code: canvas must be an object');
100
+ }
101
+
102
+ const shape = String(canvas.shape ?? 'square').toLowerCase();
103
+ if (!FRAMEQR_CANVAS_SHAPES.includes(shape)) {
104
+ throw new RangeError(`FrameQR Code: unsupported canvas shape "${shape}"`);
105
+ }
106
+
107
+ const defaultSize = oddAtMost(Math.max(3, Math.round(symbolSize * 0.17)), symbolSize - 16);
108
+ const requestedSize = canvas.size;
109
+ const requestedWidth = canvas.width ?? requestedSize ?? defaultSize;
110
+ const requestedHeight = canvas.height ?? requestedSize ?? defaultSize;
111
+ const maximumDimension = symbolSize - 16;
112
+ for (const [name, value] of [['width', requestedWidth], ['height', requestedHeight]]) {
113
+ if (!Number.isFinite(Number(value)) || Number(value) < 1 || Number(value) > maximumDimension) {
114
+ throw new RangeError(
115
+ `FrameQR Code: canvas ${name} must be between 1 and ${maximumDimension} modules`
116
+ );
117
+ }
118
+ }
119
+ const width = oddAtMost(Number(requestedWidth), maximumDimension);
120
+ const height = oddAtMost(Number(requestedHeight), maximumDimension);
121
+ const centerX = Math.round(canvas.centerX ?? (symbolSize - 1) / 2);
122
+ const centerY = Math.round(canvas.centerY ?? (symbolSize - 1) / 2);
123
+ const angle = ((Number(canvas.angle ?? 0) % 360) + 360) % 360;
124
+
125
+ if (![0, 90, 180, 270].includes(angle)) {
126
+ throw new RangeError('FrameQR Code: angle must be 0, 90, 180 or 270 degrees');
127
+ }
128
+ if (centerX < 8 || centerY < 8 || centerX >= symbolSize - 8 || centerY >= symbolSize - 8) {
129
+ throw new RangeError('FrameQR Code: canvas centre must remain inside the finder-pattern boundary');
130
+ }
131
+
132
+ return { shape, centerX, centerY, width, height, angle };
133
+ }
134
+
135
+ /**
136
+ * Enumerate canvas modules, including any overlaps with QR function modules.
137
+ * A conforming encoder rejects such overlaps rather than damaging function
138
+ * patterns.
139
+ *
140
+ * @param {number} symbolSize
141
+ * @param {object} [canvas]
142
+ * @returns {Array<[number, number]>}
143
+ */
144
+ export function canvasModules(symbolSize, canvas = {}) {
145
+ const spec = normalizeCanvasSpec(symbolSize, canvas);
146
+ const rotate = spec.angle === 90 || spec.angle === 270;
147
+ const width = rotate ? spec.height : spec.width;
148
+ const height = rotate ? spec.width : spec.height;
149
+ const halfWidth = (width - 1) / 2;
150
+ const halfHeight = (height - 1) / 2;
151
+ const modules = [];
152
+
153
+ for (let dy = -halfHeight; dy <= halfHeight; dy++) {
154
+ for (let dx = -halfWidth; dx <= halfWidth; dx++) {
155
+ const nx = halfWidth === 0 ? 0 : dx / halfWidth;
156
+ const ny = halfHeight === 0 ? 0 : dy / halfHeight;
157
+ let inside;
158
+ if (spec.shape === 'circle') inside = nx * nx + ny * ny <= 1 + Number.EPSILON;
159
+ else if (spec.shape === 'diamond') inside = Math.abs(nx) + Math.abs(ny) <= 1 + Number.EPSILON;
160
+ else inside = true;
161
+ if (inside) modules.push([spec.centerX + dx, spec.centerY + dy]);
162
+ }
163
+ }
164
+ return modules;
165
+ }
166
+
167
+ /** Build the interleaved-codeword to RS-block map used by QR Model 2. */
168
+ function codewordBlockMap(layout) {
169
+ const dataCounts = new Array(layout.blockCount);
170
+ for (let block = 0; block < layout.blockCount; block++) {
171
+ dataCounts[block] = block < layout.group1Blocks
172
+ ? layout.group1DataCount
173
+ : layout.group2DataCount;
174
+ }
175
+
176
+ const map = [];
177
+ const maxData = Math.max(...dataCounts);
178
+ for (let i = 0; i < maxData; i++) {
179
+ for (let block = 0; block < layout.blockCount; block++) {
180
+ if (i < dataCounts[block]) map.push(block);
181
+ }
182
+ }
183
+ for (let i = 0; i < layout.eccPerBlock; i++) {
184
+ for (let block = 0; block < layout.blockCount; block++) map.push(block);
185
+ }
186
+ return map;
187
+ }
188
+
189
+ /**
190
+ * Calculate worst-case QR codeword damage caused by a canvas.
191
+ * A codeword is counted if any of its modules is touched. This is conservative:
192
+ * clearing an already-light module does no damage, but safety cannot depend on
193
+ * one payload or mask.
194
+ *
195
+ * @param {number} version QR version 1..40.
196
+ * @param {object} [canvas]
197
+ */
198
+ export function analyzeCanvasDamage(version, canvas = {}) {
199
+ if (!Number.isInteger(version) || version < 1 || version > 40) {
200
+ throw new RangeError(`FrameQR Code: version must be an integer 1-40, got ${version}`);
201
+ }
202
+ const symbolSize = versionSize(version);
203
+ const spec = normalizeCanvasSpec(symbolSize, canvas);
204
+ const modules = canvasModules(symbolSize, spec);
205
+ const reserved = reservedModules(version);
206
+ const reservedOverlaps = modules.filter(([x, y]) => reserved.get(x, y));
207
+
208
+ const moduleKeys = new Set(modules.map(([x, y]) => `${x},${y}`));
209
+ const order = dataModuleOrder(version);
210
+ const touchedCodewords = new Set();
211
+ for (let bit = 0; bit < order.length / 2; bit++) {
212
+ if (moduleKeys.has(`${order[bit * 2]},${order[bit * 2 + 1]}`)) touchedCodewords.add(bit >> 3);
213
+ }
214
+
215
+ const layout = blockLayout(version, 'H');
216
+ const blockMap = codewordBlockMap(layout);
217
+ const touchedByBlockSets = Array.from({ length: layout.blockCount }, () => new Set());
218
+ for (const codeword of touchedCodewords) {
219
+ const block = blockMap[codeword];
220
+ if (block !== undefined) touchedByBlockSets[block].add(codeword);
221
+ }
222
+ const touchedCodewordsByBlock = touchedByBlockSets.map((set) => set.size);
223
+ const correctionBudgetPerBlock = Math.floor(layout.eccPerBlock / 2);
224
+ const safe = reservedOverlaps.length === 0 &&
225
+ touchedCodewordsByBlock.every((count) => count <= correctionBudgetPerBlock);
226
+
227
+ return {
228
+ profile: FRAMEQR_PROFILE.id,
229
+ certified: false,
230
+ version,
231
+ symbolSize,
232
+ canvas: spec,
233
+ canvasModuleCount: modules.length,
234
+ reservedOverlaps,
235
+ touchedCodewordCount: touchedCodewords.size,
236
+ touchedCodewordsByBlock,
237
+ correctionBudgetPerBlock,
238
+ safe,
239
+ };
240
+ }
241
+
242
+ /** Validate a canvas and return the non-certifying structural analysis. */
243
+ export function validateCanvasSpec(version, canvas = {}) {
244
+ return analyzeCanvasDamage(version, canvas);
245
+ }
246
+
247
+ /** Self-check the fixed profile contract and representative QR geometries. */
248
+ export function validateFrameQrTables() {
249
+ const problems = [];
250
+ if (FRAMEQR_PROFILE.certified !== false || FRAMEQR_PROFILE.densoFrameQrCompatible !== false) {
251
+ problems.push('profile compatibility boundary must remain explicitly non-certified');
252
+ }
253
+ if (new Set(FRAMEQR_CANVAS_SHAPES).size !== FRAMEQR_CANVAS_SHAPES.length) {
254
+ problems.push('canvas shape identifiers must be unique');
255
+ }
256
+ for (const version of [1, 2, 3, 4, 7, 10, 20, 30, 40]) {
257
+ const size = versionSize(version);
258
+ const spec = normalizeCanvasSpec(size);
259
+ const modules = canvasModules(size, spec);
260
+ const unique = new Set(modules.map(([x, y]) => `${x},${y}`));
261
+ if (unique.size !== modules.length) problems.push(`v${version}: duplicate canvas modules`);
262
+ if (modules.some(([x, y]) => x < 0 || y < 0 || x >= size || y >= size)) {
263
+ problems.push(`v${version}: canvas escapes the symbol`);
264
+ }
265
+ const analysis = analyzeCanvasDamage(version, spec);
266
+ if (analysis.touchedCodewordsByBlock.length !== blockLayout(version, 'H').blockCount) {
267
+ problems.push(`v${version}: damage analysis block count mismatch`);
268
+ }
269
+ }
270
+ return problems;
271
+ }
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.
@@ -23,6 +23,7 @@
23
23
  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24
24
  * SOFTWARE.
25
25
  *
26
+ * SPDX-FileCopyrightText: 2026 Sythos (https://www.sythos.net)
26
27
  * SPDX-License-Identifier: MIT
27
28
  *
28
29
  * Original work. No code from any other barcode implementation.