qr 0.5.4 → 0.5.5
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 +7 -7
- package/decode.d.ts +15 -1
- package/decode.d.ts.map +1 -1
- package/decode.js +394 -49
- package/decode.js.map +1 -1
- package/dom.d.ts +1 -0
- package/dom.d.ts.map +1 -1
- package/dom.js +15 -0
- package/dom.js.map +1 -1
- package/package.json +1 -1
- package/src/decode.ts +361 -53
- package/src/dom.ts +14 -0
package/README.md
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
Minimal 0-dependency QR code generator & reader.
|
|
4
4
|
|
|
5
5
|
- 🔒 Auditable, 0-dependency
|
|
6
|
+
- 🏎 Fast: [faster](#speed) than all JS implementations
|
|
7
|
+
- 🔍 Reliable: 100MB+ of extensive test vectors ensure correctness
|
|
6
8
|
- 🏞️ Encoding (generating) supports ASCII, term, gif, svg and png codes
|
|
7
9
|
- 📷 Decoding (reading) supports camera feed input, files and non-browser environments
|
|
8
|
-
-
|
|
9
|
-
- 🔍 Extensive tests ensure correctness: 100MB+ of vectors
|
|
10
|
-
- 🪶 16KB (gzipped) for encoding + decoding, 9KB for encoding
|
|
10
|
+
- 🪶 18KB (gzipped) for encoding + decoding, 9KB for encoding
|
|
11
11
|
|
|
12
12
|
Check out:
|
|
13
13
|
|
|
@@ -248,14 +248,14 @@ qrcode-generator x 2,909 ops/sec @ 343μs/op
|
|
|
248
248
|
nuintun x 3,470 ops/sec @ 288μs/op
|
|
249
249
|
|
|
250
250
|
# encode of large qr
|
|
251
|
-
@paulmillr/qr x
|
|
251
|
+
@paulmillr/qr x 334 ops/sec @ 2ms/op
|
|
252
252
|
qrcode-generator x 174 ops/sec @ 5ms/op
|
|
253
253
|
nuintun x 221 ops/sec @ 4ms/op
|
|
254
254
|
|
|
255
255
|
# decode
|
|
256
|
-
@paulmillr/qr x
|
|
257
|
-
jsqr x 50 ops/sec @ 19ms/op
|
|
258
|
-
nuintun x 49 ops/sec @ 20ms/op
|
|
256
|
+
@paulmillr/qr x 662 ops/sec @ 1ms/op
|
|
257
|
+
jsqr x 50 ops/sec @ 19ms/op
|
|
258
|
+
nuintun x 49 ops/sec @ 20ms/op
|
|
259
259
|
instascan x 128 ops/sec @ 7ms/op ± 31.44% (4ms..166ms)
|
|
260
260
|
```
|
|
261
261
|
|
package/decode.d.ts
CHANGED
|
@@ -27,7 +27,21 @@ import { Bitmap } from './index.ts';
|
|
|
27
27
|
export type FinderPoints = [Pattern, Pattern, Point, Pattern];
|
|
28
28
|
/**
|
|
29
29
|
* Convert to grayscale. The function is the most expensive part of decoding:
|
|
30
|
-
* it takes up to 90% of time.
|
|
30
|
+
* it takes up to 90% of time.
|
|
31
|
+
*
|
|
32
|
+
* Binarization pipeline:
|
|
33
|
+
* 1. Convert RGB/RGBA image to one luma byte per pixel.
|
|
34
|
+
* 2. Split the image into 8x8 blocks and collect per-block mean/min/max.
|
|
35
|
+
* 3. Build a 5x5 neighborhood mean over those block means.
|
|
36
|
+
* 4. Turn each 8x8 block into bitmap bits using a local cut derived from:
|
|
37
|
+
* - the neighborhood mean,
|
|
38
|
+
* - the current block statistics,
|
|
39
|
+
* - a cheap whole-image color-spread estimate,
|
|
40
|
+
* - and, on risky scenes, a local variance field over block means.
|
|
41
|
+
*
|
|
42
|
+
* Instead of producing "best looking" thresholding: we produce a
|
|
43
|
+
* bitmap where finder patterns survive perspective / blur / highlights while
|
|
44
|
+
* keeping false dark regions low enough for downstream finder selection.
|
|
31
45
|
*/
|
|
32
46
|
declare function toBitmap(img: Image): Bitmap;
|
|
33
47
|
type Pattern = Point & {
|
package/decode.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["src/decode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;EAeE;AACF;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAiC,KAAK,EAAQ,KAAK,EAAE,MAAM,YAAY,CAAC;AACpF,OAAO,EAAE,MAAM,EAAS,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["src/decode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;EAeE;AACF;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAiC,KAAK,EAAQ,KAAK,EAAE,MAAM,YAAY,CAAC;AACpF,OAAO,EAAE,MAAM,EAAS,MAAM,YAAY,CAAC;AA6B3C,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;AAoC9D;;;;;;;;;;;;;;;;;GAiBG;AACH,iBAAS,QAAQ,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CA2TpC;AAGD,KAAK,OAAO,GAAG,KAAK,GAAG;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AA8K7D,iBAAS,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,EAAE,OAAO,CAAC;IACZ,EAAE,EAAE,OAAO,CAAC;CACb,CA8HA;AAoHD,iBAAS,MAAM,CAAC,CAAC,EAAE,MAAM,GAAG;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,YAAY,CAAC;CACtB,CA6DA;AAqLD,iBAAS,YAAY,CACnB,CAAC,EAAE,MAAM,EACT,OAAO,GAAE,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,KAAK,MAAsB,GAClE,MAAM,CAuFR;AAED,MAAM,MAAM,UAAU,GAAG;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,MAAM,CAAC;IAC5C,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,CAAC;IAChD,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IACrC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;IACrC,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAC;CACtC,CAAC;AAsBF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,GAAE,UAAe,GAAG,MAAM,CA2BlE;AAED,eAAe,QAAQ,CAAC;AAGxB,eAAO,MAAM,MAAM,EAAE;IACnB,QAAQ,EAAE,OAAO,QAAQ,CAAC;IAC1B,YAAY,EAAE,OAAO,YAAY,CAAC;IAClC,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,MAAM,EAAE,OAAO,MAAM,CAAC;CAMvB,CAAC"}
|
package/decode.js
CHANGED
|
@@ -32,6 +32,18 @@ const PATTERN_VARIANCE = 2;
|
|
|
32
32
|
const PATTERN_VARIANCE_DIAGONAL = 1.333;
|
|
33
33
|
const PATTERN_MIN_CONFIRMATIONS = 2;
|
|
34
34
|
const DETECT_MIN_ROW_SKIP = 3;
|
|
35
|
+
// Pair LUTs for the 8x8 block-stat fast path: each 16-bit lane holds two
|
|
36
|
+
// brightness bytes, so we can accumulate sum/min/max four pixels at a time.
|
|
37
|
+
const SUM16 = new Uint16Array(1 << 16);
|
|
38
|
+
const MIN16 = new Uint8Array(1 << 16);
|
|
39
|
+
const MAX16 = new Uint8Array(1 << 16);
|
|
40
|
+
for (let i = 0; i < SUM16.length; i++) {
|
|
41
|
+
const lo = i & 0xff;
|
|
42
|
+
const hi = i >>> 8;
|
|
43
|
+
SUM16[i] = lo + hi;
|
|
44
|
+
MIN16[i] = lo < hi ? lo : hi;
|
|
45
|
+
MAX16[i] = lo > hi ? lo : hi;
|
|
46
|
+
}
|
|
35
47
|
// TODO: move to index, nearby with bitmap and other graph related stuff?
|
|
36
48
|
const int = (n) => n >>> 0;
|
|
37
49
|
// distance ^ 2
|
|
@@ -51,37 +63,135 @@ const pointMirror = (p) => ({ x: p.y, y: p.x });
|
|
|
51
63
|
const pointClone = (p) => ({ x: p.x, y: p.y });
|
|
52
64
|
const pointInt = (p) => ({ x: int(p.x), y: int(p.y) });
|
|
53
65
|
const pointAdd = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
|
|
66
|
+
// Count trailing zeroes in a packed bitmap word so scanLine can skip whole
|
|
67
|
+
// runs instead of testing one bit at a time.
|
|
68
|
+
const ctz32 = (v) => {
|
|
69
|
+
v = v >>> 0;
|
|
70
|
+
if (v === 0)
|
|
71
|
+
return 32;
|
|
72
|
+
return 31 - Math.clz32((v & -v) >>> 0);
|
|
73
|
+
};
|
|
54
74
|
function cap(value, min, max) {
|
|
55
75
|
return Math.max(Math.min(value, max || value), min || value);
|
|
56
76
|
}
|
|
57
|
-
|
|
77
|
+
function getBytesPerPixel(img) {
|
|
58
78
|
const perPixel = img.data.length / (img.width * img.height);
|
|
59
79
|
if (perPixel === 3 || perPixel === 4)
|
|
60
80
|
return perPixel; // RGB or RGBA
|
|
61
81
|
throw new Error(`Unknown image format, bytes per pixel=${perPixel}`);
|
|
62
|
-
}
|
|
82
|
+
}
|
|
83
|
+
function isBytes(data) {
|
|
84
|
+
return data instanceof Uint8Array || data instanceof Uint8ClampedArray;
|
|
85
|
+
}
|
|
63
86
|
/**
|
|
64
87
|
* Convert to grayscale. The function is the most expensive part of decoding:
|
|
65
|
-
* it takes up to 90% of time.
|
|
88
|
+
* it takes up to 90% of time.
|
|
89
|
+
*
|
|
90
|
+
* Binarization pipeline:
|
|
91
|
+
* 1. Convert RGB/RGBA image to one luma byte per pixel.
|
|
92
|
+
* 2. Split the image into 8x8 blocks and collect per-block mean/min/max.
|
|
93
|
+
* 3. Build a 5x5 neighborhood mean over those block means.
|
|
94
|
+
* 4. Turn each 8x8 block into bitmap bits using a local cut derived from:
|
|
95
|
+
* - the neighborhood mean,
|
|
96
|
+
* - the current block statistics,
|
|
97
|
+
* - a cheap whole-image color-spread estimate,
|
|
98
|
+
* - and, on risky scenes, a local variance field over block means.
|
|
99
|
+
*
|
|
100
|
+
* Instead of producing "best looking" thresholding: we produce a
|
|
101
|
+
* bitmap where finder patterns survive perspective / blur / highlights while
|
|
102
|
+
* keeping false dark regions low enough for downstream finder selection.
|
|
66
103
|
*/
|
|
67
104
|
function toBitmap(img) {
|
|
105
|
+
const width = img.width;
|
|
106
|
+
const height = img.height;
|
|
107
|
+
const data = img.data;
|
|
68
108
|
const bytesPerPixel = getBytesPerPixel(img);
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
109
|
+
const pixLen = height * width;
|
|
110
|
+
const brightness = new Uint8Array(pixLen);
|
|
111
|
+
if (bytesPerPixel === 4 && isBytes(data) && (data.byteOffset & 3) === 0) {
|
|
112
|
+
// Little-endian RGBA: compute four grayscale bytes and commit as one u32 store.
|
|
113
|
+
// Unaligned RGBA subarray views are still valid inputs; they fall back to
|
|
114
|
+
// the scalar path because Uint32Array would throw on a misaligned offset.
|
|
115
|
+
const pixels = new Uint32Array(data.buffer, data.byteOffset, pixLen);
|
|
116
|
+
const bright32 = new Uint32Array(brightness.buffer, brightness.byteOffset, brightness.length >>> 2);
|
|
117
|
+
const n4 = pixels.length & ~3;
|
|
118
|
+
for (let i = 0, j = 0; i < n4; i += 4, j++) {
|
|
119
|
+
const v0 = pixels[i] >>> 0;
|
|
120
|
+
const v1 = pixels[i + 1] >>> 0;
|
|
121
|
+
const v2 = pixels[i + 2] >>> 0;
|
|
122
|
+
const v3 = pixels[i + 3] >>> 0;
|
|
123
|
+
// RGBA words are little-endian here, so this is `(r + 2*g + b) / 4`
|
|
124
|
+
// computed from the packed byte lanes for four pixels at once.
|
|
125
|
+
const b0 = ((v0 & 0xff) + (((v0 >>> 8) & 0xff) << 1) + ((v0 >>> 16) & 0xff)) >>> 2;
|
|
126
|
+
const b1 = ((v1 & 0xff) + (((v1 >>> 8) & 0xff) << 1) + ((v1 >>> 16) & 0xff)) >>> 2;
|
|
127
|
+
const b2 = ((v2 & 0xff) + (((v2 >>> 8) & 0xff) << 1) + ((v2 >>> 16) & 0xff)) >>> 2;
|
|
128
|
+
const b3 = ((v3 & 0xff) + (((v3 >>> 8) & 0xff) << 1) + ((v3 >>> 16) & 0xff)) >>> 2;
|
|
129
|
+
bright32[j] = b0 | (b1 << 8) | (b2 << 16) | (b3 << 24);
|
|
130
|
+
}
|
|
131
|
+
for (let i = n4; i < pixels.length; i++) {
|
|
132
|
+
const v = pixels[i] >>> 0;
|
|
133
|
+
brightness[i] = ((v & 0xff) + (((v >>> 8) & 0xff) << 1) + ((v >>> 16) & 0xff)) >>> 2;
|
|
134
|
+
}
|
|
75
135
|
}
|
|
136
|
+
else {
|
|
137
|
+
for (let i = 0, j = 0, d = data; i < d.length; i += bytesPerPixel) {
|
|
138
|
+
const r = d[i];
|
|
139
|
+
const g = d[i + 1];
|
|
140
|
+
const b = d[i + 2];
|
|
141
|
+
brightness[j++] = int((r + 2 * g + b) / 4) & 0xff;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Sampled color spread is a cheap "scene type" signal:
|
|
145
|
+
// grayscale / flat lighting scenes want conservative cuts, while colorful or
|
|
146
|
+
// high-spread scenes benefit from a slightly darker threshold.
|
|
147
|
+
let spreadSum = 0;
|
|
148
|
+
let spreadCnt = 0;
|
|
149
|
+
const spreadStep = bytesPerPixel * 16;
|
|
150
|
+
for (let i = 0; i < data.length; i += spreadStep) {
|
|
151
|
+
const r = data[i];
|
|
152
|
+
const g = data[i + 1];
|
|
153
|
+
const b = data[i + 2];
|
|
154
|
+
// hi=max(r,g,b), lo=min(r,g,b): this sampled channel spread is a cheap
|
|
155
|
+
// scene-level proxy for "how colorful / highlighty is this frame?".
|
|
156
|
+
const hi = r > g ? (r > b ? r : b) : g > b ? g : b;
|
|
157
|
+
const lo = r < g ? (r < b ? r : b) : g < b ? g : b;
|
|
158
|
+
spreadSum += hi - lo;
|
|
159
|
+
spreadCnt++;
|
|
160
|
+
}
|
|
161
|
+
const spreadMean = spreadSum / spreadCnt;
|
|
76
162
|
// Convert to bitmap
|
|
77
163
|
const block = GRAYSCALE_BLOCK_SIZE;
|
|
78
|
-
if (
|
|
164
|
+
if (width < block * 5 || height < block * 5)
|
|
79
165
|
throw new Error('image too small');
|
|
80
|
-
const bWidth = Math.ceil(
|
|
81
|
-
const bHeight = Math.ceil(
|
|
82
|
-
const maxY =
|
|
83
|
-
const maxX =
|
|
84
|
-
const
|
|
166
|
+
const bWidth = Math.ceil(width / block);
|
|
167
|
+
const bHeight = Math.ceil(height / block);
|
|
168
|
+
const maxY = height - block;
|
|
169
|
+
const maxX = width - block;
|
|
170
|
+
const blockLen = bWidth * bHeight;
|
|
171
|
+
const blockState = new Uint32Array(blockLen);
|
|
172
|
+
// Each 8x8 block stores packed:
|
|
173
|
+
// - bits 0..7: block baseline brightness used by the threshold field
|
|
174
|
+
// - bits 8..15: block min
|
|
175
|
+
// - bits 16..23: block max
|
|
176
|
+
let hiRangeCnt = 0;
|
|
177
|
+
let veryLowCnt = 0;
|
|
178
|
+
const padW = (width + 3) & ~3;
|
|
179
|
+
let statStride = width;
|
|
180
|
+
let stat32;
|
|
181
|
+
if ((width & 3) !== 0) {
|
|
182
|
+
const padLen = padW * height;
|
|
183
|
+
const brightPad = new Uint8Array(padLen);
|
|
184
|
+
for (let y = 0; y < height; y++) {
|
|
185
|
+
const src = y * width;
|
|
186
|
+
const dst = y * padW;
|
|
187
|
+
brightPad.set(brightness.subarray(src, src + width), dst);
|
|
188
|
+
}
|
|
189
|
+
// Misaligned widths are padded only for the block-stat fast path.
|
|
190
|
+
statStride = padW;
|
|
191
|
+
stat32 = new Uint32Array(brightPad.buffer, brightPad.byteOffset, (padW * height) >>> 2);
|
|
192
|
+
}
|
|
193
|
+
else
|
|
194
|
+
stat32 = new Uint32Array(brightness.buffer, brightness.byteOffset, brightness.length >>> 2);
|
|
85
195
|
for (let y = 0; y < bHeight; y++) {
|
|
86
196
|
const yPos = cap(y * block, 0, maxY);
|
|
87
197
|
for (let x = 0; x < bWidth; x++) {
|
|
@@ -89,48 +199,248 @@ function toBitmap(img) {
|
|
|
89
199
|
let sum = 0;
|
|
90
200
|
let min = 0xff;
|
|
91
201
|
let max = 0;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
202
|
+
// The stat-LUT fast path needs the 8-pixel row start to be 32-bit aligned
|
|
203
|
+
// so each row can be read as two full u32 words without any shifts.
|
|
204
|
+
if ((xPos & 3) === 0) {
|
|
205
|
+
for (let yy = 0, pos = yPos * statStride + xPos; yy < block; yy++, pos += statStride) {
|
|
206
|
+
const p = pos >>> 2;
|
|
207
|
+
const w0 = stat32[p] >>> 0;
|
|
208
|
+
const w1 = stat32[p + 1] >>> 0;
|
|
209
|
+
const a0 = w0 & 0xffff;
|
|
210
|
+
const a1 = w0 >>> 16;
|
|
211
|
+
const b0 = w1 & 0xffff;
|
|
212
|
+
const b1 = w1 >>> 16;
|
|
213
|
+
sum += SUM16[a0] + SUM16[a1] + SUM16[b0] + SUM16[b1];
|
|
214
|
+
const min0 = MIN16[a0];
|
|
215
|
+
const min1 = MIN16[a1];
|
|
216
|
+
const min2 = MIN16[b0];
|
|
217
|
+
const min3 = MIN16[b1];
|
|
218
|
+
if (min0 < min)
|
|
219
|
+
min = min0;
|
|
220
|
+
if (min1 < min)
|
|
221
|
+
min = min1;
|
|
222
|
+
if (min2 < min)
|
|
223
|
+
min = min2;
|
|
224
|
+
if (min3 < min)
|
|
225
|
+
min = min3;
|
|
226
|
+
const max0 = MAX16[a0];
|
|
227
|
+
const max1 = MAX16[a1];
|
|
228
|
+
const max2 = MAX16[b0];
|
|
229
|
+
const max3 = MAX16[b1];
|
|
230
|
+
if (max0 > max)
|
|
231
|
+
max = max0;
|
|
232
|
+
if (max1 > max)
|
|
233
|
+
max = max1;
|
|
234
|
+
if (max2 > max)
|
|
235
|
+
max = max2;
|
|
236
|
+
if (max3 > max)
|
|
237
|
+
max = max3;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
for (let yy = 0, pos = yPos * width + xPos; yy < block; yy++, pos += width) {
|
|
242
|
+
for (let xx = 0; xx < block; xx++) {
|
|
243
|
+
const pixel = brightness[pos + xx];
|
|
244
|
+
sum += pixel;
|
|
245
|
+
if (pixel < min)
|
|
246
|
+
min = pixel;
|
|
247
|
+
if (pixel > max)
|
|
248
|
+
max = pixel;
|
|
249
|
+
}
|
|
98
250
|
}
|
|
99
251
|
}
|
|
252
|
+
const bIdx = bWidth * y + x;
|
|
253
|
+
const range = max - min;
|
|
100
254
|
// Average brightness of block
|
|
101
|
-
let average =
|
|
102
|
-
if (
|
|
255
|
+
let average = sum >>> 6;
|
|
256
|
+
if (range <= GRAYSCALE_RANGE) {
|
|
257
|
+
// Low-contrast blocks are unstable if we threshold from their raw mean.
|
|
258
|
+
// Bias toward the local dark floor, then smooth with already-seen
|
|
259
|
+
// neighbors so finder rings don't disappear in washed-out regions.
|
|
103
260
|
average = min / 2;
|
|
104
261
|
if (y > 0 && x > 0) {
|
|
105
262
|
const idx = (x, y) => y * bWidth + x;
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
263
|
+
const neighborNumerator = (blockState[idx(x, y - 1)] & 0xff) +
|
|
264
|
+
2 * (blockState[idx(x - 1, y)] & 0xff) +
|
|
265
|
+
(blockState[idx(x - 1, y - 1)] & 0xff);
|
|
266
|
+
if (min * 4 < neighborNumerator)
|
|
267
|
+
average = neighborNumerator / 4;
|
|
109
268
|
}
|
|
110
269
|
}
|
|
111
|
-
|
|
270
|
+
blockState[bIdx] = int(average) | (min << 8) | (max << 16);
|
|
271
|
+
if (range > 40 && average < 224)
|
|
272
|
+
hiRangeCnt++;
|
|
273
|
+
if (range <= 10)
|
|
274
|
+
veryLowCnt++;
|
|
112
275
|
}
|
|
113
276
|
}
|
|
114
|
-
const
|
|
277
|
+
const hiRangeFrac = hiRangeCnt / blockLen;
|
|
278
|
+
const veryLowFrac = veryLowCnt / blockLen;
|
|
279
|
+
// These two scene gates are the main "policy" layer on top of the local cut:
|
|
280
|
+
// - `spotBias` darkens globally flat, slightly colorful scenes that otherwise
|
|
281
|
+
// miss bright-spot / washed-out QR modules.
|
|
282
|
+
// - `useVarField` avoids paying the variance-field cost on scenes where the
|
|
283
|
+
// plain 5x5 mean is already stable enough.
|
|
284
|
+
const spotBias = veryLowFrac > 0.55 &&
|
|
285
|
+
veryLowFrac < 0.66 &&
|
|
286
|
+
hiRangeFrac < 0.02 &&
|
|
287
|
+
spreadMean > 10 &&
|
|
288
|
+
spreadMean < 20
|
|
289
|
+
? -1
|
|
290
|
+
: 0;
|
|
291
|
+
const useVarField = veryLowFrac < 0.62 || spreadMean > 30;
|
|
292
|
+
const iWidth = bWidth + 1;
|
|
293
|
+
const iHeight = bHeight + 1;
|
|
294
|
+
const integLen = iHeight * iWidth;
|
|
295
|
+
// `integ` is the standard summed-area table of block means.
|
|
296
|
+
const integ = new Uint32Array(integLen);
|
|
297
|
+
// `integSqr` is the square-integral / summed-area table of `v * v` over the
|
|
298
|
+
// same block means, not a u8 pixel buffer. Those prefix sums can overflow
|
|
299
|
+
// 32-bit integer storage on large images, and Float32 was the measured
|
|
300
|
+
// faster compromise vs Float64 for this heuristic field.
|
|
301
|
+
const integSqr = useVarField ? new Float32Array(integLen) : undefined;
|
|
302
|
+
for (let y = 0; y < bHeight; y++) {
|
|
303
|
+
let rowSum = 0;
|
|
304
|
+
let rowSq = 0;
|
|
305
|
+
const bRow = y * bWidth;
|
|
306
|
+
const iRow = (y + 1) * iWidth;
|
|
307
|
+
const iPrev = y * iWidth;
|
|
308
|
+
for (let x = 0; x < bWidth; x++) {
|
|
309
|
+
const v = blockState[bRow + x] & 0xff;
|
|
310
|
+
rowSum += v;
|
|
311
|
+
if (integSqr)
|
|
312
|
+
rowSq += v * v;
|
|
313
|
+
integ[iRow + x + 1] = integ[iPrev + x + 1] + rowSum;
|
|
314
|
+
if (integSqr)
|
|
315
|
+
integSqr[iRow + x + 1] = integSqr[iPrev + x + 1] + rowSq;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const matrix = new Bitmap({ width, height });
|
|
319
|
+
const rows = Math.ceil(width / 32);
|
|
320
|
+
// Decode intentionally writes the packed bitmap words directly here. The
|
|
321
|
+
// per-pixel Bitmap API is too expensive on this hot path, so this must stay
|
|
322
|
+
// in sync with Bitmap's internal `value` layout.
|
|
323
|
+
const bm = matrix.value;
|
|
324
|
+
const rad = 2;
|
|
325
|
+
const win = rad * 2 + 1;
|
|
326
|
+
const area = win * win;
|
|
115
327
|
for (let y = 0; y < bHeight; y++) {
|
|
116
328
|
const yPos = cap(y * block, 0, maxY);
|
|
117
|
-
const top = cap(y,
|
|
329
|
+
const top = cap(y, rad, bHeight - rad - 1);
|
|
330
|
+
const y0 = top - rad;
|
|
331
|
+
const y1 = top + rad;
|
|
332
|
+
const r0 = y0 * iWidth;
|
|
333
|
+
const r1 = (y1 + 1) * iWidth;
|
|
118
334
|
for (let x = 0; x < bWidth; x++) {
|
|
119
335
|
const xPos = cap(x * block, 0, maxX);
|
|
120
|
-
const
|
|
336
|
+
const shift = xPos & 31;
|
|
337
|
+
const col = xPos >>> 5;
|
|
338
|
+
const left = cap(x, rad, bWidth - rad - 1);
|
|
339
|
+
const x0 = left - rad;
|
|
340
|
+
const x1 = left + rad;
|
|
121
341
|
// 5x5 blocks average
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
342
|
+
const sum = integ[r1 + (x1 + 1)] - integ[r0 + (x1 + 1)] - integ[r1 + x0] + integ[r0 + x0];
|
|
343
|
+
// `average` is the coarse threshold surface: a 5x5 neighborhood mean of
|
|
344
|
+
// the 8x8 block means. The adjustments below decide when to move away
|
|
345
|
+
// from that surface for the current block.
|
|
346
|
+
const average = (sum / area) | 0;
|
|
347
|
+
let cut = average;
|
|
348
|
+
const bIdx = bWidth * y + x;
|
|
349
|
+
const blk = blockState[bIdx];
|
|
350
|
+
const blockAvg = blk & 0xff;
|
|
351
|
+
const min = (blk >>> 8) & 0xff;
|
|
352
|
+
const max = blk >>> 16;
|
|
353
|
+
const range = max - min;
|
|
354
|
+
if (average < min)
|
|
355
|
+
continue;
|
|
356
|
+
if (average >= max) {
|
|
357
|
+
const m = 0xff;
|
|
358
|
+
for (let yy = 0, row = yPos * rows + col; yy < block; yy++, row += rows) {
|
|
359
|
+
const lo = (m << shift) >>> 0;
|
|
360
|
+
bm[row] |= lo;
|
|
361
|
+
if (shift > 24)
|
|
362
|
+
bm[row + 1] |= m >>> (32 - shift);
|
|
133
363
|
}
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
// `localAdj`: nudge toward the current block when it is darker than
|
|
367
|
+
// its neighborhood. This helps preserve dark rings / modules that are
|
|
368
|
+
// locally meaningful but diluted by the 5x5 field.
|
|
369
|
+
let localAdj = (blockAvg - average) >> 4;
|
|
370
|
+
if (localAdj < 0)
|
|
371
|
+
localAdj = 0;
|
|
372
|
+
if (localAdj > 1)
|
|
373
|
+
localAdj = 1;
|
|
374
|
+
// `chromaAdj`: in colorful, mid-tone blocks, slight extra darkening
|
|
375
|
+
// helps where luma alone underestimates QR structure.
|
|
376
|
+
let chromaAdj = 0;
|
|
377
|
+
if (range > 6 && average > 48 && average < 232) {
|
|
378
|
+
const spreadBoost = spreadMean > 8 ? spreadMean - 8 : 0;
|
|
379
|
+
const mid = 128 - Math.abs(average - 128);
|
|
380
|
+
chromaAdj = int((spreadBoost * (range - 6) * mid) / 2200000);
|
|
381
|
+
if (chromaAdj > 1)
|
|
382
|
+
chromaAdj = 1;
|
|
383
|
+
}
|
|
384
|
+
// `varAdj`: if the surrounding block field has real variance, darken
|
|
385
|
+
// more aggressively when the local mean still sits far above the
|
|
386
|
+
// block minimum. This is what rescues many weak finder cases.
|
|
387
|
+
let varAdj = 0;
|
|
388
|
+
if (integSqr && range >= 6 && range <= 128) {
|
|
389
|
+
const sq = integSqr[r1 + (x1 + 1)] - integSqr[r0 + (x1 + 1)] - integSqr[r1 + x0] + integSqr[r0 + x0];
|
|
390
|
+
const meanSq = sq / area;
|
|
391
|
+
let variance = meanSq - average * average;
|
|
392
|
+
if (variance < 0)
|
|
393
|
+
variance = 0;
|
|
394
|
+
const gap = average - min;
|
|
395
|
+
const num = gap * (variance - 196);
|
|
396
|
+
const den = (variance + 832) * 9;
|
|
397
|
+
// `variance` is clamped non-negative, so `den` stays strictly positive.
|
|
398
|
+
varAdj = int(num / den);
|
|
399
|
+
if (varAdj < -1)
|
|
400
|
+
varAdj = -1;
|
|
401
|
+
if (varAdj > 4)
|
|
402
|
+
varAdj = 4;
|
|
403
|
+
}
|
|
404
|
+
cut = average + localAdj + chromaAdj + varAdj;
|
|
405
|
+
// Small scene-level nudges are intentionally separate from the three
|
|
406
|
+
// local terms above: they are cheap and only target known whole-scene
|
|
407
|
+
// failure modes such as washed-out bright-spot images.
|
|
408
|
+
if (spreadMean > 10 && range >= 8 && range <= 96 && average > min + 8 && average < 192)
|
|
409
|
+
cut++;
|
|
410
|
+
if (veryLowFrac > 0.68 && veryLowFrac < 0.86 && range >= 6 && range <= 20 && average < 196)
|
|
411
|
+
cut++;
|
|
412
|
+
cut += spotBias;
|
|
413
|
+
if (cut < min)
|
|
414
|
+
cut = min;
|
|
415
|
+
if (cut > max)
|
|
416
|
+
cut = max;
|
|
417
|
+
// Emit one 8-pixel row of the current 8x8 block: compare against the
|
|
418
|
+
// block cut, pack the 8 black/white decisions into one byte, then OR
|
|
419
|
+
// that byte into the bitmap word(s) at the current x-bit offset.
|
|
420
|
+
for (let yy = 0, pos = yPos * width + xPos, row = yPos * rows + col; yy < block; yy++, pos += width, row += rows) {
|
|
421
|
+
let m = 0;
|
|
422
|
+
if (brightness[pos] <= cut)
|
|
423
|
+
m |= 1;
|
|
424
|
+
if (brightness[pos + 1] <= cut)
|
|
425
|
+
m |= 2;
|
|
426
|
+
if (brightness[pos + 2] <= cut)
|
|
427
|
+
m |= 4;
|
|
428
|
+
if (brightness[pos + 3] <= cut)
|
|
429
|
+
m |= 8;
|
|
430
|
+
if (brightness[pos + 4] <= cut)
|
|
431
|
+
m |= 16;
|
|
432
|
+
if (brightness[pos + 5] <= cut)
|
|
433
|
+
m |= 32;
|
|
434
|
+
if (brightness[pos + 6] <= cut)
|
|
435
|
+
m |= 64;
|
|
436
|
+
if (brightness[pos + 7] <= cut)
|
|
437
|
+
m |= 128;
|
|
438
|
+
if (m === 0)
|
|
439
|
+
continue;
|
|
440
|
+
const lo = (m << shift) >>> 0;
|
|
441
|
+
bm[row] |= lo;
|
|
442
|
+
if (shift > 24)
|
|
443
|
+
bm[row + 1] |= m >>> (32 - shift);
|
|
134
444
|
}
|
|
135
445
|
}
|
|
136
446
|
}
|
|
@@ -235,17 +545,53 @@ function pattern(p, size) {
|
|
|
235
545
|
},
|
|
236
546
|
scanLine(b, y, xStart, xEnd, fn) {
|
|
237
547
|
const runs = res.runs();
|
|
548
|
+
// Finder scanning also couples to Bitmap internals so it can scan packed
|
|
549
|
+
// 32-bit words directly instead of re-reading one pixel bit at a time.
|
|
550
|
+
const words = b.words;
|
|
551
|
+
const vals = b.value;
|
|
552
|
+
const row = y * words;
|
|
553
|
+
const pattern = res.pattern;
|
|
554
|
+
// Scan one packed bitmap row by jumping whole equal-bit runs inside 32-bit
|
|
555
|
+
// words; this keeps finder scanning from degenerating into per-pixel work.
|
|
556
|
+
const bitAt = (x) => ((vals[row + (x >>> 5)] >>> (x & 31)) & 1) === 1;
|
|
557
|
+
const runLen = (x, want) => {
|
|
558
|
+
let wi = row + (x >>> 5);
|
|
559
|
+
let bit = x & 31;
|
|
560
|
+
let w = (vals[wi] >>> bit) >>> 0;
|
|
561
|
+
let left = xEnd - x;
|
|
562
|
+
let len = 0;
|
|
563
|
+
while (left > 0) {
|
|
564
|
+
const room = 32 - bit;
|
|
565
|
+
let n = want ? ctz32(~w >>> 0) : ctz32(w);
|
|
566
|
+
if (n > room)
|
|
567
|
+
n = room;
|
|
568
|
+
if (n > left)
|
|
569
|
+
n = left;
|
|
570
|
+
len += n;
|
|
571
|
+
if (n < room && n < left)
|
|
572
|
+
break;
|
|
573
|
+
left -= n;
|
|
574
|
+
if (left <= 0)
|
|
575
|
+
break;
|
|
576
|
+
wi++;
|
|
577
|
+
bit = 0;
|
|
578
|
+
w = vals[wi] >>> 0;
|
|
579
|
+
}
|
|
580
|
+
return len;
|
|
581
|
+
};
|
|
238
582
|
let pos = 0;
|
|
239
583
|
let x = xStart;
|
|
240
584
|
// If we start in middle of an image, skip first pattern run,
|
|
241
585
|
// since we don't know run length of pixels from left side
|
|
242
586
|
if (xStart)
|
|
243
|
-
|
|
244
|
-
x++;
|
|
587
|
+
x += runLen(x, pattern[0]);
|
|
245
588
|
for (; x < xEnd; x++) {
|
|
589
|
+
const cur = bitAt(x);
|
|
246
590
|
// Same run, continue counting
|
|
247
|
-
if (
|
|
248
|
-
|
|
591
|
+
if (cur === pattern[pos]) {
|
|
592
|
+
const n = runLen(x, cur);
|
|
593
|
+
runs[pos] += n;
|
|
594
|
+
x += n - 1;
|
|
249
595
|
// If not last element - continue counting
|
|
250
596
|
if (x !== b.width - 1)
|
|
251
597
|
continue;
|
|
@@ -937,10 +1283,9 @@ export function decodeQR(img, opts = {}) {
|
|
|
937
1283
|
if (!Number.isSafeInteger(img[field]) || img[field] <= 0)
|
|
938
1284
|
throw new Error(`invalid img.${field}=${img[field]} (${typeof img[field]})`);
|
|
939
1285
|
}
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
throw new Error(`invalid image.data=${img.data} (${typeof img.data})`);
|
|
1286
|
+
const { data } = img;
|
|
1287
|
+
if (!Array.isArray(data) && !isBytes(data))
|
|
1288
|
+
throw new Error(`invalid image.data=${data} (${typeof data})`);
|
|
944
1289
|
if (opts.cropToSquare !== undefined && typeof opts.cropToSquare !== 'boolean')
|
|
945
1290
|
throw new Error(`invalid opts.cropToSquare=${opts.cropToSquare}`);
|
|
946
1291
|
for (const fn of ['pointsOnDetect', 'imageOnBitmap', 'imageOnDetect', 'imageOnResult']) {
|