@scanly/core 2.0.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.
Files changed (226) hide show
  1. package/README.md +3 -0
  2. package/dist/barcode/contracts.d.ts +44 -0
  3. package/dist/barcode/contracts.d.ts.map +1 -0
  4. package/dist/barcode/contracts.js +33 -0
  5. package/dist/barcode/contracts.js.map +1 -0
  6. package/dist/barcode/format-selection.d.ts +12 -0
  7. package/dist/barcode/format-selection.d.ts.map +1 -0
  8. package/dist/barcode/format-selection.js +32 -0
  9. package/dist/barcode/format-selection.js.map +1 -0
  10. package/dist/barcode/format.d.ts +7 -0
  11. package/dist/barcode/format.d.ts.map +1 -0
  12. package/dist/barcode/format.js +20 -0
  13. package/dist/barcode/format.js.map +1 -0
  14. package/dist/barcode/index.d.ts +5 -0
  15. package/dist/barcode/index.d.ts.map +1 -0
  16. package/dist/barcode/index.js +5 -0
  17. package/dist/barcode/index.js.map +1 -0
  18. package/dist/barcode/retail.d.ts +16 -0
  19. package/dist/barcode/retail.d.ts.map +1 -0
  20. package/dist/barcode/retail.js +95 -0
  21. package/dist/barcode/retail.js.map +1 -0
  22. package/dist/contracts/engine.d.ts +84 -0
  23. package/dist/contracts/engine.d.ts.map +1 -0
  24. package/dist/contracts/engine.js +2 -0
  25. package/dist/contracts/engine.js.map +1 -0
  26. package/dist/contracts/errors.d.ts +17 -0
  27. package/dist/contracts/errors.d.ts.map +1 -0
  28. package/dist/contracts/errors.js +31 -0
  29. package/dist/contracts/errors.js.map +1 -0
  30. package/dist/contracts/frame.d.ts +37 -0
  31. package/dist/contracts/frame.d.ts.map +1 -0
  32. package/dist/contracts/frame.js +77 -0
  33. package/dist/contracts/frame.js.map +1 -0
  34. package/dist/contracts/operator.d.ts +46 -0
  35. package/dist/contracts/operator.d.ts.map +1 -0
  36. package/dist/contracts/operator.js +14 -0
  37. package/dist/contracts/operator.js.map +1 -0
  38. package/dist/contracts/result.d.ts +135 -0
  39. package/dist/contracts/result.d.ts.map +1 -0
  40. package/dist/contracts/result.js +2 -0
  41. package/dist/contracts/result.js.map +1 -0
  42. package/dist/contracts/validator.d.ts +15 -0
  43. package/dist/contracts/validator.d.ts.map +1 -0
  44. package/dist/contracts/validator.js +2 -0
  45. package/dist/contracts/validator.js.map +1 -0
  46. package/dist/index.d.ts +29 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +28 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/industrial/candidate-region.d.ts +15 -0
  51. package/dist/industrial/candidate-region.d.ts.map +1 -0
  52. package/dist/industrial/candidate-region.js +108 -0
  53. package/dist/industrial/candidate-region.js.map +1 -0
  54. package/dist/industrial/coordinate-transform.d.ts +9 -0
  55. package/dist/industrial/coordinate-transform.d.ts.map +1 -0
  56. package/dist/industrial/coordinate-transform.js +42 -0
  57. package/dist/industrial/coordinate-transform.js.map +1 -0
  58. package/dist/industrial/decode-candidate-conflict.d.ts +8 -0
  59. package/dist/industrial/decode-candidate-conflict.d.ts.map +1 -0
  60. package/dist/industrial/decode-candidate-conflict.js +97 -0
  61. package/dist/industrial/decode-candidate-conflict.js.map +1 -0
  62. package/dist/industrial/difficulty-diagnosis.d.ts +15 -0
  63. package/dist/industrial/difficulty-diagnosis.d.ts.map +1 -0
  64. package/dist/industrial/difficulty-diagnosis.js +291 -0
  65. package/dist/industrial/difficulty-diagnosis.js.map +1 -0
  66. package/dist/industrial/index.d.ts +13 -0
  67. package/dist/industrial/index.d.ts.map +1 -0
  68. package/dist/industrial/index.js +13 -0
  69. package/dist/industrial/index.js.map +1 -0
  70. package/dist/industrial/memory.d.ts +28 -0
  71. package/dist/industrial/memory.d.ts.map +1 -0
  72. package/dist/industrial/memory.js +65 -0
  73. package/dist/industrial/memory.js.map +1 -0
  74. package/dist/industrial/pixels.d.ts +44 -0
  75. package/dist/industrial/pixels.d.ts.map +1 -0
  76. package/dist/industrial/pixels.js +295 -0
  77. package/dist/industrial/pixels.js.map +1 -0
  78. package/dist/industrial/recovery-pipeline.d.ts +26 -0
  79. package/dist/industrial/recovery-pipeline.d.ts.map +1 -0
  80. package/dist/industrial/recovery-pipeline.js +149 -0
  81. package/dist/industrial/recovery-pipeline.js.map +1 -0
  82. package/dist/industrial/recovery-planner.d.ts +10 -0
  83. package/dist/industrial/recovery-planner.d.ts.map +1 -0
  84. package/dist/industrial/recovery-planner.js +107 -0
  85. package/dist/industrial/recovery-planner.js.map +1 -0
  86. package/dist/industrial/recovery-route-registry.d.ts +12 -0
  87. package/dist/industrial/recovery-route-registry.d.ts.map +1 -0
  88. package/dist/industrial/recovery-route-registry.js +15 -0
  89. package/dist/industrial/recovery-route-registry.js.map +1 -0
  90. package/dist/industrial/recovery-routes.d.ts +89 -0
  91. package/dist/industrial/recovery-routes.d.ts.map +1 -0
  92. package/dist/industrial/recovery-routes.js +280 -0
  93. package/dist/industrial/recovery-routes.js.map +1 -0
  94. package/dist/industrial/scan-evidence.d.ts +5 -0
  95. package/dist/industrial/scan-evidence.d.ts.map +1 -0
  96. package/dist/industrial/scan-evidence.js +63 -0
  97. package/dist/industrial/scan-evidence.js.map +1 -0
  98. package/dist/industrial/scanner-diagnostics.d.ts +19 -0
  99. package/dist/industrial/scanner-diagnostics.d.ts.map +1 -0
  100. package/dist/industrial/scanner-diagnostics.js +35 -0
  101. package/dist/industrial/scanner-diagnostics.js.map +1 -0
  102. package/dist/industrial/types.d.ts +189 -0
  103. package/dist/industrial/types.d.ts.map +1 -0
  104. package/dist/industrial/types.js +2 -0
  105. package/dist/industrial/types.js.map +1 -0
  106. package/dist/node.d.ts +2 -0
  107. package/dist/node.d.ts.map +1 -0
  108. package/dist/node.js +2 -0
  109. package/dist/node.js.map +1 -0
  110. package/dist/qr/candidate-dedupe.d.ts +6 -0
  111. package/dist/qr/candidate-dedupe.d.ts.map +1 -0
  112. package/dist/qr/candidate-dedupe.js +27 -0
  113. package/dist/qr/candidate-dedupe.js.map +1 -0
  114. package/dist/qr/candidate-generation.d.ts +46 -0
  115. package/dist/qr/candidate-generation.d.ts.map +1 -0
  116. package/dist/qr/candidate-generation.js +277 -0
  117. package/dist/qr/candidate-generation.js.map +1 -0
  118. package/dist/qr/decode-pipeline.d.ts +14 -0
  119. package/dist/qr/decode-pipeline.d.ts.map +1 -0
  120. package/dist/qr/decode-pipeline.js +615 -0
  121. package/dist/qr/decode-pipeline.js.map +1 -0
  122. package/dist/qr/geometry.d.ts +24 -0
  123. package/dist/qr/geometry.d.ts.map +1 -0
  124. package/dist/qr/geometry.js +64 -0
  125. package/dist/qr/geometry.js.map +1 -0
  126. package/dist/qr/grayscale.d.ts +13 -0
  127. package/dist/qr/grayscale.d.ts.map +1 -0
  128. package/dist/qr/grayscale.js +46 -0
  129. package/dist/qr/grayscale.js.map +1 -0
  130. package/dist/qr/image-loader-node.d.ts +4 -0
  131. package/dist/qr/image-loader-node.d.ts.map +1 -0
  132. package/dist/qr/image-loader-node.js +26 -0
  133. package/dist/qr/image-loader-node.js.map +1 -0
  134. package/dist/qr/index.d.ts +15 -0
  135. package/dist/qr/index.d.ts.map +1 -0
  136. package/dist/qr/index.js +15 -0
  137. package/dist/qr/index.js.map +1 -0
  138. package/dist/qr/jsqr-decoder.d.ts +10 -0
  139. package/dist/qr/jsqr-decoder.d.ts.map +1 -0
  140. package/dist/qr/jsqr-decoder.js +18 -0
  141. package/dist/qr/jsqr-decoder.js.map +1 -0
  142. package/dist/qr/preprocess.d.ts +18 -0
  143. package/dist/qr/preprocess.d.ts.map +1 -0
  144. package/dist/qr/preprocess.js +165 -0
  145. package/dist/qr/preprocess.js.map +1 -0
  146. package/dist/qr/region-detection.d.ts +25 -0
  147. package/dist/qr/region-detection.d.ts.map +1 -0
  148. package/dist/qr/region-detection.js +129 -0
  149. package/dist/qr/region-detection.js.map +1 -0
  150. package/dist/qr/result-normalizer.d.ts +9 -0
  151. package/dist/qr/result-normalizer.d.ts.map +1 -0
  152. package/dist/qr/result-normalizer.js +70 -0
  153. package/dist/qr/result-normalizer.js.map +1 -0
  154. package/dist/qr/rotate.d.ts +5 -0
  155. package/dist/qr/rotate.d.ts.map +1 -0
  156. package/dist/qr/rotate.js +51 -0
  157. package/dist/qr/rotate.js.map +1 -0
  158. package/dist/qr/types.d.ts +186 -0
  159. package/dist/qr/types.d.ts.map +1 -0
  160. package/dist/qr/types.js +67 -0
  161. package/dist/qr/types.js.map +1 -0
  162. package/dist/qr/zxing-decoder.d.ts +8 -0
  163. package/dist/qr/zxing-decoder.d.ts.map +1 -0
  164. package/dist/qr/zxing-decoder.js +33 -0
  165. package/dist/qr/zxing-decoder.js.map +1 -0
  166. package/dist/runtime/artifacts.d.ts +16 -0
  167. package/dist/runtime/artifacts.d.ts.map +1 -0
  168. package/dist/runtime/artifacts.js +52 -0
  169. package/dist/runtime/artifacts.js.map +1 -0
  170. package/dist/runtime/builtin-operators.d.ts +12 -0
  171. package/dist/runtime/builtin-operators.d.ts.map +1 -0
  172. package/dist/runtime/builtin-operators.js +487 -0
  173. package/dist/runtime/builtin-operators.js.map +1 -0
  174. package/dist/runtime/capabilities.d.ts +32 -0
  175. package/dist/runtime/capabilities.d.ts.map +1 -0
  176. package/dist/runtime/capabilities.js +18 -0
  177. package/dist/runtime/capabilities.js.map +1 -0
  178. package/dist/runtime/engine-registry.d.ts +19 -0
  179. package/dist/runtime/engine-registry.d.ts.map +1 -0
  180. package/dist/runtime/engine-registry.js +136 -0
  181. package/dist/runtime/engine-registry.js.map +1 -0
  182. package/dist/runtime/execution-budget.d.ts +25 -0
  183. package/dist/runtime/execution-budget.d.ts.map +1 -0
  184. package/dist/runtime/execution-budget.js +41 -0
  185. package/dist/runtime/execution-budget.js.map +1 -0
  186. package/dist/runtime/frame-lease.d.ts +10 -0
  187. package/dist/runtime/frame-lease.d.ts.map +1 -0
  188. package/dist/runtime/frame-lease.js +17 -0
  189. package/dist/runtime/frame-lease.js.map +1 -0
  190. package/dist/runtime/frame-normalization.d.ts +13 -0
  191. package/dist/runtime/frame-normalization.d.ts.map +1 -0
  192. package/dist/runtime/frame-normalization.js +21 -0
  193. package/dist/runtime/frame-normalization.js.map +1 -0
  194. package/dist/runtime/memory-budget.d.ts +32 -0
  195. package/dist/runtime/memory-budget.d.ts.map +1 -0
  196. package/dist/runtime/memory-budget.js +109 -0
  197. package/dist/runtime/memory-budget.js.map +1 -0
  198. package/dist/runtime/operator-registry.d.ts +11 -0
  199. package/dist/runtime/operator-registry.d.ts.map +1 -0
  200. package/dist/runtime/operator-registry.js +19 -0
  201. package/dist/runtime/operator-registry.js.map +1 -0
  202. package/dist/runtime/result-policies.d.ts +21 -0
  203. package/dist/runtime/result-policies.d.ts.map +1 -0
  204. package/dist/runtime/result-policies.js +62 -0
  205. package/dist/runtime/result-policies.js.map +1 -0
  206. package/dist/runtime/router.d.ts +45 -0
  207. package/dist/runtime/router.d.ts.map +1 -0
  208. package/dist/runtime/router.js +202 -0
  209. package/dist/runtime/router.js.map +1 -0
  210. package/dist/runtime/scenario-compiler.d.ts +32 -0
  211. package/dist/runtime/scenario-compiler.d.ts.map +1 -0
  212. package/dist/runtime/scenario-compiler.js +131 -0
  213. package/dist/runtime/scenario-compiler.js.map +1 -0
  214. package/dist/runtime/scenario-runtime.d.ts +4 -0
  215. package/dist/runtime/scenario-runtime.d.ts.map +1 -0
  216. package/dist/runtime/scenario-runtime.js +32 -0
  217. package/dist/runtime/scenario-runtime.js.map +1 -0
  218. package/dist/runtime/session.d.ts +40 -0
  219. package/dist/runtime/session.d.ts.map +1 -0
  220. package/dist/runtime/session.js +117 -0
  221. package/dist/runtime/session.js.map +1 -0
  222. package/dist/runtime/validator-registry.d.ts +11 -0
  223. package/dist/runtime/validator-registry.d.ts.map +1 -0
  224. package/dist/runtime/validator-registry.js +16 -0
  225. package/dist/runtime/validator-registry.js.map +1 -0
  226. package/package.json +39 -0
