@saeris/hanko 0.0.0 → 0.2.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/CHANGELOG.md +22 -0
- package/LICENSE.md +21 -0
- package/README.md +605 -0
- package/dist/approve/index.d.mts +318 -0
- package/dist/approve/index.d.mts.map +1 -0
- package/dist/approve/index.mjs +393 -0
- package/dist/approve/index.mjs.map +1 -0
- package/dist/client/index.d.mts +101 -0
- package/dist/client/index.d.mts.map +1 -0
- package/dist/client/index.mjs +215 -0
- package/dist/client/index.mjs.map +1 -0
- package/dist/codes-Ba_qYH6u.mjs +93 -0
- package/dist/codes-Ba_qYH6u.mjs.map +1 -0
- package/dist/handlers.d.mts +113 -0
- package/dist/handlers.d.mts.map +1 -0
- package/dist/handlers.mjs +194 -0
- package/dist/handlers.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +345 -0
- package/dist/index.mjs.map +1 -0
- package/dist/linking-DcQSMgem.mjs +177 -0
- package/dist/linking-DcQSMgem.mjs.map +1 -0
- package/dist/linking-nKoayyHf.d.mts +133 -0
- package/dist/linking-nKoayyHf.d.mts.map +1 -0
- package/dist/machine-CRHKjtoP.d.mts +223 -0
- package/dist/machine-CRHKjtoP.d.mts.map +1 -0
- package/dist/machine-D_5DAFxi.mjs +155 -0
- package/dist/machine-D_5DAFxi.mjs.map +1 -0
- package/dist/qr.d.mts +58 -0
- package/dist/qr.d.mts.map +1 -0
- package/dist/qr.mjs +27 -0
- package/dist/qr.mjs.map +1 -0
- package/dist/scan/index.d.mts +381 -0
- package/dist/scan/index.d.mts.map +1 -0
- package/dist/scan/index.mjs +409 -0
- package/dist/scan/index.mjs.map +1 -0
- package/dist/scan/worker.d.mts +2 -0
- package/dist/scan/worker.mjs +2 -0
- package/dist/server-BhoYRkCm.d.mts +257 -0
- package/dist/server-BhoYRkCm.d.mts.map +1 -0
- package/dist/stores/kv.d.mts +64 -0
- package/dist/stores/kv.d.mts.map +1 -0
- package/dist/stores/kv.mjs +87 -0
- package/dist/stores/kv.mjs.map +1 -0
- package/dist/stores/memory.d.mts +22 -0
- package/dist/stores/memory.d.mts.map +1 -0
- package/dist/stores/memory.mjs +42 -0
- package/dist/stores/memory.mjs.map +1 -0
- package/dist/types-BvBIFPH6.mjs +7 -0
- package/dist/types-BvBIFPH6.mjs.map +1 -0
- package/dist/types-C82lb-zX.d.mts +82 -0
- package/dist/types-C82lb-zX.d.mts.map +1 -0
- package/dist/worker-BdwaK1uX.mjs +5291 -0
- package/dist/worker-BdwaK1uX.mjs.map +1 -0
- package/dist/worker-DxbdBA2z.d.mts +164 -0
- package/dist/worker-DxbdBA2z.d.mts.map +1 -0
- package/package.json +116 -3
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import { a as binarize, c as close, i as withClearance, l as downscale, n as createQrDecoder, o as binarizeGlobal, r as decodeMatrix, s as blur, t as serveDecoder, u as toGray } from "../worker-BdwaK1uX.mjs";
|
|
2
|
+
//#region src/scan/qr/progressive.ts
|
|
3
|
+
/**
|
|
4
|
+
* Create a scanner that spreads decoding effort across frames.
|
|
5
|
+
*
|
|
6
|
+
* The ramp is driven by evidence rather than by a timer. A frame in which the
|
|
7
|
+
* decoder finds nothing resets it, because nothing is there to spend effort
|
|
8
|
+
* on; a frame that finds a symbol but cannot read it raises the budget,
|
|
9
|
+
* because that is exactly the case where more effort pays.
|
|
10
|
+
*/
|
|
11
|
+
const createProgressiveScanner = ({ initialBudgetMs = 40, maxBudgetMs = 400, growth = 1.6, ...decoderOptions } = {}) => {
|
|
12
|
+
let budget = initialBudgetMs;
|
|
13
|
+
const decoders = /* @__PURE__ */ new Map();
|
|
14
|
+
const decoderFor = (ms) => {
|
|
15
|
+
const existing = decoders.get(ms);
|
|
16
|
+
if (existing !== void 0) return existing;
|
|
17
|
+
const created = createQrDecoder({
|
|
18
|
+
...decoderOptions,
|
|
19
|
+
timeBudgetMs: ms
|
|
20
|
+
});
|
|
21
|
+
decoders.set(ms, created);
|
|
22
|
+
return created;
|
|
23
|
+
};
|
|
24
|
+
return {
|
|
25
|
+
get budgetMs() {
|
|
26
|
+
return budget;
|
|
27
|
+
},
|
|
28
|
+
scan: (image) => {
|
|
29
|
+
const result = decoderFor(Math.round(budget)).decode(image);
|
|
30
|
+
if (result !== null) {
|
|
31
|
+
budget = initialBudgetMs;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
budget = Math.min(maxBudgetMs, budget * growth);
|
|
35
|
+
return null;
|
|
36
|
+
},
|
|
37
|
+
reset: () => {
|
|
38
|
+
budget = initialBudgetMs;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Wrap a worker in the progressive interface.
|
|
44
|
+
*
|
|
45
|
+
* The worker is supplied rather than constructed here, because how a worker
|
|
46
|
+
* is created is a bundler question — `new Worker(new URL(...), { type:
|
|
47
|
+
* "module" })` in Vite, a blob URL elsewhere — and a library that guessed
|
|
48
|
+
* would be wrong for most consumers.
|
|
49
|
+
*/
|
|
50
|
+
const createWorkerScanner = (worker, { initialBudgetMs = 40, maxBudgetMs = 400, growth = 1.6 } = {}) => {
|
|
51
|
+
let budget = initialBudgetMs;
|
|
52
|
+
let nextId = 0;
|
|
53
|
+
let inFlight = false;
|
|
54
|
+
const pending = /* @__PURE__ */ new Map();
|
|
55
|
+
const receive = (payload) => {
|
|
56
|
+
if (payload === null || typeof payload !== `object`) return;
|
|
57
|
+
const envelope = payload;
|
|
58
|
+
const body = typeof envelope.data === `object` && envelope.data !== null ? envelope.data : payload;
|
|
59
|
+
if (body === null || typeof body !== `object`) return;
|
|
60
|
+
if (!(`id` in body) || typeof body.id !== `number`) return;
|
|
61
|
+
const response = body;
|
|
62
|
+
const resolve = pending.get(response.id);
|
|
63
|
+
if (resolve === void 0) return;
|
|
64
|
+
pending.delete(response.id);
|
|
65
|
+
inFlight = false;
|
|
66
|
+
resolve(response);
|
|
67
|
+
};
|
|
68
|
+
if (typeof worker.addEventListener === `function`) worker.addEventListener(`message`, receive);
|
|
69
|
+
else if (typeof worker.on === `function`) worker.on(`message`, receive);
|
|
70
|
+
return {
|
|
71
|
+
get busy() {
|
|
72
|
+
return inFlight;
|
|
73
|
+
},
|
|
74
|
+
scan: async (image) => {
|
|
75
|
+
if (inFlight) return null;
|
|
76
|
+
inFlight = true;
|
|
77
|
+
const id = nextId++;
|
|
78
|
+
const settled = new Promise((resolve) => {
|
|
79
|
+
pending.set(id, resolve);
|
|
80
|
+
});
|
|
81
|
+
const copy = new Uint8ClampedArray(image.data);
|
|
82
|
+
worker.postMessage({
|
|
83
|
+
id,
|
|
84
|
+
data: copy,
|
|
85
|
+
width: image.width,
|
|
86
|
+
height: image.height
|
|
87
|
+
}, [copy.buffer]);
|
|
88
|
+
const { result } = await settled;
|
|
89
|
+
budget = result === null ? Math.min(maxBudgetMs, budget * growth) : initialBudgetMs;
|
|
90
|
+
return result;
|
|
91
|
+
},
|
|
92
|
+
reset: () => {
|
|
93
|
+
budget = initialBudgetMs;
|
|
94
|
+
},
|
|
95
|
+
close: () => {
|
|
96
|
+
pending.clear();
|
|
97
|
+
inFlight = false;
|
|
98
|
+
worker.terminate?.();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
//#endregion
|
|
103
|
+
//#region src/scan/gpu.ts
|
|
104
|
+
/**
|
|
105
|
+
* The scoring kernel.
|
|
106
|
+
*
|
|
107
|
+
* Mirrors `scoreTransform` exactly — same nine-point module sampling, same
|
|
108
|
+
* ring structure, same weights. Any divergence would make GPU and CPU
|
|
109
|
+
* disagree about which transform is best, which is worse than not
|
|
110
|
+
* accelerating at all: results would depend on the device.
|
|
111
|
+
*/
|
|
112
|
+
const SHADER = `
|
|
113
|
+
struct Params {
|
|
114
|
+
width: u32,
|
|
115
|
+
height: u32,
|
|
116
|
+
size: u32,
|
|
117
|
+
centerCount: u32,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
@group(0) @binding(0) var<storage, read> bits: array<u32>;
|
|
121
|
+
@group(0) @binding(1) var<storage, read> transforms: array<f32>;
|
|
122
|
+
@group(0) @binding(2) var<storage, read> centers: array<u32>;
|
|
123
|
+
@group(0) @binding(3) var<storage, read_write> scores: array<i32>;
|
|
124
|
+
@group(0) @binding(4) var<uniform> params: Params;
|
|
125
|
+
|
|
126
|
+
fn pixel(x: i32, y: i32) -> i32 {
|
|
127
|
+
if (x < 0 || y < 0 || x >= i32(params.width) || y >= i32(params.height)) {
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
let index = u32(y) * params.width + u32(x);
|
|
131
|
+
// Bits are packed one per u32 for simplicity: the readback is tiny and the
|
|
132
|
+
// upload happens once per frame, so the memory is not the bottleneck.
|
|
133
|
+
return select(-1, 1, bits[index] == 1u);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn mapPoint(base: u32, x: f32, y: f32) -> vec2<f32> {
|
|
137
|
+
let a11 = transforms[base + 0u];
|
|
138
|
+
let a12 = transforms[base + 1u];
|
|
139
|
+
let a13 = transforms[base + 2u];
|
|
140
|
+
let a21 = transforms[base + 3u];
|
|
141
|
+
let a22 = transforms[base + 4u];
|
|
142
|
+
let a23 = transforms[base + 5u];
|
|
143
|
+
let a31 = transforms[base + 6u];
|
|
144
|
+
let a32 = transforms[base + 7u];
|
|
145
|
+
let a33 = transforms[base + 8u];
|
|
146
|
+
|
|
147
|
+
let w = a13 * x + a23 * y + a33;
|
|
148
|
+
if (abs(w) < 1e-9) { return vec2<f32>(-1.0, -1.0); }
|
|
149
|
+
return vec2<f32>((a11 * x + a21 * y + a31) / w, (a12 * x + a22 * y + a32) / w);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn cell(base: u32, span: f32, mx: f32, my: f32) -> i32 {
|
|
153
|
+
var score = 0;
|
|
154
|
+
let offsets = array<f32, 3>(0.3, 0.5, 0.7);
|
|
155
|
+
for (var v = 0u; v < 3u; v = v + 1u) {
|
|
156
|
+
for (var u = 0u; u < 3u; u = u + 1u) {
|
|
157
|
+
let p = mapPoint(base, (mx + offsets[u] - 3.5) / span, (my + offsets[v] - 3.5) / span);
|
|
158
|
+
score = score + pixel(i32(round(p.x)), i32(round(p.y)));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return score;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
fn ring(base: u32, span: f32, cx: f32, cy: f32, radius: f32) -> i32 {
|
|
165
|
+
var score = 0;
|
|
166
|
+
let steps = i32(radius * 2.0);
|
|
167
|
+
for (var i = 0; i < steps; i = i + 1) {
|
|
168
|
+
let f = f32(i);
|
|
169
|
+
score = score + cell(base, span, cx - radius + f, cy - radius);
|
|
170
|
+
score = score + cell(base, span, cx - radius, cy + radius - f);
|
|
171
|
+
score = score + cell(base, span, cx + radius, cy - radius + f);
|
|
172
|
+
score = score + cell(base, span, cx + radius - f, cy + radius);
|
|
173
|
+
}
|
|
174
|
+
return score;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@compute @workgroup_size(64)
|
|
178
|
+
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
|
|
179
|
+
let index = id.x;
|
|
180
|
+
if (index >= arrayLength(&scores)) { return; }
|
|
181
|
+
|
|
182
|
+
let base = index * 9u;
|
|
183
|
+
let size = f32(params.size);
|
|
184
|
+
let span = size - 7.0;
|
|
185
|
+
var score = 0;
|
|
186
|
+
|
|
187
|
+
// Timing patterns alternate; the expected value flips each module.
|
|
188
|
+
for (var i = 0u; i < params.size - 14u; i = i + 1u) {
|
|
189
|
+
let expected = select(-1, 1, (i & 1u) == 1u);
|
|
190
|
+
score = score + cell(base, span, f32(i) + 7.0, 6.0) * expected;
|
|
191
|
+
score = score + cell(base, span, 6.0, f32(i) + 7.0) * expected;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Finders: dark centre, dark ring, light ring, dark ring.
|
|
195
|
+
let corners = array<vec2<f32>, 3>(
|
|
196
|
+
vec2<f32>(0.0, 0.0),
|
|
197
|
+
vec2<f32>(size - 7.0, 0.0),
|
|
198
|
+
vec2<f32>(0.0, size - 7.0)
|
|
199
|
+
);
|
|
200
|
+
for (var c = 0u; c < 3u; c = c + 1u) {
|
|
201
|
+
let x = corners[c].x + 3.0;
|
|
202
|
+
let y = corners[c].y + 3.0;
|
|
203
|
+
score = score + cell(base, span, x, y)
|
|
204
|
+
+ ring(base, span, x, y, 1.0)
|
|
205
|
+
- ring(base, span, x, y, 2.0)
|
|
206
|
+
+ ring(base, span, x, y, 3.0);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Alignment patterns: dark centre, light ring, dark ring.
|
|
210
|
+
//
|
|
211
|
+
// Two groups, matching the CPU exactly. The edge row and column come first
|
|
212
|
+
// (skipping the last, which overlaps a finder), then the interior grid.
|
|
213
|
+
// Scoring only the interior would give systematically different totals, and
|
|
214
|
+
// since the whole technique ranks transforms against each other, GPU and
|
|
215
|
+
// CPU would disagree about which corner is best — decoding that depends on
|
|
216
|
+
// the device is worse than not accelerating at all.
|
|
217
|
+
for (var i = 1u; i + 1u < params.centerCount; i = i + 1u) {
|
|
218
|
+
let c = f32(centers[i]);
|
|
219
|
+
score = score + cell(base, span, 6.0, c)
|
|
220
|
+
- ring(base, span, 6.0, c, 1.0)
|
|
221
|
+
+ ring(base, span, 6.0, c, 2.0);
|
|
222
|
+
score = score + cell(base, span, c, 6.0)
|
|
223
|
+
- ring(base, span, c, 6.0, 1.0)
|
|
224
|
+
+ ring(base, span, c, 6.0, 2.0);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for (var i = 1u; i < params.centerCount; i = i + 1u) {
|
|
228
|
+
for (var j = 1u; j < params.centerCount; j = j + 1u) {
|
|
229
|
+
let cx = f32(centers[i]);
|
|
230
|
+
let cy = f32(centers[j]);
|
|
231
|
+
score = score + cell(base, span, cx, cy)
|
|
232
|
+
- ring(base, span, cx, cy, 1.0)
|
|
233
|
+
+ ring(base, span, cx, cy, 2.0);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
scores[index] = score;
|
|
238
|
+
}
|
|
239
|
+
`;
|
|
240
|
+
/** Whether this runtime exposes WebGPU at all. */
|
|
241
|
+
const hasWebGpu = () => typeof navigator === `object` && `gpu` in navigator;
|
|
242
|
+
/**
|
|
243
|
+
* Create a GPU scorer, or `null` where WebGPU is unavailable.
|
|
244
|
+
*
|
|
245
|
+
* Returns `null` rather than throwing: every caller must have a CPU path
|
|
246
|
+
* anyway, so an absent GPU is a normal condition and not an error.
|
|
247
|
+
*/
|
|
248
|
+
const createGpuScorer = async () => {
|
|
249
|
+
if (!hasWebGpu()) return null;
|
|
250
|
+
const gpu = navigator.gpu;
|
|
251
|
+
if (typeof gpu !== `object` || gpu === null) return null;
|
|
252
|
+
const adapter = await gpu.requestAdapter();
|
|
253
|
+
if (adapter === null || typeof adapter !== `object`) return null;
|
|
254
|
+
const device = await adapter.requestDevice();
|
|
255
|
+
const module = device.createShaderModule({ code: SHADER });
|
|
256
|
+
const pipeline = device.createComputePipeline({
|
|
257
|
+
layout: `auto`,
|
|
258
|
+
compute: {
|
|
259
|
+
module,
|
|
260
|
+
entryPoint: `main`
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
return {
|
|
264
|
+
score: async (image, transforms, size, alignmentCenters) => {
|
|
265
|
+
const count = transforms.length;
|
|
266
|
+
if (count === 0) return /* @__PURE__ */ new Int32Array(0);
|
|
267
|
+
const bits = new Uint32Array(image.bits.length);
|
|
268
|
+
for (let i = 0; i < image.bits.length; i++) bits[i] = image.bits[i];
|
|
269
|
+
const flat = new Float32Array(count * 9);
|
|
270
|
+
for (const [index, transform] of transforms.entries()) {
|
|
271
|
+
const base = index * 9;
|
|
272
|
+
flat[base] = transform.a11;
|
|
273
|
+
flat[base + 1] = transform.a12;
|
|
274
|
+
flat[base + 2] = transform.a13;
|
|
275
|
+
flat[base + 3] = transform.a21;
|
|
276
|
+
flat[base + 4] = transform.a22;
|
|
277
|
+
flat[base + 5] = transform.a23;
|
|
278
|
+
flat[base + 6] = transform.a31;
|
|
279
|
+
flat[base + 7] = transform.a32;
|
|
280
|
+
flat[base + 8] = transform.a33;
|
|
281
|
+
}
|
|
282
|
+
const centers = Uint32Array.from(alignmentCenters);
|
|
283
|
+
const params = new Uint32Array([
|
|
284
|
+
image.width,
|
|
285
|
+
image.height,
|
|
286
|
+
size,
|
|
287
|
+
centers.length
|
|
288
|
+
]);
|
|
289
|
+
const upload = (data, usage) => {
|
|
290
|
+
const buffer = device.createBuffer({
|
|
291
|
+
size: Math.max(4, data.byteLength),
|
|
292
|
+
usage,
|
|
293
|
+
mappedAtCreation: true
|
|
294
|
+
});
|
|
295
|
+
new Uint8Array(buffer.getMappedRange()).set(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
|
|
296
|
+
buffer.unmap();
|
|
297
|
+
return buffer;
|
|
298
|
+
};
|
|
299
|
+
const STORAGE = 132;
|
|
300
|
+
const UNIFORM = 68;
|
|
301
|
+
const MAP_READ = 1;
|
|
302
|
+
const bitsBuffer = upload(bits, STORAGE);
|
|
303
|
+
const transformBuffer = upload(flat, STORAGE);
|
|
304
|
+
const centerBuffer = upload(centers.length > 0 ? centers : /* @__PURE__ */ new Uint32Array(1), STORAGE);
|
|
305
|
+
const paramsBuffer = upload(params, UNIFORM);
|
|
306
|
+
const scoreBuffer = device.createBuffer({
|
|
307
|
+
size: count * 4,
|
|
308
|
+
usage: 132
|
|
309
|
+
});
|
|
310
|
+
const readBuffer = device.createBuffer({
|
|
311
|
+
size: count * 4,
|
|
312
|
+
usage: 9
|
|
313
|
+
});
|
|
314
|
+
const bindGroup = device.createBindGroup({
|
|
315
|
+
layout: pipeline.getBindGroupLayout(0),
|
|
316
|
+
entries: [
|
|
317
|
+
{
|
|
318
|
+
binding: 0,
|
|
319
|
+
resource: { buffer: bitsBuffer }
|
|
320
|
+
},
|
|
321
|
+
{
|
|
322
|
+
binding: 1,
|
|
323
|
+
resource: { buffer: transformBuffer }
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
binding: 2,
|
|
327
|
+
resource: { buffer: centerBuffer }
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
binding: 3,
|
|
331
|
+
resource: { buffer: scoreBuffer }
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
binding: 4,
|
|
335
|
+
resource: { buffer: paramsBuffer }
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
});
|
|
339
|
+
const encoder = device.createCommandEncoder();
|
|
340
|
+
const pass = encoder.beginComputePass();
|
|
341
|
+
pass.setPipeline(pipeline);
|
|
342
|
+
pass.setBindGroup(0, bindGroup);
|
|
343
|
+
pass.dispatchWorkgroups(Math.ceil(count / 64));
|
|
344
|
+
pass.end();
|
|
345
|
+
encoder.copyBufferToBuffer(scoreBuffer, 0, readBuffer, 0, count * 4);
|
|
346
|
+
device.queue.submit([encoder.finish()]);
|
|
347
|
+
await readBuffer.mapAsync(MAP_READ);
|
|
348
|
+
const scores = new Int32Array(readBuffer.getMappedRange().slice(0));
|
|
349
|
+
readBuffer.unmap();
|
|
350
|
+
for (const buffer of [
|
|
351
|
+
bitsBuffer,
|
|
352
|
+
transformBuffer,
|
|
353
|
+
centerBuffer,
|
|
354
|
+
paramsBuffer,
|
|
355
|
+
scoreBuffer,
|
|
356
|
+
readBuffer
|
|
357
|
+
]) buffer.destroy();
|
|
358
|
+
return scores;
|
|
359
|
+
},
|
|
360
|
+
destroy: () => {
|
|
361
|
+
device.destroy();
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
};
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/scan/machine.ts
|
|
367
|
+
/**
|
|
368
|
+
* Transitions, as a table.
|
|
369
|
+
*
|
|
370
|
+
* Everything absent is illegal by construction. That is what makes a `DECODE`
|
|
371
|
+
* arriving after `STOP` a no-op rather than a race — a real hazard here,
|
|
372
|
+
* because a decode in flight when the camera stops would otherwise resolve
|
|
373
|
+
* into a torn-down session.
|
|
374
|
+
*/
|
|
375
|
+
const SCAN_TRANSITIONS = {
|
|
376
|
+
idle: { START: `starting` },
|
|
377
|
+
starting: {
|
|
378
|
+
READY: `scanning`,
|
|
379
|
+
DENY: `denied`,
|
|
380
|
+
FAIL: `failed`,
|
|
381
|
+
STOP: `stopped`
|
|
382
|
+
},
|
|
383
|
+
scanning: {
|
|
384
|
+
DECODE: `decoded`,
|
|
385
|
+
PAUSE: `paused`,
|
|
386
|
+
FAIL: `failed`,
|
|
387
|
+
STOP: `stopped`
|
|
388
|
+
},
|
|
389
|
+
paused: {
|
|
390
|
+
RESUME: `scanning`,
|
|
391
|
+
FAIL: `failed`,
|
|
392
|
+
STOP: `stopped`
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
/** Whether `event` would move `state`. */
|
|
396
|
+
const canTransitionScan = (state, event) => SCAN_TRANSITIONS[state]?.[event] !== void 0;
|
|
397
|
+
/** Apply an event. Unknown events for the current state are no-ops. */
|
|
398
|
+
const scanTransition = (state, event) => SCAN_TRANSITIONS[state]?.[event.type] ?? state;
|
|
399
|
+
/**
|
|
400
|
+
* Terminal states accept no further events.
|
|
401
|
+
*
|
|
402
|
+
* Derived from the table rather than listed, so a new state cannot be added
|
|
403
|
+
* without its terminality following automatically.
|
|
404
|
+
*/
|
|
405
|
+
const isScanSettled = (state) => SCAN_TRANSITIONS[state] === void 0;
|
|
406
|
+
//#endregion
|
|
407
|
+
export { binarize, binarizeGlobal, blur, canTransitionScan, close, createGpuScorer, createProgressiveScanner, createQrDecoder, createWorkerScanner, decodeMatrix, downscale, hasWebGpu, isScanSettled, scanTransition, serveDecoder, toGray, withClearance };
|
|
408
|
+
|
|
409
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/scan/qr/progressive.ts","../../src/scan/gpu.ts","../../src/scan/machine.ts"],"sourcesContent":["/**\n * Scanning across frames rather than within one.\n *\n * The retry ladder is what makes difficult symbols readable, and it costs far\n * more than one frame can afford. Measured on the benchmark corpus, the\n * decoder reads **61.6%** with no time limit and **41.9%** at the 120ms\n * default a viewfinder needs — so a third of its capability is unreachable in\n * the configuration people actually run, and no budget recovers it: a sweep\n * showed recognition still climbing at 700ms.\n *\n * That is a false choice, because a camera is not a still image. It supplies\n * thirty frames a second, each nearly free and each slightly different. Ten\n * frames at 120ms is 1.2 seconds of decoding work at full frame rate, without\n * ever stalling the preview.\n *\n * So this spends a small budget per frame and ADVANCES through the ladder\n * across successive frames: cheap rungs on every frame, expensive ones only\n * once the cheap ones have failed repeatedly. A code held steadily in view\n * gets the whole ladder within a second; a frame with nothing in it costs one\n * cheap pass.\n *\n * The decoder itself stays pure and synchronous — this wraps it rather than\n * changing it, so a still image can still run the whole ladder at once.\n */\n\nimport type { DecodedSymbol, GrayImage } from \"../types.js\";\nimport { createQrDecoder, type QrDecoderOptions } from \"./decoder.js\";\n\n/** A scanner that improves its effort as frames arrive. */\nexport interface ProgressiveScanner {\n /**\n * Offer one frame.\n *\n * Returns a symbol as soon as one is read, and `null` otherwise — including\n * while effort is still ramping up.\n */\n scan(image: GrayImage): DecodedSymbol | null;\n\n /**\n * Reset the effort ramp.\n *\n * Call when the scene changes — a new code presented, the camera moved\n * somewhere else — so the next frame starts cheap again rather than\n * inheriting effort earned by a symbol that is no longer there.\n */\n reset(): void;\n\n /** How much effort the next frame will receive, in milliseconds. */\n readonly budgetMs: number;\n}\n\n/** Options for {@link createProgressiveScanner}. */\nexport interface ProgressiveOptions extends Omit<\n QrDecoderOptions,\n `timeBudgetMs`\n> {\n /**\n * Budget for the first frame, in milliseconds.\n *\n * Deliberately small. Most frames a camera delivers contain no code at all,\n * and this is what every one of them costs.\n */\n initialBudgetMs?: number;\n\n /**\n * Ceiling on the per-frame budget.\n *\n * 400ms by default: beyond that a single frame stalls the preview\n * noticeably, and at 30fps the ladder has had a dozen frames to work with\n * by the time the ramp reaches it.\n */\n maxBudgetMs?: number;\n\n /**\n * How much the budget grows per consecutive frame that shows a symbol.\n *\n * Growth is conditional on there being something to find: a frame with no\n * finder candidate at all should not earn the next frame more time, or a\n * camera pointed at a wall would ramp to its ceiling and stay there.\n */\n growth?: number;\n}\n\n/**\n * Create a scanner that spreads decoding effort across frames.\n *\n * The ramp is driven by evidence rather than by a timer. A frame in which the\n * decoder finds nothing resets it, because nothing is there to spend effort\n * on; a frame that finds a symbol but cannot read it raises the budget,\n * because that is exactly the case where more effort pays.\n */\nexport const createProgressiveScanner = ({\n initialBudgetMs = 40,\n maxBudgetMs = 400,\n growth = 1.6,\n ...decoderOptions\n}: ProgressiveOptions = {}): ProgressiveScanner => {\n let budget = initialBudgetMs;\n\n // One decoder per budget level, cached: constructing one is cheap, but a\n // camera loop calls this thirty times a second and there are only a handful\n // of distinct budgets in the ramp.\n const decoders = new Map<number, ReturnType<typeof createQrDecoder>>();\n const decoderFor = (ms: number): ReturnType<typeof createQrDecoder> => {\n const existing = decoders.get(ms);\n if (existing !== undefined) return existing;\n\n const created = createQrDecoder({ ...decoderOptions, timeBudgetMs: ms });\n decoders.set(ms, created);\n return created;\n };\n\n return {\n get budgetMs(): number {\n return budget;\n },\n\n scan: (image: GrayImage): DecodedSymbol | null => {\n const rounded = Math.round(budget);\n const result = decoderFor(rounded).decode(image);\n\n if (result !== null) {\n // Read it. Drop back to the cheap budget: whatever comes next is a\n // different scan, and starting expensive would waste effort on the\n // frames after a successful read.\n budget = initialBudgetMs;\n return result;\n }\n\n // Nothing read. Spend more next time, up to the ceiling — the symbol may\n // simply need more of the ladder than this frame could afford.\n budget = Math.min(maxBudgetMs, budget * growth);\n return null;\n },\n\n reset: (): void => {\n budget = initialBudgetMs;\n }\n };\n};\n\n/**\n * Drive a decoder running in a worker.\n *\n * Same shape as {@link createProgressiveScanner} but asynchronous, because\n * the work happens elsewhere. The main thread stays free: measured on a hard\n * frame, an in-thread decode blocks 208ms at a 40ms budget and 481ms at\n * 400ms, which at 60fps is 12 and 28 dropped frames respectively — a visible\n * freeze on exactly the frames where someone is lining up a shot.\n *\n * Frames offered while a decode is in flight are DROPPED rather than queued.\n * A camera produces frames faster than they can be decoded, and a queue would\n * grow without bound while returning increasingly stale results; the next\n * frame is always a better input than a backlogged one.\n */\nexport interface WorkerScanner {\n /** Offer a frame. Resolves `null` if dropped, still ramping, or unreadable. */\n scan(image: GrayImage): Promise<DecodedSymbol | null>;\n /** Reset the effort ramp — call when the scene changes. */\n reset(): void;\n /** Stop the worker and release it. */\n close(): void;\n /** Whether a decode is currently in flight. */\n readonly busy: boolean;\n}\n\n/**\n * Wrap a worker in the progressive interface.\n *\n * The worker is supplied rather than constructed here, because how a worker\n * is created is a bundler question — `new Worker(new URL(...), { type:\n * \"module\" })` in Vite, a blob URL elsewhere — and a library that guessed\n * would be wrong for most consumers.\n */\nexport const createWorkerScanner = (\n worker: {\n // `ArrayBufferLike[]` rather than `Transferable[]`: this package compiles\n // without the DOM lib on purpose, and a buffer is the only thing\n // transferred here.\n postMessage: (message: unknown, transfer?: ArrayBufferLike[]) => void;\n addEventListener?: (\n type: string,\n listener: (event: { data: unknown }) => void\n ) => void;\n on?: (type: string, listener: (message: unknown) => void) => void;\n terminate?: () => void;\n },\n {\n initialBudgetMs = 40,\n maxBudgetMs = 400,\n growth = 1.6\n }: ProgressiveOptions = {}\n): WorkerScanner => {\n let budget = initialBudgetMs;\n let nextId = 0;\n let inFlight = false;\n const pending = new Map<\n number,\n (response: { result: DecodedSymbol | null }) => void\n >();\n\n const receive = (payload: unknown): void => {\n // Narrowed by runtime checks rather than a cast: a DOM MessageEvent\n // carries the payload on `.data` while a Node message IS the payload, and\n // asserting one shape would silently misread the other.\n if (payload === null || typeof payload !== `object`) return;\n\n const envelope = payload as { data?: unknown };\n const body: unknown =\n typeof envelope.data === `object` && envelope.data !== null\n ? envelope.data\n : payload;\n\n if (body === null || typeof body !== `object`) return;\n if (!(`id` in body) || typeof body.id !== `number`) return;\n\n const response = body as { id: number; result: DecodedSymbol | null };\n const resolve = pending.get(response.id);\n if (resolve === undefined) return;\n\n pending.delete(response.id);\n inFlight = false;\n resolve(response);\n };\n\n if (typeof worker.addEventListener === `function`) {\n worker.addEventListener(`message`, receive);\n } else if (typeof worker.on === `function`) {\n worker.on(`message`, receive);\n }\n\n return {\n get busy(): boolean {\n return inFlight;\n },\n\n scan: async (image: GrayImage): Promise<DecodedSymbol | null> => {\n // Dropped rather than queued: a camera outruns the decoder, and a queue\n // would grow unbounded while returning ever staler results.\n if (inFlight) return null;\n\n inFlight = true;\n const id = nextId++;\n\n const settled = new Promise<{ result: DecodedSymbol | null }>(\n (resolve) => {\n pending.set(id, resolve);\n }\n );\n\n // Copied before transfer: transferring detaches the caller's buffer,\n // and a caller reusing one frame buffer across frames — which is the\n // normal pattern — would find it empty on the next read.\n const copy = new Uint8ClampedArray(image.data);\n worker.postMessage(\n { id, data: copy, width: image.width, height: image.height },\n [copy.buffer]\n );\n\n const { result } = await settled;\n\n budget =\n result === null\n ? Math.min(maxBudgetMs, budget * growth)\n : initialBudgetMs;\n\n return result;\n },\n\n reset: (): void => {\n budget = initialBudgetMs;\n },\n\n close: (): void => {\n pending.clear();\n inFlight = false;\n worker.terminate?.();\n }\n };\n};\n","/**\n * GPU-accelerated transform scoring.\n *\n * One stage of this decoder is worth moving to the GPU and the rest are not.\n * Profiling a 1024x768 frame: binarization costs 16ms, blur 28ms, downscale\n * 6ms — all far too small to survive the cost of uploading a buffer,\n * dispatching a shader and reading the result back. But the corner search\n * makes **625 independent calls** to `scoreTransform` for about 179ms, which\n * is 78% of that stage and the largest single cost in the decoder.\n *\n * Those 625 calls score the same image under different transforms, share no\n * state, and each reads a few hundred pixels. That is the shape GPU compute\n * exists for: one upload of the image, 625 workgroups, one small readback.\n *\n * Availability is unusually good for a modern web API. WebGPU shipped\n * enabled-by-default in iOS 26, macOS Tahoe 26 and iPadOS 26, which removed\n * the last major holdout — so unlike `BarcodeDetector`, this is something the\n * target platforms actually have. It still degrades to the CPU path\n * everywhere else, because a scanner that only works on new hardware is not a\n * scanner.\n */\n\nimport type { BitMatrix } from \"./types.js\";\nimport type { Transform } from \"./qr/sample.js\";\n\n/**\n * The scoring kernel.\n *\n * Mirrors `scoreTransform` exactly — same nine-point module sampling, same\n * ring structure, same weights. Any divergence would make GPU and CPU\n * disagree about which transform is best, which is worse than not\n * accelerating at all: results would depend on the device.\n */\nconst SHADER = `\nstruct Params {\n width: u32,\n height: u32,\n size: u32,\n centerCount: u32,\n};\n\n@group(0) @binding(0) var<storage, read> bits: array<u32>;\n@group(0) @binding(1) var<storage, read> transforms: array<f32>;\n@group(0) @binding(2) var<storage, read> centers: array<u32>;\n@group(0) @binding(3) var<storage, read_write> scores: array<i32>;\n@group(0) @binding(4) var<uniform> params: Params;\n\nfn pixel(x: i32, y: i32) -> i32 {\n if (x < 0 || y < 0 || x >= i32(params.width) || y >= i32(params.height)) {\n return 0;\n }\n let index = u32(y) * params.width + u32(x);\n // Bits are packed one per u32 for simplicity: the readback is tiny and the\n // upload happens once per frame, so the memory is not the bottleneck.\n return select(-1, 1, bits[index] == 1u);\n}\n\nfn mapPoint(base: u32, x: f32, y: f32) -> vec2<f32> {\n let a11 = transforms[base + 0u];\n let a12 = transforms[base + 1u];\n let a13 = transforms[base + 2u];\n let a21 = transforms[base + 3u];\n let a22 = transforms[base + 4u];\n let a23 = transforms[base + 5u];\n let a31 = transforms[base + 6u];\n let a32 = transforms[base + 7u];\n let a33 = transforms[base + 8u];\n\n let w = a13 * x + a23 * y + a33;\n if (abs(w) < 1e-9) { return vec2<f32>(-1.0, -1.0); }\n return vec2<f32>((a11 * x + a21 * y + a31) / w, (a12 * x + a22 * y + a32) / w);\n}\n\nfn cell(base: u32, span: f32, mx: f32, my: f32) -> i32 {\n var score = 0;\n let offsets = array<f32, 3>(0.3, 0.5, 0.7);\n for (var v = 0u; v < 3u; v = v + 1u) {\n for (var u = 0u; u < 3u; u = u + 1u) {\n let p = mapPoint(base, (mx + offsets[u] - 3.5) / span, (my + offsets[v] - 3.5) / span);\n score = score + pixel(i32(round(p.x)), i32(round(p.y)));\n }\n }\n return score;\n}\n\nfn ring(base: u32, span: f32, cx: f32, cy: f32, radius: f32) -> i32 {\n var score = 0;\n let steps = i32(radius * 2.0);\n for (var i = 0; i < steps; i = i + 1) {\n let f = f32(i);\n score = score + cell(base, span, cx - radius + f, cy - radius);\n score = score + cell(base, span, cx - radius, cy + radius - f);\n score = score + cell(base, span, cx + radius, cy - radius + f);\n score = score + cell(base, span, cx + radius - f, cy + radius);\n }\n return score;\n}\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) id: vec3<u32>) {\n let index = id.x;\n if (index >= arrayLength(&scores)) { return; }\n\n let base = index * 9u;\n let size = f32(params.size);\n let span = size - 7.0;\n var score = 0;\n\n // Timing patterns alternate; the expected value flips each module.\n for (var i = 0u; i < params.size - 14u; i = i + 1u) {\n let expected = select(-1, 1, (i & 1u) == 1u);\n score = score + cell(base, span, f32(i) + 7.0, 6.0) * expected;\n score = score + cell(base, span, 6.0, f32(i) + 7.0) * expected;\n }\n\n // Finders: dark centre, dark ring, light ring, dark ring.\n let corners = array<vec2<f32>, 3>(\n vec2<f32>(0.0, 0.0),\n vec2<f32>(size - 7.0, 0.0),\n vec2<f32>(0.0, size - 7.0)\n );\n for (var c = 0u; c < 3u; c = c + 1u) {\n let x = corners[c].x + 3.0;\n let y = corners[c].y + 3.0;\n score = score + cell(base, span, x, y)\n + ring(base, span, x, y, 1.0)\n - ring(base, span, x, y, 2.0)\n + ring(base, span, x, y, 3.0);\n }\n\n // Alignment patterns: dark centre, light ring, dark ring.\n //\n // Two groups, matching the CPU exactly. The edge row and column come first\n // (skipping the last, which overlaps a finder), then the interior grid.\n // Scoring only the interior would give systematically different totals, and\n // since the whole technique ranks transforms against each other, GPU and\n // CPU would disagree about which corner is best — decoding that depends on\n // the device is worse than not accelerating at all.\n for (var i = 1u; i + 1u < params.centerCount; i = i + 1u) {\n let c = f32(centers[i]);\n score = score + cell(base, span, 6.0, c)\n - ring(base, span, 6.0, c, 1.0)\n + ring(base, span, 6.0, c, 2.0);\n score = score + cell(base, span, c, 6.0)\n - ring(base, span, c, 6.0, 1.0)\n + ring(base, span, c, 6.0, 2.0);\n }\n\n for (var i = 1u; i < params.centerCount; i = i + 1u) {\n for (var j = 1u; j < params.centerCount; j = j + 1u) {\n let cx = f32(centers[i]);\n let cy = f32(centers[j]);\n score = score + cell(base, span, cx, cy)\n - ring(base, span, cx, cy, 1.0)\n + ring(base, span, cx, cy, 2.0);\n }\n }\n\n scores[index] = score;\n}\n`;\n\n/** Scores many transforms against one image, on the GPU. */\nexport interface GpuScorer {\n score(\n image: BitMatrix,\n transforms: readonly Transform[],\n size: number,\n alignmentCenters: readonly number[]\n ): Promise<Int32Array>;\n destroy(): void;\n}\n\n/** Whether this runtime exposes WebGPU at all. */\nexport const hasWebGpu = (): boolean =>\n typeof navigator === `object` && `gpu` in navigator;\n\n/**\n * Create a GPU scorer, or `null` where WebGPU is unavailable.\n *\n * Returns `null` rather than throwing: every caller must have a CPU path\n * anyway, so an absent GPU is a normal condition and not an error.\n */\nexport const createGpuScorer = async (): Promise<GpuScorer | null> => {\n if (!hasWebGpu()) return null;\n\n // Narrowed through a runtime check rather than a cast: `navigator.gpu` is\n // absent on every server runtime and on older browsers.\n const gpu: unknown = (navigator as unknown as { gpu?: unknown }).gpu;\n if (typeof gpu !== `object` || gpu === null) return null;\n\n const adapter = await (\n gpu as { requestAdapter: () => Promise<unknown> }\n ).requestAdapter();\n if (adapter === null || typeof adapter !== `object`) return null;\n\n const device = await (\n adapter as { requestDevice: () => Promise<GpuDeviceLike> }\n ).requestDevice();\n\n const module = device.createShaderModule({ code: SHADER });\n const pipeline = device.createComputePipeline({\n layout: `auto`,\n compute: { module, entryPoint: `main` }\n });\n\n return {\n score: async (image, transforms, size, alignmentCenters) => {\n const count = transforms.length;\n if (count === 0) return new Int32Array(0);\n\n // One u32 per pixel. Packing to bits would quarter the upload but adds\n // shifting in the inner loop, and the upload is not what costs here.\n const bits = new Uint32Array(image.bits.length);\n for (let i = 0; i < image.bits.length; i++) bits[i] = image.bits[i]!;\n\n const flat = new Float32Array(count * 9);\n for (const [index, transform] of transforms.entries()) {\n const base = index * 9;\n flat[base] = transform.a11;\n flat[base + 1] = transform.a12;\n flat[base + 2] = transform.a13;\n flat[base + 3] = transform.a21;\n flat[base + 4] = transform.a22;\n flat[base + 5] = transform.a23;\n flat[base + 6] = transform.a31;\n flat[base + 7] = transform.a32;\n flat[base + 8] = transform.a33;\n }\n\n const centers = Uint32Array.from(alignmentCenters);\n const params = new Uint32Array([\n image.width,\n image.height,\n size,\n centers.length\n ]);\n\n const upload = (data: ArrayBufferView, usage: number): GpuBufferLike => {\n const buffer = device.createBuffer({\n size: Math.max(4, data.byteLength),\n usage,\n mappedAtCreation: true\n });\n new Uint8Array(buffer.getMappedRange()).set(\n new Uint8Array(data.buffer, data.byteOffset, data.byteLength)\n );\n buffer.unmap();\n return buffer;\n };\n\n const STORAGE = 0x80 | 0x4;\n const UNIFORM = 0x40 | 0x4;\n const COPY_SRC = 0x4;\n const COPY_DST = 0x8;\n const MAP_READ = 0x1;\n\n const bitsBuffer = upload(bits, STORAGE);\n const transformBuffer = upload(flat, STORAGE);\n const centerBuffer = upload(\n centers.length > 0 ? centers : new Uint32Array(1),\n STORAGE\n );\n const paramsBuffer = upload(params, UNIFORM);\n\n const scoreBuffer = device.createBuffer({\n size: count * 4,\n usage: 0x80 | COPY_SRC\n });\n const readBuffer = device.createBuffer({\n size: count * 4,\n usage: COPY_DST | MAP_READ\n });\n\n const bindGroup = device.createBindGroup({\n layout: pipeline.getBindGroupLayout(0),\n entries: [\n { binding: 0, resource: { buffer: bitsBuffer } },\n { binding: 1, resource: { buffer: transformBuffer } },\n { binding: 2, resource: { buffer: centerBuffer } },\n { binding: 3, resource: { buffer: scoreBuffer } },\n { binding: 4, resource: { buffer: paramsBuffer } }\n ]\n });\n\n const encoder = device.createCommandEncoder();\n const pass = encoder.beginComputePass();\n pass.setPipeline(pipeline);\n pass.setBindGroup(0, bindGroup);\n pass.dispatchWorkgroups(Math.ceil(count / 64));\n pass.end();\n encoder.copyBufferToBuffer(scoreBuffer, 0, readBuffer, 0, count * 4);\n device.queue.submit([encoder.finish()]);\n\n await readBuffer.mapAsync(MAP_READ);\n const scores = new Int32Array(readBuffer.getMappedRange().slice(0));\n readBuffer.unmap();\n\n for (const buffer of [\n bitsBuffer,\n transformBuffer,\n centerBuffer,\n paramsBuffer,\n scoreBuffer,\n readBuffer\n ]) {\n buffer.destroy();\n }\n\n return scores;\n },\n\n destroy: () => {\n device.destroy();\n }\n };\n};\n\n/**\n * The subset of WebGPU this module uses.\n *\n * Declared locally rather than pulled from a types package: this library ships\n * zero dependencies, and `@webgpu/types` would be one for a surface this\n * small.\n */\ninterface GpuBufferLike {\n getMappedRange(): ArrayBuffer;\n unmap(): void;\n destroy(): void;\n mapAsync(mode: number): Promise<void>;\n}\n\ninterface GpuDeviceLike {\n createShaderModule(descriptor: { code: string }): unknown;\n createComputePipeline(descriptor: {\n layout: string;\n compute: { module: unknown; entryPoint: string };\n }): { getBindGroupLayout(index: number): unknown };\n createBuffer(descriptor: {\n size: number;\n usage: number;\n mappedAtCreation?: boolean;\n }): GpuBufferLike;\n createBindGroup(descriptor: {\n layout: unknown;\n entries: Array<{ binding: number; resource: { buffer: GpuBufferLike } }>;\n }): unknown;\n createCommandEncoder(): {\n beginComputePass(): {\n setPipeline(pipeline: unknown): void;\n setBindGroup(index: number, group: unknown): void;\n dispatchWorkgroups(count: number): void;\n end(): void;\n };\n copyBufferToBuffer(\n source: GpuBufferLike,\n sourceOffset: number,\n destination: GpuBufferLike,\n destinationOffset: number,\n size: number\n ): void;\n finish(): unknown;\n };\n queue: { submit(buffers: unknown[]): void };\n destroy(): void;\n}\n","/**\n * A scan session, as a state machine.\n *\n * Modeled the way `machine.ts` models the device flow: states, events, and a\n * declarative transition table, hand-rolled so this stays dependency-free.\n *\n * Deliberately NOT applied to the decode pipeline itself. Binarize → locate →\n * extract → decode is a fallible sequential pipeline, not a machine: each\n * stage runs once per frame and either produces a value or does not. Modeling\n * it as states would add ceremony without making a single illegal move\n * impossible, which is the only thing a transition table buys.\n *\n * What IS a machine is the session around it — permission, camera lifecycle,\n * pause and resume — and that part is identical whether the frames are being\n * searched for a QR, a DataMatrix, or an Aztec code. So it lives here,\n * symbology-agnostic, and the decoders plug into it.\n */\n\n/**\n * Lifecycle of one scanning session.\n *\n * `scanning` is the only state that consumes frames. `decoded` is terminal\n * rather than a return to `scanning`: a scanner that kept reading after a\n * successful decode would fire twice on the same symbol, and every consumer\n * would have to debounce it. Restarting is explicit.\n *\n * `denied` is separate from `failed` because they are different conversations\n * with the user — one asks them to change a browser setting, the other is a\n * fault they cannot act on. Collapsing them is how a permission prompt ends up\n * reported as \"something went wrong\".\n */\nexport type ScanState =\n | `idle`\n | `starting`\n | `scanning`\n | `paused`\n | `decoded`\n | `denied`\n | `failed`\n | `stopped`;\n\n/** Why a scan session ended without a result. */\nexport type ScanFailure =\n /** No camera on this device, or none matching the requested facing mode. */\n | `no-camera`\n /** The page is not a secure context, so `getUserMedia` is unavailable. */\n | `insecure-context`\n /** The camera was lost mid-session — unplugged, or claimed by another app. */\n | `camera-lost`\n /** The decoder itself threw. Distinct from \"no code in this frame\". */\n | `decoder-error`;\n\n/** Events driving a scan session. */\nexport type ScanEvent =\n /** Consumer asked to begin. */\n | { type: `START` }\n /** Camera acquired and frames are flowing. */\n | { type: `READY` }\n /** A frame decoded successfully. Terminal — restarting is explicit. */\n | { type: `DECODE`; value: string }\n /** The user refused camera access. */\n | { type: `DENY` }\n /** Something broke that the user cannot act on. */\n | { type: `FAIL`; reason: ScanFailure }\n /** Tab hidden, or the consumer suspended scanning. */\n | { type: `PAUSE` }\n /** Visible again. */\n | { type: `RESUME` }\n /** Consumer tore the session down. */\n | { type: `STOP` };\n\n/**\n * Transitions, as a table.\n *\n * Everything absent is illegal by construction. That is what makes a `DECODE`\n * arriving after `STOP` a no-op rather than a race — a real hazard here,\n * because a decode in flight when the camera stops would otherwise resolve\n * into a torn-down session.\n */\nconst SCAN_TRANSITIONS: {\n readonly [S in ScanState]?: {\n readonly [E in ScanEvent[`type`]]?: ScanState;\n };\n} = {\n idle: {\n START: `starting`\n },\n // Permission is resolved here, so this is the only state that can be denied.\n starting: {\n READY: `scanning`,\n DENY: `denied`,\n FAIL: `failed`,\n STOP: `stopped`\n },\n scanning: {\n DECODE: `decoded`,\n PAUSE: `paused`,\n FAIL: `failed`,\n STOP: `stopped`\n },\n // No DECODE here: a paused session is not reading frames, and accepting one\n // would mean a frame queued before the pause could resolve after it.\n paused: {\n RESUME: `scanning`,\n FAIL: `failed`,\n STOP: `stopped`\n }\n};\n\n/** Whether `event` would move `state`. */\nexport const canTransitionScan = (\n state: ScanState,\n event: ScanEvent[`type`]\n): boolean => SCAN_TRANSITIONS[state]?.[event] !== undefined;\n\n/** Apply an event. Unknown events for the current state are no-ops. */\nexport const scanTransition = (state: ScanState, event: ScanEvent): ScanState =>\n SCAN_TRANSITIONS[state]?.[event.type] ?? state;\n\n/**\n * Terminal states accept no further events.\n *\n * Derived from the table rather than listed, so a new state cannot be added\n * without its terminality following automatically.\n */\nexport const isScanSettled = (state: ScanState): boolean =>\n SCAN_TRANSITIONS[state] === undefined;\n"],"mappings":"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"}
|