@sythos/js_barcode_universal 1.5.8 → 1.5.10
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 +5 -4
- package/NOTICE.md +2 -2
- package/README.md +61 -13
- package/bundle/sythos-barcode.esm.js +11658 -11244
- package/bundle/sythos-barcode.js +11658 -11244
- package/package.json +10 -3
- package/src/index.d.ts +1 -1
- package/src/index.js +508 -516
- package/src/js/aztec/decoder.js +208 -199
- package/src/js/aztec/detector.js +153 -126
- package/src/js/aztec/encoder.js +172 -153
- package/src/js/aztec/high-level.js +106 -101
- package/src/js/aztec/index.js +1 -12
- package/src/js/aztec/tables.js +106 -120
- package/src/js/aztecrune/decoder.js +76 -85
- package/src/js/aztecrune/detector.js +107 -93
- package/src/js/aztecrune/encoder.js +50 -68
- package/src/js/aztecrune/index.js +1 -16
- package/src/js/aztecrune/tables.js +59 -54
- package/src/js/compactpdf417/decoder.js +69 -74
- package/src/js/compactpdf417/detector.js +74 -75
- package/src/js/compactpdf417/encoder.js +68 -81
- package/src/js/compactpdf417/index.js +1 -13
- package/src/js/compactpdf417/tables.js +71 -80
- package/src/js/core/bit-buffer.js +106 -122
- package/src/js/core/bit-matrix.js +196 -194
- package/src/js/core/errors.js +13 -15
- package/src/js/core/galois-field.js +119 -133
- package/src/js/core/index.js +2 -19
- package/src/js/core/reed-solomon.js +195 -204
- package/src/js/databar/codec.js +93 -108
- package/src/js/databar/decoder.js +149 -140
- package/src/js/databar/encoder.js +47 -55
- package/src/js/databar/gs1.js +122 -123
- package/src/js/databar/index.js +4 -36
- package/src/js/databar/patterns.js +81 -85
- package/src/js/databar/tables.js +51 -52
- package/src/js/datamatrix/decoder.js +216 -182
- package/src/js/datamatrix/detector.js +161 -132
- package/src/js/datamatrix/encoder.js +192 -133
- package/src/js/datamatrix/index.js +1 -9
- package/src/js/datamatrix/tables.js +68 -69
- package/src/js/frameqr/decoder.js +102 -125
- package/src/js/frameqr/detector.js +101 -94
- package/src/js/frameqr/encoder.js +67 -80
- package/src/js/frameqr/index.js +1 -10
- package/src/js/frameqr/tables.js +145 -163
- package/src/js/image/binarizer.js +146 -162
- package/src/js/image/grid-sampler.js +63 -79
- package/src/js/image/index.js +0 -2
- package/src/js/image/luminance.js +129 -140
- package/src/js/image/perspective.js +112 -149
- package/src/js/micropdf417/compaction.js +64 -65
- package/src/js/micropdf417/decoder.js +106 -109
- package/src/js/micropdf417/detector.js +73 -70
- package/src/js/micropdf417/encoder.js +129 -154
- package/src/js/micropdf417/error-correction.js +6 -11
- package/src/js/micropdf417/index.js +2 -16
- package/src/js/micropdf417/tables.js +112 -99
- package/src/js/microqr/decoder.js +177 -182
- package/src/js/microqr/detector.js +240 -256
- package/src/js/microqr/encoder.js +239 -194
- package/src/js/microqr/index.js +0 -2
- package/src/js/microqr/tables.js +208 -200
- package/src/js/oned/addons.js +167 -191
- package/src/js/oned/index.js +27 -58
- package/src/js/oned/patterns.js +194 -213
- package/src/js/oned/reader.js +1003 -984
- package/src/js/oned/writers.js +411 -466
- package/src/js/pdf417/compaction.js +315 -222
- package/src/js/pdf417/decoder.js +71 -36
- package/src/js/pdf417/detector.js +377 -361
- package/src/js/pdf417/encoder.js +57 -46
- package/src/js/pdf417/error-correction.js +5 -8
- package/src/js/pdf417/tables.js +248 -250
- package/src/js/qr/decoder.js +311 -347
- package/src/js/qr/detector.js +378 -395
- package/src/js/qr/encoder.js +514 -599
- package/src/js/qr/index.js +0 -2
- package/src/js/qr/tables.js +375 -422
- package/src/js/render/image-data.js +57 -62
- package/src/js/render/index.js +21 -23
- package/src/js/render/options.js +82 -90
- package/src/js/render/png.js +144 -169
- package/src/js/render/svg.js +41 -44
- package/src/js/render/webgl.js +103 -109
- package/src/js/render/webgpu.js +205 -217
- package/src/js/rmqr/decoder.js +161 -53
- package/src/js/rmqr/detector.js +73 -41
- package/src/js/rmqr/encoder.js +253 -104
- package/src/js/rmqr/index.js +1 -5
- package/src/js/rmqr/tables.js +108 -85
- package/src/ts/aztec/decoder.ts +317 -0
- package/src/ts/aztec/detector.ts +224 -0
- package/src/ts/aztec/encoder.ts +257 -0
- package/src/ts/aztec/high-level.ts +211 -0
- package/src/ts/aztec/index.ts +45 -0
- package/src/ts/aztec/tables.ts +210 -0
- package/src/ts/aztecrune/decoder.ts +155 -0
- package/src/ts/aztecrune/detector.ts +166 -0
- package/src/ts/aztecrune/encoder.ts +121 -0
- package/src/ts/aztecrune/index.ts +49 -0
- package/src/ts/aztecrune/tables.ts +137 -0
- package/src/ts/compactpdf417/decoder.ts +128 -0
- package/src/ts/compactpdf417/detector.ts +139 -0
- package/src/ts/compactpdf417/encoder.ts +140 -0
- package/src/ts/compactpdf417/index.ts +17 -0
- package/src/ts/compactpdf417/tables.ts +173 -0
- package/src/ts/core/bit-buffer.ts +174 -0
- package/src/ts/core/bit-matrix.ts +241 -0
- package/src/ts/core/errors.ts +61 -0
- package/src/ts/core/galois-field.ts +207 -0
- package/src/ts/core/index.ts +56 -0
- package/src/ts/core/reed-solomon.ts +327 -0
- package/src/ts/databar/codec.ts +181 -0
- package/src/ts/databar/decoder.ts +215 -0
- package/src/ts/databar/encoder.ts +96 -0
- package/src/ts/databar/gs1.ts +177 -0
- package/src/ts/databar/index.ts +68 -0
- package/src/ts/databar/patterns.ts +143 -0
- package/src/ts/databar/tables.ts +124 -0
- package/src/ts/datamatrix/decoder.ts +262 -0
- package/src/ts/datamatrix/detector.ts +225 -0
- package/src/ts/datamatrix/encoder.ts +191 -0
- package/src/ts/datamatrix/index.ts +42 -0
- package/src/ts/datamatrix/tables.ts +123 -0
- package/src/ts/frameqr/decoder.ts +239 -0
- package/src/ts/frameqr/detector.ts +192 -0
- package/src/ts/frameqr/encoder.ts +156 -0
- package/src/ts/frameqr/index.ts +42 -0
- package/src/ts/frameqr/tables.ts +270 -0
- package/src/ts/image/binarizer.ts +270 -0
- package/src/ts/image/grid-sampler.ts +164 -0
- package/src/ts/image/index.ts +40 -0
- package/src/ts/image/luminance.ts +196 -0
- package/src/ts/image/perspective.ts +195 -0
- package/src/ts/index.d.ts +1 -1
- package/src/ts/index.ts +790 -0
- package/src/ts/micropdf417/compaction.ts +116 -0
- package/src/ts/micropdf417/decoder.ts +183 -0
- package/src/ts/micropdf417/detector.ts +149 -0
- package/src/ts/micropdf417/encoder.ts +209 -0
- package/src/ts/micropdf417/error-correction.ts +55 -0
- package/src/ts/micropdf417/index.ts +49 -0
- package/src/ts/micropdf417/tables.ts +184 -0
- package/src/ts/microqr/decoder.ts +245 -0
- package/src/ts/microqr/detector.ts +355 -0
- package/src/ts/microqr/encoder.ts +269 -0
- package/src/ts/microqr/index.ts +36 -0
- package/src/ts/microqr/tables.ts +316 -0
- package/src/ts/oned/addons.ts +420 -0
- package/src/ts/oned/index.ts +106 -0
- package/src/ts/oned/patterns.ts +384 -0
- package/src/ts/oned/reader.ts +1364 -0
- package/src/ts/oned/writers.ts +746 -0
- package/src/ts/pdf417/compaction.ts +298 -0
- package/src/ts/pdf417/decoder.ts +75 -0
- package/src/ts/pdf417/detector.ts +468 -0
- package/src/ts/pdf417/encoder.ts +91 -0
- package/src/ts/pdf417/error-correction.ts +47 -0
- package/src/ts/pdf417/index.ts +6 -0
- package/src/ts/pdf417/tables.ts +317 -0
- package/src/ts/qr/decoder.ts +575 -0
- package/src/ts/qr/detector.ts +630 -0
- package/src/ts/qr/encoder.ts +958 -0
- package/src/ts/qr/index.ts +44 -0
- package/src/ts/qr/tables.ts +737 -0
- package/src/ts/render/image-data.ts +125 -0
- package/src/ts/render/index.ts +130 -0
- package/src/ts/render/options.ts +160 -0
- package/src/ts/render/png.ts +295 -0
- package/src/ts/render/svg.ts +120 -0
- package/src/ts/render/webgl.ts +206 -0
- package/src/ts/render/webgpu.ts +369 -0
- package/src/ts/rmqr/decoder.ts +101 -0
- package/src/ts/rmqr/detector.ts +90 -0
- package/src/ts/rmqr/encoder.ts +172 -0
- package/src/ts/rmqr/index.ts +37 -0
- package/src/ts/rmqr/tables.ts +154 -0
|
@@ -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
|
+
}
|
|
@@ -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
|
+
* Greyscale to black-and-white.
|
|
33
|
+
*
|
|
34
|
+
* This is the single biggest determinant of whether a reader works on real
|
|
35
|
+
* photographs. Decoding logic is exact and either right or wrong; binarization
|
|
36
|
+
* is a judgement call made a million times per image, and every downstream
|
|
37
|
+
* stage inherits its mistakes. A symbol lost here is lost permanently.
|
|
38
|
+
*
|
|
39
|
+
* Two strategies:
|
|
40
|
+
*
|
|
41
|
+
* global — one threshold for the whole image. Fast, and correct for clean
|
|
42
|
+
* synthetic images: screenshots, generated PNGs, flat scans.
|
|
43
|
+
* hybrid — a threshold per 8x8 block, smoothed across neighbours. Handles
|
|
44
|
+
* the uneven lighting that dominates camera input: shadows,
|
|
45
|
+
* glare, vignetting, a page curving away from the lens.
|
|
46
|
+
*
|
|
47
|
+
* @module image/binarizer
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
51
|
+
import { NotFoundError } from '../core/errors.js';
|
|
52
|
+
|
|
53
|
+
/** Side of a block, in pixels. */
|
|
54
|
+
const BLOCK = 8;
|
|
55
|
+
const BLOCK_SHIFT = 3;
|
|
56
|
+
|
|
57
|
+
/** Below this spread, a block is treated as uniform rather than as edges. */
|
|
58
|
+
const MIN_DYNAMIC_RANGE = 24;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* One threshold for the entire image, chosen from the luminance histogram.
|
|
62
|
+
*
|
|
63
|
+
* Finds the two strongest peaks — ideally ink and paper — and cuts at the
|
|
64
|
+
* point of lowest population between them, weighted by distance so a narrow
|
|
65
|
+
* secondary peak does not drag the threshold onto a shoulder.
|
|
66
|
+
*
|
|
67
|
+
* @param {import('./luminance.js').LuminanceSource} source
|
|
68
|
+
* @returns {BitMatrix} Set bit = dark module.
|
|
69
|
+
*/
|
|
70
|
+
export function binarizeGlobal(source) {
|
|
71
|
+
const { grey, width, height } = source;
|
|
72
|
+
const buckets = new Int32Array(32);
|
|
73
|
+
for (let i = 0; i < grey.length; i++) buckets[grey[i] >> 3]++;
|
|
74
|
+
|
|
75
|
+
const threshold = pickThreshold(buckets);
|
|
76
|
+
const out = new BitMatrix(width, height);
|
|
77
|
+
for (let y = 0; y < height; y++) {
|
|
78
|
+
const base = y * width;
|
|
79
|
+
for (let x = 0; x < width; x++) {
|
|
80
|
+
if (grey[base + x] < threshold) out.set(x, y);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {Int32Array} buckets
|
|
88
|
+
* @returns {number} Threshold in 0-255.
|
|
89
|
+
*/
|
|
90
|
+
function pickThreshold(buckets) {
|
|
91
|
+
const n = buckets.length;
|
|
92
|
+
|
|
93
|
+
let firstPeak = 0, firstPeakSize = 0;
|
|
94
|
+
for (let i = 0; i < n; i++) {
|
|
95
|
+
if (buckets[i] > firstPeakSize) {
|
|
96
|
+
firstPeakSize = buckets[i];
|
|
97
|
+
firstPeak = i;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Score candidates by population scaled by squared distance from the first
|
|
102
|
+
// peak: the second peak must be both populous and clearly separated.
|
|
103
|
+
let secondPeak = 0, secondScore = 0;
|
|
104
|
+
for (let i = 0; i < n; i++) {
|
|
105
|
+
const d = i - firstPeak;
|
|
106
|
+
const score = buckets[i] * d * d;
|
|
107
|
+
if (score > secondScore) {
|
|
108
|
+
secondScore = score;
|
|
109
|
+
secondPeak = i;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let low = Math.min(firstPeak, secondPeak);
|
|
114
|
+
let high = Math.max(firstPeak, secondPeak);
|
|
115
|
+
|
|
116
|
+
if (high - low <= n >> 4) {
|
|
117
|
+
// Effectively unimodal — a blank region, or an image with no ink. Fall
|
|
118
|
+
// back to the midpoint of the occupied range rather than inventing edges.
|
|
119
|
+
let lo = 0, hi = n - 1;
|
|
120
|
+
while (lo < n && buckets[lo] === 0) lo++;
|
|
121
|
+
while (hi > 0 && buckets[hi] === 0) hi--;
|
|
122
|
+
return (((lo + hi) >> 1) << 3) + 4;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Deepest valley between the peaks, biased toward the middle.
|
|
126
|
+
let valley = low, valleyScore = -1;
|
|
127
|
+
for (let i = low + 1; i < high; i++) {
|
|
128
|
+
const fromLow = i - low;
|
|
129
|
+
const fromHigh = high - i;
|
|
130
|
+
const score = fromLow * fromHigh * (firstPeakSize - buckets[i]);
|
|
131
|
+
if (score > valleyScore) {
|
|
132
|
+
valleyScore = score;
|
|
133
|
+
valley = i;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return (valley << 3) + 4;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Locally adaptive thresholding.
|
|
141
|
+
*
|
|
142
|
+
* Per 8x8 block: compute min, max and mean. A block with real contrast gets
|
|
143
|
+
* its own mean as the threshold. A block that is flat is ambiguous on its own
|
|
144
|
+
* — solid paper and solid ink look identical from the inside — so it inherits
|
|
145
|
+
* from its neighbourhood, which is what stops large quiet zones from being
|
|
146
|
+
* speckled into noise.
|
|
147
|
+
*
|
|
148
|
+
* Thresholds are then averaged over a 5x5 block window, so lighting gradients
|
|
149
|
+
* are followed smoothly instead of producing visible block seams that the
|
|
150
|
+
* detectors would read as edges.
|
|
151
|
+
*
|
|
152
|
+
* @param {import('./luminance.js').LuminanceSource} source
|
|
153
|
+
* @returns {BitMatrix}
|
|
154
|
+
*/
|
|
155
|
+
export function binarizeHybrid(source) {
|
|
156
|
+
const { grey, width, height } = source;
|
|
157
|
+
|
|
158
|
+
// Too small to block up meaningfully; the global pass is strictly better.
|
|
159
|
+
if (width < BLOCK * 5 || height < BLOCK * 5) return binarizeGlobal(source);
|
|
160
|
+
|
|
161
|
+
const bw = (width + BLOCK - 1) >> BLOCK_SHIFT;
|
|
162
|
+
const bh = (height + BLOCK - 1) >> BLOCK_SHIFT;
|
|
163
|
+
const means = new Int32Array(bw * bh);
|
|
164
|
+
const ranges = new Int32Array(bw * bh);
|
|
165
|
+
|
|
166
|
+
for (let by = 0; by < bh; by++) {
|
|
167
|
+
const y0 = by << BLOCK_SHIFT;
|
|
168
|
+
const y1 = Math.min(y0 + BLOCK, height);
|
|
169
|
+
for (let bx = 0; bx < bw; bx++) {
|
|
170
|
+
const x0 = bx << BLOCK_SHIFT;
|
|
171
|
+
const x1 = Math.min(x0 + BLOCK, width);
|
|
172
|
+
|
|
173
|
+
let sum = 0, min = 255, max = 0, count = 0;
|
|
174
|
+
for (let y = y0; y < y1; y++) {
|
|
175
|
+
const base = y * width;
|
|
176
|
+
for (let x = x0; x < x1; x++) {
|
|
177
|
+
const v = grey[base + x];
|
|
178
|
+
sum += v;
|
|
179
|
+
if (v < min) min = v;
|
|
180
|
+
if (v > max) max = v;
|
|
181
|
+
count++;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const idx = by * bw + bx;
|
|
185
|
+
means[idx] = count ? (sum / count) | 0 : 128;
|
|
186
|
+
ranges[idx] = max - min;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Flat blocks adopt a threshold from context. Looking left and up is enough
|
|
191
|
+
// because those neighbours are already resolved, and it makes the pass
|
|
192
|
+
// single-shot rather than iterative.
|
|
193
|
+
for (let by = 0; by < bh; by++) {
|
|
194
|
+
for (let bx = 0; bx < bw; bx++) {
|
|
195
|
+
const idx = by * bw + bx;
|
|
196
|
+
if (ranges[idx] >= MIN_DYNAMIC_RANGE) continue;
|
|
197
|
+
|
|
198
|
+
let inherited = means[idx];
|
|
199
|
+
if (bx > 0 && by > 0) {
|
|
200
|
+
const neighbours = [
|
|
201
|
+
means[idx - 1],
|
|
202
|
+
means[idx - bw],
|
|
203
|
+
means[idx - bw - 1],
|
|
204
|
+
];
|
|
205
|
+
const avg = (neighbours[0] + neighbours[1] + neighbours[2]) / 3;
|
|
206
|
+
// A flat block darker than its surroundings is ink; lighter is paper.
|
|
207
|
+
// Either way the local minimum is the safer threshold than the mean.
|
|
208
|
+
inherited = Math.min(avg, means[idx]);
|
|
209
|
+
}
|
|
210
|
+
means[idx] = inherited | 0;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const out = new BitMatrix(width, height);
|
|
215
|
+
const R = 2; // 5x5 block window
|
|
216
|
+
|
|
217
|
+
for (let by = 0; by < bh; by++) {
|
|
218
|
+
const y0 = by << BLOCK_SHIFT;
|
|
219
|
+
const y1 = Math.min(y0 + BLOCK, height);
|
|
220
|
+
const byLo = Math.max(0, by - R);
|
|
221
|
+
const byHi = Math.min(bh - 1, by + R);
|
|
222
|
+
|
|
223
|
+
for (let bx = 0; bx < bw; bx++) {
|
|
224
|
+
const bxLo = Math.max(0, bx - R);
|
|
225
|
+
const bxHi = Math.min(bw - 1, bx + R);
|
|
226
|
+
|
|
227
|
+
let sum = 0, count = 0;
|
|
228
|
+
for (let ny = byLo; ny <= byHi; ny++) {
|
|
229
|
+
for (let nx = bxLo; nx <= bxHi; nx++) {
|
|
230
|
+
sum += means[ny * bw + nx];
|
|
231
|
+
count++;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const threshold = sum / count;
|
|
235
|
+
|
|
236
|
+
const x0 = bx << BLOCK_SHIFT;
|
|
237
|
+
const x1 = Math.min(x0 + BLOCK, width);
|
|
238
|
+
for (let y = y0; y < y1; y++) {
|
|
239
|
+
const base = y * width;
|
|
240
|
+
for (let x = x0; x < x1; x++) {
|
|
241
|
+
if (grey[base + x] < threshold) out.set(x, y);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Binarize with the named strategy.
|
|
252
|
+
*
|
|
253
|
+
* @param {import('./luminance.js').LuminanceSource} source
|
|
254
|
+
* @param {'global' | 'hybrid' | 'auto'} [strategy]
|
|
255
|
+
* @returns {BitMatrix}
|
|
256
|
+
*/
|
|
257
|
+
export function binarize(source, strategy = 'auto') {
|
|
258
|
+
if (!source || !source.grey) throw new NotFoundError('binarize: no luminance source');
|
|
259
|
+
switch (strategy) {
|
|
260
|
+
case 'global': return binarizeGlobal(source);
|
|
261
|
+
case 'hybrid': return binarizeHybrid(source);
|
|
262
|
+
case 'auto':
|
|
263
|
+
// Small images are almost always generated rather than photographed.
|
|
264
|
+
return source.width * source.height < 200 * 200
|
|
265
|
+
? binarizeGlobal(source)
|
|
266
|
+
: binarizeHybrid(source);
|
|
267
|
+
default:
|
|
268
|
+
throw new NotFoundError(`Unknown binarizer strategy: ${strategy}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
* Resample a distorted symbol in the image into an upright module grid.
|
|
33
|
+
*
|
|
34
|
+
* Given a transform that maps grid coordinates to image coordinates, this
|
|
35
|
+
* samples the centre of every module. Sampling centres rather than averaging
|
|
36
|
+
* whole cells is deliberate: module edges are where blur and bleed live, and
|
|
37
|
+
* including them turns a marginal symbol into an unreadable one.
|
|
38
|
+
*
|
|
39
|
+
* @module image/grid-sampler
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { BitMatrix } from '../core/bit-matrix.js';
|
|
43
|
+
import { NotFoundError } from '../core/errors.js';
|
|
44
|
+
import { PerspectiveTransform } from './perspective.js';
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Sample a `dimension` x `dimension` grid (or `width` x `height`).
|
|
48
|
+
*
|
|
49
|
+
* @param {BitMatrix} image Binarized source image.
|
|
50
|
+
* @param {number} width Modules across.
|
|
51
|
+
* @param {number} height Modules down.
|
|
52
|
+
* @param {PerspectiveTransform} transform Grid space -> image space.
|
|
53
|
+
* @returns {BitMatrix}
|
|
54
|
+
* @throws {NotFoundError} If the grid falls outside the image.
|
|
55
|
+
*/
|
|
56
|
+
export function sampleGrid(image, width, height, transform) {
|
|
57
|
+
const out = new BitMatrix(width, height);
|
|
58
|
+
const points = new Float32Array(width * 2);
|
|
59
|
+
|
|
60
|
+
for (let y = 0; y < height; y++) {
|
|
61
|
+
// Module centres: offset by half a module in both axes.
|
|
62
|
+
const gridY = y + 0.5;
|
|
63
|
+
for (let x = 0; x < width; x++) {
|
|
64
|
+
points[x * 2] = x + 0.5;
|
|
65
|
+
points[x * 2 + 1] = gridY;
|
|
66
|
+
}
|
|
67
|
+
transform.transform(points);
|
|
68
|
+
|
|
69
|
+
for (let x = 0; x < width; x++) {
|
|
70
|
+
const px = points[x * 2] | 0;
|
|
71
|
+
const py = points[x * 2 + 1] | 0;
|
|
72
|
+
if (px < 0 || py < 0 || px >= image.width || py >= image.height) {
|
|
73
|
+
throw new NotFoundError(
|
|
74
|
+
`Sampling grid escapes the image at module (${x}, ${y})`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if (image.get(px, py)) out.set(x, y);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Sample with a 3x3 majority vote per module.
|
|
86
|
+
*
|
|
87
|
+
* Slower, and worth it when a single-point sample lands on a speck of noise or
|
|
88
|
+
* a JPEG artefact. Readers fall back to this after a clean sample fails to
|
|
89
|
+
* decode, rather than paying for it on every attempt.
|
|
90
|
+
*
|
|
91
|
+
* @param {BitMatrix} image
|
|
92
|
+
* @param {number} width
|
|
93
|
+
* @param {number} height
|
|
94
|
+
* @param {PerspectiveTransform} transform
|
|
95
|
+
* @returns {BitMatrix}
|
|
96
|
+
*/
|
|
97
|
+
export function sampleGridVoting(image, width, height, transform) {
|
|
98
|
+
const out = new BitMatrix(width, height);
|
|
99
|
+
|
|
100
|
+
// Spacing between module centres, measured in image pixels, so the vote
|
|
101
|
+
// spreads across the module rather than a fixed pixel radius that would be
|
|
102
|
+
// meaningless at a different scale.
|
|
103
|
+
const p0 = transform.transformPoint(0.5, 0.5);
|
|
104
|
+
const p1 = transform.transformPoint(1.5, 0.5);
|
|
105
|
+
const p2 = transform.transformPoint(0.5, 1.5);
|
|
106
|
+
const stepX = Math.hypot(p1.x - p0.x, p1.y - p0.y);
|
|
107
|
+
const stepY = Math.hypot(p2.x - p0.x, p2.y - p0.y);
|
|
108
|
+
const rx = Math.max(1, Math.round(stepX / 4));
|
|
109
|
+
const ry = Math.max(1, Math.round(stepY / 4));
|
|
110
|
+
|
|
111
|
+
for (let y = 0; y < height; y++) {
|
|
112
|
+
for (let x = 0; x < width; x++) {
|
|
113
|
+
const c = transform.transformPoint(x + 0.5, y + 0.5);
|
|
114
|
+
const cx = c.x | 0;
|
|
115
|
+
const cy = c.y | 0;
|
|
116
|
+
if (cx < 0 || cy < 0 || cx >= image.width || cy >= image.height) {
|
|
117
|
+
throw new NotFoundError(
|
|
118
|
+
`Sampling grid escapes the image at module (${x}, ${y})`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let dark = 0;
|
|
123
|
+
let total = 0;
|
|
124
|
+
for (let dy = -1; dy <= 1; dy++) {
|
|
125
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
126
|
+
const sx = cx + dx * rx;
|
|
127
|
+
const sy = cy + dy * ry;
|
|
128
|
+
if (sx < 0 || sy < 0 || sx >= image.width || sy >= image.height) continue;
|
|
129
|
+
total++;
|
|
130
|
+
if (image.get(sx, sy)) dark++;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (total > 0 && dark * 2 > total) out.set(x, y);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Build the transform for a symbol whose four corners are known, and sample it.
|
|
142
|
+
*
|
|
143
|
+
* Corners are in reading order: top-left, top-right, bottom-right, bottom-left.
|
|
144
|
+
*
|
|
145
|
+
* @param {BitMatrix} image
|
|
146
|
+
* @param {number} dimension Modules per side.
|
|
147
|
+
* @param {Array<{x: number, y: number}>} corners
|
|
148
|
+
* @param {boolean} [voting]
|
|
149
|
+
* @returns {BitMatrix}
|
|
150
|
+
*/
|
|
151
|
+
export function sampleQuad(image, dimension, corners, voting = false) {
|
|
152
|
+
if (corners.length !== 4) throw new NotFoundError('sampleQuad needs exactly 4 corners');
|
|
153
|
+
const [tl, tr, br, bl] = corners;
|
|
154
|
+
const d = dimension;
|
|
155
|
+
|
|
156
|
+
const transform = PerspectiveTransform.quadToQuad(
|
|
157
|
+
0, 0, d, 0, d, d, 0, d,
|
|
158
|
+
tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
return voting
|
|
162
|
+
? sampleGridVoting(image, d, d, transform)
|
|
163
|
+
: sampleGrid(image, d, d, transform);
|
|
164
|
+
}
|