@@ -0,0 +1,615 @@
1
+ import { DEFAULT_PIPELINE_CONFIG, validatePipelineConfig } from "./types.js";
2
+ import { dedupeCandidates } from "./candidate-dedupe.js";
3
+ import { generateCandidates } from "./candidate-generation.js";
4
+ import { applyPreprocess } from "./preprocess.js";
5
+ import { rotateBuffer } from "./rotate.js";
6
+ import { dedupeResults, normalizePayload } from "./result-normalizer.js";
7
+ import { mapAndClampPoints, multiplyMatrices, rotatedToSourceMatrix } from "./geometry.js";
8
+ import { ExecutionBudget, monotonicNow } from "../runtime/execution-budget.js";
9
+ function mergeConfig(partial) {
10
+ return { ...DEFAULT_PIPELINE_CONFIG, ...partial };
11
+ }
12
+ function throwIfAborted(signal) {
13
+ if (signal?.aborted) {
14
+ const err = new Error("cancelled");
15
+ err.name = "AbortError";
16
+ throw err;
17
+ }
18
+ }
19
+ function timedOut(start, timeoutMs, now) {
20
+ return now() - start >= timeoutMs;
21
+ }
22
+ /** Yield to the event loop so AbortSignal and UI can run between attempts. */
23
+ async function cooperativeYield(signal) {
24
+ await new Promise((resolve) => setTimeout(resolve, 0));
25
+ throwIfAborted(signal);
26
+ }
27
+ function decoderEnabled(config, name) {
28
+ return config.decoders?.order.includes(name) ?? false;
29
+ }
30
+ function allRequiredFound(payloads, required) {
31
+ if (!required?.length)
32
+ return false;
33
+ return required.every((p) => payloads.includes(p));
34
+ }
35
+ export function buildAttemptPlan(preprocessOrder, rotations, budgetRemaining) {
36
+ const plan = [];
37
+ for (const preprocessing of preprocessOrder) {
38
+ plan.push({ preprocessing, rotation: 0 });
39
+ }
40
+ const rotatedPreprocess = ["original", "contrast", "invert", "otsu"];
41
+ for (const rotation of rotations) {
42
+ if (rotation === 0)
43
+ continue;
44
+ for (const preprocessing of rotatedPreprocess) {
45
+ plan.push({ preprocessing, rotation });
46
+ }
47
+ }
48
+ return plan.slice(0, Math.max(0, budgetRemaining));
49
+ }
50
+ const DIAGNOSTIC_STATUS_PRIORITY = {
51
+ "not-found": 0, cancelled: 1, unsupported: 2, timeout: 3, "initialization-failure": 4, "execution-failure": 5, success: 6,
52
+ };
53
+ function recordDiagnostic(ctx, engineId, status, elapsedMs, resultCount, message, errorCode) {
54
+ let diagnostic = ctx.diagnostics.find((entry) => entry.engineId === engineId);
55
+ if (!diagnostic) {
56
+ if (ctx.diagnostics.length >= 16)
57
+ return;
58
+ diagnostic = { engineId, engineVersion: ctx.engineExecutor.versions?.[engineId] ?? "unknown", status, elapsedMs: 0, attemptCount: 0, resultCount: 0 };
59
+ ctx.diagnostics.push(diagnostic);
60
+ }
61
+ diagnostic.elapsedMs += Math.max(0, elapsedMs);
62
+ diagnostic.attemptCount += 1;
63
+ diagnostic.resultCount += Math.max(0, resultCount);
64
+ if (DIAGNOSTIC_STATUS_PRIORITY[status] >= DIAGNOSTIC_STATUS_PRIORITY[diagnostic.status])
65
+ diagnostic.status = status;
66
+ if (message && status !== "not-found")
67
+ diagnostic.message = message.slice(0, 256);
68
+ if (errorCode)
69
+ diagnostic.errorCode = errorCode.slice(0, 64);
70
+ }
71
+ class AttemptImageCache {
72
+ maxEntries;
73
+ maxBytes;
74
+ memoryBudget;
75
+ entries = new Map();
76
+ ids = new WeakMap();
77
+ sequence = 0;
78
+ retainedBytes = 0;
79
+ leases = [];
80
+ constructor(maxEntries, maxBytes, memoryBudget) {
81
+ this.maxEntries = maxEntries;
82
+ this.maxBytes = maxBytes;
83
+ this.memoryBudget = memoryBudget;
84
+ }
85
+ key(buffer, preprocessing, rotation) {
86
+ let id = this.ids.get(buffer);
87
+ if (id === undefined) {
88
+ id = ++this.sequence;
89
+ this.ids.set(buffer, id);
90
+ }
91
+ return `${id}:${preprocessing}:${rotation}`;
92
+ }
93
+ getOrCreate(key, create) {
94
+ const existing = this.entries.get(key);
95
+ if (existing)
96
+ return existing;
97
+ const value = create();
98
+ const bytes = value.data.byteLength;
99
+ const sharedCapacity = !this.memoryBudget || this.memoryBudget.remainingBytes >= bytes;
100
+ if (this.entries.size < this.maxEntries && this.retainedBytes + bytes <= this.maxBytes && sharedCapacity) {
101
+ const lease = this.memoryBudget?.reserve(bytes, `preprocess-cache:${key}`, "cache");
102
+ this.entries.set(key, value);
103
+ this.retainedBytes += bytes;
104
+ if (lease)
105
+ this.leases.push(lease);
106
+ }
107
+ return value;
108
+ }
109
+ dispose() {
110
+ for (const lease of this.leases)
111
+ lease.release();
112
+ this.leases.length = 0;
113
+ this.entries.clear();
114
+ this.retainedBytes = 0;
115
+ }
116
+ }
117
+ function processedFor(ctx, candidate, preprocessing, rotation) {
118
+ const key = ctx.intermediateCache.key(candidate.buffer, preprocessing, rotation);
119
+ return ctx.intermediateCache.getOrCreate(key, () => {
120
+ ctx.budget.throwIfExceeded("preprocessing");
121
+ const preprocessStarted = ctx.now();
122
+ let processed = applyPreprocess(candidate.buffer, preprocessing, ctx.budget);
123
+ ctx.phaseTiming.preprocessMs += ctx.now() - preprocessStarted;
124
+ if (rotation !== 0) {
125
+ const rotationStarted = ctx.now();
126
+ processed = rotateBuffer(processed, rotation, ctx.budget);
127
+ ctx.phaseTiming.rotationMs += ctx.now() - rotationStarted;
128
+ }
129
+ return processed;
130
+ });
131
+ }
132
+ function shouldStopMultiple(ctx) {
133
+ const payloads = ctx.found.map((r) => r.payload);
134
+ const { config } = ctx;
135
+ // A stall can end collection only after at least one real decode.
136
+ if (payloads.length === 0)
137
+ return false;
138
+ if (config.requiredPayloads?.length) {
139
+ // Known completeness contracts must not stop on a heuristic stall.
140
+ return allRequiredFound(payloads, config.requiredPayloads);
141
+ }
142
+ if (config.expectedResultCount && payloads.length >= config.expectedResultCount) {
143
+ return true;
144
+ }
145
+ if (!config.findMultiple)
146
+ return false;
147
+ if (payloads.length >= config.maxMultipleResults)
148
+ return true;
149
+ if (config.multiCodeStallPolicy) {
150
+ const policy = config.multiCodeStallPolicy;
151
+ const attemptsWithoutNew = ctx.attempts.length - ctx.attemptsAtLastNewResult;
152
+ const coverage = ctx.primaryCandidateCount ? ctx.visitedPrimaryCandidates.size / ctx.primaryCandidateCount : 1;
153
+ const primaryComplete = !policy.requireAllPrimaryCandidatesVisited || ctx.visitedPrimaryCandidates.size >= ctx.primaryCandidateCount;
154
+ if (attemptsWithoutNew >= policy.maximumAttemptsWithoutNewResult && coverage >= policy.minimumCandidateCoverageBeforeStop && primaryComplete)
155
+ return true;
156
+ }
157
+ const stall = config.stallCandidateLimit ?? 8;
158
+ const scaledStall = stall + Math.max(0, payloads.length - 1) * 3;
159
+ if (ctx.candidatesSinceNew >= scaledStall)
160
+ return true;
161
+ return false;
162
+ }
163
+ function pushResult(ctx, code) {
164
+ const prevSize = ctx.found.length;
165
+ ctx.found.push(code);
166
+ const unique = dedupeResults(ctx.found, ctx.config.resultDeduplication);
167
+ ctx.found.length = 0;
168
+ ctx.found.push(...unique);
169
+ if (unique.length > prevSize) {
170
+ ctx.candidatesSinceNew = 0;
171
+ ctx.attemptsAtLastNewResult = ctx.attempts.length;
172
+ }
173
+ if (!ctx.config.findMultiple) {
174
+ return successOutcome(unique, ctx.attempts, ctx.start, false, ctx.phaseTiming, ctx.diagnostics);
175
+ }
176
+ if (shouldStopMultiple(ctx)) {
177
+ return successOutcome(unique, ctx.attempts, ctx.start, false, ctx.phaseTiming, ctx.diagnostics);
178
+ }
179
+ return null;
180
+ }
181
+ function onCandidateDone(ctx, hadNewResult) {
182
+ if (!hadNewResult)
183
+ ctx.candidatesSinceNew += 1;
184
+ }
185
+ async function tryEngine(ctx, engineId, candidate, preprocessing, rotation) {
186
+ throwIfAborted(ctx.signal);
187
+ ctx.budget.throwIfExceeded("decoder-attempt");
188
+ if (timedOut(ctx.start, ctx.config.timeoutMs, ctx.now))
189
+ return null;
190
+ if (ctx.attempts.length >= ctx.config.maxAttempts)
191
+ return null;
192
+ if (!decoderEnabled(ctx.config, engineId))
193
+ return null;
194
+ if (ctx.failedEngineIds.has(engineId))
195
+ return null;
196
+ if (!ctx.budget.tryConsumeAttempt())
197
+ return null;
198
+ if (candidate.candidateIndex >= 0 && candidate.candidateIndex < 100)
199
+ ctx.visitedPrimaryCandidates.add(candidate.candidateIndex);
200
+ const t0 = ctx.now();
201
+ const processed = processedFor(ctx, candidate, preprocessing, rotation);
202
+ const t1 = ctx.now();
203
+ let decoded;
204
+ try {
205
+ decoded = await ctx.engineExecutor.decode(engineId, processed, {
206
+ formats: ctx.config.formats,
207
+ signal: ctx.signal,
208
+ findMultiple: ctx.config.findMultiple,
209
+ inversion: preprocessing === "invert" ? "inverted" : "original",
210
+ });
211
+ }
212
+ catch (error) {
213
+ const nestedCode = error && typeof error === "object" && "error" in error ? String(error.error?.code ?? "") : "";
214
+ const directCode = error && typeof error === "object" && "code" in error ? String(error.code ?? "") : "";
215
+ const code = nestedCode || directCode;
216
+ const status = code.includes("initialization") ? "initialization-failure" : code === "cancelled" ? "cancelled" : "execution-failure";
217
+ recordDiagnostic(ctx, engineId, status, ctx.now() - t1, 0, error instanceof Error ? error.message : String(error));
218
+ if (ctx.config.decoders.failurePolicy === "success-wins" && status !== "cancelled") {
219
+ ctx.failedEngineIds.add(engineId);
220
+ return null;
221
+ }
222
+ throw error;
223
+ }
224
+ const engineElapsed = ctx.now() - t1;
225
+ const engineTiming = (ctx.phaseTiming.engineMs ??= {});
226
+ engineTiming[engineId] = (engineTiming[engineId] ?? 0) + engineElapsed;
227
+ ctx.attempts.push({
228
+ candidateIndex: candidate.candidateIndex,
229
+ candidateScore: candidate.candidateScore,
230
+ cropPadding: candidate.cropPadding,
231
+ preprocessing,
232
+ scale: candidate.scale,
233
+ scaleFactor: candidate.scaleFactor,
234
+ rotation,
235
+ decoder: engineId,
236
+ elapsedMs: ctx.now() - t0,
237
+ success: decoded.ok && decoded.results.length > 0,
238
+ });
239
+ ctx.onProgress?.({ attemptCount: ctx.attempts.length });
240
+ if (!decoded.ok) {
241
+ const status = decoded.category === "not-found" ? "not-found" : decoded.category === "unsupported-format" || decoded.category === "invalid-input" ? "unsupported" : decoded.category === "initialization" ? "initialization-failure" : decoded.category === "execution" ? "execution-failure" : decoded.category;
242
+ recordDiagnostic(ctx, engineId, status, engineElapsed, 0, decoded.message, decoded.code);
243
+ if (decoded.category === "not-found")
244
+ return null;
245
+ if (ctx.config.decoders.failurePolicy === "success-wins"
246
+ && (decoded.category === "initialization" || decoded.category === "execution" || decoded.category === "unsupported-format")) {
247
+ ctx.failedEngineIds.add(engineId);
248
+ return null;
249
+ }
250
+ const reasons = {
251
+ "unsupported-format": "unsupported_format",
252
+ "invalid-input": "invalid_image",
253
+ initialization: "engine_initialization_failure",
254
+ execution: "engine_execution_failure",
255
+ cancelled: "cancelled",
256
+ timeout: "timeout",
257
+ };
258
+ const reason = reasons[decoded.category];
259
+ const error = Object.assign(new Error(decoded.message), { code: reason, name: reason === "cancelled" ? "AbortError" : reason === "timeout" ? "TimeoutError" : "EngineError" });
260
+ throw error;
261
+ }
262
+ recordDiagnostic(ctx, engineId, "success", engineElapsed, decoded.results.length);
263
+ let completed = null;
264
+ for (const result of decoded.results) {
265
+ // A plugin must never widen a caller's explicit format selection at the result boundary.
266
+ if (!ctx.config.formats.includes(result.format))
267
+ continue;
268
+ const payload = normalizePayload(result.text);
269
+ if (!payload)
270
+ continue;
271
+ const candidateToFrame = multiplyMatrices(candidate.transform.matrix, rotatedToSourceMatrix(rotation, candidate.buffer.width, candidate.buffer.height));
272
+ const cornerPoints = result.cornerPoints
273
+ ? mapAndClampPoints(result.cornerPoints, candidateToFrame, candidate.transform.targetWidth, candidate.transform.targetHeight)
274
+ : undefined;
275
+ const symbolOrientation = result.orientation === undefined
276
+ ? undefined
277
+ : ((result.orientation - rotation) % 360 + 360) % 360;
278
+ completed = pushResult(ctx, {
279
+ payload,
280
+ format: result.format,
281
+ rawBytes: result.rawBytes,
282
+ decoder: engineId,
283
+ engineVersion: ctx.engineExecutor.versions?.[engineId],
284
+ engineMetadata: result.engineMetadata,
285
+ cornerPoints,
286
+ symbologyIdentifier: result.symbologyIdentifier,
287
+ isGs1: result.isGs1,
288
+ metadata: result.metadata,
289
+ preprocessing,
290
+ candidateIndex: candidate.candidateIndex,
291
+ scale: candidate.scale,
292
+ rotation,
293
+ symbolOrientation,
294
+ cropPadding: candidate.cropPadding,
295
+ attemptIndex: ctx.attempts.length - 1,
296
+ foundAtMs: ctx.now() - ctx.start,
297
+ }) ?? completed;
298
+ }
299
+ return completed;
300
+ }
301
+ export function orderCandidates(candidates, findMultiple) {
302
+ const earlyFull = candidates.filter((c) => c.cropPadding === "full");
303
+ const splits = candidates.filter((c) => c.candidateIndex >= 100);
304
+ const crops = candidates.filter((c) => c.cropPadding !== "full" && c.candidateIndex < 100);
305
+ return findMultiple
306
+ ? [...crops.slice(0, 2), ...splits, ...earlyFull.slice(0, 1), ...crops.slice(2), ...earlyFull.slice(1)]
307
+ : [...crops.slice(0, 3), ...earlyFull.slice(0, 1), ...crops.slice(3), ...earlyFull.slice(1)];
308
+ }
309
+ /**
310
+ * Breadth-first decode pipeline with dedupe, adaptive multiple stop, and fail-fast.
311
+ */
312
+ export async function decodePixelBuffer(image, options = {}) {
313
+ const config = mergeConfig(options.config);
314
+ const now = options.executionBudget ? () => options.executionBudget.now() : monotonicNow;
315
+ const start = now();
316
+ const attempts = [];
317
+ const found = [];
318
+ const diagnostics = [];
319
+ const signal = options.signal;
320
+ const engineExecutor = options.engineExecutor;
321
+ const phaseTiming = {
322
+ candidateGenerationMs: 0,
323
+ preprocessMs: 0,
324
+ rotationMs: 0,
325
+ };
326
+ const fail = (reason, message, cancelled = false) => ({
327
+ ok: false,
328
+ reason,
329
+ message,
330
+ attempts,
331
+ attemptCount: attempts.length,
332
+ elapsedMs: now() - start,
333
+ cancelled,
334
+ engineDiagnostics: diagnostics,
335
+ phaseTiming,
336
+ });
337
+ const ctx = {
338
+ config,
339
+ signal,
340
+ start,
341
+ attempts,
342
+ found,
343
+ onStage: options.onStage,
344
+ onProgress: options.onProgress,
345
+ phaseTiming,
346
+ candidatesSinceNew: 0,
347
+ attemptsAtLastNewResult: 0,
348
+ visitedPrimaryCandidates: new Set(),
349
+ primaryCandidateCount: 0,
350
+ failFast: false,
351
+ intermediateCache: new AttemptImageCache(config.maxIntermediateAllocations ?? 24, config.maxIntermediateBytes ?? 64 * 1024 * 1024, options.memoryBudget),
352
+ engineExecutor: engineExecutor,
353
+ budget: options.executionBudget ?? new ExecutionBudget({
354
+ signal,
355
+ deadlineMs: start + config.timeoutMs,
356
+ now,
357
+ remainingAttempts: () => config.maxAttempts - attempts.length,
358
+ memory: options.memoryBudget,
359
+ }),
360
+ now,
361
+ diagnostics,
362
+ failedEngineIds: new Set(),
363
+ };
364
+ try {
365
+ throwIfAborted(signal);
366
+ const configurationIssues = validatePipelineConfig(config);
367
+ if (!engineExecutor)
368
+ configurationIssues.push("engineExecutor is required; register decoder plugins and execute through CaptureRouter.");
369
+ if (engineExecutor && config.decoders.order.some((id) => !engineExecutor.engineIds.includes(id))) {
370
+ configurationIssues.push("decoder order contains an engine that is not available from engineExecutor.");
371
+ }
372
+ if (configurationIssues.length) {
373
+ return fail("invalid_configuration", configurationIssues.join(" "));
374
+ }
375
+ if (image.width < 1 || image.height < 1 || image.data.length === 0) {
376
+ return fail("empty_image", "Image has no pixel data.");
377
+ }
378
+ if (!Number.isInteger(image.width) || !Number.isInteger(image.height) || image.data.length < image.width * image.height * 4) {
379
+ return fail("invalid_image", "Pixel buffer dimensions, stride, or byte length are invalid.");
380
+ }
381
+ if (image.width * image.height > config.maxPixels * 6) {
382
+ return fail("image_too_large", "Pixel buffer exceeds the direct-input safety limit for this scenario.");
383
+ }
384
+ options.onStage?.("Detecting candidate regions…");
385
+ const cgStart = now();
386
+ const rawCandidates = options.candidates ?? generateCandidates(image, {
387
+ previewSize: config.previewSize,
388
+ maxCandidates: config.maxCandidates,
389
+ paddings: config.paddings,
390
+ scales: config.scales,
391
+ maxPixels: config.maxPixels,
392
+ enableLocalization: config.enableLocalization,
393
+ enableFullImageFallback: config.enableFullImageFallback,
394
+ enableSplitImageFallback: config.enableSplitImageFallback,
395
+ enableGridImageFallback: config.enableGridImageFallback,
396
+ budget: ctx.budget,
397
+ });
398
+ const candidates = dedupeCandidates(rawCandidates);
399
+ ctx.primaryCandidateCount = new Set(candidates.filter((candidate) => candidate.candidateIndex >= 0 && candidate.candidateIndex < 100).map((candidate) => candidate.candidateIndex)).size;
400
+ phaseTiming.candidateGenerationMs = now() - cgStart;
401
+ const ordered = orderCandidates(candidates, config.findMultiple);
402
+ const primaryEngineId = config.decoders.order[0];
403
+ const fallbackEngineIds = config.decoders.order.slice(1);
404
+ const pathological = ordered.some((candidate) => candidate.pathologicalInput);
405
+ const cheap = ["original", "contrast", "invert"];
406
+ const deep = pathological ? [] : config.preprocessOrder.filter((m) => !cheap.includes(m));
407
+ const cheapTargets = config.findMultiple ? ordered : ordered.slice(0, 12);
408
+ const earlyFallbackEngineIds = config.fallbackTiming === "after-cheap"
409
+ ? fallbackEngineIds.filter((engineId) => engineId === "zxing-cpp-wasm")
410
+ : [];
411
+ let earlyFallbackExecuted = false;
412
+ // Phase 1 — cheap pass
413
+ for (const [candidateIndex, candidate] of cheapTargets.entries()) {
414
+ await cooperativeYield(signal);
415
+ throwIfAborted(signal);
416
+ if (timedOut(start, config.timeoutMs, now)) {
417
+ if (signal?.aborted) {
418
+ if (found.length)
419
+ return successOutcome(dedupeResults(found, config.resultDeduplication), attempts, start, true, phaseTiming, diagnostics);
420
+ return fail("cancelled", "Decode cancelled.", true);
421
+ }
422
+ if (found.length)
423
+ return successOutcome(dedupeResults(found, config.resultDeduplication), attempts, start, false, phaseTiming, diagnostics);
424
+ return fail("timeout", "Decode timed out. Try cropping closer to the QR code, using a clearer image, or reducing image size.");
425
+ }
426
+ if (shouldStopMultiple(ctx)) {
427
+ return successOutcome(dedupeResults(found, config.resultDeduplication), attempts, start, false, phaseTiming, diagnostics);
428
+ }
429
+ options.onStage?.(candidate.cropPadding === "full"
430
+ ? "Trying full image…"
431
+ : `Decoding candidate ${Math.max(0, candidate.candidateIndex) + 1}…`);
432
+ const before = found.length;
433
+ const candidateMethods = config.findMultiple && found.length > 0 ? ["original", "contrast"] : cheap;
434
+ for (const preprocessing of candidateMethods) {
435
+ const hit = await tryEngine(ctx, primaryEngineId, candidate, preprocessing, 0);
436
+ if (hit)
437
+ return hit;
438
+ if (config.findMultiple && shouldStopMultiple(ctx))
439
+ break;
440
+ }
441
+ onCandidateDone(ctx, found.length > before);
442
+ if (!config.findMultiple && found.length > 0) {
443
+ return successOutcome(dedupeResults(found, config.resultDeduplication), attempts, start, false, phaseTiming, diagnostics);
444
+ }
445
+ // Balanced/Robust give native WASM one high-value full-frame attempt
446
+ // after a deliberately limited jsQR probe. This avoids paying for the
447
+ // entire combinatorial JavaScript preprocessing graph before fallback.
448
+ if (candidateIndex === 0 && earlyFallbackEngineIds.length && !shouldStopMultiple(ctx)) {
449
+ earlyFallbackExecuted = true;
450
+ options.onStage?.("Trying native WebAssembly decoder...");
451
+ const fullFrameTargets = ordered.filter((entry) => entry.cropPadding === "full").slice(0, 1);
452
+ const earlyTargets = fullFrameTargets.length ? fullFrameTargets : ordered.slice(0, 1);
453
+ for (const engineId of earlyFallbackEngineIds) {
454
+ for (const target of earlyTargets) {
455
+ const hit = await tryEngine(ctx, engineId, target, "original", 0);
456
+ if (hit)
457
+ return hit;
458
+ }
459
+ }
460
+ }
461
+ if (ctx.failFast)
462
+ continue;
463
+ if (!config.findMultiple && attempts.length >= (config.failFastAfterAttempts ?? 48) && found.length === 0) {
464
+ ctx.failFast = true;
465
+ }
466
+ }
467
+ if (found.length > 0 && shouldStopMultiple(ctx)) {
468
+ return successOutcome(dedupeResults(found, config.resultDeduplication), attempts, start, false, phaseTiming, diagnostics);
469
+ }
470
+ // Balanced/Robust give the native engine one high-value full-frame pass
471
+ // before entering the combinatorial deep preprocessing graph. Fast keeps
472
+ // fallback work in the final phase to protect its first-frame latency.
473
+ if (!earlyFallbackExecuted && earlyFallbackEngineIds.length && !shouldStopMultiple(ctx)) {
474
+ options.onStage?.("Trying native WebAssembly decoder...");
475
+ const fullFrameTargets = ordered.filter((candidate) => candidate.cropPadding === "full").slice(0, 1);
476
+ const earlyTargets = fullFrameTargets.length ? fullFrameTargets : ordered.slice(0, config.findMultiple ? 2 : 1);
477
+ for (const engineId of earlyFallbackEngineIds) {
478
+ for (const candidate of earlyTargets) {
479
+ const hit = await tryEngine(ctx, engineId, candidate, "original", 0);
480
+ if (hit)
481
+ return hit;
482
+ }
483
+ }
484
+ }
485
+ // Phase 2 — deep preprocess (skip if fail-fast with no results)
486
+ if (!pathological && (!ctx.failFast || found.length > 0)) {
487
+ const eligibleDeepTargets = ordered.filter((c, idx) => c.cropPadding === "full" || idx < 6 || c.scale === "original");
488
+ const deepTargets = [
489
+ ...eligibleDeepTargets.filter((candidate) => candidate.cropPadding === "full"),
490
+ ...eligibleDeepTargets.filter((candidate) => candidate.cropPadding !== "full"),
491
+ ];
492
+ for (const candidate of deepTargets) {
493
+ await cooperativeYield(signal);
494
+ throwIfAborted(signal);
495
+ if (timedOut(start, config.timeoutMs, now))
496
+ break;
497
+ if (attempts.length >= config.maxAttempts)
498
+ break;
499
+ if (shouldStopMultiple(ctx))
500
+ break;
501
+ const before = found.length;
502
+ for (const preprocessing of deep) {
503
+ const hit = await tryEngine(ctx, primaryEngineId, candidate, preprocessing, 0);
504
+ if (hit)
505
+ return hit;
506
+ }
507
+ onCandidateDone(ctx, found.length > before);
508
+ }
509
+ }
510
+ if (found.length > 0 && shouldStopMultiple(ctx)) {
511
+ return successOutcome(dedupeResults(found, config.resultDeduplication), attempts, start, false, phaseTiming, diagnostics);
512
+ }
513
+ // Return to lower-ranked candidates only after high-value deep preprocessing.
514
+ if (!config.findMultiple && found.length === 0) {
515
+ for (const candidate of ordered.slice(cheapTargets.length)) {
516
+ await cooperativeYield(signal);
517
+ throwIfAborted(signal);
518
+ if (timedOut(start, config.timeoutMs, now) || attempts.length >= config.maxAttempts)
519
+ break;
520
+ for (const preprocessing of cheap) {
521
+ const hit = await tryEngine(ctx, primaryEngineId, candidate, preprocessing, 0);
522
+ if (hit)
523
+ return hit;
524
+ }
525
+ }
526
+ }
527
+ // Phase 3 — rotations (only if not fail-fast empty)
528
+ if (!pathological && !ctx.failFast && found.length === 0) {
529
+ const rotateTargets = ordered.filter((c) => c.cropPadding !== "full").slice(0, 3);
530
+ const rotations = config.rotations.filter((r) => r !== 0);
531
+ for (const candidate of rotateTargets) {
532
+ if (attempts.length >= config.maxAttempts)
533
+ break;
534
+ for (const rotation of rotations) {
535
+ for (const preprocessing of ["original", "contrast", "invert"]) {
536
+ const hit = await tryEngine(ctx, primaryEngineId, candidate, preprocessing, rotation);
537
+ if (hit)
538
+ return hit;
539
+ }
540
+ }
541
+ }
542
+ }
543
+ // Phase 4 - registered fallback engines in scenario order.
544
+ if (found.length === 0 && fallbackEngineIds.length) {
545
+ options.onStage?.("Backup decoder...");
546
+ const zxTargets = ordered.filter((c) => c.cropPadding === "full").slice(0, pathological ? 1 : 2);
547
+ if (zxTargets.length === 0) {
548
+ zxTargets.push(...ordered.slice(0, 2));
549
+ }
550
+ for (const engineId of fallbackEngineIds) {
551
+ for (const candidate of zxTargets) {
552
+ for (const preprocessing of (pathological ? ["original"] : ["original", "contrast", "invert"])) {
553
+ const hit = await tryEngine(ctx, engineId, candidate, preprocessing, 0);
554
+ if (hit)
555
+ return hit;
556
+ }
557
+ }
558
+ }
559
+ }
560
+ if (found.length > 0) {
561
+ return successOutcome(dedupeResults(found, config.resultDeduplication), attempts, start, false, phaseTiming, diagnostics);
562
+ }
563
+ if (timedOut(start, config.timeoutMs, now)) {
564
+ if (signal?.aborted) {
565
+ return fail("cancelled", "Decode cancelled.", true);
566
+ }
567
+ return fail("timeout", "Decode timed out. Try cropping closer to the QR code, using a clearer image, or reducing image size.");
568
+ }
569
+ if (config.decoders.order.length > 0 && config.decoders.order.every((engineId) => ctx.failedEngineIds.has(engineId))) {
570
+ const initializationFailed = diagnostics.some((entry) => entry.status === "initialization-failure");
571
+ return fail(initializationFailed ? "engine_initialization_failure" : "engine_execution_failure", initializationFailed ? "Every configured decoder failed to initialize." : "Every configured decoder failed during execution.");
572
+ }
573
+ return fail("no_qr_found", "Could not decode a QR code. Try a clearer crop, better lighting, or a less damaged image.");
574
+ }
575
+ catch (e) {
576
+ if (e instanceof Error && (e.name === "AbortError" || e.message === "cancelled")) {
577
+ if (found.length > 0) {
578
+ return successOutcome(dedupeResults(found, config.resultDeduplication), attempts, start, true, phaseTiming, diagnostics);
579
+ }
580
+ return fail("cancelled", "Decode cancelled.", true);
581
+ }
582
+ const code = e && typeof e === "object" && "code" in e ? String(e.code) : "";
583
+ if (["timeout", "unsupported_format", "invalid_image", "engine_initialization_failure", "engine_execution_failure"].includes(code)) {
584
+ return fail(code, code === "timeout"
585
+ ? "Decode timed out. Try cropping closer to the QR code, using a clearer image, or reducing image size."
586
+ : e instanceof Error ? e.message : String(e));
587
+ }
588
+ throw e;
589
+ }
590
+ finally {
591
+ ctx.intermediateCache.dispose();
592
+ }
593
+ }
594
+ export function successOutcome(results, attempts, start, cancelled, phaseTiming, engineDiagnostics = []) {
595
+ if (results.length === 0) {
596
+ throw new Error("Decode success invariant violated: results must be non-empty.");
597
+ }
598
+ const nonEmptyResults = results;
599
+ const foundOffsets = nonEmptyResults
600
+ .map((result) => result.foundAtMs)
601
+ .filter((value) => value !== undefined);
602
+ return {
603
+ ok: true,
604
+ results: nonEmptyResults,
605
+ primary: nonEmptyResults[0],
606
+ attempts,
607
+ attemptCount: attempts.length,
608
+ elapsedMs: (start > 10_000_000_000 ? Date.now() : monotonicNow()) - start,
609
+ ...(foundOffsets.length ? { timeToFirstResultMs: Math.min(...foundOffsets) } : {}),
610
+ cancelled,
611
+ engineDiagnostics,
612
+ phaseTiming,
613
+ };
614
+ }
615
+ //# sourceMappingURL=decode-pipeline.js.map