qr 0.5.4 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -36
- package/decode.d.ts +100 -13
- package/decode.d.ts.map +1 -1
- package/decode.js +623 -153
- package/decode.js.map +1 -1
- package/dom.d.ts +110 -13
- package/dom.d.ts.map +1 -1
- package/dom.js +138 -18
- package/dom.js.map +1 -1
- package/index.d.ts +175 -35
- package/index.d.ts.map +1 -1
- package/index.js +276 -83
- package/index.js.map +1 -1
- package/package.json +8 -4
- package/src/decode.ts +648 -177
- package/src/dom.ts +169 -31
- package/src/index.ts +500 -129
package/decode.js
CHANGED
|
@@ -17,22 +17,35 @@ limitations under the License.
|
|
|
17
17
|
/**
|
|
18
18
|
* Methods for decoding (reading) QR code patterns.
|
|
19
19
|
* @module
|
|
20
|
-
* @example
|
|
21
|
-
```js
|
|
22
|
-
|
|
23
|
-
```
|
|
24
20
|
*/
|
|
25
21
|
import { Bitmap, utils } from "./index.js";
|
|
26
|
-
const { best, bin, drawTemplate, fillArr, info, interleave, validateVersion, zigzag, popcnt } = utils;
|
|
27
22
|
// Constants
|
|
28
23
|
const MAX_BITS_ERROR = 3; // Up to 3 bit errors in version/format
|
|
24
|
+
// Kept at 8: the block-stat fast path reads two u32 words per row and the
|
|
25
|
+
// average uses `sum >>> 6`, so the current binarizer assumes 8x8 = 64 pixels.
|
|
29
26
|
const GRAYSCALE_BLOCK_SIZE = 8;
|
|
30
27
|
const GRAYSCALE_RANGE = 24;
|
|
31
28
|
const PATTERN_VARIANCE = 2;
|
|
29
|
+
// Diagonal finder scans are noisier under blur/perspective than horizontal or
|
|
30
|
+
// vertical runs, so they use a looser ratio tolerance.
|
|
32
31
|
const PATTERN_VARIANCE_DIAGONAL = 1.333;
|
|
33
32
|
const PATTERN_MIN_CONFIRMATIONS = 2;
|
|
34
33
|
const DETECT_MIN_ROW_SKIP = 3;
|
|
34
|
+
// Pair LUTs for the 8x8 block-stat fast path: each 16-bit lane holds two
|
|
35
|
+
// brightness bytes, so we can accumulate sum/min/max four pixels at a time.
|
|
36
|
+
const SUM16 = new Uint16Array(1 << 16);
|
|
37
|
+
const MIN16 = new Uint8Array(1 << 16);
|
|
38
|
+
const MAX16 = new Uint8Array(1 << 16);
|
|
39
|
+
for (let i = 0; i < SUM16.length; i++) {
|
|
40
|
+
const lo = i & 0xff;
|
|
41
|
+
const hi = i >>> 8;
|
|
42
|
+
SUM16[i] = lo + hi;
|
|
43
|
+
MIN16[i] = lo < hi ? lo : hi;
|
|
44
|
+
MAX16[i] = lo > hi ? lo : hi;
|
|
45
|
+
}
|
|
35
46
|
// TODO: move to index, nearby with bitmap and other graph related stuff?
|
|
47
|
+
// Fast truncation for values expected to be non-negative; negatives would wrap
|
|
48
|
+
// through uint32 because this uses `>>> 0`, not `Math.floor()`.
|
|
36
49
|
const int = (n) => n >>> 0;
|
|
37
50
|
// distance ^ 2
|
|
38
51
|
const distance2 = (p1, p2) => {
|
|
@@ -51,37 +64,145 @@ const pointMirror = (p) => ({ x: p.y, y: p.x });
|
|
|
51
64
|
const pointClone = (p) => ({ x: p.x, y: p.y });
|
|
52
65
|
const pointInt = (p) => ({ x: int(p.x), y: int(p.y) });
|
|
53
66
|
const pointAdd = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
67
|
+
// Count trailing zeroes in a packed bitmap word so scanLine can skip whole
|
|
68
|
+
// runs instead of testing one bit at a time.
|
|
69
|
+
const ctz32 = (v) => {
|
|
70
|
+
v = v >>> 0;
|
|
71
|
+
if (v === 0)
|
|
72
|
+
return 32;
|
|
73
|
+
return 31 - Math.clz32((v & -v) >>> 0);
|
|
74
|
+
};
|
|
54
75
|
function cap(value, min, max) {
|
|
55
|
-
|
|
76
|
+
// ISO/IEC 18004:2024 §12 h) builds the sampling grid from "module centres";
|
|
77
|
+
// detector callers pass `0` as a real image edge when clipping those samples
|
|
78
|
+
// and search windows. `|| value` treats that bound as absent.
|
|
79
|
+
let res = value;
|
|
80
|
+
if (max !== undefined)
|
|
81
|
+
res = Math.min(res, max);
|
|
82
|
+
if (min !== undefined)
|
|
83
|
+
res = Math.max(res, min);
|
|
84
|
+
return res;
|
|
56
85
|
}
|
|
57
|
-
|
|
58
|
-
const
|
|
86
|
+
function getBytesPerPixel(img) {
|
|
87
|
+
const image = img;
|
|
88
|
+
const perPixel = image.data.length / (image.width * image.height);
|
|
59
89
|
if (perPixel === 3 || perPixel === 4)
|
|
60
90
|
return perPixel; // RGB or RGBA
|
|
61
91
|
throw new Error(`Unknown image format, bytes per pixel=${perPixel}`);
|
|
62
|
-
}
|
|
92
|
+
}
|
|
93
|
+
function isBytes(data) {
|
|
94
|
+
return data instanceof Uint8Array || data instanceof Uint8ClampedArray;
|
|
95
|
+
}
|
|
63
96
|
/**
|
|
64
97
|
* Convert to grayscale. The function is the most expensive part of decoding:
|
|
65
|
-
* it takes up to 90% of time.
|
|
98
|
+
* it takes up to 90% of time.
|
|
99
|
+
*
|
|
100
|
+
* Binarization pipeline:
|
|
101
|
+
* 1. Convert RGB/RGBA image to one luma byte per pixel.
|
|
102
|
+
* 2. Split the image into 8x8 blocks and collect per-block mean/min/max.
|
|
103
|
+
* 3. Build a 5x5 neighborhood mean over those block means.
|
|
104
|
+
* 4. Turn each 8x8 block into bitmap bits using a local cut derived from:
|
|
105
|
+
* - the neighborhood mean,
|
|
106
|
+
* - the current block statistics,
|
|
107
|
+
* - a cheap whole-image color-spread estimate,
|
|
108
|
+
* - and, on risky scenes, a local variance field over block means.
|
|
109
|
+
*
|
|
110
|
+
* Instead of producing "best looking" thresholding: we produce a
|
|
111
|
+
* bitmap where finder patterns survive perspective / blur / highlights while
|
|
112
|
+
* keeping false dark regions low enough for downstream finder selection.
|
|
66
113
|
*/
|
|
67
114
|
function toBitmap(img) {
|
|
115
|
+
const image = img;
|
|
116
|
+
const width = image.width;
|
|
117
|
+
const height = image.height;
|
|
118
|
+
const data = image.data;
|
|
68
119
|
const bytesPerPixel = getBytesPerPixel(img);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
120
|
+
const pixLen = height * width;
|
|
121
|
+
const brightness = new Uint8Array(pixLen);
|
|
122
|
+
if (bytesPerPixel === 4 && isBytes(data) && (data.byteOffset & 3) === 0) {
|
|
123
|
+
// Little-endian RGBA: compute four grayscale bytes and commit as one u32 store.
|
|
124
|
+
// Unaligned RGBA subarray views are still valid inputs; they fall back to
|
|
125
|
+
// the scalar path because Uint32Array would throw on a misaligned offset.
|
|
126
|
+
const pixels = new Uint32Array(data.buffer, data.byteOffset, pixLen);
|
|
127
|
+
const bright32 = new Uint32Array(brightness.buffer, brightness.byteOffset, brightness.length >>> 2);
|
|
128
|
+
const n4 = pixels.length & ~3;
|
|
129
|
+
for (let i = 0, j = 0; i < n4; i += 4, j++) {
|
|
130
|
+
const v0 = pixels[i] >>> 0;
|
|
131
|
+
const v1 = pixels[i + 1] >>> 0;
|
|
132
|
+
const v2 = pixels[i + 2] >>> 0;
|
|
133
|
+
const v3 = pixels[i + 3] >>> 0;
|
|
134
|
+
// RGBA words are little-endian here, so this is `(r + 2*g + b) / 4`
|
|
135
|
+
// computed from the packed byte lanes for four pixels at once.
|
|
136
|
+
const b0 = ((v0 & 0xff) + (((v0 >>> 8) & 0xff) << 1) + ((v0 >>> 16) & 0xff)) >>> 2;
|
|
137
|
+
const b1 = ((v1 & 0xff) + (((v1 >>> 8) & 0xff) << 1) + ((v1 >>> 16) & 0xff)) >>> 2;
|
|
138
|
+
const b2 = ((v2 & 0xff) + (((v2 >>> 8) & 0xff) << 1) + ((v2 >>> 16) & 0xff)) >>> 2;
|
|
139
|
+
const b3 = ((v3 & 0xff) + (((v3 >>> 8) & 0xff) << 1) + ((v3 >>> 16) & 0xff)) >>> 2;
|
|
140
|
+
bright32[j] = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
|
|
141
|
+
}
|
|
142
|
+
for (let i = n4; i < pixels.length; i++) {
|
|
143
|
+
const v = pixels[i] >>> 0;
|
|
144
|
+
brightness[i] = ((v & 0xff) + (((v >>> 8) & 0xff) << 1) + ((v >>> 16) & 0xff)) >>> 2;
|
|
145
|
+
}
|
|
75
146
|
}
|
|
147
|
+
else {
|
|
148
|
+
for (let i = 0, j = 0, d = data; i < d.length; i += bytesPerPixel) {
|
|
149
|
+
const r = d[i];
|
|
150
|
+
const g = d[i + 1];
|
|
151
|
+
const b = d[i + 2];
|
|
152
|
+
brightness[j++] = int((r + 2 * g + b) / 4) & 0xff;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// Sampled color spread is a cheap "scene type" signal:
|
|
156
|
+
// grayscale / flat lighting scenes want conservative cuts, while colorful or
|
|
157
|
+
// high-spread scenes benefit from a slightly darker threshold.
|
|
158
|
+
let spreadSum = 0;
|
|
159
|
+
let spreadCnt = 0;
|
|
160
|
+
const spreadStep = bytesPerPixel * 16;
|
|
161
|
+
for (let i = 0; i < data.length; i += spreadStep) {
|
|
162
|
+
const r = data[i];
|
|
163
|
+
const g = data[i + 1];
|
|
164
|
+
const b = data[i + 2];
|
|
165
|
+
// hi=max(r,g,b), lo=min(r,g,b): this sampled channel spread is a cheap
|
|
166
|
+
// scene-level proxy for "how colorful / highlighty is this frame?".
|
|
167
|
+
const hi = r > g ? (r > b ? r : b) : g > b ? g : b;
|
|
168
|
+
const lo = r < g ? (r < b ? r : b) : g < b ? g : b;
|
|
169
|
+
spreadSum += hi - lo;
|
|
170
|
+
spreadCnt++;
|
|
171
|
+
}
|
|
172
|
+
const spreadMean = spreadSum / spreadCnt;
|
|
76
173
|
// Convert to bitmap
|
|
77
174
|
const block = GRAYSCALE_BLOCK_SIZE;
|
|
78
|
-
if (
|
|
175
|
+
if (width < block * 5 || height < block * 5)
|
|
79
176
|
throw new Error('image too small');
|
|
80
|
-
const bWidth = Math.ceil(
|
|
81
|
-
const bHeight = Math.ceil(
|
|
82
|
-
const maxY =
|
|
83
|
-
const maxX =
|
|
84
|
-
const
|
|
177
|
+
const bWidth = Math.ceil(width / block);
|
|
178
|
+
const bHeight = Math.ceil(height / block);
|
|
179
|
+
const maxY = height - block;
|
|
180
|
+
const maxX = width - block;
|
|
181
|
+
const blockLen = bWidth * bHeight;
|
|
182
|
+
const blockState = new Uint32Array(blockLen);
|
|
183
|
+
// Each 8x8 block stores packed:
|
|
184
|
+
// - bits 0..7: block baseline brightness used by the threshold field
|
|
185
|
+
// - bits 8..15: block min
|
|
186
|
+
// - bits 16..23: block max
|
|
187
|
+
let hiRangeCnt = 0;
|
|
188
|
+
let veryLowCnt = 0;
|
|
189
|
+
const padW = (width + 3) & ~3;
|
|
190
|
+
let statStride = width;
|
|
191
|
+
let stat32;
|
|
192
|
+
if ((width & 3) !== 0) {
|
|
193
|
+
const padLen = padW * height;
|
|
194
|
+
const brightPad = new Uint8Array(padLen);
|
|
195
|
+
for (let y = 0; y < height; y++) {
|
|
196
|
+
const src = y * width;
|
|
197
|
+
const dst = y * padW;
|
|
198
|
+
brightPad.set(brightness.subarray(src, src + width), dst);
|
|
199
|
+
}
|
|
200
|
+
// Misaligned widths are padded only for the block-stat fast path.
|
|
201
|
+
statStride = padW;
|
|
202
|
+
stat32 = new Uint32Array(brightPad.buffer, brightPad.byteOffset, (padW * height) >>> 2);
|
|
203
|
+
}
|
|
204
|
+
else
|
|
205
|
+
stat32 = new Uint32Array(brightness.buffer, brightness.byteOffset, brightness.length >>> 2);
|
|
85
206
|
for (let y = 0; y < bHeight; y++) {
|
|
86
207
|
const yPos = cap(y * block, 0, maxY);
|
|
87
208
|
for (let x = 0; x < bWidth; x++) {
|
|
@@ -89,48 +210,248 @@ function toBitmap(img) {
|
|
|
89
210
|
let sum = 0;
|
|
90
211
|
let min = 0xff;
|
|
91
212
|
let max = 0;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
213
|
+
// The stat-LUT fast path needs the 8-pixel row start to be 32-bit aligned
|
|
214
|
+
// so each row can be read as two full u32 words without any shifts.
|
|
215
|
+
if ((xPos & 3) === 0) {
|
|
216
|
+
for (let yy = 0, pos = yPos * statStride + xPos; yy < block; yy++, pos += statStride) {
|
|
217
|
+
const p = pos >>> 2;
|
|
218
|
+
const w0 = stat32[p] >>> 0;
|
|
219
|
+
const w1 = stat32[p + 1] >>> 0;
|
|
220
|
+
const a0 = w0 & 0xffff;
|
|
221
|
+
const a1 = w0 >>> 16;
|
|
222
|
+
const b0 = w1 & 0xffff;
|
|
223
|
+
const b1 = w1 >>> 16;
|
|
224
|
+
sum += SUM16[a0] + SUM16[a1] + SUM16[b0] + SUM16[b1];
|
|
225
|
+
const min0 = MIN16[a0];
|
|
226
|
+
const min1 = MIN16[a1];
|
|
227
|
+
const min2 = MIN16[b0];
|
|
228
|
+
const min3 = MIN16[b1];
|
|
229
|
+
if (min0 < min)
|
|
230
|
+
min = min0;
|
|
231
|
+
if (min1 < min)
|
|
232
|
+
min = min1;
|
|
233
|
+
if (min2 < min)
|
|
234
|
+
min = min2;
|
|
235
|
+
if (min3 < min)
|
|
236
|
+
min = min3;
|
|
237
|
+
const max0 = MAX16[a0];
|
|
238
|
+
const max1 = MAX16[a1];
|
|
239
|
+
const max2 = MAX16[b0];
|
|
240
|
+
const max3 = MAX16[b1];
|
|
241
|
+
if (max0 > max)
|
|
242
|
+
max = max0;
|
|
243
|
+
if (max1 > max)
|
|
244
|
+
max = max1;
|
|
245
|
+
if (max2 > max)
|
|
246
|
+
max = max2;
|
|
247
|
+
if (max3 > max)
|
|
248
|
+
max = max3;
|
|
98
249
|
}
|
|
99
250
|
}
|
|
251
|
+
else {
|
|
252
|
+
for (let yy = 0, pos = yPos * width + xPos; yy < block; yy++, pos += width) {
|
|
253
|
+
for (let xx = 0; xx < block; xx++) {
|
|
254
|
+
const pixel = brightness[pos + xx];
|
|
255
|
+
sum += pixel;
|
|
256
|
+
if (pixel < min)
|
|
257
|
+
min = pixel;
|
|
258
|
+
if (pixel > max)
|
|
259
|
+
max = pixel;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const bIdx = bWidth * y + x;
|
|
264
|
+
const range = max - min;
|
|
100
265
|
// Average brightness of block
|
|
101
|
-
let average =
|
|
102
|
-
if (
|
|
266
|
+
let average = sum >>> 6;
|
|
267
|
+
if (range <= GRAYSCALE_RANGE) {
|
|
268
|
+
// Low-contrast blocks are unstable if we threshold from their raw mean.
|
|
269
|
+
// Bias toward the local dark floor, then smooth with already-seen
|
|
270
|
+
// neighbors so finder rings don't disappear in washed-out regions.
|
|
103
271
|
average = min / 2;
|
|
104
272
|
if (y > 0 && x > 0) {
|
|
105
273
|
const idx = (x, y) => y * bWidth + x;
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
274
|
+
const neighborNumerator = (blockState[idx(x, y - 1)] & 0xff) +
|
|
275
|
+
2 * (blockState[idx(x - 1, y)] & 0xff) +
|
|
276
|
+
(blockState[idx(x - 1, y - 1)] & 0xff);
|
|
277
|
+
if (min * 4 < neighborNumerator)
|
|
278
|
+
average = neighborNumerator / 4;
|
|
109
279
|
}
|
|
110
280
|
}
|
|
111
|
-
|
|
281
|
+
blockState[bIdx] = int(average) | (min << 8) | (max << 16);
|
|
282
|
+
if (range > 40 && average < 224)
|
|
283
|
+
hiRangeCnt++;
|
|
284
|
+
if (range <= 10)
|
|
285
|
+
veryLowCnt++;
|
|
112
286
|
}
|
|
113
287
|
}
|
|
114
|
-
const
|
|
288
|
+
const hiRangeFrac = hiRangeCnt / blockLen;
|
|
289
|
+
const veryLowFrac = veryLowCnt / blockLen;
|
|
290
|
+
// These two scene gates are the main "policy" layer on top of the local cut:
|
|
291
|
+
// - `spotBias` darkens globally flat, slightly colorful scenes that otherwise
|
|
292
|
+
// miss bright-spot / washed-out QR modules.
|
|
293
|
+
// - `useVarField` avoids paying the variance-field cost on scenes where the
|
|
294
|
+
// plain 5x5 mean is already stable enough.
|
|
295
|
+
const spotBias = veryLowFrac > 0.55 &&
|
|
296
|
+
veryLowFrac < 0.66 &&
|
|
297
|
+
hiRangeFrac < 0.02 &&
|
|
298
|
+
spreadMean > 10 &&
|
|
299
|
+
spreadMean < 20
|
|
300
|
+
? -1
|
|
301
|
+
: 0;
|
|
302
|
+
const useVarField = veryLowFrac < 0.62 || spreadMean > 30;
|
|
303
|
+
const iWidth = bWidth + 1;
|
|
304
|
+
const iHeight = bHeight + 1;
|
|
305
|
+
const integLen = iHeight * iWidth;
|
|
306
|
+
// `integ` is the standard summed-area table of block means.
|
|
307
|
+
const integ = new Uint32Array(integLen);
|
|
308
|
+
// `integSqr` is the square-integral / summed-area table of `v * v` over the
|
|
309
|
+
// same block means, not a u8 pixel buffer. Those prefix sums can overflow
|
|
310
|
+
// 32-bit integer storage on large images, and Float32 was the measured
|
|
311
|
+
// faster compromise vs Float64 for this heuristic field.
|
|
312
|
+
const integSqr = useVarField ? new Float32Array(integLen) : undefined;
|
|
313
|
+
for (let y = 0; y < bHeight; y++) {
|
|
314
|
+
let rowSum = 0;
|
|
315
|
+
let rowSq = 0;
|
|
316
|
+
const bRow = y * bWidth;
|
|
317
|
+
const iRow = (y + 1) * iWidth;
|
|
318
|
+
const iPrev = y * iWidth;
|
|
319
|
+
for (let x = 0; x < bWidth; x++) {
|
|
320
|
+
const v = blockState[bRow + x] & 0xff;
|
|
321
|
+
rowSum += v;
|
|
322
|
+
if (integSqr)
|
|
323
|
+
rowSq += v * v;
|
|
324
|
+
integ[iRow + x + 1] = integ[iPrev + x + 1] + rowSum;
|
|
325
|
+
if (integSqr)
|
|
326
|
+
integSqr[iRow + x + 1] = integSqr[iPrev + x + 1] + rowSq;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const matrix = new Bitmap({ width, height });
|
|
330
|
+
const rows = Math.ceil(width / 32);
|
|
331
|
+
// Decode intentionally writes the packed bitmap words directly here. The
|
|
332
|
+
// per-pixel Bitmap API is too expensive on this hot path, so this must stay
|
|
333
|
+
// in sync with Bitmap's internal `value` layout.
|
|
334
|
+
const bm = matrix.value;
|
|
335
|
+
const rad = 2;
|
|
336
|
+
const win = rad * 2 + 1;
|
|
337
|
+
const area = win * win;
|
|
115
338
|
for (let y = 0; y < bHeight; y++) {
|
|
116
339
|
const yPos = cap(y * block, 0, maxY);
|
|
117
|
-
const top = cap(y,
|
|
340
|
+
const top = cap(y, rad, bHeight - rad - 1);
|
|
341
|
+
const y0 = top - rad;
|
|
342
|
+
const y1 = top + rad;
|
|
343
|
+
const r0 = y0 * iWidth;
|
|
344
|
+
const r1 = (y1 + 1) * iWidth;
|
|
118
345
|
for (let x = 0; x < bWidth; x++) {
|
|
119
346
|
const xPos = cap(x * block, 0, maxX);
|
|
120
|
-
const
|
|
347
|
+
const shift = xPos & 31;
|
|
348
|
+
const col = xPos >>> 5;
|
|
349
|
+
const left = cap(x, rad, bWidth - rad - 1);
|
|
350
|
+
const x0 = left - rad;
|
|
351
|
+
const x1 = left + rad;
|
|
121
352
|
// 5x5 blocks average
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
353
|
+
const sum = integ[r1 + (x1 + 1)] - integ[r0 + (x1 + 1)] - integ[r1 + x0] + integ[r0 + x0];
|
|
354
|
+
// `average` is the coarse threshold surface: a 5x5 neighborhood mean of
|
|
355
|
+
// the 8x8 block means. The adjustments below decide when to move away
|
|
356
|
+
// from that surface for the current block.
|
|
357
|
+
const average = (sum / area) | 0;
|
|
358
|
+
let cut = average;
|
|
359
|
+
const bIdx = bWidth * y + x;
|
|
360
|
+
const blk = blockState[bIdx];
|
|
361
|
+
const blockAvg = blk & 0xff;
|
|
362
|
+
const min = (blk >>> 8) & 0xff;
|
|
363
|
+
const max = blk >>> 16;
|
|
364
|
+
const range = max - min;
|
|
365
|
+
if (average < min)
|
|
366
|
+
continue;
|
|
367
|
+
if (average >= max) {
|
|
368
|
+
const m = 0xff;
|
|
369
|
+
for (let yy = 0, row = yPos * rows + col; yy < block; yy++, row += rows) {
|
|
370
|
+
const lo = (m << shift) >>> 0;
|
|
371
|
+
bm[row] |= lo;
|
|
372
|
+
if (shift > 24)
|
|
373
|
+
bm[row + 1] |= m >>> (32 - shift);
|
|
133
374
|
}
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
// `localAdj`: nudge toward the current block when it is darker than
|
|
378
|
+
// its neighborhood. This helps preserve dark rings / modules that are
|
|
379
|
+
// locally meaningful but diluted by the 5x5 field.
|
|
380
|
+
let localAdj = (blockAvg - average) >> 4;
|
|
381
|
+
if (localAdj < 0)
|
|
382
|
+
localAdj = 0;
|
|
383
|
+
if (localAdj > 1)
|
|
384
|
+
localAdj = 1;
|
|
385
|
+
// `chromaAdj`: in colorful, mid-tone blocks, slight extra darkening
|
|
386
|
+
// helps where luma alone underestimates QR structure.
|
|
387
|
+
let chromaAdj = 0;
|
|
388
|
+
if (range > 6 && average > 48 && average < 232) {
|
|
389
|
+
const spreadBoost = spreadMean > 8 ? spreadMean - 8 : 0;
|
|
390
|
+
const mid = 128 - Math.abs(average - 128);
|
|
391
|
+
chromaAdj = int((spreadBoost * (range - 6) * mid) / 2200000);
|
|
392
|
+
if (chromaAdj > 1)
|
|
393
|
+
chromaAdj = 1;
|
|
394
|
+
}
|
|
395
|
+
// `varAdj`: if the surrounding block field has real variance, darken
|
|
396
|
+
// more aggressively when the local mean still sits far above the
|
|
397
|
+
// block minimum. This is what rescues many weak finder cases.
|
|
398
|
+
let varAdj = 0;
|
|
399
|
+
if (integSqr && range >= 6 && range <= 128) {
|
|
400
|
+
const sq = integSqr[r1 + (x1 + 1)] - integSqr[r0 + (x1 + 1)] - integSqr[r1 + x0] + integSqr[r0 + x0];
|
|
401
|
+
const meanSq = sq / area;
|
|
402
|
+
let variance = meanSq - average * average;
|
|
403
|
+
if (variance < 0)
|
|
404
|
+
variance = 0;
|
|
405
|
+
const gap = average - min;
|
|
406
|
+
const num = gap * (variance - 196);
|
|
407
|
+
const den = (variance + 832) * 9;
|
|
408
|
+
// `variance` is clamped non-negative, so `den` stays strictly positive.
|
|
409
|
+
varAdj = int(num / den);
|
|
410
|
+
if (varAdj < -1)
|
|
411
|
+
varAdj = -1;
|
|
412
|
+
if (varAdj > 4)
|
|
413
|
+
varAdj = 4;
|
|
414
|
+
}
|
|
415
|
+
cut = average + localAdj + chromaAdj + varAdj;
|
|
416
|
+
// Small scene-level nudges are intentionally separate from the three
|
|
417
|
+
// local terms above: they are cheap and only target known whole-scene
|
|
418
|
+
// failure modes such as washed-out bright-spot images.
|
|
419
|
+
if (spreadMean > 10 && range >= 8 && range <= 96 && average > min + 8 && average < 192)
|
|
420
|
+
cut++;
|
|
421
|
+
if (veryLowFrac > 0.68 && veryLowFrac < 0.86 && range >= 6 && range <= 20 && average < 196)
|
|
422
|
+
cut++;
|
|
423
|
+
cut += spotBias;
|
|
424
|
+
if (cut < min)
|
|
425
|
+
cut = min;
|
|
426
|
+
if (cut > max)
|
|
427
|
+
cut = max;
|
|
428
|
+
// Emit one 8-pixel row of the current 8x8 block: compare against the
|
|
429
|
+
// block cut, pack the 8 black/white decisions into one byte, then OR
|
|
430
|
+
// that byte into the bitmap word(s) at the current x-bit offset.
|
|
431
|
+
for (let yy = 0, pos = yPos * width + xPos, row = yPos * rows + col; yy < block; yy++, pos += width, row += rows) {
|
|
432
|
+
let m = 0;
|
|
433
|
+
if (brightness[pos] <= cut)
|
|
434
|
+
m |= 1;
|
|
435
|
+
if (brightness[pos + 1] <= cut)
|
|
436
|
+
m |= 2;
|
|
437
|
+
if (brightness[pos + 2] <= cut)
|
|
438
|
+
m |= 4;
|
|
439
|
+
if (brightness[pos + 3] <= cut)
|
|
440
|
+
m |= 8;
|
|
441
|
+
if (brightness[pos + 4] <= cut)
|
|
442
|
+
m |= 16;
|
|
443
|
+
if (brightness[pos + 5] <= cut)
|
|
444
|
+
m |= 32;
|
|
445
|
+
if (brightness[pos + 6] <= cut)
|
|
446
|
+
m |= 64;
|
|
447
|
+
if (brightness[pos + 7] <= cut)
|
|
448
|
+
m |= 128;
|
|
449
|
+
if (m === 0)
|
|
450
|
+
continue;
|
|
451
|
+
const lo = (m << shift) >>> 0;
|
|
452
|
+
bm[row] |= lo;
|
|
453
|
+
if (shift > 24)
|
|
454
|
+
bm[row + 1] |= m >>> (32 - shift);
|
|
134
455
|
}
|
|
135
456
|
}
|
|
136
457
|
}
|
|
@@ -161,7 +482,7 @@ const patternsConfirmed = (lst) => lst.filter((i) => i.count >= PATTERN_MIN_CONF
|
|
|
161
482
|
* @returns
|
|
162
483
|
*/
|
|
163
484
|
function pattern(p, size) {
|
|
164
|
-
const _size = size || fillArr(p.length, 1);
|
|
485
|
+
const _size = size || utils.fillArr(p.length, 1);
|
|
165
486
|
if (p.length !== _size.length)
|
|
166
487
|
throw new Error('invalid pattern');
|
|
167
488
|
if (!(p.length & 1))
|
|
@@ -171,7 +492,7 @@ function pattern(p, size) {
|
|
|
171
492
|
length: p.length,
|
|
172
493
|
pattern: p,
|
|
173
494
|
size: _size,
|
|
174
|
-
runs: () => fillArr(p.length, 0),
|
|
495
|
+
runs: () => utils.fillArr(p.length, 0),
|
|
175
496
|
totalSize: sum(_size),
|
|
176
497
|
total: (runs) => runs.reduce((acc, i) => acc + i),
|
|
177
498
|
shift: (runs, n) => {
|
|
@@ -189,7 +510,11 @@ function pattern(p, size) {
|
|
|
189
510
|
return true;
|
|
190
511
|
},
|
|
191
512
|
add(out, x, y, total) {
|
|
192
|
-
|
|
513
|
+
// ISO/IEC 18004:2024 §5.3.3.1 gives finder runs as "1:1:3:1:1";
|
|
514
|
+
// §5.3.6 defines alignment as "5 x 5 dark modules", "3 x 3 light
|
|
515
|
+
// modules", and a central dark module. Use this pattern's own run width
|
|
516
|
+
// so alignment candidates are not divided by finder width 7.
|
|
517
|
+
const moduleSize = total / res.totalSize;
|
|
193
518
|
const cur = { x, y, moduleSize, count: 1 };
|
|
194
519
|
for (let idx = 0; idx < out.length; idx++) {
|
|
195
520
|
const f = out[idx];
|
|
@@ -207,11 +532,12 @@ function pattern(p, size) {
|
|
|
207
532
|
return end;
|
|
208
533
|
},
|
|
209
534
|
check(b, runs, center, incr, maxCount) {
|
|
535
|
+
const bm = b;
|
|
210
536
|
let j = 0;
|
|
211
537
|
let i = pointClone(center);
|
|
212
538
|
const neg = pointNeg(incr);
|
|
213
539
|
const check = (p, step) => {
|
|
214
|
-
for (;
|
|
540
|
+
for (; bm.isInside(i) && !!bm.point(i) === res.pattern[p]; pointIncr(i, step)) {
|
|
215
541
|
runs[p]++;
|
|
216
542
|
j++;
|
|
217
543
|
}
|
|
@@ -234,20 +560,57 @@ function pattern(p, size) {
|
|
|
234
560
|
return j;
|
|
235
561
|
},
|
|
236
562
|
scanLine(b, y, xStart, xEnd, fn) {
|
|
563
|
+
const bm = b;
|
|
237
564
|
const runs = res.runs();
|
|
565
|
+
// Finder scanning also couples to Bitmap internals so it can scan packed
|
|
566
|
+
// 32-bit words directly instead of re-reading one pixel bit at a time.
|
|
567
|
+
const words = bm.words;
|
|
568
|
+
const vals = bm.value;
|
|
569
|
+
const row = y * words;
|
|
570
|
+
const pattern = res.pattern;
|
|
571
|
+
// Scan one packed bitmap row by jumping whole equal-bit runs inside 32-bit
|
|
572
|
+
// words; this keeps finder scanning from degenerating into per-pixel work.
|
|
573
|
+
const bitAt = (x) => ((vals[row + (x >>> 5)] >>> (x & 31)) & 1) === 1;
|
|
574
|
+
const runLen = (x, want) => {
|
|
575
|
+
let wi = row + (x >>> 5);
|
|
576
|
+
let bit = x & 31;
|
|
577
|
+
let w = (vals[wi] >>> bit) >>> 0;
|
|
578
|
+
let left = xEnd - x;
|
|
579
|
+
let len = 0;
|
|
580
|
+
while (left > 0) {
|
|
581
|
+
const room = 32 - bit;
|
|
582
|
+
let n = want ? ctz32(~w >>> 0) : ctz32(w);
|
|
583
|
+
if (n > room)
|
|
584
|
+
n = room;
|
|
585
|
+
if (n > left)
|
|
586
|
+
n = left;
|
|
587
|
+
len += n;
|
|
588
|
+
if (n < room && n < left)
|
|
589
|
+
break;
|
|
590
|
+
left -= n;
|
|
591
|
+
if (left <= 0)
|
|
592
|
+
break;
|
|
593
|
+
wi++;
|
|
594
|
+
bit = 0;
|
|
595
|
+
w = vals[wi] >>> 0;
|
|
596
|
+
}
|
|
597
|
+
return len;
|
|
598
|
+
};
|
|
238
599
|
let pos = 0;
|
|
239
600
|
let x = xStart;
|
|
240
601
|
// If we start in middle of an image, skip first pattern run,
|
|
241
602
|
// since we don't know run length of pixels from left side
|
|
242
603
|
if (xStart)
|
|
243
|
-
|
|
244
|
-
x++;
|
|
604
|
+
x += runLen(x, pattern[0]);
|
|
245
605
|
for (; x < xEnd; x++) {
|
|
606
|
+
const cur = bitAt(x);
|
|
246
607
|
// Same run, continue counting
|
|
247
|
-
if (
|
|
248
|
-
|
|
608
|
+
if (cur === pattern[pos]) {
|
|
609
|
+
const n = runLen(x, cur);
|
|
610
|
+
runs[pos] += n;
|
|
611
|
+
x += n - 1;
|
|
249
612
|
// If not last element - continue counting
|
|
250
|
-
if (x !==
|
|
613
|
+
if (x !== bm.width - 1)
|
|
251
614
|
continue;
|
|
252
615
|
// Last element finishes run, set x outside of run
|
|
253
616
|
x++;
|
|
@@ -278,11 +641,12 @@ function pattern(p, size) {
|
|
|
278
641
|
};
|
|
279
642
|
return res;
|
|
280
643
|
}
|
|
281
|
-
// light/dark/light/dark
|
|
282
|
-
const FINDER = pattern([true, false, true, false, true], [1, 1, 3, 1, 1]);
|
|
283
|
-
// dark/light
|
|
284
|
-
const ALIGNMENT = pattern([false, true, false]);
|
|
644
|
+
// dark/light/dark/light/dark in 1:1:3:1:1 ratio
|
|
645
|
+
const FINDER = /* @__PURE__ */ pattern([true, false, true, false, true], [1, 1, 3, 1, 1]);
|
|
646
|
+
// central light/dark/light runs of an alignment pattern in 1:1:1 ratio
|
|
647
|
+
const ALIGNMENT = /* @__PURE__ */ pattern([false, true, false]);
|
|
285
648
|
function findFinder(b) {
|
|
649
|
+
const bm = b;
|
|
286
650
|
let found = [];
|
|
287
651
|
function checkRuns(runs, v = 2) {
|
|
288
652
|
const total = sum(runs);
|
|
@@ -294,7 +658,7 @@ function findFinder(b) {
|
|
|
294
658
|
// Non-diagonal line (horizontal or vertical)
|
|
295
659
|
function checkLine(center, maxCount, total, incr) {
|
|
296
660
|
const runs = FINDER.runs();
|
|
297
|
-
let i = FINDER.check(
|
|
661
|
+
let i = FINDER.check(bm, runs, center, incr, maxCount);
|
|
298
662
|
if (i === false)
|
|
299
663
|
return false;
|
|
300
664
|
const runsTotal = sum(runs);
|
|
@@ -321,7 +685,7 @@ function findFinder(b) {
|
|
|
321
685
|
x = xx + int(x);
|
|
322
686
|
// Diagonal
|
|
323
687
|
const dRuns = FINDER.runs();
|
|
324
|
-
if (!FINDER.check(
|
|
688
|
+
if (!FINDER.check(bm, dRuns, { x: int(x), y: int(y) }, { x: 1, y: 1 }))
|
|
325
689
|
return false;
|
|
326
690
|
if (!checkRuns(dRuns, PATTERN_VARIANCE_DIAGONAL))
|
|
327
691
|
return false;
|
|
@@ -330,10 +694,10 @@ function findFinder(b) {
|
|
|
330
694
|
}
|
|
331
695
|
let skipped = false;
|
|
332
696
|
// Start with high skip lines count until we find first pattern
|
|
333
|
-
let ySkip = cap(int((3 *
|
|
697
|
+
let ySkip = cap(int((3 * bm.height) / (4 * 97)), DETECT_MIN_ROW_SKIP);
|
|
334
698
|
let done = false;
|
|
335
|
-
for (let y = ySkip - 1; y <
|
|
336
|
-
FINDER.scanLine(
|
|
699
|
+
for (let y = ySkip - 1; y < bm.height && !done; y += ySkip) {
|
|
700
|
+
FINDER.scanLine(bm, y, 0, bm.width, (runs, x) => {
|
|
337
701
|
if (!check(runs, y, x))
|
|
338
702
|
return;
|
|
339
703
|
// Found pattern
|
|
@@ -379,7 +743,7 @@ function findFinder(b) {
|
|
|
379
743
|
if (flen < 3)
|
|
380
744
|
throw new Error(`Finder: len(found) = ${flen}`);
|
|
381
745
|
found.sort((i, j) => i.moduleSize - j.moduleSize);
|
|
382
|
-
const pBest = best();
|
|
746
|
+
const pBest = utils.best();
|
|
383
747
|
// Qubic complexity, but we stop search when we found 3 patterns, so not a problem
|
|
384
748
|
for (let i = 0; i < flen - 2; i++) {
|
|
385
749
|
const fi = found[i];
|
|
@@ -429,20 +793,25 @@ function findFinder(b) {
|
|
|
429
793
|
return { bl, tl, tr };
|
|
430
794
|
}
|
|
431
795
|
function findAlignment(b, est, allowanceFactor) {
|
|
796
|
+
const bm = b;
|
|
432
797
|
const { moduleSize } = est;
|
|
433
798
|
const allowance = int(allowanceFactor * moduleSize);
|
|
434
799
|
const leftX = cap(est.x - allowance, 0);
|
|
435
|
-
const rightX = cap(est.x + allowance, undefined,
|
|
800
|
+
const rightX = cap(est.x + allowance, undefined, bm.width - 1);
|
|
436
801
|
const x = rightX - leftX;
|
|
437
802
|
const topY = cap(est.y - allowance, 0);
|
|
438
|
-
const bottomY = cap(est.y + allowance, undefined,
|
|
803
|
+
const bottomY = cap(est.y + allowance, undefined, bm.height - 1);
|
|
439
804
|
const y = bottomY - topY;
|
|
440
805
|
if (x < moduleSize * 3 || y < moduleSize * 3)
|
|
441
806
|
throw new Error(`x = ${x}, y=${y} moduleSize = ${moduleSize}`);
|
|
442
807
|
const xStart = leftX;
|
|
443
808
|
const yStart = topY;
|
|
444
|
-
|
|
445
|
-
|
|
809
|
+
// ISO/IEC 18004:2024 §12 h)3 scans the alignment pattern's white-square
|
|
810
|
+
// outline from the provisional centre. `rightX` / `bottomY` are inclusive
|
|
811
|
+
// clipped image coordinates; convert them to exclusive scan bounds below so
|
|
812
|
+
// an exact 5x5 search window still includes its final row and column.
|
|
813
|
+
const width = rightX - leftX + 1;
|
|
814
|
+
const height = bottomY - topY + 1;
|
|
446
815
|
const found = [];
|
|
447
816
|
const xEnd = xStart + width;
|
|
448
817
|
const middleY = int(yStart + height / 2);
|
|
@@ -450,14 +819,14 @@ function findAlignment(b, est, allowanceFactor) {
|
|
|
450
819
|
const diff = int((yGen + 1) / 2);
|
|
451
820
|
const y = middleY + (yGen & 1 ? -diff : diff);
|
|
452
821
|
let res;
|
|
453
|
-
ALIGNMENT.scanLine(
|
|
822
|
+
ALIGNMENT.scanLine(bm, y, xStart, xEnd, (runs, x) => {
|
|
454
823
|
if (!ALIGNMENT.checkSize(runs, moduleSize))
|
|
455
824
|
return;
|
|
456
825
|
const total = sum(runs);
|
|
457
826
|
const xx = ALIGNMENT.toCenter(runs, x);
|
|
458
827
|
// Vertical
|
|
459
828
|
const rVert = ALIGNMENT.runs();
|
|
460
|
-
let v = ALIGNMENT.check(
|
|
829
|
+
let v = ALIGNMENT.check(bm, rVert, { x: int(xx), y }, { y: 1, x: 0 }, 2 * runs[1]);
|
|
461
830
|
if (v === false)
|
|
462
831
|
return;
|
|
463
832
|
v += y;
|
|
@@ -480,6 +849,7 @@ function findAlignment(b, est, allowanceFactor) {
|
|
|
480
849
|
throw new Error('Alignment pattern not found');
|
|
481
850
|
}
|
|
482
851
|
function _single(b, from, to) {
|
|
852
|
+
const bm = b;
|
|
483
853
|
// http://en.wikipedia.org/wiki/Bresenham's_line_algorithm
|
|
484
854
|
let steep = false;
|
|
485
855
|
let d = { x: Math.abs(to.x - from.x), y: Math.abs(to.y - from.y) };
|
|
@@ -498,8 +868,10 @@ function _single(b, from, to) {
|
|
|
498
868
|
let real = { x, y };
|
|
499
869
|
if (steep)
|
|
500
870
|
real = pointMirror(real);
|
|
501
|
-
//
|
|
502
|
-
|
|
871
|
+
// Starting from a dark finder center, walk until the ray crosses
|
|
872
|
+
// light -> dark -> light; `BWBRunLength()` mirrors this to recover the
|
|
873
|
+
// full finder width around the center point.
|
|
874
|
+
if ((runPos === 1) === !!bm.point(real)) {
|
|
503
875
|
if (runPos === 2)
|
|
504
876
|
return distance({ x, y }, from);
|
|
505
877
|
runPos++;
|
|
@@ -517,11 +889,12 @@ function _single(b, from, to) {
|
|
|
517
889
|
return NaN;
|
|
518
890
|
}
|
|
519
891
|
function BWBRunLength(b, from, to) {
|
|
520
|
-
|
|
892
|
+
const bm = b;
|
|
893
|
+
let result = _single(bm, from, to);
|
|
521
894
|
let scaleY = 1.0;
|
|
522
895
|
const { x: fx, y: fy } = from;
|
|
523
896
|
let otherToX = fx - (to.x - fx);
|
|
524
|
-
const bw =
|
|
897
|
+
const bw = bm.width;
|
|
525
898
|
if (otherToX < 0) {
|
|
526
899
|
scaleY = fx / (fx - otherToX);
|
|
527
900
|
otherToX = 0;
|
|
@@ -530,9 +903,15 @@ function BWBRunLength(b, from, to) {
|
|
|
530
903
|
scaleY = (bw - 1 - fx) / (otherToX - fx);
|
|
531
904
|
otherToX = bw - 1;
|
|
532
905
|
}
|
|
906
|
+
// ISO/IEC 18004:2024 §12 b) uses finder runs in the 1:1:3:1:1 ratio,
|
|
907
|
+
// and §12 h)1 derives module size from finder pattern width. Clipping the
|
|
908
|
+
// reflected ray before `int()` would avoid `>>> 0` wrapping negative near-edge
|
|
909
|
+
// coordinates to a huge uint32 and clamping to the wrong edge. Policy: keep
|
|
910
|
+
// the existing heuristic because the direct fix degraded decode performance
|
|
911
|
+
// on current vectors (BoofCV sweep 134/485 -> 132/485).
|
|
533
912
|
let otherToY = int(fy - (to.y - fy) * scaleY);
|
|
534
913
|
let scaleX = 1.0;
|
|
535
|
-
const bh =
|
|
914
|
+
const bh = bm.height;
|
|
536
915
|
if (otherToY < 0) {
|
|
537
916
|
scaleX = fy / (fy - otherToY);
|
|
538
917
|
otherToY = 0;
|
|
@@ -542,12 +921,16 @@ function BWBRunLength(b, from, to) {
|
|
|
542
921
|
otherToY = bh - 1;
|
|
543
922
|
}
|
|
544
923
|
otherToX = int(fx + (otherToX - fx) * scaleX);
|
|
545
|
-
result += _single(
|
|
924
|
+
result += _single(bm, from, { x: otherToX, y: otherToY });
|
|
925
|
+
// Both mirrored rays include the center module once, so drop one module
|
|
926
|
+
// after summing them into the full finder-width estimate.
|
|
546
927
|
return result - 1.0;
|
|
547
928
|
}
|
|
548
929
|
function moduleSizeAvg(b, p1, p2) {
|
|
549
930
|
const est1 = BWBRunLength(b, pointInt(p1), pointInt(p2));
|
|
550
931
|
const est2 = BWBRunLength(b, pointInt(p2), pointInt(p1));
|
|
932
|
+
// One ray can fail near image edges, so keep the surviving estimate
|
|
933
|
+
// instead of discarding the finder-width measurement outright.
|
|
551
934
|
if (Number.isNaN(est1))
|
|
552
935
|
return est2 / FINDER.totalSize;
|
|
553
936
|
if (Number.isNaN(est2))
|
|
@@ -555,27 +938,34 @@ function moduleSizeAvg(b, p1, p2) {
|
|
|
555
938
|
return (est1 + est2) / (2 * FINDER.totalSize);
|
|
556
939
|
}
|
|
557
940
|
function detect(b) {
|
|
941
|
+
const bm = b;
|
|
558
942
|
let bl, tl, tr;
|
|
559
943
|
try {
|
|
560
|
-
({ bl, tl, tr } = findFinder(
|
|
944
|
+
({ bl, tl, tr } = findFinder(bm));
|
|
561
945
|
}
|
|
562
946
|
catch (e) {
|
|
563
947
|
try {
|
|
564
|
-
b
|
|
565
|
-
|
|
948
|
+
// ISO/IEC 18004:2024 §12 b)5 says to "reverse the colouring of the
|
|
949
|
+
// light and dark pixels" for reflectance reversal. `detect()` works on
|
|
950
|
+
// the `decodeQR()`-owned scratch bitmap, so keep the retry in-place for
|
|
951
|
+
// performance instead of cloning before this private/test-only helper.
|
|
952
|
+
bm.negate();
|
|
953
|
+
({ bl, tl, tr } = findFinder(bm));
|
|
566
954
|
}
|
|
567
955
|
catch (e) {
|
|
568
|
-
|
|
956
|
+
bm.negate(); // undo negate
|
|
569
957
|
throw e;
|
|
570
958
|
}
|
|
571
959
|
}
|
|
572
|
-
const moduleSize = (moduleSizeAvg(
|
|
960
|
+
const moduleSize = (moduleSizeAvg(bm, tl, tr) + moduleSizeAvg(bm, tl, bl)) / 2;
|
|
573
961
|
if (moduleSize < 1.0)
|
|
574
962
|
throw new Error(`invalid moduleSize = ${moduleSize}`);
|
|
575
963
|
// Estimate size
|
|
576
964
|
const tltr = int(distance(tl, tr) / moduleSize + 0.5);
|
|
577
965
|
const tlbl = int(distance(tl, bl) / moduleSize + 0.5);
|
|
578
966
|
let size = int((tltr + tlbl) / 2 + 7);
|
|
967
|
+
// QR side lengths are 21 + 4 * (version - 1), so normalize the estimate
|
|
968
|
+
// to the nearest size that is 1 modulo 4 before decoding the version.
|
|
579
969
|
const rem = size % 4;
|
|
580
970
|
if (rem === 0)
|
|
581
971
|
size++; // -> 1
|
|
@@ -583,13 +973,13 @@ function detect(b) {
|
|
|
583
973
|
size--; // -> 1
|
|
584
974
|
else if (rem === 3)
|
|
585
975
|
size -= 2;
|
|
586
|
-
const version = info.size.decode(size);
|
|
587
|
-
validateVersion(version);
|
|
976
|
+
const version = utils.info.size.decode(size);
|
|
977
|
+
utils.validateVersion(version);
|
|
588
978
|
let alignmentPattern;
|
|
589
|
-
if (info.alignmentPatterns(version).length > 0) {
|
|
979
|
+
if (utils.info.alignmentPatterns(version).length > 0) {
|
|
590
980
|
// Bottom right estimate
|
|
591
981
|
const br = { x: tr.x - tl.x + bl.x, y: tr.y - tl.y + bl.y };
|
|
592
|
-
const c = 1.0 - 3.0 / (info.size.encode(version) - 7);
|
|
982
|
+
const c = 1.0 - 3.0 / (utils.info.size.encode(version) - 7);
|
|
593
983
|
// Estimated alignment pattern position
|
|
594
984
|
const est = {
|
|
595
985
|
x: int(tl.x + c * (br.x - tl.x)),
|
|
@@ -599,7 +989,7 @@ function detect(b) {
|
|
|
599
989
|
};
|
|
600
990
|
for (let i = 4; i <= 16; i <<= 1) {
|
|
601
991
|
try {
|
|
602
|
-
alignmentPattern = findAlignment(
|
|
992
|
+
alignmentPattern = findAlignment(bm, est, i);
|
|
603
993
|
break;
|
|
604
994
|
}
|
|
605
995
|
catch (e) { }
|
|
@@ -619,13 +1009,15 @@ function detect(b) {
|
|
|
619
1009
|
toBR = { x: size - 3.5, y: size - 3.5 };
|
|
620
1010
|
}
|
|
621
1011
|
const from = [tl, tr, br, bl];
|
|
622
|
-
const bits = transform(
|
|
1012
|
+
const bits = transform(bm, size, from, [toTL, toTR, toBR, toBL]);
|
|
623
1013
|
return { bits: bits, points: from };
|
|
624
1014
|
}
|
|
625
1015
|
// Perspective transform by 4 points
|
|
626
1016
|
function squareToQuadrilateral(p) {
|
|
627
1017
|
const d3 = { x: p[0].x - p[1].x + p[2].x - p[3].x, y: p[0].y - p[1].y + p[2].y - p[3].y };
|
|
628
1018
|
if (d3.x === 0.0 && d3.y === 0.0) {
|
|
1019
|
+
// Parallelogram fast path: perspective terms vanish, so the homography
|
|
1020
|
+
// reduces to an affine transform.
|
|
629
1021
|
return [
|
|
630
1022
|
[p[1].x - p[0].x, p[2].x - p[1].x, p[0].x],
|
|
631
1023
|
[p[1].y - p[0].y, p[2].y - p[1].y, p[0].y],
|
|
@@ -647,9 +1039,12 @@ function squareToQuadrilateral(p) {
|
|
|
647
1039
|
}
|
|
648
1040
|
// Transform quadrilateral to square by 4 points
|
|
649
1041
|
function transform(b, size, from, to) {
|
|
1042
|
+
const bm = b;
|
|
650
1043
|
// TODO: check
|
|
651
1044
|
// https://math.stackexchange.com/questions/13404/mapping-irregular-quadrilateral-to-a-rectangle
|
|
652
1045
|
const p = squareToQuadrilateral(to);
|
|
1046
|
+
// Homographies are scale-invariant, so the adjugate is enough here;
|
|
1047
|
+
// there is no need to divide by the determinant when inverting `p`.
|
|
653
1048
|
const qToS = [
|
|
654
1049
|
[
|
|
655
1050
|
p[1][1] * p[2][2] - p[2][1] * p[1][2],
|
|
@@ -670,7 +1065,7 @@ function transform(b, size, from, to) {
|
|
|
670
1065
|
const sToQ = squareToQuadrilateral(from);
|
|
671
1066
|
const transform = sToQ.map((i) => i.map((_, qx) => i.reduce((acc, v, j) => acc + v * qToS[j][qx], 0)));
|
|
672
1067
|
const res = new Bitmap(size);
|
|
673
|
-
const points = fillArr(2 * size, 0);
|
|
1068
|
+
const points = utils.fillArr(2 * size, 0);
|
|
674
1069
|
const pointsLength = points.length;
|
|
675
1070
|
for (let y = 0; y < size; y++) {
|
|
676
1071
|
const p = transform;
|
|
@@ -678,13 +1073,15 @@ function transform(b, size, from, to) {
|
|
|
678
1073
|
const x = i / 2 + 0.5;
|
|
679
1074
|
const y2 = y + 0.5;
|
|
680
1075
|
const den = p[2][0] * x + p[2][1] * y2 + p[2][2];
|
|
681
|
-
|
|
682
|
-
|
|
1076
|
+
// ISO/IEC 18004:2024 §12 h) maps sampling grid intersections back to
|
|
1077
|
+
// image coordinates before deciding dark/light state. Clip projected
|
|
1078
|
+
// coordinates before `int()` because `>>> 0` wraps negative samples to a
|
|
1079
|
+
// huge uint32 and would clamp them to the far image edge.
|
|
1080
|
+
points[i] = int(cap((p[0][0] * x + p[0][1] * y2 + p[0][2]) / den, 0, bm.width - 1));
|
|
1081
|
+
points[i + 1] = int(cap((p[1][0] * x + p[1][1] * y2 + p[1][2]) / den, 0, bm.height - 1));
|
|
683
1082
|
}
|
|
684
1083
|
for (let i = 0; i < pointsLength; i += 2) {
|
|
685
|
-
|
|
686
|
-
const py = cap(points[i + 1], 0, b.height - 1);
|
|
687
|
-
if (b.get(px, py))
|
|
1084
|
+
if (bm.get(points[i], points[i + 1]))
|
|
688
1085
|
res.set((i / 2) | 0, y, true);
|
|
689
1086
|
}
|
|
690
1087
|
}
|
|
@@ -693,8 +1090,11 @@ function transform(b, size, from, to) {
|
|
|
693
1090
|
// Same as in drawTemplate, but reading
|
|
694
1091
|
// TODO: merge in CoderType?
|
|
695
1092
|
function readInfoBits(b) {
|
|
696
|
-
const
|
|
697
|
-
|
|
1093
|
+
const bm = b;
|
|
1094
|
+
// Walk each reserved copy from the highest-numbered module back to module 0
|
|
1095
|
+
// so the shift accumulator rebuilds the canonical bit string from MSB to LSB.
|
|
1096
|
+
const readBit = (x, y, out) => (out << 1) | (bm.get(x, y) ? 1 : 0);
|
|
1097
|
+
const size = bm.height;
|
|
698
1098
|
// Version information
|
|
699
1099
|
let version1 = 0;
|
|
700
1100
|
for (let y = 5; y >= 0; y--)
|
|
@@ -721,56 +1121,62 @@ function readInfoBits(b) {
|
|
|
721
1121
|
return { version1, version2, format1, format2 };
|
|
722
1122
|
}
|
|
723
1123
|
function parseInfo(b) {
|
|
1124
|
+
const bm = b;
|
|
724
1125
|
// Population count over xor -> hamming distance
|
|
725
|
-
const size =
|
|
726
|
-
const { version1, version2, format1, format2 } = readInfoBits(
|
|
1126
|
+
const size = bm.height;
|
|
1127
|
+
const { version1, version2, format1, format2 } = readInfoBits(bm);
|
|
727
1128
|
// Guess format
|
|
728
1129
|
let format;
|
|
729
|
-
const bestFormat = best();
|
|
1130
|
+
const bestFormat = utils.best();
|
|
730
1131
|
for (const ecc of ['medium', 'low', 'high', 'quartile']) {
|
|
731
1132
|
for (let mask = 0; mask < 8; mask++) {
|
|
732
|
-
const bits = info.formatBits(ecc, mask);
|
|
1133
|
+
const bits = utils.info.formatBits(ecc, mask);
|
|
733
1134
|
const cur = { ecc, mask: mask };
|
|
734
1135
|
if (bits === format1 || bits === format2) {
|
|
735
1136
|
format = cur;
|
|
736
1137
|
break;
|
|
737
1138
|
}
|
|
738
|
-
bestFormat.add(popcnt(format1 ^ bits), cur);
|
|
1139
|
+
bestFormat.add(utils.popcnt(format1 ^ bits), cur);
|
|
739
1140
|
if (format1 !== format2)
|
|
740
|
-
bestFormat.add(popcnt(format2 ^ bits), cur);
|
|
1141
|
+
bestFormat.add(utils.popcnt(format2 ^ bits), cur);
|
|
741
1142
|
}
|
|
742
1143
|
}
|
|
743
1144
|
if (format === undefined && bestFormat.score() <= MAX_BITS_ERROR)
|
|
744
1145
|
format = bestFormat.get();
|
|
745
1146
|
if (format === undefined)
|
|
746
1147
|
throw new Error('invalid format pattern');
|
|
747
|
-
let version = info.size.decode(size); // Guess version based on bitmap size
|
|
1148
|
+
let version = utils.info.size.decode(size); // Guess version based on bitmap size
|
|
1149
|
+
// Versions 1-6 do not carry version-information words, so side length is
|
|
1150
|
+
// the only authoritative version source until version 7 adds those fields.
|
|
748
1151
|
if (version < 7)
|
|
749
|
-
validateVersion(version);
|
|
1152
|
+
utils.validateVersion(version);
|
|
750
1153
|
else {
|
|
751
1154
|
version = undefined;
|
|
752
1155
|
// Guess version
|
|
753
|
-
const bestVer = best();
|
|
1156
|
+
const bestVer = utils.best();
|
|
754
1157
|
for (let ver = 7; ver <= 40; ver++) {
|
|
755
|
-
const bits = info.versionBits(ver);
|
|
1158
|
+
const bits = utils.info.versionBits(ver);
|
|
756
1159
|
if (bits === version1 || bits === version2) {
|
|
757
1160
|
version = ver;
|
|
758
1161
|
break;
|
|
759
1162
|
}
|
|
760
|
-
bestVer.add(popcnt(version1 ^ bits), ver);
|
|
1163
|
+
bestVer.add(utils.popcnt(version1 ^ bits), ver);
|
|
761
1164
|
if (version1 !== version2)
|
|
762
|
-
bestVer.add(popcnt(version2 ^ bits), ver);
|
|
1165
|
+
bestVer.add(utils.popcnt(version2 ^ bits), ver);
|
|
763
1166
|
}
|
|
764
1167
|
if (version === undefined && bestVer.score() <= MAX_BITS_ERROR)
|
|
765
1168
|
version = bestVer.get();
|
|
766
1169
|
if (version === undefined)
|
|
767
1170
|
throw new Error('invalid version pattern');
|
|
768
|
-
if (info.size.encode(version) !== size)
|
|
1171
|
+
if (utils.info.size.encode(version) !== size)
|
|
769
1172
|
throw new Error('invalid version size');
|
|
770
1173
|
}
|
|
771
1174
|
return { version, ...format };
|
|
772
1175
|
}
|
|
773
|
-
//
|
|
1176
|
+
// ISO/IEC 18004:2024 §7.4.3.2 says each ECI is a "6-digit assignment
|
|
1177
|
+
// number"; §7.4.3.4 says invoked ECIs apply until "a change of ECI".
|
|
1178
|
+
// Decode through platform TextDecoder only: unsupported labels may throw on
|
|
1179
|
+
// some runtimes, and callers needing them can provide a custom textDecoder.
|
|
774
1180
|
const eciToEncoding = {
|
|
775
1181
|
1: 'iso-8859-1',
|
|
776
1182
|
2: 'ibm437',
|
|
@@ -800,26 +1206,30 @@ const eciToEncoding = {
|
|
|
800
1206
|
30: 'euc-kr',
|
|
801
1207
|
};
|
|
802
1208
|
function decodeWithEci(bytes, eci = 26) {
|
|
1209
|
+
// ISO/IEC 18004:2024 §7.3.2 says QR's "default interpretation" is
|
|
1210
|
+
// "ECI 000003 representing the ISO/IEC 8859-1 character set". Keep UTF-8
|
|
1211
|
+
// here so this library's UTF-8 byte-mode encoder round-trips without ECI.
|
|
803
1212
|
const encoding = eciToEncoding[eci];
|
|
804
1213
|
if (!encoding)
|
|
805
1214
|
throw new Error(`Unsupported ECI: ${eci}`);
|
|
806
1215
|
return new TextDecoder(encoding).decode(bytes);
|
|
807
1216
|
}
|
|
808
1217
|
function decodeBitmap(b, decoder = decodeWithEci) {
|
|
809
|
-
const
|
|
810
|
-
|
|
1218
|
+
const bm = b;
|
|
1219
|
+
const size = bm.height;
|
|
1220
|
+
if (size < 21 || (size & 0b11) !== 1 || size !== bm.width)
|
|
811
1221
|
throw new Error(`decode: invalid size=${size}`);
|
|
812
|
-
const { version, mask, ecc } = parseInfo(
|
|
813
|
-
const tpl = drawTemplate(version, ecc, mask);
|
|
814
|
-
const { total } = info.capacity(version, ecc);
|
|
1222
|
+
const { version, mask, ecc } = parseInfo(bm);
|
|
1223
|
+
const tpl = utils.drawTemplate(version, ecc, mask);
|
|
1224
|
+
const { total } = utils.info.capacity(version, ecc);
|
|
815
1225
|
const bytes = new Uint8Array(total);
|
|
816
1226
|
let pos = 0;
|
|
817
1227
|
let buf = 0;
|
|
818
1228
|
let bitPos = 0;
|
|
819
|
-
zigzag(tpl, mask, (x, y, m) => {
|
|
1229
|
+
utils.zigzag(tpl, mask, (x, y, m) => {
|
|
820
1230
|
bitPos++;
|
|
821
1231
|
buf <<= 1;
|
|
822
|
-
buf |= +(!!
|
|
1232
|
+
buf |= +(!!bm.get(x, y) !== m);
|
|
823
1233
|
if (bitPos !== 8)
|
|
824
1234
|
return;
|
|
825
1235
|
bytes[pos++] = buf;
|
|
@@ -828,8 +1238,8 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
828
1238
|
});
|
|
829
1239
|
if (pos !== total)
|
|
830
1240
|
throw new Error(`decode: pos=${pos}, total=${total}`);
|
|
831
|
-
let bits = Array.from(interleave(version, ecc).decode(bytes))
|
|
832
|
-
.map((i) => bin(i, 8))
|
|
1241
|
+
let bits = Array.from(utils.interleave(version, ecc).decode(bytes))
|
|
1242
|
+
.map((i) => utils.bin(i, 8))
|
|
833
1243
|
.join('');
|
|
834
1244
|
// Reverse operation of index.ts/encode working on bits
|
|
835
1245
|
const readBits = (n) => {
|
|
@@ -850,6 +1260,9 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
850
1260
|
'1000': 'kanji',
|
|
851
1261
|
};
|
|
852
1262
|
let res = '';
|
|
1263
|
+
// ISO/IEC 18004:2024 §7.3.2 defines no-ECI byte data as ECI 000003
|
|
1264
|
+
// / ISO-8859-1. Keep ECI 26 for compatibility with legacy no-ECI UTF-8
|
|
1265
|
+
// payloads and with encodeQR's UTF-8 byte-mode default.
|
|
853
1266
|
let eci = 26; // Default to utf-8 for compat with old behavior
|
|
854
1267
|
while (true) {
|
|
855
1268
|
if (bits.length < 4)
|
|
@@ -860,7 +1273,7 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
860
1273
|
throw new Error(`Unknown modeBits=${modeBits} res="${res}"`);
|
|
861
1274
|
if (mode === 'terminator')
|
|
862
1275
|
break;
|
|
863
|
-
const countBits = info.lengthBits(version, mode);
|
|
1276
|
+
const countBits = utils.info.lengthBits(version, mode);
|
|
864
1277
|
let count = toNum(readBits(countBits));
|
|
865
1278
|
if (mode === 'numeric') {
|
|
866
1279
|
while (count >= 3) {
|
|
@@ -886,11 +1299,11 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
886
1299
|
else if (mode === 'alphanumeric') {
|
|
887
1300
|
while (count >= 2) {
|
|
888
1301
|
const v = toNum(readBits(11));
|
|
889
|
-
res += info.alphabet.alphanumerc.encode([Math.floor(v / 45), v % 45]).join('');
|
|
1302
|
+
res += utils.info.alphabet.alphanumerc.encode([Math.floor(v / 45), v % 45]).join('');
|
|
890
1303
|
count -= 2;
|
|
891
1304
|
}
|
|
892
1305
|
if (count === 1)
|
|
893
|
-
res += info.alphabet.alphanumerc.encode([toNum(readBits(6))]).join('');
|
|
1306
|
+
res += utils.info.alphabet.alphanumerc.encode([toNum(readBits(6))]).join('');
|
|
894
1307
|
}
|
|
895
1308
|
else if (mode === 'eci') {
|
|
896
1309
|
const first = toNum(readBits(8));
|
|
@@ -908,15 +1321,24 @@ function decodeBitmap(b, decoder = decodeWithEci) {
|
|
|
908
1321
|
data[i] = toNum(readBits(8));
|
|
909
1322
|
res += decoder(data, eci);
|
|
910
1323
|
}
|
|
1324
|
+
else if (mode === 'kanji') {
|
|
1325
|
+
// ISO/IEC 18004:2024 §7.3.6 says Kanji mode uses Shift JIS and
|
|
1326
|
+
// "Each two-byte character value is compacted to a 13-bit binary
|
|
1327
|
+
// codeword." Keep it unsupported until a real interop fixture can verify
|
|
1328
|
+
// the 13-bit-to-Shift-JIS mapping rather than adding untested decoding.
|
|
1329
|
+
throw new Error('Kanji mode is not supported');
|
|
1330
|
+
}
|
|
911
1331
|
else
|
|
912
1332
|
throw new Error(`Unknown mode=${mode}`);
|
|
913
1333
|
}
|
|
914
1334
|
return res;
|
|
915
1335
|
}
|
|
916
|
-
//
|
|
1336
|
+
// Center-crop to a square and return the original-image offset so
|
|
1337
|
+
// `decodeQR()` can map detected points back into caller coordinates.
|
|
917
1338
|
function cropToSquare(img) {
|
|
918
|
-
const
|
|
919
|
-
const
|
|
1339
|
+
const image = img;
|
|
1340
|
+
const data = Array.isArray(image.data) ? new Uint8Array(image.data) : image.data;
|
|
1341
|
+
const { height, width } = image;
|
|
920
1342
|
const squareSize = Math.min(height, width);
|
|
921
1343
|
const offset = {
|
|
922
1344
|
x: Math.floor((width - squareSize) / 2),
|
|
@@ -932,45 +1354,93 @@ function cropToSquare(img) {
|
|
|
932
1354
|
}
|
|
933
1355
|
return { offset, img: { height: squareSize, width: squareSize, data: croppedData } };
|
|
934
1356
|
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Decode text from a QR image.
|
|
1359
|
+
* @param img - RGB or RGBA image data that contains a QR code.
|
|
1360
|
+
* @param opts - Decoder hooks and image preprocessing options. See {@link DecodeOpts}.
|
|
1361
|
+
* @returns Decoded QR payload as a string.
|
|
1362
|
+
* @throws If the image, decoder options, or QR contents are invalid. {@link Error}
|
|
1363
|
+
* @example
|
|
1364
|
+
* Decode text from a QR image.
|
|
1365
|
+
* ```ts
|
|
1366
|
+
* import encodeQR, { Bitmap } from 'qr';
|
|
1367
|
+
* import decodeQR from 'qr/decode.js';
|
|
1368
|
+
* const bits = encodeQR('Hello world', 'raw', { scale: 4 });
|
|
1369
|
+
* const bm = new Bitmap({ width: bits[0].length, height: bits.length }, bits);
|
|
1370
|
+
* const text = decodeQR(bm.toImage());
|
|
1371
|
+
* ```
|
|
1372
|
+
*/
|
|
935
1373
|
export function decodeQR(img, opts = {}) {
|
|
1374
|
+
let image = img;
|
|
1375
|
+
const options = opts;
|
|
936
1376
|
for (const field of ['height', 'width']) {
|
|
937
|
-
if (!Number.isSafeInteger(
|
|
938
|
-
throw new Error(`invalid img.${field}=${
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1377
|
+
if (!Number.isSafeInteger(image[field]) || image[field] <= 0)
|
|
1378
|
+
throw new Error(`invalid img.${field}=${image[field]} (${typeof image[field]})`);
|
|
1379
|
+
}
|
|
1380
|
+
const { data } = image;
|
|
1381
|
+
if (!Array.isArray(data) && !isBytes(data))
|
|
1382
|
+
throw new Error(`invalid image.data=${data} (${typeof data})`);
|
|
1383
|
+
if (options.cropToSquare !== undefined && typeof options.cropToSquare !== 'boolean')
|
|
1384
|
+
throw new Error(`invalid opts.cropToSquare=${options.cropToSquare}`);
|
|
1385
|
+
// Validate callbacks before decoding so payload mode does not decide whether
|
|
1386
|
+
// an invalid public option is accepted.
|
|
1387
|
+
for (const fn of [
|
|
1388
|
+
'textDecoder',
|
|
1389
|
+
'pointsOnDetect',
|
|
1390
|
+
'imageOnBitmap',
|
|
1391
|
+
'imageOnDetect',
|
|
1392
|
+
'imageOnResult',
|
|
1393
|
+
]) {
|
|
1394
|
+
if (options[fn] !== undefined && typeof options[fn] !== 'function')
|
|
1395
|
+
throw new Error(`invalid opts.${fn}=${options[fn]} (${typeof options[fn]})`);
|
|
949
1396
|
}
|
|
950
1397
|
let offset = { x: 0, y: 0 };
|
|
951
|
-
if (
|
|
952
|
-
({ img, offset } = cropToSquare(
|
|
953
|
-
const bmp = toBitmap(
|
|
954
|
-
if (
|
|
955
|
-
|
|
1398
|
+
if (options.cropToSquare)
|
|
1399
|
+
({ img: image, offset } = cropToSquare(image));
|
|
1400
|
+
const bmp = toBitmap(image);
|
|
1401
|
+
if (options.imageOnBitmap)
|
|
1402
|
+
options.imageOnBitmap(bmp.toImage());
|
|
956
1403
|
const { bits, points } = detect(bmp);
|
|
957
|
-
if (
|
|
1404
|
+
if (options.pointsOnDetect) {
|
|
1405
|
+
// Report finder points in the caller's original coordinate space after any center-crop.
|
|
958
1406
|
const p = points.map((i) => ({ ...i, ...pointAdd(i, offset) }));
|
|
959
|
-
|
|
1407
|
+
options.pointsOnDetect(p);
|
|
960
1408
|
}
|
|
961
|
-
if (
|
|
962
|
-
|
|
963
|
-
const res = decodeBitmap(bits,
|
|
964
|
-
if (
|
|
965
|
-
|
|
1409
|
+
if (options.imageOnDetect)
|
|
1410
|
+
options.imageOnDetect(bits.toImage());
|
|
1411
|
+
const res = decodeBitmap(bits, options.textDecoder);
|
|
1412
|
+
if (options.imageOnResult)
|
|
1413
|
+
options.imageOnResult(bits.toImage());
|
|
966
1414
|
return res;
|
|
967
1415
|
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Default export alias for {@link decodeQR}.
|
|
1418
|
+
* @param img - RGB or RGBA image data that contains a QR code.
|
|
1419
|
+
* @param opts - Decoder hooks and image preprocessing options. See {@link DecodeOpts}.
|
|
1420
|
+
* @returns Decoded QR payload as a string.
|
|
1421
|
+
* @throws If the image, decoder options, or QR contents are invalid. {@link Error}
|
|
1422
|
+
* @example
|
|
1423
|
+
* Decode a rendered QR image via the default decoder export.
|
|
1424
|
+
* ```ts
|
|
1425
|
+
* import encodeQR, { Bitmap } from 'qr';
|
|
1426
|
+
* import decodeQR from 'qr/decode.js';
|
|
1427
|
+
* const bits = encodeQR('Hello world', 'raw', { scale: 4 });
|
|
1428
|
+
* const bm = new Bitmap({ width: bits[0].length, height: bits.length }, bits);
|
|
1429
|
+
* decodeQR(bm.toImage());
|
|
1430
|
+
* ```
|
|
1431
|
+
*/
|
|
968
1432
|
export default decodeQR;
|
|
1433
|
+
// Additional private helpers for focused regression tests; separate from the
|
|
1434
|
+
// existing `_tests` object so its current keys remain stable.
|
|
1435
|
+
export const _TESTS = /* @__PURE__ */ Object.freeze({
|
|
1436
|
+
findAlignment: findAlignment,
|
|
1437
|
+
transform: transform,
|
|
1438
|
+
});
|
|
969
1439
|
// Unsafe API utils, exported only for tests
|
|
970
|
-
export const _tests = {
|
|
1440
|
+
export const _tests = /* @__PURE__ */ Object.freeze({
|
|
971
1441
|
toBitmap,
|
|
972
1442
|
decodeBitmap,
|
|
973
1443
|
findFinder,
|
|
974
1444
|
detect,
|
|
975
|
-
};
|
|
1445
|
+
});
|
|
976
1446
|
//# sourceMappingURL=decode.js.map
|