ReviewOS

also looking at this

stacks/ts-qr-codes

chore(deps): update dependency react to 19.2.7

#2137
Closed chris wants to merge buddy-bot/update-major-update-react into main
222 files +16350 -1825

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

.vscode/dictionary.txtmodified+2-0
Changes to .vscode/dictionary.txt
@@ -5,6 +5,7 @@ barcodes
55biomejs
66bluesky
77booleanish
8Bresenham
89bumpp
910bunfig
1011bunx
@@ -22,6 +23,7 @@ destructurable
2223dnsx
2324dtsx
2425entrypoints
26Exif
2527gexp
2628GGLLL
2729GLGLL
README.mdmodified+2-1
Changes to README.md
@@ -296,8 +296,9 @@ We would like to extend our thanks to the following sponsors for funding Stacks
296296
297297Many thanks for the libraries that laid the groundwork:
298298
299- **QRCode.js**: <https://github.com/davidshimjs/qrcodejs>
300299- **JsBarcode**: <https://github.com/lindell/JsBarcode>
300- **Quagga**: <https://github.com/ericblade/quagga2>
301- **QRCode.js**: <https://github.com/davidshimjs/qrcodejs>
301302
302303## License
303304
packages/qrx/package.jsonmodified+14-0
Changes to packages/qrx/package.json
@@ -20,6 +20,13 @@
2020 "qr",
2121 "bar",
2222 "code",
23 "ean",
24 "code128",
25 "code39",
26 "codabar",
27 "i2of5",
28 "upc",
29 "getUserMedia",
2330 "generate",
2431 "read",
2532 "lightweight",
@@ -64,6 +71,13 @@
6471 "test": "bun test",
6572 "typecheck": "bun --bun tsc --noEmit"
6673 },
74 "dependencies": {
75 "get-pixels": "^3.3.3",
76 "gl-matrix": "^3.4.3",
77 "lodash": "^4.17.21",
78 "ndarray": "^1.0.19",
79 "ndarray-linear-interpolate": "^1.0.0"
80 },
6781 "devDependencies": {
6882 "@types/bun": "^1.1.14",
6983 "bun-plugin-dtsx": "^0.21.9",
packages/qrx/src/quagga/QuaggaContext.tsadded+41-0
Changes to packages/qrx/src/quagga/QuaggaContext.ts
@@ -0,0 +1,41 @@
1import type ImageWrapper from 'common/image_wrapper'
2import type { QuaggaJSConfigObject } from '../type-definitions/quagga'
3
4export class QuaggaContext {
5 public config?: QuaggaJSConfigObject
6
7 public inputStream: any
8
9 public framegrabber: any
10
11 public inputImageWrapper?: ImageWrapper
12
13 public stopped: boolean = false
14
15 public boxSize: any
16
17 public resultCollector: any
18
19 public decoder: any
20
21 public workerPool: any[] = []
22
23 public onUIThread = true
24
25 public readonly canvasContainer = new CanvasContainer()
26}
27
28export class CanvasInfo {
29 image: any
30 overlay: any
31}
32
33export class CanvasContainer {
34 public readonly ctx: CanvasInfo
35 public readonly dom: CanvasInfo
36
37 constructor() {
38 this.ctx = new CanvasInfo()
39 this.dom = new CanvasInfo()
40 }
41}
packages/qrx/src/quagga/analytics/result_collector.tsadded+64-0
Changes to packages/qrx/src/quagga/analytics/result_collector.ts
@@ -0,0 +1,64 @@
1import type {
2 QuaggaImageData,
3 QuaggaJSCodeResult,
4 QuaggaJSResultCollector,
5 QuaggaJSResultCollectorFilterFunction,
6 XYSize,
7} from '../../type-definitions/quagga.d'
8import ImageDebug from '../common/image_debug'
9
10function contains(codeResult: QuaggaJSCodeResult, list: Array<QuaggaJSCodeResult>): boolean {
11 return list && list.some((item) => {
12 const keys = Object.keys(item) as Array<keyof QuaggaJSCodeResult>
13 return keys.every(key => item[key] === codeResult[key])
14 })
15}
16
17function passesFilter(
18 codeResult: QuaggaJSCodeResult,
19 filter: QuaggaJSResultCollectorFilterFunction | undefined,
20): boolean {
21 return typeof filter === 'function' ? filter(codeResult) : true
22}
23
24interface ResultCollector {
25 addResult: (data: QuaggaImageData, imageSize: XYSize, codeResult: QuaggaJSCodeResult) => void
26 getResults: () => Array<QuaggaJSCodeResult>
27}
28
29export default {
30 create(config: QuaggaJSResultCollector): ResultCollector {
31 const canvas = document.createElement('canvas')
32 const ctx = canvas.getContext('2d', { willReadFrequently: !!config.willReadFrequently }) as CanvasRenderingContext2D
33 const results: Array<QuaggaJSCodeResult> = []
34 let capacity = config.capacity ?? 20
35 const capture = config.capture === true
36
37 function matchesConstraints(codeResult: QuaggaJSCodeResult): boolean {
38 return !!capacity
39 && codeResult
40 && !contains(codeResult, config.blacklist as Array<QuaggaJSCodeResult>)
41 && passesFilter(codeResult, config.filter)
42 }
43
44 return {
45 addResult(data: QuaggaImageData, imageSize: XYSize, codeResult: QuaggaJSCodeResult): void {
46 const result: any = { } // this is 'any' to avoid having to construct a whole QuaggaJSCodeResult :|
47 if (matchesConstraints(codeResult)) {
48 capacity--
49 result.codeResult = codeResult
50 if (capture) {
51 canvas.width = imageSize.x
52 canvas.height = imageSize.y
53 ImageDebug.drawImage(data, imageSize, ctx)
54 result.frame = canvas.toDataURL()
55 }
56 results.push(result)
57 }
58 },
59 getResults(): Array<QuaggaJSCodeResult> {
60 return results
61 },
62 }
63 },
64}
packages/qrx/src/quagga/common/array_helper.tsadded+74-0
Changes to packages/qrx/src/quagga/common/array_helper.ts
@@ -0,0 +1,74 @@
1/* eslint-disable no-param-reassign */
2import { TypedArray } from '../../type-definitions/quagga';
3
4export default {
5 init(arr: TypedArray | Array<number>, val: number) {
6 arr.fill(val);
7 },
8
9 /**
10 * IN-PLACE Shuffles the content of an array
11 */
12 shuffle(arr: Array<number>) {
13 // Durstenfeld shuffle algorithm
14 // https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
15 for (let i = arr.length - 1; i > 0; i--) {
16 const j = Math.floor(Math.random() * (i + 1));
17 [arr[i], arr[j]] = [arr[j], arr[i]];
18 }
19 return arr;
20 },
21
22 toPointList(arr: Array<Array<number>>) {
23 const rows = arr.reduce((p, n) => {
24 const row = `[${n.join(',')}]`;
25 p.push(row);
26 return p;
27 }, [] as Array<string>);
28 return `[${rows.join(',\r\n')}]`;
29 },
30
31 /**
32 * returns the elements which's score is bigger than the threshold
33 */
34 // eslint-disable-next-line no-unused-vars
35 threshold(arr: Array<number>, threshold: number, scoreFunc: ((score: number) => number)) {
36 const queue = arr.reduce((prev: Array<number>, next) => {
37 if (scoreFunc.apply(arr, [next]) >= threshold) {
38 prev.push(next);
39 }
40 return prev;
41 }, []);
42 return queue;
43 },
44
45 maxIndex(arr: Array<number>) {
46 let max = 0;
47 for (let i = 0; i < arr.length; i++) {
48 if (arr[i] > arr[max]) {
49 max = i;
50 }
51 }
52 return max;
53 },
54
55 max(arr: Array<number>) {
56 let max = 0;
57 for (let i = 0; i < arr.length; i++) {
58 if (arr[i] > max) {
59 max = arr[i];
60 }
61 }
62 return max;
63 },
64
65 sum(arr: Array<number> | TypedArray): number {
66 let { length } = arr;
67 let sum = 0;
68
69 while (length--) {
70 sum += arr[length];
71 }
72 return sum;
73 },
74};
packages/qrx/src/quagga/common/cluster.jsadded+71-0
Changes to packages/qrx/src/quagga/common/cluster.js
@@ -0,0 +1,71 @@
1// TODO: cluster.js and cv_utils.js are pretty tightly intertwined, making for a complex conversion
2// into typescript. be warned. :-)
3
4import { glMatrix, vec2 } from 'gl-matrix';
5
6glMatrix.setMatrixArrayType(Array);
7/**
8 * Creates a cluster for grouping similar orientations of datapoints
9 */
10export default {
11 create(point, threshold) {
12 const points = [];
13 const center = {
14 rad: 0,
15 vec: vec2.clone([0, 0]),
16 };
17 const pointMap = {};
18
19 function add(pointToAdd) {
20 pointMap[pointToAdd.id] = pointToAdd;
21 points.push(pointToAdd);
22 }
23
24 function updateCenter() {
25 let i; let
26 sum = 0;
27 for (i = 0; i < points.length; i++) {
28 sum += points[i].rad;
29 }
30 center.rad = sum / points.length;
31 center.vec = vec2.clone([Math.cos(center.rad), Math.sin(center.rad)]);
32 }
33
34 function init() {
35 add(point);
36 updateCenter();
37 }
38
39 init();
40
41 return {
42 add(pointToAdd) {
43 if (!pointMap[pointToAdd.id]) {
44 add(pointToAdd);
45 updateCenter();
46 }
47 },
48 fits(otherPoint) {
49 // check cosine similarity to center-angle
50 const similarity = Math.abs(vec2.dot(otherPoint.point.vec, center.vec));
51 if (similarity > threshold) {
52 return true;
53 }
54 return false;
55 },
56 getPoints() {
57 return points;
58 },
59 getCenter() {
60 return center;
61 },
62 };
63 },
64 createPoint(newPoint, id, property) {
65 return {
66 rad: newPoint[property],
67 point: newPoint,
68 id,
69 };
70 },
71};
packages/qrx/src/quagga/common/cv_utils.jsadded+773-0
Changes to packages/qrx/src/quagga/common/cv_utils.js
@@ -0,0 +1,773 @@
1/* eslint-disable no-mixed-operators */
2/* eslint-disable no-bitwise */
3import { glMatrix, vec2, vec3 } from 'gl-matrix';
4import ArrayHelper from './array_helper';
5import Cluster2 from './cluster';
6
7glMatrix.setMatrixArrayType(Array);
8
9/**
10 * @param x x-coordinate
11 * @param y y-coordinate
12 * @return ImageReference {x,y} Coordinate
13 */
14export function imageRef(x, y) {
15 const that = {
16 x,
17 y,
18 toVec2() {
19 return vec2.clone([this.x, this.y]);
20 },
21 toVec3() {
22 return vec3.clone([this.x, this.y, 1]);
23 },
24 round() {
25 this.x = this.x > 0.0 ? Math.floor(this.x + 0.5) : Math.floor(this.x - 0.5);
26 this.y = this.y > 0.0 ? Math.floor(this.y + 0.5) : Math.floor(this.y - 0.5);
27 return this;
28 },
29 };
30 return that;
31}
32
33/**
34 * Computes an integral image of a given grayscale image.
35 * @param imageDataContainer {ImageDataContainer} the image to be integrated
36 */
37export function computeIntegralImage2(imageWrapper, integralWrapper) {
38 const imageData = imageWrapper.data;
39 const width = imageWrapper.size.x;
40 const height = imageWrapper.size.y;
41 const integralImageData = integralWrapper.data;
42 let sum = 0; let posA = 0; let posB = 0; let posC = 0; let posD = 0; let x; let
43 y;
44
45 // sum up first column
46 posB = width;
47 sum = 0;
48 for (y = 1; y < height; y++) {
49 sum += imageData[posA];
50 integralImageData[posB] += sum;
51 posA += width;
52 posB += width;
53 }
54
55 posA = 0;
56 posB = 1;
57 sum = 0;
58 for (x = 1; x < width; x++) {
59 sum += imageData[posA];
60 integralImageData[posB] += sum;
61 posA++;
62 posB++;
63 }
64
65 for (y = 1; y < height; y++) {
66 posA = y * width + 1;
67 posB = (y - 1) * width + 1;
68 posC = y * width;
69 posD = (y - 1) * width;
70 for (x = 1; x < width; x++) {
71 integralImageData[posA]
72 += imageData[posA] + integralImageData[posB] + integralImageData[posC] - integralImageData[posD];
73 posA++;
74 posB++;
75 posC++;
76 posD++;
77 }
78 }
79}
80
81export function computeIntegralImage(imageWrapper, integralWrapper) {
82 const imageData = imageWrapper.data;
83 const width = imageWrapper.size.x;
84 const height = imageWrapper.size.y;
85 const integralImageData = integralWrapper.data;
86 let sum = 0;
87
88 // sum up first row
89 for (let i = 0; i < width; i++) {
90 sum += imageData[i];
91 integralImageData[i] = sum;
92 }
93
94 for (let v = 1; v < height; v++) {
95 sum = 0;
96 for (let u = 0; u < width; u++) {
97 sum += imageData[v * width + u];
98 integralImageData[((v) * width) + u] = sum + integralImageData[(v - 1) * width + u];
99 }
100 }
101}
102
103export function thresholdImage(imageWrapper, threshold, targetWrapper) {
104 if (!targetWrapper) {
105 // eslint-disable-next-line no-param-reassign
106 targetWrapper = imageWrapper;
107 }
108 const imageData = imageWrapper.data; let { length } = imageData; const
109 targetData = targetWrapper.data;
110
111 while (length--) {
112 targetData[length] = imageData[length] < threshold ? 1 : 0;
113 }
114}
115
116export function computeHistogram(imageWrapper, bitsPerPixel) {
117 if (!bitsPerPixel) {
118 // eslint-disable-next-line no-param-reassign
119 bitsPerPixel = 8;
120 }
121 const imageData = imageWrapper.data;
122 let { length } = imageData;
123 const bitShift = 8 - bitsPerPixel;
124 const bucketCnt = 1 << bitsPerPixel;
125 const hist = new Int32Array(bucketCnt);
126
127 while (length--) {
128 hist[imageData[length] >> bitShift]++;
129 }
130 return hist;
131}
132
133export function sharpenLine(line) {
134 let i;
135 const { length } = line;
136 let left = line[0];
137 let center = line[1];
138 let right;
139
140 for (i = 1; i < length - 1; i++) {
141 right = line[i + 1];
142 // -1 4 -1 kernel
143 // eslint-disable-next-line no-param-reassign
144 line[i - 1] = (((center * 2) - left - right)) & 255;
145 left = center;
146 center = right;
147 }
148 return line;
149}
150
151export function determineOtsuThreshold(imageWrapper, bitsPerPixel = 8) {
152 let hist;
153 const bitShift = 8 - bitsPerPixel;
154
155 function px(init, end) {
156 let sum = 0;
157 for (let i = init; i <= end; i++) {
158 sum += hist[i];
159 }
160 return sum;
161 }
162
163 function mx(init, end) {
164 let sum = 0;
165
166 for (let i = init; i <= end; i++) {
167 sum += i * hist[i];
168 }
169
170 return sum;
171 }
172
173 function determineThreshold() {
174 const vet = [0];
175 let p1;
176 let p2;
177 let p12;
178 let m1;
179 let m2;
180 let m12;
181 const max = (1 << bitsPerPixel) - 1;
182
183 hist = computeHistogram(imageWrapper, bitsPerPixel);
184 for (let k = 1; k < max; k++) {
185 p1 = px(0, k);
186 p2 = px(k + 1, max);
187 p12 = p1 * p2;
188 if (p12 === 0) {
189 p12 = 1;
190 }
191 m1 = mx(0, k) * p2;
192 m2 = mx(k + 1, max) * p1;
193 m12 = m1 - m2;
194 vet[k] = m12 * m12 / p12;
195 }
196 return ArrayHelper.maxIndex(vet);
197 }
198
199 const threshold = determineThreshold();
200 return threshold << bitShift;
201}
202
203export function otsuThreshold(imageWrapper, targetWrapper) {
204 const threshold = determineOtsuThreshold(imageWrapper);
205
206 thresholdImage(imageWrapper, threshold, targetWrapper);
207 return threshold;
208}
209
210// local thresholding
211export function computeBinaryImage(imageWrapper, integralWrapper, targetWrapper) {
212 computeIntegralImage(imageWrapper, integralWrapper);
213
214 if (!targetWrapper) {
215 // eslint-disable-next-line no-param-reassign
216 targetWrapper = imageWrapper;
217 }
218 const imageData = imageWrapper.data;
219 const targetData = targetWrapper.data;
220 const width = imageWrapper.size.x;
221 const height = imageWrapper.size.y;
222 const integralImageData = integralWrapper.data;
223 let sum = 0; let v; let u; const kernel = 3; let A; let B; let C; let D; let avg; const
224 size = (kernel * 2 + 1) * (kernel * 2 + 1);
225
226 // clear out top & bottom-border
227 for (v = 0; v <= kernel; v++) {
228 for (u = 0; u < width; u++) {
229 targetData[((v) * width) + u] = 0;
230 targetData[(((height - 1) - v) * width) + u] = 0;
231 }
232 }
233
234 // clear out left & right border
235 for (v = kernel; v < height - kernel; v++) {
236 for (u = 0; u <= kernel; u++) {
237 targetData[((v) * width) + u] = 0;
238 targetData[((v) * width) + (width - 1 - u)] = 0;
239 }
240 }
241
242 for (v = kernel + 1; v < height - kernel - 1; v++) {
243 for (u = kernel + 1; u < width - kernel; u++) {
244 A = integralImageData[(v - kernel - 1) * width + (u - kernel - 1)];
245 B = integralImageData[(v - kernel - 1) * width + (u + kernel)];
246 C = integralImageData[(v + kernel) * width + (u - kernel - 1)];
247 D = integralImageData[(v + kernel) * width + (u + kernel)];
248 sum = D - C - B + A;
249 avg = sum / (size);
250 targetData[v * width + u] = imageData[v * width + u] > (avg + 5) ? 0 : 1;
251 }
252 }
253}
254
255export function cluster(points, threshold, property) {
256 let i; let k; let thisCluster; let point; const
257 clusters = [];
258
259 if (!property) {
260 // eslint-disable-next-line no-param-reassign
261 property = 'rad';
262 }
263
264 function addToCluster(newPoint) {
265 let found = false;
266 for (k = 0; k < clusters.length; k++) {
267 thisCluster = clusters[k];
268 if (thisCluster.fits(newPoint)) {
269 thisCluster.add(newPoint);
270 found = true;
271 }
272 }
273 return found;
274 }
275
276 // iterate over each cloud
277 for (i = 0; i < points.length; i++) {
278 point = Cluster2.createPoint(points[i], i, property);
279 if (!addToCluster(point)) {
280 clusters.push(Cluster2.create(point, threshold));
281 }
282 }
283 return clusters;
284}
285
286export const Tracer = {
287 trace(points, vec) {
288 let iteration;
289 const maxIterations = 10;
290 let top = [];
291 let result = [];
292 let centerPos = 0;
293 let currentPos = 0;
294
295 function trace(idx, forward) {
296 let to;
297 let toIdx;
298 let predictedPos;
299 const thresholdX = 1;
300 const thresholdY = Math.abs(vec[1] / 10);
301 let found = false;
302
303 function match(pos, predicted) {
304 if (pos.x > (predicted.x - thresholdX)
305 && pos.x < (predicted.x + thresholdX)
306 && pos.y > (predicted.y - thresholdY)
307 && pos.y < (predicted.y + thresholdY)) {
308 return true;
309 }
310 return false;
311 }
312
313 // check if the next index is within the vec specifications
314 // if not, check as long as the threshold is met
315
316 const from = points[idx];
317 if (forward) {
318 predictedPos = {
319 x: from.x + vec[0],
320 y: from.y + vec[1],
321 };
322 } else {
323 predictedPos = {
324 x: from.x - vec[0],
325 y: from.y - vec[1],
326 };
327 }
328
329 toIdx = forward ? idx + 1 : idx - 1;
330 to = points[toIdx];
331 // eslint-disable-next-line no-cond-assign
332 while (to && (found = match(to, predictedPos)) !== true && (Math.abs(to.y - from.y) < vec[1])) {
333 toIdx = forward ? toIdx + 1 : toIdx - 1;
334 to = points[toIdx];
335 }
336
337 return found ? toIdx : null;
338 }
339
340 for (iteration = 0; iteration < maxIterations; iteration++) {
341 // randomly select point to start with
342 centerPos = Math.floor(Math.random() * points.length);
343
344 // trace forward
345 top = [];
346 currentPos = centerPos;
347 top.push(points[currentPos]);
348 // eslint-disable-next-line no-cond-assign
349 while ((currentPos = trace(currentPos, true)) !== null) {
350 top.push(points[currentPos]);
351 }
352 if (centerPos > 0) {
353 currentPos = centerPos;
354 // eslint-disable-next-line no-cond-assign
355 while ((currentPos = trace(currentPos, false)) !== null) {
356 top.push(points[currentPos]);
357 }
358 }
359
360 if (top.length > result.length) {
361 result = top;
362 }
363 }
364 return result;
365 },
366};
367
368export const DILATE = 1;
369export const ERODE = 2;
370
371export function dilate(inImageWrapper, outImageWrapper) {
372 let v;
373 let u;
374 const inImageData = inImageWrapper.data;
375 const outImageData = outImageWrapper.data;
376 const height = inImageWrapper.size.y;
377 const width = inImageWrapper.size.x;
378 let sum;
379 let yStart1;
380 let yStart2;
381 let xStart1;
382 let xStart2;
383
384 for (v = 1; v < height - 1; v++) {
385 for (u = 1; u < width - 1; u++) {
386 yStart1 = v - 1;
387 yStart2 = v + 1;
388 xStart1 = u - 1;
389 xStart2 = u + 1;
390 sum = inImageData[yStart1 * width + xStart1] + inImageData[yStart1 * width + xStart2]
391 + inImageData[v * width + u]
392 + inImageData[yStart2 * width + xStart1] + inImageData[yStart2 * width + xStart2];
393 outImageData[v * width + u] = sum > 0 ? 1 : 0;
394 }
395 }
396}
397
398export function erode(inImageWrapper, outImageWrapper) {
399 let v;
400 let u;
401 const inImageData = inImageWrapper.data;
402 const outImageData = outImageWrapper.data;
403 const height = inImageWrapper.size.y;
404 const width = inImageWrapper.size.x;
405 let sum;
406 let yStart1;
407 let yStart2;
408 let xStart1;
409 let xStart2;
410
411 for (v = 1; v < height - 1; v++) {
412 for (u = 1; u < width - 1; u++) {
413 yStart1 = v - 1;
414 yStart2 = v + 1;
415 xStart1 = u - 1;
416 xStart2 = u + 1;
417 sum = inImageData[yStart1 * width + xStart1] + inImageData[yStart1 * width + xStart2]
418 + inImageData[v * width + u]
419 + inImageData[yStart2 * width + xStart1] + inImageData[yStart2 * width + xStart2];
420 outImageData[v * width + u] = sum === 5 ? 1 : 0;
421 }
422 }
423}
424
425export function subtract(aImageWrapper, bImageWrapper, resultImageWrapper) {
426 if (!resultImageWrapper) {
427 // eslint-disable-next-line no-param-reassign
428 resultImageWrapper = aImageWrapper;
429 }
430 let { length } = aImageWrapper.data;
431 const aImageData = aImageWrapper.data;
432 const bImageData = bImageWrapper.data;
433 const cImageData = resultImageWrapper.data;
434
435 while (length--) {
436 cImageData[length] = aImageData[length] - bImageData[length];
437 }
438}
439
440export function bitwiseOr(aImageWrapper, bImageWrapper, resultImageWrapper) {
441 if (!resultImageWrapper) {
442 // eslint-disable-next-line no-param-reassign
443 resultImageWrapper = aImageWrapper;
444 }
445 let { length } = aImageWrapper.data;
446 const aImageData = aImageWrapper.data;
447 const bImageData = bImageWrapper.data;
448 const cImageData = resultImageWrapper.data;
449
450 while (length--) {
451 cImageData[length] = aImageData[length] || bImageData[length];
452 }
453}
454
455export function countNonZero(imageWrapper) {
456 let { length } = imageWrapper.data;
457 const { data } = imageWrapper;
458 let sum = 0;
459
460 while (length--) {
461 sum += data[length];
462 }
463 return sum;
464}
465
466export function topGeneric(list, top, scoreFunc) {
467 let i; let minIdx = 0; let min = 0; const queue = []; let score; let hit; let
468 pos;
469
470 for (i = 0; i < top; i++) {
471 queue[i] = {
472 score: 0,
473 item: null,
474 };
475 }
476
477 for (i = 0; i < list.length; i++) {
478 score = scoreFunc.apply(this, [list[i]]);
479 if (score > min) {
480 hit = queue[minIdx];
481 hit.score = score;
482 hit.item = list[i];
483 min = Number.MAX_VALUE;
484 for (pos = 0; pos < top; pos++) {
485 if (queue[pos].score < min) {
486 min = queue[pos].score;
487 minIdx = pos;
488 }
489 }
490 }
491 }
492
493 return queue;
494}
495
496export function grayArrayFromImage(htmlImage, offsetX, ctx, array) {
497 ctx.drawImage(htmlImage, offsetX, 0, htmlImage.width, htmlImage.height);
498 const ctxData = ctx.getImageData(offsetX, 0, htmlImage.width, htmlImage.height).data;
499 computeGray(ctxData, array);
500}
501
502export function grayArrayFromContext(ctx, size, offset, array) {
503 const ctxData = ctx.getImageData(offset.x, offset.y, size.x, size.y).data;
504 computeGray(ctxData, array);
505}
506
507export function grayAndHalfSampleFromCanvasData(canvasData, size, outArray) {
508 let topRowIdx = 0;
509 let bottomRowIdx = size.x;
510 const endIdx = Math.floor(canvasData.length / 4);
511 const outWidth = size.x / 2;
512 let outImgIdx = 0;
513 const inWidth = size.x;
514 let i;
515
516 while (bottomRowIdx < endIdx) {
517 for (i = 0; i < outWidth; i++) {
518 // eslint-disable-next-line no-param-reassign
519 outArray[outImgIdx] = (
520 (0.299 * canvasData[topRowIdx * 4 + 0]
521 + 0.587 * canvasData[topRowIdx * 4 + 1]
522 + 0.114 * canvasData[topRowIdx * 4 + 2])
523 + (0.299 * canvasData[(topRowIdx + 1) * 4 + 0]
524 + 0.587 * canvasData[(topRowIdx + 1) * 4 + 1]
525 + 0.114 * canvasData[(topRowIdx + 1) * 4 + 2])
526 + (0.299 * canvasData[(bottomRowIdx) * 4 + 0]
527 + 0.587 * canvasData[(bottomRowIdx) * 4 + 1]
528 + 0.114 * canvasData[(bottomRowIdx) * 4 + 2])
529 + (0.299 * canvasData[(bottomRowIdx + 1) * 4 + 0]
530 + 0.587 * canvasData[(bottomRowIdx + 1) * 4 + 1]
531 + 0.114 * canvasData[(bottomRowIdx + 1) * 4 + 2])) / 4;
532 outImgIdx++;
533 topRowIdx += 2;
534 bottomRowIdx += 2;
535 }
536 topRowIdx += inWidth;
537 bottomRowIdx += inWidth;
538 }
539}
540
541export function computeGray(imageData, outArray, config) {
542 const l = (imageData.length / 4) | 0;
543 const singleChannel = config && config.singleChannel === true;
544
545 if (singleChannel) {
546 for (let i = 0; i < l; i++) {
547 // eslint-disable-next-line no-param-reassign
548 outArray[i] = imageData[i * 4 + 0];
549 }
550 } else {
551 for (let i = 0; i < l; i++) {
552 // eslint-disable-next-line no-param-reassign
553 outArray[i] = 0.299 * imageData[i * 4 + 0] + 0.587 * imageData[i * 4 + 1] + 0.114 * imageData[i * 4 + 2];
554 }
555 }
556}
557
558export function loadImageArray(src, callback, canvas = document && document.createElement('canvas')) {
559 const img = new Image();
560 img.callback = callback;
561 img.onload = function () {
562 // eslint-disable-next-line no-param-reassign
563 canvas.width = this.width;
564 // eslint-disable-next-line no-param-reassign
565 canvas.height = this.height;
566 console.warn('* loadImageArray getContext 2d');
567 const ctx = canvas.getContext('2d');
568 ctx.drawImage(this, 0, 0);
569 const array = new Uint8Array(this.width * this.height);
570 ctx.drawImage(this, 0, 0);
571 const { data } = ctx.getImageData(0, 0, this.width, this.height);
572 computeGray(data, array);
573 this.callback(array, {
574 x: this.width,
575 y: this.height,
576 }, this);
577 };
578 img.src = src;
579}
580
581/**
582 * @param inImg {ImageWrapper} input image to be sampled
583 * @param outImg {ImageWrapper} to be stored in
584 */
585export function halfSample(inImgWrapper, outImgWrapper) {
586 const inImg = inImgWrapper.data;
587 const inWidth = inImgWrapper.size.x;
588 const outImg = outImgWrapper.data;
589 let topRowIdx = 0;
590 let bottomRowIdx = inWidth;
591 const endIdx = inImg.length;
592 const outWidth = inWidth / 2;
593 let outImgIdx = 0;
594 while (bottomRowIdx < endIdx) {
595 for (let i = 0; i < outWidth; i++) {
596 outImg[outImgIdx] = Math.floor(
597 (inImg[topRowIdx] + inImg[topRowIdx + 1] + inImg[bottomRowIdx] + inImg[bottomRowIdx + 1]) / 4,
598 );
599 outImgIdx++;
600 topRowIdx += 2;
601 bottomRowIdx += 2;
602 }
603 topRowIdx += inWidth;
604 bottomRowIdx += inWidth;
605 }
606}
607
608export function hsv2rgb(hsv, rgb = [0, 0, 0]) {
609 const h = hsv[0];
610 const s = hsv[1];
611 const v = hsv[2];
612 const c = v * s;
613 const x = c * (1 - Math.abs((h / 60) % 2 - 1));
614 const m = v - c;
615 let r = 0;
616 let g = 0;
617 let b = 0;
618
619 if (h < 60) {
620 r = c;
621 g = x;
622 } else if (h < 120) {
623 r = x;
624 g = c;
625 } else if (h < 180) {
626 g = c;
627 b = x;
628 } else if (h < 240) {
629 g = x;
630 b = c;
631 } else if (h < 300) {
632 r = x;
633 b = c;
634 } else if (h < 360) {
635 r = c;
636 b = x;
637 }
638 // eslint-disable-next-line no-param-reassign
639 rgb[0] = ((r + m) * 255) | 0;
640 // eslint-disable-next-line no-param-reassign
641 rgb[1] = ((g + m) * 255) | 0;
642 // eslint-disable-next-line no-param-reassign
643 rgb[2] = ((b + m) * 255) | 0;
644 return rgb;
645}
646
647export function _computeDivisors(n) {
648 const largeDivisors = [];
649 const divisors = [];
650
651 for (let i = 1; i < Math.sqrt(n) + 1; i++) {
652 if (n % i === 0) {
653 divisors.push(i);
654 if (i !== n / i) {
655 largeDivisors.unshift(Math.floor(n / i));
656 }
657 }
658 }
659 return divisors.concat(largeDivisors);
660}
661
662function _computeIntersection(arr1, arr2) {
663 let i = 0;
664 let j = 0;
665 const result = [];
666
667 while (i < arr1.length && j < arr2.length) {
668 if (arr1[i] === arr2[j]) {
669 result.push(arr1[i]);
670 i++;
671 j++;
672 } else if (arr1[i] > arr2[j]) {
673 j++;
674 } else {
675 i++;
676 }
677 }
678 return result;
679}
680
681export function calculatePatchSize(patchSize, imgSize) {
682 const divisorsX = _computeDivisors(imgSize.x);
683 const divisorsY = _computeDivisors(imgSize.y);
684 const wideSide = Math.max(imgSize.x, imgSize.y);
685 const common = _computeIntersection(divisorsX, divisorsY);
686 const nrOfPatchesList = [8, 10, 15, 20, 32, 60, 80];
687 const nrOfPatchesMap = {
688 'x-small': 5,
689 small: 4,
690 medium: 3,
691 large: 2,
692 'x-large': 1,
693 };
694 const nrOfPatchesIdx = nrOfPatchesMap[patchSize] || nrOfPatchesMap.medium;
695 const nrOfPatches = nrOfPatchesList[nrOfPatchesIdx];
696 const desiredPatchSize = Math.floor(wideSide / nrOfPatches);
697 let optimalPatchSize;
698
699 function findPatchSizeForDivisors(divisors) {
700 let i = 0;
701 let found = divisors[Math.floor(divisors.length / 2)];
702
703 while (i < (divisors.length - 1) && divisors[i] < desiredPatchSize) {
704 i++;
705 }
706 if (i > 0) {
707 if (Math.abs(divisors[i] - desiredPatchSize) > Math.abs(divisors[i - 1] - desiredPatchSize)) {
708 found = divisors[i - 1];
709 } else {
710 found = divisors[i];
711 }
712 }
713 if (desiredPatchSize / found < nrOfPatchesList[nrOfPatchesIdx + 1] / nrOfPatchesList[nrOfPatchesIdx]
714 && desiredPatchSize / found > nrOfPatchesList[nrOfPatchesIdx - 1] / nrOfPatchesList[nrOfPatchesIdx]) {
715 return { x: found, y: found };
716 }
717 return null;
718 }
719
720 optimalPatchSize = findPatchSizeForDivisors(common);
721 if (!optimalPatchSize) {
722 optimalPatchSize = findPatchSizeForDivisors(_computeDivisors(wideSide));
723 if (!optimalPatchSize) {
724 optimalPatchSize = findPatchSizeForDivisors((_computeDivisors(desiredPatchSize * nrOfPatches)));
725 }
726 }
727 return optimalPatchSize;
728}
729
730export function _parseCSSDimensionValues(value) {
731 const dimension = {
732 value: parseFloat(value),
733 unit: value.indexOf('%') === value.length - 1 ? '%' : '%',
734 };
735
736 return dimension;
737}
738
739export const _dimensionsConverters = {
740 top(dimension, context) {
741 return dimension.unit === '%' ? Math.floor(context.height * (dimension.value / 100)) : null;
742 },
743 right(dimension, context) {
744 return dimension.unit === '%' ? Math.floor(context.width - (context.width * (dimension.value / 100))) : null;
745 },
746 bottom(dimension, context) {
747 return dimension.unit === '%' ? Math.floor(context.height - (context.height * (dimension.value / 100))) : null;
748 },
749 left(dimension, context) {
750 return dimension.unit === '%' ? Math.floor(context.width * (dimension.value / 100)) : null;
751 },
752};
753
754export function computeImageArea(inputWidth, inputHeight, area) {
755 const context = { width: inputWidth, height: inputHeight };
756
757 const parsedArea = Object.keys(area).reduce((result, key) => {
758 const value = area[key];
759 const parsed = _parseCSSDimensionValues(value);
760 const calculated = _dimensionsConverters[key](parsed, context);
761
762 // eslint-disable-next-line no-param-reassign
763 result[key] = calculated;
764 return result;
765 }, {});
766
767 return {
768 sx: parsedArea.left,
769 sy: parsedArea.top,
770 sw: parsedArea.right - parsedArea.left,
771 sh: parsedArea.bottom - parsedArea.top,
772 };
773}
packages/qrx/src/quagga/common/events.tsadded+109-0
Changes to packages/qrx/src/quagga/common/events.ts
@@ -0,0 +1,109 @@
1type EventName = string;
2
3interface Subscription {
4 async?: boolean;
5 callback: Function;
6 once?: boolean;
7}
8
9interface EventData {
10 subscribers: Array<Subscription>;
11}
12
13interface Events {
14 [key: string]: EventData;
15}
16
17interface EventInterface {
18 subscribe(event: EventName, callback: Function | Subscription, async?: boolean): void;
19 publish(eventName: EventName, data?: never): void;
20 once(event: EventName, callback: Function, async?: boolean): void;
21 unsubscribe(eventName?: EventName, callback?: Function | Subscription): void;
22}
23
24export default (function EventInterface(): EventInterface {
25 let events: Events = {};
26
27 function getEvent(eventName: EventName): EventData {
28 if (!events[eventName]) {
29 events[eventName] = {
30 subscribers: [],
31 };
32 }
33 return events[eventName];
34 }
35
36 function clearEvents(): void {
37 events = {};
38 }
39
40 function publishSubscription(subscription: Subscription, data: never): void {
41 if (subscription.async) {
42 setTimeout(() => {
43 subscription.callback(data);
44 }, 4);
45 } else {
46 subscription.callback(data);
47 }
48 }
49
50 function _subscribe(event: EventName, callback: Function | Subscription, async?: boolean): void {
51 let subscription;
52
53 if (typeof callback === 'function') {
54 subscription = {
55 callback,
56 async,
57 };
58 } else {
59 subscription = callback;
60 if (!subscription.callback) {
61 throw new Error('Callback was not specified on options');
62 }
63 }
64
65 getEvent(event).subscribers.push(subscription);
66 }
67
68 return {
69 subscribe(event: EventName, callback: Function | Subscription, async?: boolean): void {
70 return _subscribe(event, callback, async);
71 },
72 publish(eventName: EventName, data?: never): void {
73 const event = getEvent(eventName);
74 const { subscribers } = event;
75
76 // Publish one-time subscriptions
77 subscribers.filter((subscriber) => !!subscriber.once).forEach((subscriber) => {
78 publishSubscription(subscriber, data as never);
79 });
80
81 // remove them from the subscriber
82 event.subscribers = subscribers.filter((subscriber) => !subscriber.once);
83
84 // publish the rest
85 event.subscribers.forEach((subscriber) => {
86 publishSubscription(subscriber, data as never);
87 });
88 },
89 once(event: EventName, callback: Function, async = false): void {
90 _subscribe(event, {
91 callback,
92 async,
93 once: true,
94 });
95 },
96 unsubscribe(eventName?: EventName, callback?: Function | Subscription): void {
97 if (eventName) {
98 const event = getEvent(eventName);
99 if (event && callback) {
100 event.subscribers = event.subscribers.filter((subscriber) => subscriber.callback !== callback);
101 } else {
102 event.subscribers = [];
103 }
104 } else {
105 clearEvents();
106 }
107 },
108 };
109}());
packages/qrx/src/quagga/common/image_debug.tsadded+62-0
Changes to packages/qrx/src/quagga/common/image_debug.ts
@@ -0,0 +1,62 @@
1import { XYSize } from '../../type-definitions/quagga.d';
2
3// TODO: XYPosition should be an XYObject, but that breaks XYDefinition, which breaks drawPath() below.
4declare interface XYPosition {
5 x: number;
6 y: number;
7}
8
9declare interface CanvasStyle {
10 color: string;
11 lineWidth: number;
12}
13
14// XYDefinition tells us which component of a given array or object is the "X" and which is the "Y".
15// Usually this is 0 for X and 1 for Y, but might be used as 'x' for x and 'y' for Y.
16declare interface XYDefinition {
17 x: keyof XYPosition;
18 y: keyof XYPosition;
19}
20
21declare type Path = Array<XYPosition>;
22
23export default {
24 drawRect(pos: XYPosition, size: XYSize, ctx: CanvasRenderingContext2D, style: CanvasStyle): void {
25 ctx.strokeStyle = style.color;
26 ctx.fillStyle = style.color;
27 ctx.lineWidth = style.lineWidth || 1;
28 ctx.beginPath();
29 ctx.strokeRect(pos.x, pos.y, size.x, size.y);
30 },
31 drawPath(path: Path, def: XYDefinition, ctx: CanvasRenderingContext2D, style: CanvasStyle): void {
32 ctx.strokeStyle = style.color;
33 ctx.fillStyle = style.color;
34 ctx.lineWidth = style.lineWidth;
35 ctx.beginPath();
36 ctx.moveTo(path[0][def.x], path[0][def.y]);
37 for (let j = 1; j < path.length; j++) {
38 ctx.lineTo(path[j][def.x], path[j][def.y]);
39 }
40 ctx.closePath();
41 ctx.stroke();
42 },
43 drawImage(imageData: Array<number>, size: XYSize, ctx: CanvasRenderingContext2D): boolean {
44 const canvasData = ctx.getImageData(0, 0, size.x, size.y);
45 const { data } = canvasData;
46 let canvasDataPos = data.length;
47 let imageDataPos = imageData.length;
48
49 if (canvasDataPos / imageDataPos !== 4) {
50 return false;
51 }
52 while (imageDataPos--) {
53 const value = imageData[imageDataPos];
54 data[--canvasDataPos] = 255;
55 data[--canvasDataPos] = value;
56 data[--canvasDataPos] = value;
57 data[--canvasDataPos] = value;
58 }
59 ctx.putImageData(canvasData, 0, 0);
60 return true;
61 },
62};
packages/qrx/src/quagga/common/image_wrapper.tsadded+272-0
Changes to packages/qrx/src/quagga/common/image_wrapper.ts
@@ -0,0 +1,272 @@
1import { glMatrix, vec2 } from 'gl-matrix';
2import {
3 XYSize,
4 TypedArrayConstructor,
5 TypedArray,
6 WrapperIndexMapping,
7 Moment,
8 SparseImageWrapper,
9} from '../../type-definitions/quagga.d';
10import ArrayHelper from './array_helper';
11import { hsv2rgb } from './cv_utils';
12
13glMatrix.setMatrixArrayType(Array);
14
15type PositiveNumber = number;
16function assertNumberPositive(val: number): asserts val is PositiveNumber {
17 if (val < 0) {
18 throw new Error(`expected positive number, received ${val}`);
19 }
20}
21
22class ImageWrapper implements SparseImageWrapper {
23 data: TypedArray | Array<number>;
24
25 size: XYSize;
26
27 indexMapping?: WrapperIndexMapping;
28
29 // Represents a basic image combining the data and size. In addition, some methods for
30 // manipulation are contained within.
31 constructor(
32 size: XYSize,
33 data?: TypedArray | Array<number>,
34 ArrayType: TypedArrayConstructor | ArrayConstructor = Uint8Array,
35 initialize?: boolean,
36 ) {
37 if (!data) {
38 this.data = new (ArrayType)(size.x * size.y);
39 if (initialize) {
40 ArrayHelper.init(this.data, 0);
41 }
42 } else {
43 this.data = data;
44 }
45 this.size = size;
46 }
47
48 // tests if a position is within the image, extended out by a border on each side
49 inImageWithBorder(imgRef: XYSize, border: PositiveNumber = 0): boolean {
50 assertNumberPositive(border);
51 return (imgRef.x >= 0)
52 && (imgRef.y >= 0)
53 && (imgRef.x < (this.size.x + (border * 2)))
54 && (imgRef.y < (this.size.y + (border * 2)));
55 }
56
57 // Copy from THIS ImageWrapper to the new imageWrapper parameter, starting at from, stopping at
58 // end of new imageWrapper size.
59 subImageAsCopy(imageWrapper: ImageWrapper, from: XYSize): ImageWrapper {
60 assertNumberPositive(from.x);
61 assertNumberPositive(from.y);
62 const { x: sizeX, y: sizeY } = imageWrapper.size;
63 for (let x = 0; x < sizeX; x++) {
64 for (let y = 0; y < sizeY; y++) {
65 // eslint-disable-next-line no-param-reassign
66 imageWrapper.data[y * sizeX + x] = this.data[(from.y + y) * this.size.x + from.x + x];
67 }
68 }
69 return imageWrapper;
70 // TODO: this function really probably should call into ImageWrapper somewhere to make
71 // sure that all of it's parameters are set properly, something like
72 // ImageWrapper.UpdateFrom()
73 // that might take a provided data and size, and make sure there's no invalid indexMapping
74 // hanging around, and such.
75 }
76
77 // Retrieve a grayscale value at the given pixel position of the image
78 get(x: number, y: number): number {
79 return this.data[y * this.size.x + x];
80 }
81
82 // Retrieve a grayscale value at the given pixel position of the image (safe, whatever that
83 // means)
84 getSafe(x: number, y: number): number {
85 // cache indexMapping because if we're using it once, we'll probably need it a bunch more
86 // too
87 if (!this.indexMapping) {
88 this.indexMapping = {
89 x: [],
90 y: [],
91 };
92 for (let i = 0; i < this.size.x; i++) {
93 this.indexMapping.x[i] = i;
94 this.indexMapping.x[i + this.size.x] = i;
95 }
96 for (let i = 0; i < this.size.y; i++) {
97 this.indexMapping.y[i] = i;
98 this.indexMapping.y[i + this.size.y] = i;
99 }
100 }
101 return this.data[(this.indexMapping.y[y + this.size.y]) * this.size.x + this.indexMapping.x[x + this.size.x]];
102 }
103
104 // Sets a given pixel position in the image to the given grayscale value
105 set(x: number, y: number, value: number): ImageWrapper {
106 this.data[y * this.size.x + x] = value;
107 delete this.indexMapping;
108 return this;
109 }
110
111 // Sets the border of the image (1 pixel) to zero
112 zeroBorder(): ImageWrapper {
113 const { x: width, y: height } = this.size;
114 for (let i = 0; i < width; i++) {
115 // eslint-disable-next-line no-multi-assign
116 this.data[i] = this.data[(height - 1) * width + i] = 0;
117 }
118 for (let i = 1; i < height - 1; i++) {
119 // eslint-disable-next-line no-multi-assign
120 this.data[i * width] = this.data[i * width + (width - 1)] = 0;
121 }
122 delete this.indexMapping;
123 return this;
124 }
125
126 // TODO: this function is entirely too large for me to reason out right at this moment that i'm handling
127 // all the rest of it, so this is a verbatim copy of the javascript source, with only tweaks
128 // necessary to get it to run, no thought put into it yet.
129 moments(labelCount: number): Array<Moment> {
130 const { data } = this;
131 let x;
132 let y;
133 const height = this.size.y;
134 const width = this.size.x;
135 let val;
136 let ysq;
137 const labelSum: Array<Moment> = [];
138 let i;
139 let label;
140 let mu11;
141 let mu02;
142 let mu20;
143 let x_;
144 let y_;
145 let tmp;
146 const result: Array<Moment> = [];
147 const { PI } = Math;
148 const PI_4 = PI / 4;
149
150 if (labelCount <= 0) {
151 return result;
152 }
153
154 for (i = 0; i < labelCount; i++) {
155 labelSum[i] = {
156 m00: 0,
157 m01: 0,
158 m10: 0,
159 m11: 0,
160 m02: 0,
161 m20: 0,
162 theta: 0,
163 rad: 0,
164 };
165 }
166
167 for (y = 0; y < height; y++) {
168 ysq = y * y;
169 for (x = 0; x < width; x++) {
170 val = data[y * width + x];
171 if (val > 0) {
172 label = labelSum[val - 1];
173 label.m00 += 1;
174 label.m01 += y;
175 label.m10 += x;
176 label.m11 += x * y;
177 label.m02 += ysq;
178 label.m20 += x * x;
179 }
180 }
181 }
182
183 for (i = 0; i < labelCount; i++) {
184 label = labelSum[i];
185 // eslint-disable-next-line no-restricted-globals
186 if (!isNaN(label.m00) && label.m00 !== 0) {
187 x_ = label.m10 / label.m00;
188 y_ = label.m01 / label.m00;
189 mu11 = label.m11 / label.m00 - x_ * y_;
190 mu02 = label.m02 / label.m00 - y_ * y_;
191 mu20 = label.m20 / label.m00 - x_ * x_;
192 tmp = (mu02 - mu20) / (2 * mu11);
193 tmp = 0.5 * Math.atan(tmp) + (mu11 >= 0 ? PI_4 : -PI_4) + PI;
194 // eslint-disable-next-line no-mixed-operators
195 label.theta = (tmp * 180 / PI + 90) % 180 - 90;
196 if (label.theta < 0) {
197 label.theta += 180;
198 }
199 label.rad = tmp > PI ? tmp - PI : tmp;
200 label.vec = vec2.clone([Math.cos(tmp), Math.sin(tmp)]);
201 result.push(label);
202 }
203 }
204 return result;
205 }
206
207 // return a Uint8ClampedArray containing this grayscale image converted to RGBA form
208 getAsRGBA(scale = 1.0): Uint8ClampedArray {
209 const ret = new Uint8ClampedArray(4 * this.size.x * this.size.y);
210 for (let y = 0; y < this.size.y; y++) {
211 for (let x = 0; x < this.size.x; x++) {
212 const pixel = y * this.size.x + x;
213 const current = this.get(x, y) * scale;
214 ret[pixel * 4 + 0] = current;
215 ret[pixel * 4 + 1] = current;
216 ret[pixel * 4 + 2] = current;
217 ret[pixel * 4 + 3] = 255;
218 }
219 }
220 return ret;
221 }
222
223 // Display this ImageWrapper in a given Canvas element at the specified scale
224 show(canvas: HTMLCanvasElement, scale = 1.0): void {
225 console.warn('* imagewrapper show getcontext 2d');
226 const ctx = canvas.getContext('2d');
227 if (!ctx) {
228 throw new Error('Unable to get canvas context');
229 }
230 const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);
231 const data = this.getAsRGBA(scale);
232 // eslint-disable-next-line no-param-reassign
233 canvas.width = this.size.x;
234 // eslint-disable-next-line no-param-reassign
235 canvas.height = this.size.y;
236 const newFrame = new ImageData(data, frame.width, frame.height);
237 ctx.putImageData(newFrame, 0, 0);
238 }
239
240 // Displays a specified SubImage area in a given canvas. This differs drastically from
241 // creating a new SubImage and using it's show() method. Why? I don't have the answer to that
242 // yet. I suspect the HSV/RGB operations involved here are making it significantly different,
243 // but until I can visualize these functions side by side, I'm just going to copy the existing
244 // implementation.
245 overlay(canvas: HTMLCanvasElement, inScale: number, from: XYSize): void {
246 const adjustedScale = (inScale < 0 || inScale > 360) ? 360 : inScale;
247 const hsv = [0, 1, 1];
248 const rgb = [0, 0, 0];
249 const whiteRgb = [255, 255, 255];
250 const blackRgb = [0, 0, 0];
251 let result = [];
252 console.warn('* imagewrapper overlay getcontext 2d');
253 const ctx = canvas.getContext('2d');
254 if (!ctx) {
255 throw new Error('Unable to get canvas context');
256 }
257 const frame = ctx.getImageData(from.x, from.y, this.size.x, this.size.y);
258 const { data } = frame;
259 let { length } = this.data;
260 while (length--) {
261 hsv[0] = this.data[length] * adjustedScale;
262 // eslint-disable-next-line no-nested-ternary
263 result = hsv[0] <= 0 ? whiteRgb : hsv[0] >= 360 ? blackRgb : hsv2rgb(hsv, rgb);
264 const pos = length * 4;
265 [data[pos], data[pos + 1], data[pos + 2]] = result;
266 data[pos + 3] = 255;
267 }
268 ctx.putImageData(frame, from.x, from.y);
269 }
270}
271
272export default ImageWrapper;
packages/qrx/src/quagga/common/mediaDevices.tsadded+21-0
Changes to packages/qrx/src/quagga/common/mediaDevices.ts
@@ -0,0 +1,21 @@
1import Exception from '../quagga/Exception';
2
3const ERROR_DESC = 'This may mean that the user has declined camera access, or the browser does not support media APIs. If you are running in iOS, you must use Safari.';
4
5export function enumerateDevices(): Promise<Array<MediaDeviceInfo>> {
6 try {
7 return navigator.mediaDevices.enumerateDevices();
8 } catch (err) {
9 const error: Error = new Exception(`enumerateDevices is not defined. ${ERROR_DESC}`, -1);
10 return Promise.reject(error);
11 }
12}
13
14export function getUserMedia(constraints: MediaStreamConstraints): Promise<MediaStream> {
15 try {
16 return navigator.mediaDevices.getUserMedia(constraints);
17 } catch (err) {
18 const error: Error = new Exception(`getUserMedia is not defined. ${ERROR_DESC}`, -1);
19 return Promise.reject(error);
20 }
21}
packages/qrx/src/quagga/common/subImage.tsadded+72-0
Changes to packages/qrx/src/quagga/common/subImage.ts
@@ -0,0 +1,72 @@
1import type { ImageRef, SparseImageWrapper, XYSize } from '../../type-definitions/quagga.d';
2import ImageWrapper from './image_wrapper';
3
4// A window into a specific area of an ImageWrapper
5export class SubImage {
6 I: ImageWrapper | SparseImageWrapper;
7
8 data: ImageWrapper['data'];
9
10 originalSize: XYSize;
11
12 from: ImageRef;
13
14 size: XYSize;
15
16 // Construct representing a part of another {ImageWrapper}. Shares data between the parent and
17 // child. Returns a shared part of the original image.
18 constructor(from: ImageRef, size: XYSize, I: SparseImageWrapper = { data: null, size }) {
19 this.data = I.data as ImageWrapper['data'];
20 this.originalSize = size;
21 this.I = I;
22 this.from = from;
23 this.size = size;
24 }
25
26 // Retrieves a given pixel position from the {SubImage}, returns the grayscale value at the position
27 get(x: number, y: number): number {
28 return this.data[(this.from.y + y) * this.originalSize.x + this.from.x + x];
29 }
30
31 // Displays the {SubImage} in a given canvas at a given scale
32 show(canvas: HTMLCanvasElement, scale = 1.0): void {
33 console.warn('* subImage.show getcontext 2d');
34 const ctx = canvas.getContext('2d');
35 if (!ctx) {
36 throw new Error('Unable to get canvas context');
37 }
38 // eslint-disable-next-line no-param-reassign
39 canvas.width = this.size.x;
40 // eslint-disable-next-line no-param-reassign
41 canvas.height = this.size.y;
42 const frame = ctx.getImageData(0, 0, canvas.width, canvas.height);
43 const { data } = frame;
44 let current = 0;
45 for (let y = 0; y < this.size.y; y++) {
46 for (let x = 0; x < this.size.x; x++) {
47 const pixel = y * this.size.x + x;
48 current = this.get(x, y) * scale;
49 data[pixel * 4 + 0] = current;
50 data[pixel * 4 + 1] = current;
51 data[pixel * 4 + 2] = current;
52 data[pixel * 4 + 3] = 255;
53 }
54 }
55 const newFrame = new ImageData(data, frame.width, frame.height);
56 ctx.putImageData(newFrame, 0, 0);
57 }
58
59 // Updates the underlying data from a given {ImageWrapper}
60 updateData(image: ImageWrapper): void {
61 this.originalSize = image.size;
62 this.data = image.data;
63 }
64
65 // Updates the position of the shared area, returns {this} for possible chaining
66 updateFrom(from: ImageRef): SubImage {
67 this.from = from;
68 return this;
69 }
70}
71
72export default SubImage;
packages/qrx/src/quagga/common/typedefs.jsadded+61-0
Changes to packages/qrx/src/quagga/common/typedefs.js
@@ -0,0 +1,61 @@
1/*
2 * typedefs.js
3 * Normalizes browser-specific prefixes and provide some basic polyfills
4 */
5
6if (typeof window !== 'undefined') {
7 if (!window.requestAnimationFrame) {
8 window.requestAnimationFrame = (function () {
9 return window.webkitRequestAnimationFrame
10 || window.mozRequestAnimationFrame
11 || window.oRequestAnimationFrame
12 || window.msRequestAnimationFrame
13 || function (/* function FrameRequestCallback */ callback) {
14 window.setTimeout(callback, 1000 / 60);
15 };
16 }());
17 }
18}
19
20if (typeof Math.imul !== 'function') {
21 /* eslint-disable no-bitwise */
22 Math.imul = function (a, b) {
23 const ah = (a >>> 16) & 0xffff;
24 const al = a & 0xffff;
25 const bh = (b >>> 16) & 0xffff;
26 const bl = b & 0xffff;
27 // the shift by 0 fixes the sign on the high part
28 // the final |0 converts the unsigned value into a signed value
29 return ((al * bl) + (((ah * bl + al * bh) << 16) >>> 0) | 0);
30 };
31 /* eslint-enable no-bitwise */
32}
33
34if (typeof Object.assign !== 'function') {
35 Object.assign = function (target) { // .length of function is 2
36
37'use strict';
38
39 if (target === null) { // TypeError if undefined or null
40 throw new TypeError('Cannot convert undefined or null to object');
41 }
42
43 const to = Object(target);
44
45 for (let index = 1; index < arguments.length; index++) {
46 // eslint-disable-next-line prefer-rest-params
47 const nextSource = arguments[index];
48
49 if (nextSource !== null) { // Skip over if undefined or null
50 // eslint-disable-next-line no-restricted-syntax
51 for (const nextKey in nextSource) {
52 // Avoid bugs when hasOwnProperty is shadowed
53 if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
54 to[nextKey] = nextSource[nextKey];
55 }
56 }
57 }
58 }
59 return to;
60 };
61}
packages/qrx/src/quagga/config/config.dev.tsadded+55-0
Changes to packages/qrx/src/quagga/config/config.dev.ts
@@ -0,0 +1,55 @@
1import type { QuaggaJSConfigObject } from '../../type-definitions/quagga.d'
2
3const DevConfig: QuaggaJSConfigObject = {
4 inputStream: {
5 name: 'Live',
6 type: 'LiveStream',
7 constraints: {
8 width: 640,
9 height: 480,
10 // aspectRatio: 640/480, // optional
11 facingMode: 'environment', // or user
12 // deviceId: "38745983457387598375983759834"
13 },
14 area: {
15 top: '0%',
16 right: '0%',
17 left: '0%',
18 bottom: '0%',
19 },
20 singleChannel: false, // true: only the red color-channel is read
21 },
22 locate: true,
23 numOfWorkers: 0,
24 decoder: {
25 readers: [
26 'code_128_reader',
27 ],
28 debug: {
29 drawBoundingBox: false,
30 showFrequency: false,
31 drawScanline: false,
32 showPattern: false,
33 },
34 },
35 locator: {
36 halfSample: true,
37 patchSize: 'medium', // x-small, small, medium, large, x-large
38 debug: {
39 showCanvas: false,
40 showPatches: false,
41 showFoundPatches: false,
42 showSkeleton: false,
43 showLabels: false,
44 showPatchLabels: false,
45 showRemainingPatchLabels: false,
46 boxFromPatches: {
47 showTransformed: false,
48 showTransformedBox: false,
49 showBB: false,
50 },
51 },
52 },
53}
54
55export default DevConfig
packages/qrx/src/quagga/config/config.node.tsadded+29-0
Changes to packages/qrx/src/quagga/config/config.node.ts
@@ -0,0 +1,29 @@
1import type { QuaggaJSConfigObject } from '../../type-definitions/quagga.d'
2
3const NodeConfig: QuaggaJSConfigObject = {
4 inputStream: {
5 type: 'ImageStream',
6 sequence: false,
7 size: 800,
8 area: {
9 top: '0%',
10 right: '0%',
11 left: '0%',
12 bottom: '0%',
13 },
14 singleChannel: false, // true: only the red color-channel is read
15 },
16 locate: true,
17 numOfWorkers: 0,
18 decoder: {
19 readers: [
20 'code_128_reader',
21 ],
22 },
23 locator: {
24 halfSample: true,
25 patchSize: 'medium', // x-small, small, medium, large, x-large
26 },
27}
28
29export default NodeConfig
packages/qrx/src/quagga/config/config.prod.tsadded+35-0
Changes to packages/qrx/src/quagga/config/config.prod.ts
@@ -0,0 +1,35 @@
1import type { QuaggaJSConfigObject } from '../../type-definitions/quagga.d'
2
3const ProdConfig: QuaggaJSConfigObject = {
4 inputStream: {
5 name: 'Live',
6 type: 'LiveStream',
7 constraints: {
8 width: 640,
9 height: 480,
10 // aspectRatio: 640/480, // optional
11 facingMode: 'environment', // or user
12 // deviceId: "38745983457387598375983759834"
13 },
14 area: {
15 top: '0%',
16 right: '0%',
17 left: '0%',
18 bottom: '0%',
19 },
20 singleChannel: false, // true: only the red color-channel is read
21 },
22 locate: true,
23 numOfWorkers: 4,
24 decoder: {
25 readers: [
26 'code_128_reader',
27 ],
28 },
29 locator: {
30 halfSample: true,
31 patchSize: 'medium', // x-small, small, medium, large, x-large
32 },
33}
34
35export default ProdConfig
packages/qrx/src/quagga/config/config.tsadded+20-0
Changes to packages/qrx/src/quagga/config/config.ts
@@ -0,0 +1,20 @@
1import type { QuaggaJSConfigObject } from '../../type-definitions/quagga.d'
2import DevConfig from './config.dev'
3import NodeConfig from './config.node'
4import ProdConfig from './config.prod'
5
6const ExportConfig: QuaggaJSConfigObject = (() => {
7 let QuaggaConfig: QuaggaJSConfigObject
8 if (typeof ENV === 'undefined' || ENV.development) {
9 QuaggaConfig = DevConfig
10 }
11 else if (ENV.node) {
12 QuaggaConfig = NodeConfig
13 }
14 else {
15 QuaggaConfig = ProdConfig
16 }
17 return QuaggaConfig
18})()
19
20export default ExportConfig
packages/qrx/src/quagga/decoder/barcode_decoder.jsadded+359-0
Changes to packages/qrx/src/quagga/decoder/barcode_decoder.js
@@ -0,0 +1,359 @@
1import ImageDebug from '../common/image_debug'
2import TwoOfFiveReader from '../reader/2of5_reader'
3import CodabarReader from '../reader/codabar_reader'
4import Code32Reader from '../reader/code_32_reader'
5import Code39Reader from '../reader/code_39_reader'
6import Code39VINReader from '../reader/code_39_vin_reader'
7import Code93Reader from '../reader/code_93_reader'
8import Code128Reader from '../reader/code_128_reader'
9import EAN2Reader from '../reader/ean_2_reader'
10import EAN5Reader from '../reader/ean_5_reader'
11import EAN8Reader from '../reader/ean_8_reader'
12import EANReader from '../reader/ean_reader'
13import I2of5Reader from '../reader/i2of5_reader'
14import UPCEReader from '../reader/upc_e_reader'
15import UPCReader from '../reader/upc_reader'
16import Bresenham from './bresenham'
17
18const READERS = {
19 'code_128_reader': Code128Reader,
20 'ean_reader': EANReader,
21 'ean_5_reader': EAN5Reader,
22 'ean_2_reader': EAN2Reader,
23 'ean_8_reader': EAN8Reader,
24 'code_39_reader': Code39Reader,
25 'code_39_vin_reader': Code39VINReader,
26 'codabar_reader': CodabarReader,
27 'upc_reader': UPCReader,
28 'upc_e_reader': UPCEReader,
29 'i2of5_reader': I2of5Reader,
30 '2of5_reader': TwoOfFiveReader,
31 'code_93_reader': Code93Reader,
32 'code_32_reader': Code32Reader,
33}
34
35export default {
36 registerReader: (name, reader) => {
37 READERS[name] = reader
38 },
39 create(config, inputImageWrapper) {
40 const _canvas = {
41 ctx: {
42 frequency: null,
43 pattern: null,
44 overlay: null,
45 },
46 dom: {
47 frequency: null,
48 pattern: null,
49 overlay: null,
50 },
51 }
52 const _barcodeReaders = []
53
54 initCanvas()
55 initReaders()
56 initConfig()
57
58 function initCanvas() {
59 if (ENV.development && typeof document !== 'undefined') {
60 const $debug = document.querySelector('#debug.detection')
61 _canvas.dom.frequency = document.querySelector('canvas.frequency')
62 if (!_canvas.dom.frequency) {
63 _canvas.dom.frequency = document.createElement('canvas')
64 _canvas.dom.frequency.className = 'frequency'
65 if ($debug) {
66 $debug.appendChild(_canvas.dom.frequency)
67 }
68 }
69 console.warn('* barcode decoder initCanvas getcontext 2d')
70 _canvas.ctx.frequency = _canvas.dom.frequency.getContext('2d')
71
72 _canvas.dom.pattern = document.querySelector('canvas.patternBuffer')
73 if (!_canvas.dom.pattern) {
74 _canvas.dom.pattern = document.createElement('canvas')
75 _canvas.dom.pattern.className = 'patternBuffer'
76 if ($debug) {
77 $debug.appendChild(_canvas.dom.pattern)
78 }
79 }
80 _canvas.ctx.pattern = _canvas.dom.pattern.getContext('2d')
81
82 _canvas.dom.overlay = document.querySelector('canvas.drawingBuffer')
83 if (_canvas.dom.overlay) {
84 _canvas.ctx.overlay = _canvas.dom.overlay.getContext('2d')
85 }
86 }
87 }
88
89 function initReaders() {
90 config.readers.forEach((readerConfig) => {
91 let reader
92 let configuration = {}
93 let supplements = []
94
95 if (typeof readerConfig === 'object') {
96 reader = readerConfig.format
97 configuration = readerConfig.config
98 }
99 else if (typeof readerConfig === 'string') {
100 reader = readerConfig
101 }
102 if (ENV.development) {
103 console.log('Before registering reader: ', reader)
104 }
105 if (configuration.supplements) {
106 supplements = configuration
107 .supplements
108 .map(supplement => new READERS[supplement]())
109 }
110 try {
111 const readerObj = new READERS[reader](configuration, supplements)
112 _barcodeReaders.push(readerObj)
113 }
114 catch (err) {
115 console.error('* Error constructing reader ', reader, err)
116 throw err
117 }
118 })
119 if (ENV.development) {
120 console.log(`Registered Readers: ${_barcodeReaders
121 .map(reader => JSON.stringify({ format: reader.FORMAT, config: reader.config }))
122 .join(', ')}`)
123 }
124 }
125
126 function initConfig() {
127 if (ENV.development && typeof document !== 'undefined') {
128 let i
129 const vis = [{
130 node: _canvas.dom.frequency,
131 prop: config.debug.showFrequency,
132 }, {
133 node: _canvas.dom.pattern,
134 prop: config.debug.showPattern,
135 }]
136
137 for (i = 0; i < vis.length; i++) {
138 if (vis[i].prop === true) {
139 vis[i].node.style.display = 'block'
140 }
141 else {
142 vis[i].node.style.display = 'none'
143 }
144 }
145 }
146 }
147
148 /**
149 * extend the line on both ends
150 * @param {Array} line
151 * @param {number} angle
152 */
153 function getExtendedLine(line, angle, ext) {
154 function extendLine(amount) {
155 const extension = {
156 y: amount * Math.sin(angle),
157 x: amount * Math.cos(angle),
158 }
159
160 line[0].y -= extension.y
161 line[0].x -= extension.x
162 line[1].y += extension.y
163 line[1].x += extension.x
164 }
165
166 // check if inside image
167 extendLine(ext)
168 while (ext > 1 && (!inputImageWrapper.inImageWithBorder(line[0])
169 || !inputImageWrapper.inImageWithBorder(line[1]))) {
170 ext -= Math.ceil(ext / 2)
171 extendLine(-ext)
172 }
173 return line
174 }
175
176 function getLine(box) {
177 return [{
178 x: (box[1][0] - box[0][0]) / 2 + box[0][0],
179 y: (box[1][1] - box[0][1]) / 2 + box[0][1],
180 }, {
181 x: (box[3][0] - box[2][0]) / 2 + box[2][0],
182 y: (box[3][1] - box[2][1]) / 2 + box[2][1],
183 }]
184 }
185
186 function tryDecode(line) {
187 let result = null
188 let i
189 const barcodeLine = Bresenham.getBarcodeLine(inputImageWrapper, line[0], line[1])
190
191 if (ENV.development && config.debug.showFrequency) {
192 ImageDebug.drawPath(line, { x: 'x', y: 'y' }, _canvas.ctx.overlay, { color: 'red', lineWidth: 3 })
193 Bresenham.debug.printFrequency(barcodeLine.line, _canvas.dom.frequency)
194 }
195
196 Bresenham.toBinaryLine(barcodeLine)
197
198 if (ENV.development && config.debug.showPattern) {
199 Bresenham.debug.printPattern(barcodeLine.line, _canvas.dom.pattern)
200 }
201
202 for (i = 0; i < _barcodeReaders.length && result === null; i++) {
203 result = _barcodeReaders[i].decodePattern(barcodeLine.line)
204 }
205 if (result === null) {
206 return null
207 }
208 return {
209 codeResult: result,
210 barcodeLine,
211 }
212 }
213
214 /**
215 * This method slices the given area apart and tries to detect a barcode-pattern
216 * for each slice. It returns the decoded barcode, or null if nothing was found
217 * @param {Array} box
218 * @param {Array} line
219 * @param {number} lineAngle
220 */
221 function tryDecodeBruteForce(box, line, lineAngle) {
222 const sideLength = Math.sqrt((box[1][0] - box[0][0]) ** 2 + (box[1][1] - box[0][1]) ** 2)
223 let i
224 const slices = 16
225 let result = null
226 let dir
227 let extension
228 const xdir = Math.sin(lineAngle)
229 const ydir = Math.cos(lineAngle)
230
231 for (i = 1; i < slices && result === null; i++) {
232 // move line perpendicular to angle
233
234 dir = sideLength / slices * i * (i % 2 === 0 ? -1 : 1)
235 extension = {
236 y: dir * xdir,
237 x: dir * ydir,
238 }
239
240 line[0].y += extension.x
241 line[0].x -= extension.y
242 line[1].y += extension.x
243 line[1].x -= extension.y
244
245 result = tryDecode(line)
246 }
247 return result
248 }
249
250 function getLineLength(line) {
251 return Math.sqrt(
252 Math.abs(line[1].y - line[0].y) ** 2
253 + Math.abs(line[1].x - line[0].x) ** 2,
254 )
255 }
256
257 async function decodeFromImage(imageWrapper) {
258 let result = null
259 for (const reader of _barcodeReaders) {
260 if (reader.decodeImage) {
261 result = await reader.decodeImage(imageWrapper)
262 if (result) {
263 break
264 }
265 }
266 }
267 return result
268 }
269 /**
270 * With the help of the configured readers (Code128 or EAN) this function tries to detect a
271 * valid barcode pattern within the given area.
272 * @param {object} box The area to search in
273 * @returns {object} the result {codeResult, line, angle, pattern, threshold}
274 */
275 function decodeFromBoundingBox(box) {
276 let line
277 const ctx = _canvas.ctx.overlay
278 let result
279
280 if (ENV.development) {
281 if (config.debug.drawBoundingBox && ctx) {
282 ImageDebug.drawPath(box, { x: 0, y: 1 }, ctx, { color: 'blue', lineWidth: 2 })
283 }
284 }
285
286 line = getLine(box)
287 const lineLength = getLineLength(line)
288 const lineAngle = Math.atan2(line[1].y - line[0].y, line[1].x - line[0].x)
289 line = getExtendedLine(line, lineAngle, Math.floor(lineLength * 0.1))
290 if (line === null) {
291 return null
292 }
293
294 result = tryDecode(line)
295 if (result === null) {
296 result = tryDecodeBruteForce(box, line, lineAngle)
297 }
298
299 if (result === null) {
300 return null
301 }
302
303 if (ENV.development && result && config.debug.drawScanline && ctx) {
304 ImageDebug.drawPath(line, { x: 'x', y: 'y' }, ctx, { color: 'red', lineWidth: 3 })
305 }
306
307 return {
308 codeResult: result.codeResult,
309 line,
310 angle: lineAngle,
311 pattern: result.barcodeLine.line,
312 threshold: result.barcodeLine.threshold,
313 }
314 }
315
316 return {
317 decodeFromBoundingBox(box) {
318 return decodeFromBoundingBox(box)
319 },
320 decodeFromBoundingBoxes(boxes) {
321 let i; let result
322 const barcodes = []
323 const { multiple } = config
324
325 for (i = 0; i < boxes.length; i++) {
326 const box = boxes[i]
327 result = decodeFromBoundingBox(box) || {}
328 result.box = box
329
330 if (multiple) {
331 barcodes.push(result)
332 }
333 else if (result.codeResult) {
334 return result
335 }
336 }
337
338 return {
339 barcodes,
340 }
341 },
342 async decodeFromImage(imageWrapperIn) {
343 const result = await decodeFromImage(imageWrapperIn)
344 return result
345 },
346 registerReader(name, reader) {
347 if (READERS[name]) {
348 throw new Error('cannot register existing reader', name)
349 }
350 READERS[name] = reader
351 },
352 setReaders(readers) {
353 config.readers = readers
354 _barcodeReaders.length = 0
355 initReaders()
356 },
357 }
358 },
359}
packages/qrx/src/quagga/decoder/bresenham.tsadded+225-0
Changes to packages/qrx/src/quagga/decoder/bresenham.ts
@@ -0,0 +1,225 @@
1const Slope = {
2 DIR: {
3 UP: 1,
4 DOWN: -1,
5 },
6}
7
8export interface BresenhamType {
9 /**
10 * Scans a line of the given image from point p1 to p2 and returns a result object containing
11 * gray-scale values (0-255) of the underlying pixels in addition to the min
12 * and max values.
13 * @param {object} imageWrapper
14 * @param {object} p1 The start point {x,y}
15 * @param {object} p2 The end point {x,y}
16 * @returns {line, min, max}
17 */
18 getBarcodeLine: (imageWrapper: any, p1: { x: number, y: number }, p2: { x: number, y: number }) => {
19 line: number[]
20 min: number
21 max: number
22 }
23
24 /**
25 * Converts the result from getBarcodeLine into a binary representation
26 * also considering the frequency and slope of the signal for more robust results
27 * @param {object} result {line, min, max}
28 */
29 toBinaryLine: (result: { line: number[], min: number, max: number }) => {
30 line: number[]
31 threshold: number
32 }
33
34 debug?: {
35 /**
36 * Used for development only
37 * @param {Array} line
38 * @param {Element} canvas
39 */
40 printFrequency: (line: number[], canvas: HTMLCanvasElement) => void
41
42 /**
43 * Used for development only
44 * @param {Array} line
45 * @param {Element} canvas
46 */
47 printPattern: (line: number[], canvas: HTMLCanvasElement) => void
48 }
49}
50
51const Bresenham: BresenhamType = {
52 getBarcodeLine(imageWrapper, p1, p2) {
53 let x0 = p1.x | 0
54 let y0 = p1.y | 0
55 let x1 = p2.x | 0
56 let y1 = p2.y | 0
57
58 const steep = Math.abs(y1 - y0) > Math.abs(x1 - x0)
59 let error
60 let y
61 let tmp
62 let x
63 const line = []
64 const imageData = imageWrapper.data
65 const width = imageWrapper.size.x
66 let val
67 let min = 255
68 let max = 0
69
70 function read(a, b) {
71 val = imageData[b * width + a]
72 min = val < min ? val : min
73 max = val > max ? val : max
74 line.push(val)
75 }
76
77 if (steep) {
78 tmp = x0
79 x0 = y0
80 y0 = tmp
81
82 tmp = x1
83 x1 = y1
84 y1 = tmp
85 }
86 if (x0 > x1) {
87 tmp = x0
88 x0 = x1
89 x1 = tmp
90
91 tmp = y0
92 y0 = y1
93 y1 = tmp
94 }
95 const deltaX = x1 - x0
96 const deltaY = Math.abs(y1 - y0)
97 error = (deltaX / 2) | 0
98 y = y0
99 const yStep = y0 < y1 ? 1 : -1
100 for (x = x0; x < x1; x++) {
101 if (steep) {
102 read(y, x)
103 }
104 else {
105 read(x, y)
106 }
107 error -= deltaY
108 if (error < 0) {
109 y += yStep
110 error += deltaX
111 }
112 }
113
114 return {
115 line,
116 min,
117 max,
118 }
119 },
120
121 toBinaryLine(result) {
122 const { min } = result
123 const { max } = result
124 const { line } = result
125 let slope
126 let slope2
127 const center = min + (max - min) / 2
128 const extrema = []
129 let currentDir
130 let dir
131 let threshold = (max - min) / 12
132 const rThreshold = -threshold
133 let i
134 let j
135
136 // 1. find extrema
137 currentDir = line[0] > center ? Slope.DIR.UP : Slope.DIR.DOWN
138 extrema.push({
139 pos: 0,
140 val: line[0],
141 })
142 for (i = 0; i < line.length - 2; i++) {
143 slope = (line[i + 1] - line[i])
144 slope2 = (line[i + 2] - line[i + 1])
145 if ((slope + slope2) < rThreshold && line[i + 1] < (center * 1.5)) {
146 dir = Slope.DIR.DOWN
147 }
148 else if ((slope + slope2) > threshold && line[i + 1] > (center * 0.5)) {
149 dir = Slope.DIR.UP
150 }
151 else {
152 dir = currentDir
153 }
154
155 if (currentDir !== dir) {
156 extrema.push({
157 pos: i,
158 val: line[i],
159 })
160 currentDir = dir
161 }
162 }
163 extrema.push({
164 pos: line.length,
165 val: line[line.length - 1],
166 })
167
168 for (j = extrema[0].pos; j < extrema[1].pos; j++) {
169 line[j] = line[j] > center ? 0 : 1
170 }
171
172 // iterate over extrema and convert to binary based on avg between minmax
173 for (i = 1; i < extrema.length - 1; i++) {
174 if (extrema[i + 1].val > extrema[i].val) {
175 threshold = (extrema[i].val + ((extrema[i + 1].val - extrema[i].val) / 3) * 2) | 0
176 }
177 else {
178 threshold = (extrema[i + 1].val + ((extrema[i].val - extrema[i + 1].val) / 3)) | 0
179 }
180
181 for (j = extrema[i].pos; j < extrema[i + 1].pos; j++) {
182 line[j] = line[j] > threshold ? 0 : 1
183 }
184 }
185
186 return {
187 line,
188 threshold,
189 }
190 },
191
192 debug: {
193 printFrequency(line, canvas) {
194 let i
195 const ctx = canvas.getContext('2d')
196
197 canvas.width = line.length
198 canvas.height = 256
199
200 ctx.beginPath()
201 ctx.strokeStyle = 'blue'
202 for (i = 0; i < line.length; i++) {
203 ctx.moveTo(i, 255)
204 ctx.lineTo(i, 255 - line[i])
205 }
206 ctx.stroke()
207 ctx.closePath()
208 },
209
210 printPattern(line, canvas) {
211 const ctx = canvas.getContext('2d')
212 let i
213
214 canvas.width = line.length
215 ctx.fillColor = 'black'
216 for (i = 0; i < line.length; i++) {
217 if (line[i] === 1) {
218 ctx.fillRect(i, 0, 1, 100)
219 }
220 }
221 },
222 },
223}
224
225export default Bresenham
packages/qrx/src/quagga/global.d.tsadded+7-0
Changes to packages/qrx/src/quagga/global.d.ts
@@ -0,0 +1,7 @@
1interface QuaggaBuildEnvironment {
2 development?: boolean
3 node?: boolean
4}
5
6// injected by the build system
7declare const ENV: QuaggaBuildEnvironment
packages/qrx/src/quagga/input/camera_access.tsadded+170-0
Changes to packages/qrx/src/quagga/input/camera_access.ts
@@ -0,0 +1,170 @@
1import type {
2 QuaggaJSCameraAccess as CameraAccessType,
3 MediaTrackConstraintsWithDeprecated,
4} from '../../type-definitions/quagga.d'
5// TODO: when this file was written years ago, HTMLMediaElement.play() did not return a useful value
6// to let us know when the video started playing. Now, it does. So, we shouldn't need to run this
7// odd waitForVideo() function that polls to see if the video has started.
8import pick from 'lodash/pick'
9import { enumerateDevices, getUserMedia } from '../common/mediaDevices'
10import Exception from '../quagga/Exception'
11
12let streamRef: MediaStream | null
13
14function waitForVideo(video: HTMLVideoElement): Promise<void> {
15 return new Promise((resolve, reject) => {
16 let attempts = 10
17
18 function checkVideo(): void {
19 if (attempts > 0) {
20 if (video.videoWidth > 10 && video.videoHeight > 10) {
21 if (ENV.development) {
22 console.log(`* dev: checkVideo found ${video.videoWidth}px x ${video.videoHeight}px`)
23 }
24 resolve()
25 }
26 else {
27 window.setTimeout(checkVideo, 500)
28 }
29 }
30 else {
31 reject(new Exception('Unable to play video stream. Is webcam working?', -1)) // TODO: add error code
32 }
33 attempts--
34 }
35 checkVideo()
36 })
37}
38
39/**
40 * Tries to attach the camera-stream to a given video-element
41 * and calls the callback function when the content is ready
42 * @param {object} constraints
43 * @param {object} video
44 */
45async function initCamera(video: HTMLVideoElement | null, constraints: MediaStreamConstraints): Promise<void> {
46 const stream = await getUserMedia(constraints)
47 streamRef = stream
48 if (video) {
49 video.setAttribute('autoplay', 'true')
50 video.setAttribute('muted', 'true')
51 video.setAttribute('playsinline', 'true') // not listed on MDN...
52
53 video.srcObject = stream
54 video.addEventListener('loadedmetadata', () => {
55 video.play().catch((err) => {
56 console.warn('* Error while trying to play video stream:', err)
57 })
58 })
59 return waitForVideo(video)
60 }
61 return Promise.resolve()
62}
63
64function deprecatedConstraints(videoConstraints: MediaTrackConstraintsWithDeprecated): MediaTrackConstraints {
65 const normalized = pick(videoConstraints, ['width', 'height', 'facingMode', 'aspectRatio', 'deviceId'])
66
67 if (typeof videoConstraints.minAspectRatio !== 'undefined'
68 && videoConstraints.minAspectRatio > 0) {
69 normalized.aspectRatio = videoConstraints.minAspectRatio
70 console.log('WARNING: Constraint \'minAspectRatio\' is deprecated; Use \'aspectRatio\' instead')
71 }
72 if (typeof videoConstraints.facing !== 'undefined') {
73 normalized.facingMode = videoConstraints.facing
74 console.log('WARNING: Constraint \'facing\' is deprecated. Use \'facingMode\' instead\'')
75 }
76 return normalized
77}
78
79// TODO: #192 I don't think there's any good reason pickConstraints should return a Promise,
80// I think it was just that way so it could be chained to other functions that did return a Promise.
81// That's not necessary with async functions being a thing, so that should be fixed.
82export function pickConstraints(videoConstraints: MediaTrackConstraintsWithDeprecated = {}): Promise<MediaStreamConstraints> {
83 const video = deprecatedConstraints(videoConstraints)
84
85 if (video && video.deviceId && video.facingMode) {
86 delete video.facingMode
87 }
88 return Promise.resolve({ audio: false, video })
89}
90
91async function enumerateVideoDevices(): Promise<Array<MediaDeviceInfo>> {
92 const devices = await enumerateDevices()
93 return devices.filter((device: MediaDeviceInfo) => device.kind === 'videoinput')
94}
95
96function getActiveTrack(): MediaStreamTrack | null {
97 if (!streamRef) {
98 return null
99 }
100 const tracks = streamRef.getVideoTracks()
101 return tracks && tracks?.length ? tracks[0] : null
102}
103
104/**
105 * Used for accessing information about the active stream track and available video devices.
106 */
107const QuaggaJSCameraAccess: CameraAccessType = {
108 requestedVideoElement: null,
109 async request(video: HTMLVideoElement | null, videoConstraints?: MediaTrackConstraintsWithDeprecated): Promise<any> {
110 QuaggaJSCameraAccess.requestedVideoElement = video
111 const newConstraints = await pickConstraints(videoConstraints)
112 return initCamera(video, newConstraints)
113 },
114 release(): Promise<void> {
115 const tracks = streamRef && streamRef.getVideoTracks()
116 if (QuaggaJSCameraAccess.requestedVideoElement !== null) {
117 QuaggaJSCameraAccess.requestedVideoElement.pause()
118 }
119 return new Promise<void>((resolve) => {
120 setTimeout(() => {
121 if (tracks && tracks.length) {
122 tracks.forEach(track => track.stop())
123 }
124 streamRef = null
125 QuaggaJSCameraAccess.requestedVideoElement = null
126 resolve()
127 }, 0)
128 })
129 },
130 enumerateVideoDevices,
131 getActiveStreamLabel(): string {
132 const track = getActiveTrack()
133 return track ? track.label : ''
134 },
135 getActiveTrack,
136 async disableTorch() {
137 const track = getActiveTrack()
138 // TODO: should we acquire camera access even if there's no current camera open?
139 // TODO: what happens on iOS or another device where torch isn't supported at all? Should we throw an error?
140 if (track) {
141 try {
142 await track.applyConstraints({ advanced: [{ torch: false }] } as MediaTrackConstraintSet)
143 }
144 catch (err) {
145 if (err instanceof OverconstrainedError) {
146 console.warn('quagga2/CameraAccess: Torch not supported on this device')
147 }
148 throw err
149 }
150 }
151 },
152 async enableTorch() {
153 const track = getActiveTrack()
154 // TODO: should we acquire camera access even if there's no current camera open?
155 // TODO: what happens on iOS or another device where torch isn't supported at all? Should we throw an error?
156 if (track) {
157 try {
158 await track.applyConstraints({ advanced: [{ torch: true }] } as MediaTrackConstraintSet)
159 }
160 catch (err) {
161 if (err instanceof OverconstrainedError) {
162 console.warn('quagga2/CameraAccess: Torch not supported on this device')
163 }
164 throw err
165 }
166 }
167 },
168}
169
170export default QuaggaJSCameraAccess
packages/qrx/src/quagga/input/exif_helper.jsadded+153-0
Changes to packages/qrx/src/quagga/input/exif_helper.js
@@ -0,0 +1,153 @@
1// NOTE: (SOME OF) THIS IS BROWSER ONLY CODE. Node does not have 'atob' built in, nor XMLHttpRequest.
2// How exactly is this set of functions used in Quagga? Do we need the browser specific code? Do we
3// need to port any part of this that doesn't work in Node to node?
4
5// Tags scraped from https://github.com/exif-js/exif-js
6const ExifTags = { 0x0112: 'orientation' }
7export const AvailableTags = Object.keys(ExifTags).map(key => ExifTags[key])
8
9export function findTagsInObjectURL(src, tags = AvailableTags) {
10 if (/^blob:/i.test(src)) {
11 return objectURLToBlob(src)
12 .then(readToBuffer)
13 .then(buffer => findTagsInBuffer(buffer, tags))
14 }
15 return Promise.resolve(null)
16}
17
18export function base64ToArrayBuffer(dataUrl) {
19 const base64 = dataUrl.replace(/^data:([^;]+);base64,/gim, '')
20 const binary = atob(base64)
21 const len = binary.length
22 const buffer = new ArrayBuffer(len)
23 const view = new Uint8Array(buffer)
24
25 for (let i = 0; i < len; i++) {
26 view[i] = binary.charCodeAt(i)
27 }
28 return buffer
29}
30
31function readToBuffer(blob) {
32 return new Promise((resolve) => {
33 const fileReader = new FileReader()
34 fileReader.onload = function (e) {
35 return resolve(e.target.result)
36 }
37 fileReader.readAsArrayBuffer(blob)
38 })
39}
40
41function objectURLToBlob(url) {
42 return new Promise((resolve, reject) => {
43 const http = new XMLHttpRequest()
44 http.open('GET', url, true)
45 http.responseType = 'blob'
46 http.onreadystatechange = function () {
47 if (http.readyState === XMLHttpRequest.DONE && (http.status === 200 || http.status === 0)) {
48 resolve(this.response)
49 }
50 }
51 http.onerror = reject
52 http.send()
53 })
54}
55
56export function findTagsInBuffer(file, selectedTags = AvailableTags) {
57 const dataView = new DataView(file)
58 const length = file.byteLength
59 const exifTags = selectedTags.reduce((result, selectedTag) => {
60 const exifTag = Object.keys(ExifTags).filter(tag => ExifTags[tag] === selectedTag)[0]
61 if (exifTag) {
62 result[exifTag] = selectedTag
63 }
64 return result
65 }, {})
66 let offset = 2
67 let marker
68
69 if ((dataView.getUint8(0) !== 0xFF) || (dataView.getUint8(1) !== 0xD8)) {
70 return false
71 }
72
73 while (offset < length) {
74 if (dataView.getUint8(offset) !== 0xFF) {
75 return false
76 }
77
78 marker = dataView.getUint8(offset + 1)
79 if (marker === 0xE1) {
80 return readEXIFData(dataView, offset + 4, exifTags)
81 }
82 offset += 2 + dataView.getUint16(offset + 2)
83 }
84
85 return false
86}
87
88function readEXIFData(file, start, exifTags) {
89 if (getStringFromBuffer(file, start, 4) !== 'Exif') {
90 return false
91 }
92
93 const tiffOffset = start + 6
94 let bigEnd
95
96 if (file.getUint16(tiffOffset) === 0x4949) {
97 bigEnd = false
98 }
99 else if (file.getUint16(tiffOffset) === 0x4D4D) {
100 bigEnd = true
101 }
102 else {
103 return false
104 }
105
106 if (file.getUint16(tiffOffset + 2, !bigEnd) !== 0x002A) {
107 return false
108 }
109
110 const firstIFDOffset = file.getUint32(tiffOffset + 4, !bigEnd)
111 if (firstIFDOffset < 0x00000008) {
112 return false
113 }
114
115 const tags = readTags(file, tiffOffset, tiffOffset + firstIFDOffset, exifTags, bigEnd)
116 return tags
117}
118
119function readTags(file, tiffStart, dirStart, strings, bigEnd) {
120 const entries = file.getUint16(dirStart, !bigEnd)
121 const tags = {}
122
123 for (let i = 0; i < entries; i++) {
124 const entryOffset = dirStart + i * 12 + 2
125 const tag = strings[file.getUint16(entryOffset, !bigEnd)]
126 if (tag) {
127 tags[tag] = readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd)
128 }
129 }
130 return tags
131}
132
133function readTagValue(file, entryOffset, tiffStart, dirStart, bigEnd) {
134 const type = file.getUint16(entryOffset + 2, !bigEnd)
135 const numValues = file.getUint32(entryOffset + 4, !bigEnd)
136
137 switch (type) {
138 case 3:
139 if (numValues === 1) {
140 return file.getUint16(entryOffset + 8, !bigEnd)
141 }
142 }
143
144 return null
145}
146
147function getStringFromBuffer(buffer, start, length) {
148 let outstr = ''
149 for (let n = start; n < start + length; n++) {
150 outstr += String.fromCharCode(buffer.getUint8(n))
151 }
152 return outstr
153}
packages/qrx/src/quagga/input/frame_grabber.jsadded+100-0
Changes to packages/qrx/src/quagga/input/frame_grabber.js
@@ -0,0 +1,100 @@
1// NOTE FOR ANYONE IN HERE IN THE FUTURE: This module is used when the module is built for use in Node.
2// Webpack.config.js explicitly REPLACES this module with the file called frame_grabber_browser when it is packing the Browser distribution.
3
4const Ndarray = require('ndarray')
5const CVUtils = require('../common/cv_utils')
6const Interp2D = require('ndarray-linear-interpolate').d2
7
8const FrameGrabber = {}
9
10FrameGrabber.create = function (inputStream, canvas) {
11 // console.warn('*** FrameGrabberNode create()');
12 const _that = {}
13 const _videoSize = CVUtils.imageRef(inputStream.getRealWidth(), inputStream.getRealHeight())
14 const _canvasSize = inputStream.getCanvasSize()
15 const _size = CVUtils.imageRef(inputStream.getWidth(), inputStream.getHeight())
16 const _topRight = inputStream.getTopRight()
17 let _data = new Uint8Array(_size.x * _size.y)
18 const _grayData = new Uint8Array(_videoSize.x * _videoSize.y)
19 const _canvasData = new Uint8Array(_canvasSize.x * _canvasSize.y)
20
21 const _grayImageArray = Ndarray(_grayData, [_videoSize.y, _videoSize.x]).transpose(1, 0)
22 const _canvasImageArray = Ndarray(_canvasData, [_canvasSize.y, _canvasSize.x]).transpose(1, 0)
23 const _targetImageArray = _canvasImageArray
24 .hi(_topRight.x + _size.x, _topRight.y + _size.y)
25 .lo(_topRight.x, _topRight.y)
26 const _stepSizeX = _videoSize.x / _canvasSize.x
27 const _stepSizeY = _videoSize.y / _canvasSize.y
28
29 if (ENV.development) {
30 console.log('FrameGrabber', JSON.stringify({
31 videoSize: _grayImageArray.shape,
32 canvasSize: _canvasImageArray.shape,
33 stepSize: [_stepSizeX, _stepSizeY],
34 size: _targetImageArray.shape,
35 topRight: _topRight,
36 }))
37 }
38
39 /**
40 * Uses the given array as frame-buffer
41 */
42 _that.attachData = function (data) {
43 _data = data
44 }
45
46 /**
47 * Returns the used frame-buffer
48 */
49 _that.getData = function () {
50 return _data
51 }
52
53 /**
54 * Fetches a frame from the input-stream and puts into the frame-buffer.
55 * The image-data is converted to gray-scale and then half-sampled if configured.
56 */
57 _that.grab = function () {
58 const frame = inputStream.getFrame()
59
60 if (frame) {
61 this.scaleAndCrop(frame)
62 return true
63 }
64 return false
65 }
66
67 // eslint-disable-next-line
68 _that.scaleAndCrop = function(frame) {
69 // 1. compute full-sized gray image
70 CVUtils.computeGray(frame.data, _grayData)
71
72 // 2. interpolate
73 for (let y = 0; y < _canvasSize.y; y++) {
74 for (let x = 0; x < _canvasSize.x; x++) {
75 _canvasImageArray.set(x, y, (Interp2D(_grayImageArray, x * _stepSizeX, y * _stepSizeY)) | 0)
76 }
77 }
78
79 // targetImageArray must be equal to targetSize
80 if (_targetImageArray.shape[0] !== _size.x
81 || _targetImageArray.shape[1] !== _size.y) {
82 throw new Error('Shapes do not match!')
83 }
84
85 // 3. crop
86 for (let y = 0; y < _size.y; y++) {
87 for (let x = 0; x < _size.x; x++) {
88 _data[y * _size.x + x] = _targetImageArray.get(x, y)
89 }
90 }
91 }
92
93 _that.getSize = function () {
94 return _size
95 }
96
97 return _that
98}
99
100module.exports = FrameGrabber
packages/qrx/src/quagga/input/frame_grabber_browser.jsadded+129-0
Changes to packages/qrx/src/quagga/input/frame_grabber_browser.js
@@ -0,0 +1,129 @@
1// NOTE FOR ANYONE IN HERE IN THE FUTURE:
2// webpack.config.js replaces the frame_grabber module with THIS module when it is building for a Browser environment.
3
4import {
5 computeGray,
6 grayAndHalfSampleFromCanvasData,
7 imageRef,
8} from '../common/cv_utils'
9
10const TO_RADIANS = Math.PI / 180
11
12function adjustCanvasSize(canvas, targetSize) {
13 if (canvas.width !== targetSize.x) {
14 if (ENV.development) {
15 console.log('WARNING: canvas-size needs to be adjusted')
16 }
17 canvas.width = targetSize.x
18 }
19 if (canvas.height !== targetSize.y) {
20 if (ENV.development) {
21 console.log('WARNING: canvas-size needs to be adjusted')
22 }
23 canvas.height = targetSize.y
24 }
25}
26
27const FrameGrabber = {}
28
29FrameGrabber.create = function (inputStream, canvas) {
30 // console.warn('*** FrameGrabberBrowser create');
31 const _that = {}
32 const _streamConfig = inputStream.getConfig()
33 const _videoSize = imageRef(inputStream.getRealWidth(), inputStream.getRealHeight())
34 const _canvasSize = inputStream.getCanvasSize()
35 const _size = imageRef(inputStream.getWidth(), inputStream.getHeight())
36 const topRight = inputStream.getTopRight()
37 const _sx = topRight.x
38 const _sy = topRight.y
39 let _canvas
40 let _ctx = null
41 let _data = null
42 const { willReadFrequently } = _streamConfig
43
44 _canvas = canvas || document.createElement('canvas')
45 _canvas.width = _canvasSize.x
46 _canvas.height = _canvasSize.y
47 console.warn('*** frame_grabber_browser: willReadFrequently=', willReadFrequently, 'canvas=', _canvas)
48 _ctx = _canvas.getContext('2d', { willReadFrequently: !!willReadFrequently }) // double not because we have an optional bool that needs to pass as a bool
49 _data = new Uint8Array(_size.x * _size.y)
50 if (ENV.development) {
51 console.log('FrameGrabber', JSON.stringify({
52 size: _size,
53 topRight,
54 videoSize: _videoSize,
55 canvasSize: _canvasSize,
56 }))
57 }
58
59 /**
60 * Uses the given array as frame-buffer
61 */
62 _that.attachData = function (data) {
63 _data = data
64 }
65
66 /**
67 * Returns the used frame-buffer
68 */
69 _that.getData = function () {
70 return _data
71 }
72
73 /**
74 * Fetches a frame from the input-stream and puts into the frame-buffer.
75 * The image-data is converted to gray-scale and then half-sampled if configured.
76 */
77 _that.grab = function () {
78 const doHalfSample = _streamConfig.halfSample
79 const frame = inputStream.getFrame()
80 let drawable = frame
81 let drawAngle = 0
82 let ctxData
83 if (drawable) {
84 adjustCanvasSize(_canvas, _canvasSize)
85 if (_streamConfig.type === 'ImageStream') {
86 drawable = frame.img
87 if (frame.tags && frame.tags.orientation) {
88 switch (frame.tags.orientation) {
89 case 6:
90 drawAngle = 90 * TO_RADIANS
91 break
92 case 8:
93 drawAngle = -90 * TO_RADIANS
94 break
95 }
96 }
97 }
98
99 if (drawAngle !== 0) {
100 _ctx.translate(_canvasSize.x / 2, _canvasSize.y / 2)
101 _ctx.rotate(drawAngle)
102 _ctx.drawImage(drawable, -_canvasSize.y / 2, -_canvasSize.x / 2, _canvasSize.y, _canvasSize.x)
103 _ctx.rotate(-drawAngle)
104 _ctx.translate(-_canvasSize.x / 2, -_canvasSize.y / 2)
105 }
106 else {
107 _ctx.drawImage(drawable, 0, 0, _canvasSize.x, _canvasSize.y)
108 }
109
110 ctxData = _ctx.getImageData(_sx, _sy, _size.x, _size.y).data
111 if (doHalfSample) {
112 grayAndHalfSampleFromCanvasData(ctxData, _size, _data)
113 }
114 else {
115 computeGray(ctxData, _data, _streamConfig)
116 }
117 return true
118 }
119 return false
120 }
121
122 _that.getSize = function () {
123 return _size
124 }
125
126 return _that
127}
128
129export default FrameGrabber
packages/qrx/src/quagga/input/image_loader.jsadded+74-0
Changes to packages/qrx/src/quagga/input/image_loader.js
@@ -0,0 +1,74 @@
1import { findTagsInObjectURL } from './exif_helper'
2
3const ImageLoader = {}
4ImageLoader.load = function (directory, callback, offset, size, sequence) {
5 const htmlImagesSrcArray = new Array(size)
6 const htmlImagesArray = Array.from({ length: htmlImagesSrcArray.length })
7 let i
8 let img
9 let num
10
11 if (sequence === false) {
12 htmlImagesSrcArray[0] = directory
13 }
14 else {
15 for (i = 0; i < htmlImagesSrcArray.length; i++) {
16 num = (offset + i)
17 htmlImagesSrcArray[i] = `${directory}image-${(`00${num}`).slice(-3)}.jpg`
18 }
19 }
20 htmlImagesArray.notLoaded = []
21 htmlImagesArray.addImage = function (image) {
22 htmlImagesArray.notLoaded.push(image)
23 }
24 htmlImagesArray.loaded = function (loadedImg) {
25 const notloadedImgs = htmlImagesArray.notLoaded
26 for (let x = 0; x < notloadedImgs.length; x++) {
27 if (notloadedImgs[x] === loadedImg) {
28 notloadedImgs.splice(x, 1)
29 for (let y = 0; y < htmlImagesSrcArray.length; y++) {
30 const imgName = htmlImagesSrcArray[y].substr(htmlImagesSrcArray[y].lastIndexOf('/'))
31 if (loadedImg.src.includes(imgName)) {
32 htmlImagesArray[y] = { img: loadedImg }
33 break
34 }
35 }
36 break
37 }
38 }
39 if (notloadedImgs.length === 0) {
40 if (ENV.development) {
41 console.log('Images loaded')
42 }
43 if (sequence === false) {
44 findTagsInObjectURL(directory, ['orientation'])
45 .then((tags) => {
46 htmlImagesArray[0].tags = tags
47 callback(htmlImagesArray)
48 })
49 .catch((e) => {
50 console.log(e)
51 callback(htmlImagesArray)
52 })
53 }
54 else {
55 callback(htmlImagesArray)
56 }
57 }
58 }
59
60 for (i = 0; i < htmlImagesSrcArray.length; i++) {
61 img = new Image()
62 htmlImagesArray.addImage(img)
63 addOnloadHandler(img, htmlImagesArray)
64 img.src = htmlImagesSrcArray[i]
65 }
66}
67
68function addOnloadHandler(img, htmlImagesArray) {
69 img.onload = function () {
70 htmlImagesArray.loaded(this)
71 }
72}
73
74export default (ImageLoader)
packages/qrx/src/quagga/input/input_stream/input_stream.d.tsadded+41-0
Changes to packages/qrx/src/quagga/input/input_stream/input_stream.d.ts
@@ -0,0 +1,41 @@
1import type { Point, XYSize } from '../../../type-definitions/quagga.d'
2
3type EventHandler = (arg?: any) => void
4
5export interface EventHandlerList {
6 [index: string]: Array<EventHandler>
7}
8
9export interface InputStream {
10 addEventListener: (event: string, f: (args?: any) => void, bool?: boolean) => void
11 clearEventHandlers: () => void
12 ended: () => boolean
13 getCanvasSize: () => XYSize
14 getConfig: () => any
15 getFrame: () => any
16 getHeight: () => number
17 getRealHeight: () => number
18 getRealWidth: () => number
19 getTopRight: () => Point
20 getWidth: () => number
21 pause: () => void
22 play: () => void
23 setAttribute: (name: any, value: any) => void
24 setCanvasSize: (size: XYSize) => void
25 setCurrentTime: (time: number) => void
26 setHeight: (height: number) => void
27 setInputStream: (config: any) => void
28 setTopRight: (topRight: Point) => void
29 setWidth: (width: number) => void
30 trigger: (eventName: any, args?: any) => void
31}
32
33type VideoStreamFactory = (video: HTMLVideoElement) => InputStream
34type LiveStreamFactory = (video: HTMLVideoElement) => InputStream
35type ImageStreamFactory = () => InputStream
36
37export interface InputStreamFactory {
38 createImageStream: ImageStreamFactory
39 createLiveStream: LiveStreamFactory
40 createVideoStream: VideoStreamFactory
41}
packages/qrx/src/quagga/input/input_stream/input_stream.tsadded+194-0
Changes to packages/qrx/src/quagga/input/input_stream/input_stream.ts
@@ -0,0 +1,194 @@
1// TODO: It's pretty likely that this shares code with the browser version, investigate that
2// FOR ANYONE IN HERE IN THE FUTURE: This is the default input_stream module used for the Node bundle.
3// webpack.config.js *replaces* this with input_stream_browser.ts when the bundle is being built for browser.
4
5import type { Point, XYSize } from '../../../type-definitions/quagga.d'
6import type { EventHandlerList, InputStream, InputStreamFactory } from './input_stream'
7import GetPixels from 'get-pixels'
8
9const inputStreamFactory: InputStreamFactory = {
10 createVideoStream(): never {
11 throw new Error('createVideoStream not available')
12 },
13 createLiveStream(): never {
14 throw new Error('createLiveStream not available')
15 },
16 createImageStream(): InputStream {
17 // console.warn('* InputStreamNode createImageStream');
18 let _config: { mime: string, size: number, src: any } | null = null
19
20 let width = 0
21 let height = 0
22 let loaded = false
23 // TODO: frame should be a type NdArray, but NdArray doesn't have ts definitions
24 // TODO: there is a ts-ndarray that might work, though
25 let frame: any = null
26 let baseUrl: string
27 const ended = false
28 let calculatedWidth: number
29 let calculatedHeight: number
30 const _eventNames = ['canrecord', 'ended']
31 const _eventHandlers: EventHandlerList = {}
32 const _topRight: Point = { x: 0, y: 0, type: 'Point' }
33 const _canvasSize: XYSize = { x: 0, y: 0, type: 'XYSize' }
34 // false eslint errors? weird.
35 // @ts-ignore
36 let size = 0
37 // @ts-ignore
38 let frameIdx = 0
39 // @ts-ignore
40 let paused = false
41
42 function loadImages(): void {
43 loaded = false
44
45 GetPixels(baseUrl, _config?.mime, (err, pixels) => {
46 if (err) {
47 console.error('**** quagga loadImages error:', err)
48 throw new Error('error decoding pixels in loadImages')
49 }
50 loaded = true
51 if (ENV.development) {
52 console.log('* InputStreamNode pixels.shape', pixels.shape)
53 }
54 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
55 frame = pixels;
56 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
57 [width, height] = pixels.shape
58
59 calculatedWidth = _config?.size
60 ? width / height > 1
61 ? _config.size
62 : Math.floor((width / height) * _config.size)
63 : width
64
65 calculatedHeight = _config?.size
66 ? width / height > 1
67 ? Math.floor((height / width) * _config.size)
68 : _config.size
69 : height
70
71 _canvasSize.x = calculatedWidth
72 _canvasSize.y = calculatedHeight
73
74 setTimeout(() => {
75 // eslint-disable-next-line @typescript-eslint/no-use-before-define
76 publishEvent('canrecord', [])
77 }, 0)
78 })
79 }
80
81 function publishEvent(eventName: string, args: Array<any>): void {
82 const handlers = _eventHandlers[eventName]
83
84 if (handlers && handlers.length > 0) {
85 for (let j = 0; j < handlers.length; j++) {
86 // eslint-disable-next-line @typescript-eslint/no-use-before-define
87 handlers[j].apply(inputStream, args as any)
88 }
89 }
90 }
91
92 // eslint-disable-next-line no-var,vars-on-top
93 var inputStream: InputStream = {
94 trigger: publishEvent,
95
96 getWidth() {
97 return calculatedWidth
98 },
99
100 getHeight() {
101 return calculatedHeight
102 },
103
104 setWidth(w) {
105 calculatedWidth = w
106 },
107
108 setHeight(h) {
109 calculatedHeight = h
110 },
111
112 getRealWidth() {
113 return width
114 },
115
116 getRealHeight() {
117 return height
118 },
119
120 setInputStream(stream) {
121 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
122 _config = stream
123 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
124 baseUrl = _config?.src
125 size = 1
126 loadImages()
127 },
128
129 ended() {
130 return ended
131 },
132
133 setAttribute() {},
134
135 getConfig() {
136 return _config
137 },
138
139 pause() {
140 paused = true
141 },
142
143 play() {
144 paused = false
145 },
146
147 setCurrentTime(time) {
148 frameIdx = time
149 },
150
151 addEventListener(event, f) {
152 if (_eventNames.includes(event)) {
153 if (!_eventHandlers[event]) {
154 _eventHandlers[event] = []
155 }
156 _eventHandlers[event].push(f)
157 }
158 },
159
160 clearEventHandlers() {
161 Object.keys(_eventHandlers).forEach(ind => delete _eventHandlers[ind])
162 },
163
164 setTopRight(topRight) {
165 _topRight.x = topRight.x
166 _topRight.y = topRight.y
167 },
168
169 getTopRight() {
170 return _topRight
171 },
172
173 setCanvasSize(sz) {
174 _canvasSize.x = sz.x
175 _canvasSize.y = sz.y
176 },
177
178 getCanvasSize() {
179 return _canvasSize
180 },
181
182 getFrame() {
183 if (!loaded) {
184 return null
185 }
186 // eslint-disable-next-line @typescript-eslint/no-unsafe-return
187 return frame
188 },
189 }
190 return inputStream
191 },
192}
193
194export default inputStreamFactory
packages/qrx/src/quagga/input/input_stream/input_stream_browser.tsadded+363-0
Changes to packages/qrx/src/quagga/input/input_stream/input_stream_browser.ts
@@ -0,0 +1,363 @@
1/* eslint-disable @typescript-eslint/no-explicit-any */
2
3import type { Point, XYSize } from '../../../type-definitions/quagga.d'
4import type { EventHandlerList, InputStream, InputStreamFactory } from './input_stream'
5import ImageLoader from '../image_loader'
6
7const inputStreamFactory: InputStreamFactory = {
8 createVideoStream(video): InputStream {
9 console.warn('**** InputStreamBrowser createVideoStream')
10 let _config: { size: number, type: string } | null = null
11 const _eventNames = ['canrecord', 'ended']
12 const _eventHandlers: EventHandlerList = {}
13 let _calculatedWidth: number
14 let _calculatedHeight: number
15 const _topRight: Point = { x: 0, y: 0, type: 'Point' }
16 const _canvasSize: XYSize = { x: 0, y: 0, type: 'XYSize' }
17
18 function initSize(): void {
19 const width = video.videoWidth
20 const height = video.videoHeight
21
22 _calculatedWidth = _config?.size ? width / height > 1 ? _config.size : Math.floor((width / height) * _config.size) : width
23
24 _calculatedHeight = _config?.size ? width / height > 1 ? Math.floor((height / width) * _config.size) : _config.size : height
25
26 _canvasSize.x = _calculatedWidth
27 _canvasSize.y = _calculatedHeight
28 }
29 const inputStream: InputStream = {
30 getRealWidth() {
31 return video.videoWidth
32 },
33
34 getRealHeight() {
35 return video.videoHeight
36 },
37
38 getWidth() {
39 return _calculatedWidth
40 },
41
42 getHeight() {
43 return _calculatedHeight
44 },
45
46 setWidth(width) {
47 _calculatedWidth = width
48 },
49
50 setHeight(height) {
51 _calculatedHeight = height
52 },
53
54 setInputStream(config) {
55 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
56 _config = config
57 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
58 this.setAttribute('src', (typeof config.src !== 'undefined') ? config.src : '')
59 },
60
61 ended() {
62 return video.ended
63 },
64
65 getConfig() {
66 return _config
67 },
68
69 setAttribute(name, value) {
70 if (video) {
71 video.setAttribute(name, value)
72 }
73 },
74
75 pause() {
76 video.pause()
77 },
78
79 play() {
80 // eslint-disable-next-line @typescript-eslint/no-floating-promises
81 video.play()
82 },
83
84 setCurrentTime(time) {
85 if (_config?.type !== 'LiveStream') {
86 this.setAttribute('currentTime', time.toString())
87 }
88 },
89
90 addEventListener(event, f, bool) {
91 if (_eventNames.includes(event)) {
92 if (!_eventHandlers[event]) {
93 _eventHandlers[event] = []
94 }
95 _eventHandlers[event].push(f)
96 }
97 else {
98 video.addEventListener(event, f, bool)
99 }
100 },
101
102 clearEventHandlers() {
103 _eventNames.forEach((eventName) => {
104 const handlers = _eventHandlers[eventName]
105 if (handlers && handlers.length > 0) {
106 handlers.forEach((handler) => {
107 video.removeEventListener(eventName, handler)
108 })
109 }
110 })
111 },
112
113 trigger(eventName, args) {
114 let j
115 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
116 const handlers = _eventHandlers[eventName]
117
118 if (eventName === 'canrecord') {
119 initSize()
120 }
121 if (handlers && handlers.length > 0) {
122 for (j = 0; j < handlers.length; j++) {
123 handlers[j].apply(inputStream, args)
124 }
125 }
126 },
127
128 setTopRight(topRight) {
129 _topRight.x = topRight.x
130 _topRight.y = topRight.y
131 },
132
133 getTopRight() {
134 return _topRight
135 },
136
137 setCanvasSize(size) {
138 _canvasSize.x = size.x
139 _canvasSize.y = size.y
140 },
141
142 getCanvasSize() {
143 return _canvasSize
144 },
145
146 getFrame() {
147 return video
148 },
149 }
150 return inputStream
151 },
152 createLiveStream(video): InputStream {
153 console.warn('**** InputStreamBrowser createLiveStream')
154 if (video) {
155 video.setAttribute('autoplay', 'true')
156 }
157 const that = inputStreamFactory.createVideoStream(video)
158 that.ended = function ended(): false {
159 return false
160 }
161 return that
162 },
163 createImageStream(): InputStream {
164 // console.warn('**** InputStreamBrowser createImageStream');
165 let _config: { size: number, sequence: any } | null = null
166
167 let width = 0
168 let height = 0
169 let frameIdx = 0
170 let paused = true
171 let loaded = false
172 let imgArray: any[] | null = null
173 let size = 0
174 const offset = 1
175 let baseUrl: string | null = null
176 let ended = false
177 let calculatedWidth: number
178 let calculatedHeight: number
179 const _eventNames = ['canrecord', 'ended']
180 const _eventHandlers: EventHandlerList = {}
181 const _topRight: Point = { x: 0, y: 0, type: 'Point' }
182 const _canvasSize: XYSize = { x: 0, y: 0, type: 'XYSize' }
183
184 function loadImages(): void {
185 loaded = false
186 ImageLoader.load(baseUrl, (imgs: Array<{ tags: any, img: HTMLImageElement }>) => {
187 imgArray = imgs
188 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
189 if (imgs[0].tags && imgs[0].tags.orientation) {
190 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
191 switch (imgs[0].tags.orientation) {
192 case 6:
193 case 8:
194 width = imgs[0].img.height
195 height = imgs[0].img.width
196 break
197 default:
198 width = imgs[0].img.width
199 height = imgs[0].img.height
200 }
201 }
202 else {
203 width = imgs[0].img.width
204 height = imgs[0].img.height
205 }
206
207 calculatedWidth = _config?.size ? width / height > 1 ? _config.size : Math.floor((width / height) * _config.size) : width
208
209 calculatedHeight = _config?.size ? width / height > 1 ? Math.floor((height / width) * _config.size) : _config.size : height
210 _canvasSize.x = calculatedWidth
211 _canvasSize.y = calculatedHeight
212 loaded = true
213 frameIdx = 0
214 setTimeout(() => {
215 // eslint-disable-next-line @typescript-eslint/no-use-before-define
216 publishEvent('canrecord', [])
217 }, 0)
218 }, offset, size, _config?.sequence)
219 }
220
221 function publishEvent(eventName: string, args: Array<any>): void {
222 let j
223 const handlers = _eventHandlers[eventName]
224
225 if (handlers && handlers.length > 0) {
226 for (j = 0; j < handlers.length; j++) {
227 // eslint-disable-next-line @typescript-eslint/no-use-before-define
228 handlers[j].apply(inputStream, args as any) // TODO: typescript complains that any[] is not valid for a second arg for apply?!
229 }
230 }
231 }
232
233 // TODO: any code shared with the first InputStream above should be shared not copied
234 // TODO: publishEvent needs access to inputStream, but inputStream needs access to publishEvent
235 // TODO: This is why it's a 'var', so it hoists back. This is ugly, and should be changed.
236 // eslint-disable-next-line no-var,vars-on-top
237 var inputStream: InputStream = {
238
239 trigger: publishEvent,
240
241 getWidth() {
242 return calculatedWidth
243 },
244
245 getHeight() {
246 return calculatedHeight
247 },
248
249 setWidth(newWidth) {
250 calculatedWidth = newWidth
251 },
252
253 setHeight(newHeight) {
254 calculatedHeight = newHeight
255 },
256
257 getRealWidth() {
258 return width
259 },
260
261 getRealHeight() {
262 return height
263 },
264
265 setInputStream(stream) {
266 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
267 _config = stream
268 // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
269 if (stream.sequence === false) {
270 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
271 baseUrl = stream.src
272 size = 1
273 }
274 else {
275 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
276 baseUrl = stream.src
277 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access
278 size = stream.length
279 }
280 loadImages()
281 },
282
283 ended() {
284 return ended
285 },
286
287 setAttribute() {},
288
289 getConfig() {
290 return _config
291 },
292
293 pause() {
294 paused = true
295 },
296
297 play() {
298 paused = false
299 },
300
301 setCurrentTime(time) {
302 frameIdx = time
303 },
304
305 addEventListener(event, f) {
306 if (_eventNames.includes(event)) {
307 if (!_eventHandlers[event]) {
308 _eventHandlers[event] = []
309 }
310 _eventHandlers[event].push(f)
311 }
312 },
313
314 clearEventHandlers() {
315 Object.keys(_eventHandlers).forEach(ind => delete _eventHandlers[ind])
316 },
317
318 setTopRight(topRight) {
319 _topRight.x = topRight.x
320 _topRight.y = topRight.y
321 },
322
323 getTopRight() {
324 return _topRight
325 },
326
327 setCanvasSize(canvasSize) {
328 _canvasSize.x = canvasSize.x
329 _canvasSize.y = canvasSize.y
330 },
331
332 getCanvasSize() {
333 return _canvasSize
334 },
335
336 getFrame() {
337 let frame
338
339 if (!loaded) {
340 return null
341 }
342 if (!paused) {
343 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
344 frame = imgArray?.[frameIdx]
345 if (frameIdx < (size - 1)) {
346 frameIdx++
347 }
348 else {
349 setTimeout(() => {
350 ended = true
351 publishEvent('ended', [])
352 }, 0)
353 }
354 }
355 // eslint-disable-next-line @typescript-eslint/no-unsafe-return
356 return frame
357 },
358 }
359 return inputStream
360 },
361}
362
363export default inputStreamFactory
packages/qrx/src/quagga/locator/barcode_locator.jsadded+589-0
Changes to packages/qrx/src/quagga/locator/barcode_locator.js
@@ -0,0 +1,589 @@
1import { glMatrix, mat2, vec2 } from 'gl-matrix'
2import ArrayHelper from '../common/array_helper'
3import {
4 calculatePatchSize,
5 cluster,
6 computeImageArea,
7 halfSample,
8 hsv2rgb,
9 imageRef,
10 otsuThreshold,
11 topGeneric,
12} from '../common/cv_utils'
13import ImageDebug from '../common/image_debug'
14import ImageWrapper from '../common/image_wrapper'
15import Rasterizer from './rasterizer'
16import skeletonizer from './skeletonizer'
17import Tracer from './tracer'
18
19glMatrix.setMatrixArrayType(Array)
20
21let _config
22let _currentImageWrapper
23let _skelImageWrapper
24let _subImageWrapper
25let _labelImageWrapper
26let _patchGrid
27let _patchLabelGrid
28let _imageToPatchGrid
29let _binaryImageWrapper
30let _patchSize
31const _canvasContainer = {
32 ctx: {
33 binary: null,
34 },
35 dom: {
36 binary: null,
37 },
38}
39const _numPatches = { x: 0, y: 0 }
40let _inputImageWrapper
41let _skeletonizer
42
43function initBuffers() {
44 if (_config.halfSample) {
45 _currentImageWrapper = new ImageWrapper({
46
47 x: _inputImageWrapper.size.x / 2 | 0,
48
49 y: _inputImageWrapper.size.y / 2 | 0,
50 })
51 }
52 else {
53 _currentImageWrapper = _inputImageWrapper
54 }
55
56 _patchSize = calculatePatchSize(_config.patchSize, _currentImageWrapper.size)
57
58 _numPatches.x = _currentImageWrapper.size.x / _patchSize.x | 0
59
60 _numPatches.y = _currentImageWrapper.size.y / _patchSize.y | 0
61
62 _binaryImageWrapper = new ImageWrapper(_currentImageWrapper.size, undefined, Uint8Array, false)
63
64 _labelImageWrapper = new ImageWrapper(_patchSize, undefined, Array, true)
65
66 const skeletonImageData = new ArrayBuffer(64 * 1024)
67 _subImageWrapper = new ImageWrapper(_patchSize, new Uint8Array(skeletonImageData, 0, _patchSize.x * _patchSize.y))
68 _skelImageWrapper = new ImageWrapper(_patchSize, new Uint8Array(skeletonImageData, _patchSize.x * _patchSize.y * 3, _patchSize.x * _patchSize.y), undefined, true)
69 _skeletonizer = skeletonizer(
70 (typeof window !== 'undefined') ? window : (typeof self !== 'undefined') ? self : global,
71 { size: _patchSize.x },
72 skeletonImageData,
73 )
74
75 _imageToPatchGrid = new ImageWrapper({
76
77 x: (_currentImageWrapper.size.x / _subImageWrapper.size.x) | 0,
78
79 y: (_currentImageWrapper.size.y / _subImageWrapper.size.y) | 0,
80 }, undefined, Array, true)
81 _patchGrid = new ImageWrapper(_imageToPatchGrid.size, undefined, undefined, true)
82 _patchLabelGrid = new ImageWrapper(_imageToPatchGrid.size, undefined, Int32Array, true)
83}
84
85function initCanvas() {
86 if (_config.useWorker || typeof document === 'undefined') {
87 return
88 }
89 _canvasContainer.dom.binary = document.createElement('canvas')
90 _canvasContainer.dom.binary.className = 'binaryBuffer'
91 if (ENV.development && _config.debug.showCanvas === true) {
92 document.querySelector('#debug').appendChild(_canvasContainer.dom.binary)
93 }
94 const willReadFrequently = !!_config.willReadFrequently
95 console.warn('* initCanvas willReadFrequently', willReadFrequently, _config)
96 _canvasContainer.ctx.binary = _canvasContainer.dom.binary.getContext('2d', { willReadFrequently })
97 _canvasContainer.dom.binary.width = _binaryImageWrapper.size.x
98 _canvasContainer.dom.binary.height = _binaryImageWrapper.size.y
99}
100
101/**
102 * Creates a bounding box which encloses all the given patches
103 * @returns {Array} The minimal bounding box
104 */
105function boxFromPatches(patches) {
106 let overAvg
107 let i
108 let j
109 let patch
110 let transMat
111 let minx = _binaryImageWrapper.size.x
112 let miny = _binaryImageWrapper.size.y
113 let maxx = -_binaryImageWrapper.size.x
114 let maxy = -_binaryImageWrapper.size.y
115 let box
116 let scale
117
118 // draw all patches which are to be taken into consideration
119 overAvg = 0
120 for (i = 0; i < patches.length; i++) {
121 patch = patches[i]
122 overAvg += patch.rad
123 if (ENV.development && _config.debug.showPatches) {
124 ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary, { color: 'red' })
125 }
126 }
127
128 overAvg /= patches.length
129 overAvg = (overAvg * 180 / Math.PI + 90) % 180 - 90
130 if (overAvg < 0) {
131 overAvg += 180
132 }
133
134 overAvg = (180 - overAvg) * Math.PI / 180
135 transMat = mat2.copy(mat2.create(), [Math.cos(overAvg), Math.sin(overAvg), -Math.sin(overAvg), Math.cos(overAvg)])
136
137 // iterate over patches and rotate by angle
138 for (i = 0; i < patches.length; i++) {
139 patch = patches[i]
140 for (j = 0; j < 4; j++) {
141 vec2.transformMat2(patch.box[j], patch.box[j], transMat)
142 }
143
144 if (ENV.development && _config.debug.boxFromPatches.showTransformed) {
145 ImageDebug.drawPath(patch.box, { x: 0, y: 1 }, _canvasContainer.ctx.binary, { color: '#99ff00', lineWidth: 2 })
146 }
147 }
148
149 // find bounding box
150 for (i = 0; i < patches.length; i++) {
151 patch = patches[i]
152 for (j = 0; j < 4; j++) {
153 if (patch.box[j][0] < minx) {
154 minx = patch.box[j][0]
155 }
156 if (patch.box[j][0] > maxx) {
157 maxx = patch.box[j][0]
158 }
159 if (patch.box[j][1] < miny) {
160 miny = patch.box[j][1]
161 }
162 if (patch.box[j][1] > maxy) {
163 maxy = patch.box[j][1]
164 }
165 }
166 }
167
168 box = [[minx, miny], [maxx, miny], [maxx, maxy], [minx, maxy]]
169
170 if (ENV.development && _config.debug.boxFromPatches.showTransformedBox) {
171 ImageDebug.drawPath(box, { x: 0, y: 1 }, _canvasContainer.ctx.binary, { color: '#ff0000', lineWidth: 2 })
172 }
173
174 scale = _config.halfSample ? 2 : 1
175 // reverse rotation;
176 transMat = mat2.invert(transMat, transMat)
177 for (j = 0; j < 4; j++) {
178 vec2.transformMat2(box[j], box[j], transMat)
179 }
180
181 if (ENV.development && _config.debug.boxFromPatches.showBB) {
182 ImageDebug.drawPath(box, { x: 0, y: 1 }, _canvasContainer.ctx.binary, { color: '#ff0000', lineWidth: 2 })
183 }
184
185 for (j = 0; j < 4; j++) {
186 vec2.scale(box[j], box[j], scale)
187 }
188
189 return box
190}
191
192/**
193 * Creates a binary image of the current image
194 */
195function binarizeImage() {
196 otsuThreshold(_currentImageWrapper, _binaryImageWrapper)
197 _binaryImageWrapper.zeroBorder()
198 if (ENV.development && _config.debug.showCanvas) {
199 _binaryImageWrapper.show(_canvasContainer.dom.binary, 255)
200 }
201}
202
203/**
204 * Iterate over the entire image
205 * extract patches
206 */
207function findPatches() {
208 let i
209 let j
210 let x
211 let y
212 let moments
213 let patchesFound = []
214 let rasterizer
215 let rasterResult
216 let patch
217 for (i = 0; i < _numPatches.x; i++) {
218 for (j = 0; j < _numPatches.y; j++) {
219 x = _subImageWrapper.size.x * i
220 y = _subImageWrapper.size.y * j
221
222 // seperate parts
223 skeletonize(x, y)
224
225 // Rasterize, find individual bars
226 _skelImageWrapper.zeroBorder()
227 ArrayHelper.init(_labelImageWrapper.data, 0)
228 rasterizer = Rasterizer.create(_skelImageWrapper, _labelImageWrapper)
229 rasterResult = rasterizer.rasterize(0)
230
231 if (ENV.development && _config.debug.showLabels) {
232 _labelImageWrapper.overlay(_canvasContainer.dom.binary, Math.floor(360 / rasterResult.count), { x, y })
233 }
234
235 // calculate moments from the skeletonized patch
236 moments = _labelImageWrapper.moments(rasterResult.count)
237
238 // extract eligible patches
239 patchesFound = patchesFound.concat(describePatch(moments, [i, j], x, y))
240 }
241 }
242
243 if (ENV.development && _config.debug.showFoundPatches) {
244 for (i = 0; i < patchesFound.length; i++) {
245 patch = patchesFound[i]
246 ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary, { color: '#99ff00', lineWidth: 2 })
247 }
248 }
249
250 return patchesFound
251}
252
253/**
254 * Finds those connected areas which contain at least 6 patches
255 * and returns them ordered DESC by the number of contained patches
256 * @param {number} maxLabel
257 */
258function findBiggestConnectedAreas(maxLabel) {
259 let i
260 let sum
261 let labelHist = []
262 let topLabels = []
263
264 for (i = 0; i < maxLabel; i++) {
265 labelHist.push(0)
266 }
267 sum = _patchLabelGrid.data.length
268 while (sum--) {
269 if (_patchLabelGrid.data[sum] > 0) {
270 labelHist[_patchLabelGrid.data[sum] - 1]++
271 }
272 }
273
274 labelHist = labelHist.map((val, idx) => ({
275 val,
276 label: idx + 1,
277 }))
278
279 labelHist.sort((a, b) => b.val - a.val)
280
281 // extract top areas with at least 6 patches present
282 topLabels = labelHist.filter(el => el.val >= 5)
283
284 return topLabels
285}
286
287/**
288 *
289 */
290function findBoxes(topLabels, maxLabel) {
291 let i
292 let j
293 let sum
294 const patches = []
295 let patch
296 let box
297 const boxes = []
298 const hsv = [0, 1, 1]
299 const rgb = [0, 0, 0]
300
301 for (i = 0; i < topLabels.length; i++) {
302 sum = _patchLabelGrid.data.length
303 patches.length = 0
304 while (sum--) {
305 if (_patchLabelGrid.data[sum] === topLabels[i].label) {
306 patch = _imageToPatchGrid.data[sum]
307 patches.push(patch)
308 }
309 }
310 box = boxFromPatches(patches)
311 if (box) {
312 boxes.push(box)
313
314 // draw patch-labels if requested
315 if (ENV.development && _config.debug.showRemainingPatchLabels) {
316 for (j = 0; j < patches.length; j++) {
317 patch = patches[j]
318 hsv[0] = (topLabels[i].label / (maxLabel + 1)) * 360
319 hsv2rgb(hsv, rgb)
320 ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary, { color: `rgb(${rgb.join(',')})`, lineWidth: 2 })
321 }
322 }
323 }
324 }
325 return boxes
326}
327
328/**
329 * Find similar moments (via cluster)
330 * @param {object} moments
331 */
332function similarMoments(moments) {
333 const clusters = cluster(moments, 0.90)
334 const topCluster = topGeneric(clusters, 1, e => e.getPoints().length)
335 let points = []; const
336 result = []
337 if (topCluster.length === 1) {
338 points = topCluster[0].item.getPoints()
339 for (let i = 0; i < points.length; i++) {
340 result.push(points[i].point)
341 }
342 }
343 return result
344}
345
346function skeletonize(x, y) {
347 _binaryImageWrapper.subImageAsCopy(_subImageWrapper, imageRef(x, y))
348 _skeletonizer.skeletonize()
349
350 // Show skeleton if requested
351 if (ENV.development && _config.debug.showSkeleton) {
352 _skelImageWrapper.overlay(_canvasContainer.dom.binary, 360, imageRef(x, y))
353 }
354}
355
356/**
357 * Extracts and describes those patches which seem to contain a barcode pattern
358 * @param {Array} moments
359 * @param {object} patchPos,
360 * @param {number} x
361 * @param {number} y
362 * @returns {Array} list of patches
363 */
364function describePatch(moments, patchPos, x, y) {
365 let k
366 let avg
367 const eligibleMoments = []
368 let matchingMoments
369 let patch
370 const patchesFound = []
371 const minComponentWeight = Math.ceil(_patchSize.x / 3)
372
373 if (moments.length >= 2) {
374 // only collect moments which's area covers at least minComponentWeight pixels.
375 for (k = 0; k < moments.length; k++) {
376 if (moments[k].m00 > minComponentWeight) {
377 eligibleMoments.push(moments[k])
378 }
379 }
380
381 // if at least 2 moments are found which have at least minComponentWeights covered
382 if (eligibleMoments.length >= 2) {
383 matchingMoments = similarMoments(eligibleMoments)
384 avg = 0
385 // determine the similarity of the moments
386 for (k = 0; k < matchingMoments.length; k++) {
387 avg += matchingMoments[k]?.rad ?? 0
388 }
389
390 // Only two of the moments are allowed not to fit into the equation
391 // add the patch to the set
392 if (matchingMoments.length > 1
393 && matchingMoments.length >= (eligibleMoments.length / 4) * 3
394 && matchingMoments.length > moments.length / 4) {
395 avg /= matchingMoments.length
396 patch = {
397 index: patchPos[1] * _numPatches.x + patchPos[0],
398 pos: {
399 x,
400 y,
401 },
402 box: [
403 vec2.clone([x, y]),
404 vec2.clone([x + _subImageWrapper.size.x, y]),
405 vec2.clone([x + _subImageWrapper.size.x, y + _subImageWrapper.size.y]),
406 vec2.clone([x, y + _subImageWrapper.size.y]),
407 ],
408 moments: matchingMoments,
409 rad: avg,
410 vec: vec2.clone([Math.cos(avg), Math.sin(avg)]),
411 }
412 patchesFound.push(patch)
413 }
414 }
415 }
416 return patchesFound
417}
418
419/**
420 * finds patches which are connected and share the same orientation
421 * @param {object} patchesFound
422 */
423function rasterizeAngularSimilarity(patchesFound) {
424 let label = 0
425 const threshold = 0.95
426 let currIdx = 0
427 let j
428 let patch
429 const hsv = [0, 1, 1]
430 const rgb = [0, 0, 0]
431
432 function notYetProcessed() {
433 let i
434 for (i = 0; i < _patchLabelGrid.data.length; i++) {
435 if (_patchLabelGrid.data[i] === 0 && _patchGrid.data[i] === 1) {
436 return i
437 }
438 }
439 return _patchLabelGrid.data.length
440 }
441
442 function trace(currentIdx) {
443 let x
444 let y
445 let currentPatch
446 let idx
447 let dir
448 const current = {
449 x: currentIdx % _patchLabelGrid.size.x,
450 y: (currentIdx / _patchLabelGrid.size.x) | 0,
451 }
452 let similarity
453
454 if (currentIdx < _patchLabelGrid.data.length) {
455 currentPatch = _imageToPatchGrid.data[currentIdx]
456 // assign label
457 _patchLabelGrid.data[currentIdx] = label
458 for (dir = 0; dir < Tracer.searchDirections.length; dir++) {
459 y = current.y + Tracer.searchDirections[dir][0]
460 x = current.x + Tracer.searchDirections[dir][1]
461 idx = y * _patchLabelGrid.size.x + x
462
463 // continue if patch empty
464 if (_patchGrid.data[idx] === 0) {
465 _patchLabelGrid.data[idx] = Number.MAX_VALUE
466
467 continue
468 }
469
470 if (_patchLabelGrid.data[idx] === 0) {
471 similarity = Math.abs(vec2.dot(_imageToPatchGrid.data[idx].vec, currentPatch.vec))
472 if (similarity > threshold) {
473 trace(idx)
474 }
475 }
476 }
477 }
478 }
479
480 // prepare for finding the right patches
481 ArrayHelper.init(_patchGrid.data, 0)
482 ArrayHelper.init(_patchLabelGrid.data, 0)
483 ArrayHelper.init(_imageToPatchGrid.data, null)
484
485 for (j = 0; j < patchesFound.length; j++) {
486 patch = patchesFound[j]
487 _imageToPatchGrid.data[patch.index] = patch
488 _patchGrid.data[patch.index] = 1
489 }
490
491 // rasterize the patches found to determine area
492 _patchGrid.zeroBorder()
493
494 // eslint-disable-next-line no-cond-assign
495 while ((currIdx = notYetProcessed()) < _patchLabelGrid.data.length) {
496 label++
497 trace(currIdx)
498 }
499
500 // draw patch-labels if requested
501 if (ENV.development && _config.debug.showPatchLabels) {
502 for (j = 0; j < _patchLabelGrid.data.length; j++) {
503 if (_patchLabelGrid.data[j] > 0 && _patchLabelGrid.data[j] <= label) {
504 patch = _imageToPatchGrid.data[j]
505 hsv[0] = (_patchLabelGrid.data[j] / (label + 1)) * 360
506 hsv2rgb(hsv, rgb)
507 ImageDebug.drawRect(patch.pos, _subImageWrapper.size, _canvasContainer.ctx.binary, { color: `rgb(${rgb.join(',')})`, lineWidth: 2 })
508 }
509 }
510 }
511
512 return label
513}
514
515export default {
516 init(inputImageWrapper, config) {
517 _config = config
518 _inputImageWrapper = inputImageWrapper
519
520 initBuffers()
521 initCanvas()
522 },
523
524 locate() {
525 if (_config.halfSample) {
526 halfSample(_inputImageWrapper, _currentImageWrapper)
527 }
528
529 binarizeImage()
530 const patchesFound = findPatches()
531 // return unless 5% or more patches are found
532 if (patchesFound.length < _numPatches.x * _numPatches.y * 0.05) {
533 return null
534 }
535
536 // rasterrize area by comparing angular similarity;
537 const maxLabel = rasterizeAngularSimilarity(patchesFound)
538 if (maxLabel < 1) {
539 return null
540 }
541
542 // search for area with the most patches (biggest connected area)
543 const topLabels = findBiggestConnectedAreas(maxLabel)
544 if (topLabels.length === 0) {
545 return null
546 }
547
548 const boxes = findBoxes(topLabels, maxLabel)
549 return boxes
550 },
551
552 checkImageConstraints(inputStream, config) {
553 let patchSize
554 let width = inputStream.getWidth()
555 let height = inputStream.getHeight()
556 const thisHalfSample = config.halfSample ? 0.5 : 1
557 let area
558
559 // calculate width and height based on area
560 if (inputStream.getConfig().area) {
561 area = computeImageArea(width, height, inputStream.getConfig().area)
562 inputStream.setTopRight({ x: area.sx, y: area.sy })
563 inputStream.setCanvasSize({ x: width, y: height })
564 width = area.sw
565 height = area.sh
566 }
567
568 const size = {
569 x: Math.floor(width * thisHalfSample),
570 y: Math.floor(height * thisHalfSample),
571 }
572
573 patchSize = calculatePatchSize(config.patchSize, size)
574 if (ENV.development) {
575 console.log(`Patch-Size: ${JSON.stringify(patchSize)}`)
576 }
577
578 inputStream.setWidth(Math.max(Math.floor(Math.floor(size.x / patchSize.x) * (1 / thisHalfSample) * patchSize.x), patchSize.x))
579 inputStream.setHeight(Math.max(Math.floor(Math.floor(size.y / patchSize.y) * (1 / thisHalfSample) * patchSize.y), patchSize.y))
580
581 if ((inputStream.getWidth() % patchSize.x) === 0 && (inputStream.getHeight() % patchSize.y) === 0) {
582 return true
583 }
584
585 throw new Error(`Image dimensions do not comply with the current settings: Width (${
586 width} )and height (${height
587 }) must a multiple of ${patchSize.x}`)
588 },
589}
packages/qrx/src/quagga/locator/rasterizer.jsadded+204-0
Changes to packages/qrx/src/quagga/locator/rasterizer.js
@@ -0,0 +1,204 @@
1import Tracer from './tracer'
2
3/**
4 * http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization
5 */
6const Rasterizer = {
7 createContour2D() {
8 return {
9 dir: null,
10 index: null,
11 firstVertex: null,
12 insideContours: null,
13 nextpeer: null,
14 prevpeer: null,
15 }
16 },
17 CONTOUR_DIR: {
18 CW_DIR: 0,
19 CCW_DIR: 1,
20 UNKNOWN_DIR: 2,
21 },
22 DIR: {
23 OUTSIDE_EDGE: -32767,
24 INSIDE_EDGE: -32766,
25 },
26 create(imageWrapper, labelWrapper) {
27 const imageData = imageWrapper.data
28 const labelData = labelWrapper.data
29 const width = imageWrapper.size.x
30 const height = imageWrapper.size.y
31 const tracer = Tracer.create(imageWrapper, labelWrapper)
32
33 return {
34 rasterize(depthlabel) {
35 let color
36 let bc
37 let lc
38 let labelindex
39 let cx
40 let cy
41 const colorMap = []
42 let vertex
43 let p
44 let cc
45 let sc
46 let pos
47 let connectedCount = 0
48 let i
49
50 for (i = 0; i < 400; i++) {
51 colorMap[i] = 0
52 }
53
54 colorMap[0] = imageData[0]
55 cc = null
56 for (cy = 1; cy < height - 1; cy++) {
57 labelindex = 0
58 bc = colorMap[0]
59 for (cx = 1; cx < width - 1; cx++) {
60 pos = cy * width + cx
61 if (labelData[pos] === 0) {
62 color = imageData[pos]
63 if (color !== bc) {
64 if (labelindex === 0) {
65 lc = connectedCount + 1
66 colorMap[lc] = color
67 bc = color
68 vertex = tracer.contourTracing(cy, cx, lc, color, Rasterizer.DIR.OUTSIDE_EDGE)
69 if (vertex !== null) {
70 connectedCount++
71 labelindex = lc
72 p = Rasterizer.createContour2D()
73 p.dir = Rasterizer.CONTOUR_DIR.CW_DIR
74 p.index = labelindex
75 p.firstVertex = vertex
76 p.nextpeer = cc
77 p.insideContours = null
78 if (cc !== null) {
79 cc.prevpeer = p
80 }
81 cc = p
82 }
83 }
84 else {
85 vertex = tracer
86 .contourTracing(cy, cx, Rasterizer.DIR.INSIDE_EDGE, color, labelindex)
87 if (vertex !== null) {
88 p = Rasterizer.createContour2D()
89 p.firstVertex = vertex
90 p.insideContours = null
91 if (depthlabel === 0) {
92 p.dir = Rasterizer.CONTOUR_DIR.CCW_DIR
93 }
94 else {
95 p.dir = Rasterizer.CONTOUR_DIR.CW_DIR
96 }
97 p.index = depthlabel
98 sc = cc
99 while ((sc !== null) && sc.index !== labelindex) {
100 sc = sc.nextpeer
101 }
102 if (sc !== null) {
103 p.nextpeer = sc.insideContours
104 if (sc.insideContours !== null) {
105 sc.insideContours.prevpeer = p
106 }
107 sc.insideContours = p
108 }
109 }
110 }
111 }
112 else {
113 labelData[pos] = labelindex
114 }
115 }
116 else if (labelData[pos] === Rasterizer.DIR.OUTSIDE_EDGE
117 || labelData[pos] === Rasterizer.DIR.INSIDE_EDGE) {
118 labelindex = 0
119 if (labelData[pos] === Rasterizer.DIR.INSIDE_EDGE) {
120 bc = imageData[pos]
121 }
122 else {
123 bc = colorMap[0]
124 }
125 }
126 else {
127 labelindex = labelData[pos]
128 bc = colorMap[labelindex]
129 }
130 }
131 }
132 sc = cc
133 while (sc !== null) {
134 sc.index = depthlabel
135 sc = sc.nextpeer
136 }
137 return {
138 cc,
139 count: connectedCount,
140 }
141 },
142 debug: {
143 drawContour(canvas, firstContour) {
144 const ctx = canvas.getContext('2d')
145 let pq = firstContour
146 let iq
147 let q
148 let p
149
150 ctx.strokeStyle = 'red'
151 ctx.fillStyle = 'red'
152 ctx.lineWidth = 1
153
154 if (pq !== null) {
155 iq = pq.insideContours
156 }
157 else {
158 iq = null
159 }
160
161 while (pq !== null) {
162 if (iq !== null) {
163 q = iq
164 iq = iq.nextpeer
165 }
166 else {
167 q = pq
168 pq = pq.nextpeer
169 if (pq !== null) {
170 iq = pq.insideContours
171 }
172 else {
173 iq = null
174 }
175 }
176
177 switch (q.dir) {
178 case Rasterizer.CONTOUR_DIR.CW_DIR:
179 ctx.strokeStyle = 'red'
180 break
181 case Rasterizer.CONTOUR_DIR.CCW_DIR:
182 ctx.strokeStyle = 'blue'
183 break
184 case Rasterizer.CONTOUR_DIR.UNKNOWN_DIR:
185 ctx.strokeStyle = 'green'
186 break
187 }
188
189 p = q.firstVertex
190 ctx.beginPath()
191 ctx.moveTo(p.x, p.y)
192 do {
193 p = p.next
194 ctx.lineTo(p.x, p.y)
195 } while (p !== q.firstVertex)
196 ctx.stroke()
197 }
198 },
199 },
200 }
201 },
202}
203
204export default Rasterizer
packages/qrx/src/quagga/locator/skeletonizer.jsadded+206-0
Changes to packages/qrx/src/quagga/locator/skeletonizer.js
@@ -0,0 +1,206 @@
1/* eslint-disable eqeqeq */
2
3/* @preserve ASM BEGIN */
4function Skeletonizer(stdlib, foreign, buffer) {
5 'use asm'
6
7 const images = new stdlib.Uint8Array(buffer)
8 const size = foreign.size | 0
9 const { imul } = stdlib.Math
10
11 function erode(inImagePtr, outImagePtr) {
12 inImagePtr |= 0
13 outImagePtr |= 0
14
15 let v = 0
16 let u = 0
17 let sum = 0
18 let yStart1 = 0
19 let yStart2 = 0
20 let xStart1 = 0
21 let xStart2 = 0
22 let offset = 0
23
24 for (v = 1; (v | 0) < ((size - 1) | 0); v = (v + 1) | 0) {
25 offset = (offset + size) | 0
26 for (u = 1; (u | 0) < ((size - 1) | 0); u = (u + 1) | 0) {
27 yStart1 = (offset - size) | 0
28 yStart2 = (offset + size) | 0
29 xStart1 = (u - 1) | 0
30 xStart2 = (u + 1) | 0
31 sum = ((images[(inImagePtr + yStart1 + xStart1) | 0] | 0)
32 + (images[(inImagePtr + yStart1 + xStart2) | 0] | 0)
33 + (images[(inImagePtr + offset + u) | 0] | 0)
34 + (images[(inImagePtr + yStart2 + xStart1) | 0] | 0)
35 + (images[(inImagePtr + yStart2 + xStart2) | 0] | 0)) | 0
36 if ((sum | 0) == (5 | 0)) {
37 images[(outImagePtr + offset + u) | 0] = 1
38 }
39 else {
40 images[(outImagePtr + offset + u) | 0] = 0
41 }
42 }
43 }
44 }
45
46 function subtract(aImagePtr, bImagePtr, outImagePtr) {
47 aImagePtr |= 0
48 bImagePtr |= 0
49 outImagePtr |= 0
50
51 let length = 0
52
53 length = imul(size, size) | 0
54
55 while ((length | 0) > 0) {
56 length = (length - 1) | 0
57 images[(outImagePtr + length) | 0] = ((images[(aImagePtr + length) | 0] | 0) - (images[(bImagePtr + length) | 0] | 0)) | 0
58 }
59 }
60
61 function bitwiseOr(aImagePtr, bImagePtr, outImagePtr) {
62 aImagePtr |= 0
63 bImagePtr |= 0
64 outImagePtr |= 0
65
66 let length = 0
67
68 length = imul(size, size) | 0
69
70 while ((length | 0) > 0) {
71 length = (length - 1) | 0
72 images[(outImagePtr + length) | 0] = ((images[(aImagePtr + length) | 0] | 0) | (images[(bImagePtr + length) | 0] | 0)) | 0
73 }
74 }
75
76 function countNonZero(imagePtr) {
77 imagePtr |= 0
78
79 let sum = 0
80 let length = 0
81
82 length = imul(size, size) | 0
83
84 while ((length | 0) > 0) {
85 length = (length - 1) | 0
86 sum = ((sum | 0) + (images[(imagePtr + length) | 0] | 0)) | 0
87 }
88
89 return (sum | 0)
90 }
91
92 function init(imagePtr, value) {
93 imagePtr |= 0
94 value |= 0
95
96 let length = 0
97
98 length = imul(size, size) | 0
99
100 while ((length | 0) > 0) {
101 length = (length - 1) | 0
102 images[(imagePtr + length) | 0] = value
103 }
104 }
105
106 function dilate(inImagePtr, outImagePtr) {
107 inImagePtr |= 0
108 outImagePtr |= 0
109
110 let v = 0
111 let u = 0
112 let sum = 0
113 let yStart1 = 0
114 let yStart2 = 0
115 let xStart1 = 0
116 let xStart2 = 0
117 let offset = 0
118
119 for (v = 1; (v | 0) < ((size - 1) | 0); v = (v + 1) | 0) {
120 offset = (offset + size) | 0
121 for (u = 1; (u | 0) < ((size - 1) | 0); u = (u + 1) | 0) {
122 yStart1 = (offset - size) | 0
123 yStart2 = (offset + size) | 0
124 xStart1 = (u - 1) | 0
125 xStart2 = (u + 1) | 0
126 sum = ((images[(inImagePtr + yStart1 + xStart1) | 0] | 0)
127 + (images[(inImagePtr + yStart1 + xStart2) | 0] | 0)
128 + (images[(inImagePtr + offset + u) | 0] | 0)
129 + (images[(inImagePtr + yStart2 + xStart1) | 0] | 0)
130 + (images[(inImagePtr + yStart2 + xStart2) | 0] | 0)) | 0
131 if ((sum | 0) > (0 | 0)) {
132 images[(outImagePtr + offset + u) | 0] = 1
133 }
134 else {
135 images[(outImagePtr + offset + u) | 0] = 0
136 }
137 }
138 }
139 }
140
141 function memcpy(srcImagePtr, dstImagePtr) {
142 srcImagePtr |= 0
143 dstImagePtr |= 0
144
145 let length = 0
146
147 length = imul(size, size) | 0
148
149 while ((length | 0) > 0) {
150 length = (length - 1) | 0
151 images[(dstImagePtr + length) | 0] = (images[(srcImagePtr + length) | 0] | 0)
152 }
153 }
154
155 function zeroBorder(imagePtr) {
156 imagePtr |= 0
157
158 let x = 0
159 let y = 0
160
161 for (x = 0; (x | 0) < ((size - 1) | 0); x = (x + 1) | 0) {
162 images[(imagePtr + x) | 0] = 0
163 images[(imagePtr + y) | 0] = 0
164 y = ((y + size) - 1) | 0
165 images[(imagePtr + y) | 0] = 0
166 y = (y + 1) | 0
167 }
168 for (x = 0; (x | 0) < (size | 0); x = (x + 1) | 0) {
169 images[(imagePtr + y) | 0] = 0
170 y = (y + 1) | 0
171 }
172 }
173
174 function skeletonize() {
175 const subImagePtr = 0
176 let erodedImagePtr = 0
177 let tempImagePtr = 0
178 let skelImagePtr = 0
179 let sum = 0
180 let done = 0
181
182 erodedImagePtr = imul(size, size) | 0
183 tempImagePtr = (erodedImagePtr + erodedImagePtr) | 0
184 skelImagePtr = (tempImagePtr + erodedImagePtr) | 0
185
186 // init skel-image
187 init(skelImagePtr, 0)
188 zeroBorder(subImagePtr)
189
190 do {
191 erode(subImagePtr, erodedImagePtr)
192 dilate(erodedImagePtr, tempImagePtr)
193 subtract(subImagePtr, tempImagePtr, tempImagePtr)
194 bitwiseOr(skelImagePtr, tempImagePtr, skelImagePtr)
195 memcpy(erodedImagePtr, subImagePtr)
196 sum = countNonZero(subImagePtr) | 0
197 done = ((sum | 0) == 0 | 0)
198 } while (!done)
199 }
200 return {
201 skeletonize,
202 }
203}
204/* @preserve ASM END */
205export default Skeletonizer
206/* eslint-enable eqeqeq */
packages/qrx/src/quagga/locator/tracer.jsadded+104-0
Changes to packages/qrx/src/quagga/locator/tracer.js
@@ -0,0 +1,104 @@
1/**
2 * http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization
3 */
4const Tracer = {
5 searchDirections: [[0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1]],
6 create(imageWrapper, labelWrapper) {
7 const imageData = imageWrapper.data
8 const labelData = labelWrapper.data
9 const { searchDirections } = this
10 const width = imageWrapper.size.x
11 let pos
12
13 function trace(current, color, label, edgelabel) {
14 let i
15 let y
16 let x
17
18 for (i = 0; i < searchDirections.length; i++) {
19 y = current.cy + searchDirections[current.dir][0]
20 x = current.cx + searchDirections[current.dir][1]
21 pos = y * width + x
22 if ((imageData[pos] === color) && ((labelData[pos] === 0) || (labelData[pos] === label))) {
23 labelData[pos] = label
24 current.cy = y
25 current.cx = x
26 return true
27 }
28 if (labelData[pos] === 0) {
29 labelData[pos] = edgelabel
30 }
31 current.dir = (current.dir + 1) % 8
32 }
33 return false
34 }
35
36 function vertex2D(x, y, dir) {
37 return {
38 dir,
39 x,
40 y,
41 next: null,
42 prev: null,
43 }
44 }
45
46 function contourTracing(sy, sx, label, color, edgelabel) {
47 let Fv = null
48 let Cv
49 let P
50 let ldir
51 const current = {
52 cx: sx,
53 cy: sy,
54 dir: 0,
55 }
56
57 if (trace(current, color, label, edgelabel)) {
58 Fv = vertex2D(sx, sy, current.dir)
59 Cv = Fv
60 ldir = current.dir
61 P = vertex2D(current.cx, current.cy, 0)
62 P.prev = Cv
63 Cv.next = P
64 P.next = null
65 Cv = P
66
67 const totalPixelCount = imageWrapper.size.x * imageWrapper.size.y
68 let pixelCounter = 0
69 do {
70 current.dir = (current.dir + 6) % 8
71 trace(current, color, label, edgelabel)
72 if (ldir !== current.dir) {
73 Cv.dir = current.dir
74 P = vertex2D(current.cx, current.cy, 0)
75 P.prev = Cv
76 Cv.next = P
77 P.next = null
78 Cv = P
79 }
80 else {
81 Cv.dir = ldir
82 Cv.x = current.cx
83 Cv.y = current.cy
84 }
85 ldir = current.dir
86 } while ((current.cx !== sx || current.cy !== sy) && ++pixelCounter < totalPixelCount)
87 Fv.prev = Cv.prev
88 Cv.prev.next = Fv
89 }
90 return Fv
91 }
92
93 return {
94 trace(current, color, label, edgelabel) {
95 return trace(current, color, label, edgelabel)
96 },
97 contourTracing(sy, sx, label, color, edgelabel) {
98 return contourTracing(sy, sx, label, color, edgelabel)
99 },
100 }
101 },
102}
103
104export default (Tracer)
packages/qrx/src/quagga/quagga.jsadded+161-0
Changes to packages/qrx/src/quagga/quagga.js
@@ -0,0 +1,161 @@
1import merge from 'lodash/merge'
2import ResultCollector from './analytics/result_collector'
3import Events from './common/events'
4import ImageDebug from './common/image_debug'
5import ImageWrapper from './common/image_wrapper'
6import TypeDefs from './common/typedefs' // eslint-disable-line no-unused-vars
7import Config from './config/config'
8import BarcodeDecoder from './decoder/barcode_decoder'
9import CameraAccess from './input/camera_access'
10import Quagga from './quagga/quagga'
11
12import * as Readers from './reader/index'
13
14const instance = new Quagga()
15const _context = instance.context
16
17const QuaggaJSStaticInterface = {
18 init(config, cb, imageWrapper, quaggaInstance = instance) {
19 let promise
20 if (!cb) {
21 promise = new Promise((resolve, reject) => {
22 cb = (err) => { err ? reject(err) : resolve() }
23 })
24 }
25 quaggaInstance.context.config = merge({}, Config, config)
26 // TODO #179: pending restructure in Issue #179, we are temp disabling workers
27 if (quaggaInstance.context.config.numOfWorkers > 0) {
28 quaggaInstance.context.config.numOfWorkers = 0
29 }
30 if (imageWrapper) {
31 quaggaInstance.context.onUIThread = false
32 quaggaInstance.initializeData(imageWrapper)
33 if (cb) {
34 cb()
35 }
36 }
37 else {
38 quaggaInstance.initInputStream(cb)
39 }
40 return promise
41 },
42 start() {
43 return instance.start()
44 },
45 stop() {
46 return instance.stop()
47 },
48 pause() {
49 _context.stopped = true
50 },
51 onDetected(callback) {
52 if (!callback || (typeof callback !== 'function' && (typeof callback !== 'object' || !callback.callback))) {
53 console.trace('* warning: Quagga.onDetected called with invalid callback, ignoring')
54 return
55 }
56 Events.subscribe('detected', callback)
57 },
58 offDetected(callback) {
59 Events.unsubscribe('detected', callback)
60 },
61 onProcessed(callback) {
62 if (!callback || (typeof callback !== 'function' && (typeof callback !== 'object' || !callback.callback))) {
63 console.trace('* warning: Quagga.onProcessed called with invalid callback, ignoring')
64 return
65 }
66 Events.subscribe('processed', callback)
67 },
68 offProcessed(callback) {
69 Events.unsubscribe('processed', callback)
70 },
71 setReaders(readers) {
72 if (!readers) {
73 console.trace('* warning: Quagga.setReaders called with no readers, ignoring')
74 return
75 }
76 instance.setReaders(readers)
77 },
78 registerReader(name, reader) {
79 if (!name) {
80 console.trace('* warning: Quagga.registerReader called with no name, ignoring')
81 return
82 }
83 if (!reader) {
84 console.trace('* warning: Quagga.registerReader called with no reader, ignoring')
85 return
86 }
87 instance.registerReader(name, reader)
88 },
89 registerResultCollector(resultCollector) {
90 if (resultCollector && typeof resultCollector.addResult === 'function') {
91 _context.resultCollector = resultCollector
92 }
93 },
94 get canvas() {
95 return _context.canvasContainer
96 },
97 decodeSingle(config, resultCallback) {
98 const quaggaInstance = new Quagga()
99 config = merge({
100 inputStream: {
101 type: 'ImageStream',
102 sequence: false,
103 size: 800,
104 src: config.src,
105 },
106 numOfWorkers: (ENV.development && config.debug) ? 0 : 1,
107 locator: {
108 halfSample: false,
109 },
110 }, config)
111 // TODO #175: restructure worker support so that it will work with typescript using worker-loader
112 // https://webpack.js.org/loaders/worker-loader/
113 if (config.numOfWorkers > 0) {
114 config.numOfWorkers = 0
115 }
116 // workers require Worker and Blob support presently, so if no Blob or Worker then set
117 // workers to 0.
118 if (config.numOfWorkers > 0 && (typeof Blob === 'undefined' || typeof Worker === 'undefined')) {
119 console.warn('* no Worker and/or Blob support - forcing numOfWorkers to 0')
120 config.numOfWorkers = 0
121 }
122 return new Promise((resolve, reject) => {
123 try {
124 this.init(config, () => {
125 Events.once('processed', (result) => {
126 quaggaInstance.stop()
127 if (resultCallback) {
128 resultCallback.call(null, result)
129 }
130 resolve(result)
131 }, true)
132 quaggaInstance.start()
133 }, null, quaggaInstance)
134 }
135 catch (err) {
136 reject(err)
137 }
138 })
139 },
140 // add the usually expected "default" for use with require, build step won't allow us to
141 // write to module.exports so do it here.
142 get default() {
143 return QuaggaJSStaticInterface
144 },
145 Readers,
146 CameraAccess,
147 ImageDebug,
148 ImageWrapper,
149 ResultCollector,
150}
151
152export default QuaggaJSStaticInterface
153// export BarcodeReader and other utilities for external plugins
154export {
155 BarcodeDecoder,
156 CameraAccess,
157 ImageDebug,
158 ImageWrapper,
159 Readers,
160 ResultCollector,
161}
packages/qrx/src/quagga/quagga/Exception.tsadded+9-0
Changes to packages/qrx/src/quagga/quagga/Exception.ts
@@ -0,0 +1,9 @@
1export default class Exception extends Error {
2 code?: number
3
4 constructor(m: string, code?: number) {
5 super(m)
6 this.code = code
7 Object.setPrototypeOf(this, Exception.prototype)
8 }
9}
packages/qrx/src/quagga/quagga/getViewPort.tsadded+13-0
Changes to packages/qrx/src/quagga/quagga/getViewPort.ts
@@ -0,0 +1,13 @@
1export default function getViewPort(target?: Element | string): Element | null {
2 if (typeof document === 'undefined') {
3 return null
4 }
5
6 // Check if target is already a DOM element
7 if (target instanceof HTMLElement && target.nodeName && target.nodeType === 1) {
8 return target
9 }
10 // Use '#interactive.viewport' as a fallback selector (backwards compatibility)
11 const selector = typeof target === 'string' ? target : '#interactive.viewport'
12 return document.querySelector(selector)
13}
packages/qrx/src/quagga/quagga/initBuffers.tsadded+31-0
Changes to packages/qrx/src/quagga/quagga/initBuffers.ts
@@ -0,0 +1,31 @@
1import type { InputStream } from '../input/input_stream/input_stream'
2import { glMatrix, vec2 } from 'gl-matrix'
3import ImageWrapper from '../common/image_wrapper'
4import BarcodeLocator from '../locator/barcode_locator'
5
6glMatrix.setMatrixArrayType(Array)
7
8// TODO: need typescript def for BarcodeLocator
9export default function initBuffers(
10 inputStream: InputStream,
11 imageWrapper: ImageWrapper | undefined,
12 locator: any,
13) {
14 const inputImageWrapper = imageWrapper || new ImageWrapper({
15 x: inputStream.getWidth(),
16 y: inputStream.getHeight(),
17 type: 'XYSize',
18 })
19
20 if (ENV.development) {
21 console.log(`image wrapper size ${inputImageWrapper.size}`)
22 }
23 const boxSize = [
24 vec2.clone([0, 0]),
25 vec2.clone([0, inputImageWrapper.size.y]),
26 vec2.clone([inputImageWrapper.size.x, inputImageWrapper.size.y]),
27 vec2.clone([inputImageWrapper.size.x, 0]),
28 ]
29 BarcodeLocator.init(inputImageWrapper, locator)
30 return { inputImageWrapper, boxSize }
31}
packages/qrx/src/quagga/quagga/initCanvas.tsadded+65-0
Changes to packages/qrx/src/quagga/quagga/initCanvas.ts
@@ -0,0 +1,65 @@
1import type { CanvasContainer, QuaggaContext } from 'QuaggaContext'
2import type { XYSize } from '../../type-definitions/quagga.d'
3import getViewPort from './getViewPort'
4
5function findOrCreateCanvas(selector: string, className: string) {
6 let canvas: HTMLCanvasElement | null = document.querySelector(selector)
7 if (!canvas) {
8 canvas = document.createElement('canvas')
9 canvas.className = className
10 }
11 return canvas
12}
13
14function getCanvasAndContext(selector: string, className: string, options: { willReadFrequently: boolean }) {
15 const canvas = findOrCreateCanvas(selector, className)
16 console.warn('* initCanvas getCanvasAndContext')
17 const context = canvas.getContext('2d', { willReadFrequently: options.willReadFrequently })
18 return { canvas, context }
19}
20
21function initCanvases(canvasSize: XYSize, { willReadFrequently }: { willReadFrequently: boolean }): CanvasContainer | null {
22 if (typeof document !== 'undefined') {
23 const image = getCanvasAndContext('canvas.imgBuffer', 'imgBuffer', { willReadFrequently })
24 const overlay = getCanvasAndContext('canvas.drawingBuffer', 'drawingBuffer', { willReadFrequently })
25
26 image.canvas.width = overlay.canvas.width = canvasSize.x
27
28 image.canvas.height = overlay.canvas.height = canvasSize.y
29
30 return {
31 dom: {
32 image: image.canvas,
33 overlay: overlay.canvas,
34 },
35 ctx: {
36 image: image.context,
37 overlay: overlay.context,
38 },
39 }
40 }
41 return null
42}
43
44export default function initCanvas(context: QuaggaContext): CanvasContainer | null {
45 const viewport = getViewPort(context?.config?.inputStream?.target)
46 const type = context?.config?.inputStream?.type
47 if (!type)
48 return null
49 const container = initCanvases(context.inputStream.getCanvasSize(), { willReadFrequently: !!context?.config?.inputStream?.willReadFrequently })
50 if (!container)
51 return { dom: { image: null, overlay: null }, ctx: { image: null, overlay: null } }
52
53 const { dom } = container
54 if (typeof document !== 'undefined') {
55 if (viewport) {
56 if (type === 'ImageStream' && !viewport.contains(dom.image)) {
57 viewport.appendChild(dom.image)
58 }
59 if (!viewport.contains(dom.overlay)) {
60 viewport.appendChild(dom.overlay)
61 }
62 }
63 }
64 return container
65}
packages/qrx/src/quagga/quagga/quagga.tsadded+294-0
Changes to packages/qrx/src/quagga/quagga/quagga.ts
@@ -0,0 +1,294 @@
1import type { QuaggaJSReaderConfig, QuaggaJSResultObject } from '../../type-definitions/quagga.d'
2import type ImageWrapper from '../common/image_wrapper.js'
3import type { BarcodeInfo } from '../reader/barcode_reader.js'
4import { glMatrix, vec2 } from 'gl-matrix'
5import Events from '../common/events.js'
6import BarcodeDecoder from '../decoder/barcode_decoder.js'
7import CameraAccess from '../input/camera_access.js'
8import FrameGrabber from '../input/frame_grabber.js'
9import InputStream from '../input/input_stream/input_stream.js'
10import BarcodeLocator from '../locator/barcode_locator.js'
11import { QuaggaContext } from '../QuaggaContext.js'
12import _getViewPort from './getViewPort.js'
13import _initBuffers from './initBuffers.js'
14import _initCanvas from './initCanvas.js'
15import * as QWorkers from './qworker.js'
16import setupInputStream from './setupInputStream.js'
17import { moveBox, moveLine } from './transform.js'
18
19glMatrix.setMatrixArrayType(Array)
20
21export default class Quagga {
22 context: QuaggaContext = new QuaggaContext()
23
24 initBuffers(imageWrapper?: ImageWrapper): void {
25 if (!this.context.config) {
26 return
27 }
28 const { inputImageWrapper, boxSize } = _initBuffers(
29 this.context.inputStream,
30 imageWrapper,
31 this.context.config.locator,
32 )
33 this.context.inputImageWrapper = inputImageWrapper
34 this.context.boxSize = boxSize
35 }
36
37 initializeData(imageWrapper?: ImageWrapper): void {
38 if (!this.context.config) {
39 return
40 }
41 this.initBuffers(imageWrapper)
42 this.context.decoder = BarcodeDecoder.create(this.context.config.decoder, this.context.inputImageWrapper)
43 }
44
45 getViewPort(): Element | null {
46 if (!this.context.config || !this.context.config.inputStream) {
47 return null
48 }
49 const { target } = this.context.config.inputStream
50 return _getViewPort(target)
51 }
52
53 ready(callback: () => void): void {
54 this.context.inputStream.play()
55 callback()
56 }
57
58 initCanvas(): void {
59 const container = _initCanvas(this.context)
60 if (!container) {
61 return
62 }
63 const { ctx, dom } = container
64 this.context.canvasContainer.dom.image = dom.image
65 this.context.canvasContainer.dom.overlay = dom.overlay
66 this.context.canvasContainer.ctx.image = ctx.image
67 this.context.canvasContainer.ctx.overlay = ctx.overlay
68 }
69
70 canRecord = (callback: () => void): void => {
71 if (!this.context.config) {
72 return
73 }
74 BarcodeLocator.checkImageConstraints(this.context.inputStream, this.context.config?.locator)
75 this.initCanvas()
76 this.context.framegrabber = FrameGrabber.create(
77 this.context.inputStream,
78 this.context.canvasContainer.dom.image,
79 )
80
81 if (this.context.config.numOfWorkers === undefined) {
82 this.context.config.numOfWorkers = 0
83 }
84
85 QWorkers.adjustWorkerPool(
86 this.context.config.numOfWorkers,
87 this.context.config,
88 this.context.inputStream,
89 () => {
90 if (this.context.config?.numOfWorkers === 0) {
91 this.initializeData()
92 }
93 this.ready(callback)
94 },
95 )
96 }
97
98 initInputStream(callback: (err?: Error) => void): void {
99 if (!this.context.config || !this.context.config.inputStream) {
100 return
101 }
102 const { type: inputType, constraints } = this.context.config.inputStream
103 const { video, inputStream } = setupInputStream(inputType, this.getViewPort(), InputStream)
104
105 if (inputType === 'LiveStream' && video) {
106 CameraAccess.request(video, constraints)
107 .then(() => inputStream.trigger('canrecord'))
108 .catch(err => callback(err))
109 }
110
111 if (inputStream) {
112 inputStream.setAttribute('preload', 'auto')
113 inputStream.setInputStream(this.context.config.inputStream)
114 inputStream.addEventListener('canrecord', this.canRecord.bind(undefined, callback))
115 }
116
117 this.context.inputStream = inputStream
118 }
119
120 getBoundingBoxes(): Array<Array<number>> | null {
121 return this.context.config?.locate
122 ? BarcodeLocator.locate()
123 : [[
124 vec2.clone(this.context.boxSize[0]),
125 vec2.clone(this.context.boxSize[1]),
126 vec2.clone(this.context.boxSize[2]),
127 vec2.clone(this.context.boxSize[3]),
128 ]]
129 }
130
131 // TODO: need a typescript type for result here.
132 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
133 transformResult(result: any): void {
134 const topRight = this.context.inputStream.getTopRight()
135 const xOffset = topRight.x
136 const yOffset = topRight.y
137
138 if (xOffset === 0 && yOffset === 0) {
139 return
140 }
141
142 if (result.barcodes) {
143 // TODO: BarcodeInfo may not be the right type here.
144 result.barcodes.forEach((barcode: BarcodeInfo) => this.transformResult(barcode))
145 }
146
147 if (result.line && result.line.length === 2) {
148 moveLine(result.line, xOffset, yOffset)
149 }
150
151 if (result.box) {
152 moveBox(result.box, xOffset, yOffset)
153 }
154
155 if (result.boxes && result.boxes.length > 0) {
156 for (let i = 0; i < result.boxes.length; i++) {
157 moveBox(result.boxes[i], xOffset, yOffset)
158 }
159 }
160 }
161
162 addResult(result: QuaggaJSResultObject, imageData: Array<number>): void {
163 if (!imageData || !this.context.resultCollector) {
164 return
165 }
166
167 // TODO: Figure out what data structure holds a "barcodes" result, if any...
168 if (result.barcodes) {
169 result.barcodes.filter((barcode: QuaggaJSResultObject) => barcode.codeResult)
170 .forEach((barcode: QuaggaJSResultObject) => this.addResult(barcode, imageData))
171 }
172 else if (result.codeResult) {
173 this.context.resultCollector.addResult(
174 imageData,
175 this.context.inputStream.getCanvasSize(),
176 result.codeResult,
177 )
178 }
179 }
180
181 hasCodeResult(result: QuaggaJSResultObject): boolean {
182 return !!(result && (result.barcodes
183 ? result.barcodes.some(barcode => barcode.codeResult)
184 : result.codeResult))
185 }
186
187 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
188 publishResult(result: QuaggaJSResultObject | null = null, imageData?: any): void {
189 let resultToPublish: Array<QuaggaJSResultObject> | QuaggaJSResultObject | null = result
190
191 if (result && this.context.onUIThread) {
192 this.transformResult(result)
193 this.addResult(result, imageData)
194 // @ts-ignore
195 resultToPublish = result?.barcodes?.length > 0 ? result.barcodes : result
196 }
197
198 Events.publish('processed', resultToPublish as never)
199 if (this.hasCodeResult(result as QuaggaJSResultObject)) {
200 Events.publish('detected', resultToPublish as never)
201 }
202 }
203
204 async locateAndDecode(): Promise<void> {
205 const boxes = this.getBoundingBoxes()
206 if (boxes) {
207 const decodeResult = (await this.context.decoder.decodeFromBoundingBoxes(boxes)) || {}
208 decodeResult.boxes = boxes
209 this.publishResult(decodeResult, this.context.inputImageWrapper?.data)
210 }
211 else {
212 const imageResult = await this.context.decoder.decodeFromImage(this.context.inputImageWrapper)
213 if (imageResult) {
214 this.publishResult(imageResult, this.context.inputImageWrapper?.data)
215 }
216 else {
217 this.publishResult()
218 }
219 }
220 }
221
222 update = (): void => {
223 if (this.context.onUIThread) {
224 const workersUpdated = QWorkers.updateWorkers(this.context.framegrabber)
225 if (!workersUpdated) {
226 this.context.framegrabber.attachData(this.context.inputImageWrapper?.data)
227 if (this.context.framegrabber.grab()) {
228 if (!workersUpdated) {
229 this.locateAndDecode()
230 }
231 }
232 }
233 }
234 else {
235 this.context.framegrabber.attachData(this.context.inputImageWrapper?.data)
236 this.context.framegrabber.grab()
237 this.locateAndDecode()
238 }
239 }
240
241 startContinuousUpdate(): void {
242 let next: number | null = null
243 const delay = 1000 / (this.context.config?.frequency || 60)
244
245 this.context.stopped = false
246 const { context } = this
247
248 const newFrame = (timestamp: number) => {
249 next = next || timestamp
250 if (!context.stopped) {
251 if (timestamp >= next) {
252 next += delay
253 this.update()
254 }
255 window.requestAnimationFrame(newFrame)
256 }
257 }
258
259 newFrame(performance.now())
260 }
261
262 start(): void {
263 if (this.context.onUIThread && this.context.config?.inputStream?.type === 'LiveStream') {
264 this.startContinuousUpdate()
265 }
266 else {
267 this.update()
268 }
269 }
270
271 async stop(): Promise<void> {
272 this.context.stopped = true
273 QWorkers.adjustWorkerPool(0)
274 if (this.context.config?.inputStream && this.context.config.inputStream.type === 'LiveStream') {
275 await CameraAccess.release()
276 this.context.inputStream.clearEventHandlers()
277 }
278 }
279
280 setReaders(readers: Array<QuaggaJSReaderConfig>): void {
281 if (this.context.decoder) {
282 this.context.decoder.setReaders(readers)
283 }
284 QWorkers.setReaders(readers)
285 }
286
287 registerReader(name: string, reader: QuaggaJSReaderConfig): void {
288 BarcodeDecoder.registerReader(name, reader)
289 if (this.context.decoder) {
290 this.context.decoder.registerReader(name, reader)
291 }
292 QWorkers.registerReader(name, reader)
293 }
294}
packages/qrx/src/quagga/quagga/qworker.tsadded+211-0
Changes to packages/qrx/src/quagga/quagga/qworker.ts
@@ -0,0 +1,211 @@
1/* Worker functions. These are straight from the original quagga.js file.
2 * Not presently used, as worker support is non-functional. Keeping them around temporarily
3 * to refer to until it is re-implemented. We may be able to fix/use some of this.
4 */
5
6import type { QuaggaJSConfigObject, QuaggaJSReaderConfig } from '../../type-definitions/quagga'
7
8// TODO: need a typescript interface for FrameGrabber
9
10interface QWorkerThread {
11 imageData: Uint8Array
12 busy: boolean
13 worker: Worker
14}
15
16let workerPool: Array<QWorkerThread> = []
17
18export function updateWorkers(frameGrabber: any) {
19 let availableWorker: QWorkerThread
20 if (workerPool.length) {
21 availableWorker = workerPool.filter(workerThread => !workerThread.busy)[0]
22 if (availableWorker) {
23 frameGrabber.attachData(availableWorker.imageData)
24 if (frameGrabber.grab()) {
25 availableWorker.busy = true
26 availableWorker.worker.postMessage({
27 cmd: 'process',
28 imageData: availableWorker.imageData,
29 }, [availableWorker.imageData.buffer])
30 }
31 return true
32 }
33 else {
34 return false
35 }
36 }
37 return null
38}
39
40function configForWorker(config: QuaggaJSConfigObject) {
41 return {
42 ...config,
43 inputStream: {
44 ...config.inputStream,
45 target: null,
46 },
47 }
48}
49
50// @ts-ignore
51function workerInterface(factory) {
52 if (factory) {
53 var Quagga = factory().default
54 if (!Quagga) {
55 // @ts-ignore
56 self.postMessage({ event: 'error', message: 'Quagga could not be created' })
57 return
58 }
59 }
60 // @ts-ignore
61 let imageWrapper
62
63 // @ts-ignore
64 function onProcessed(result) {
65 self.postMessage({
66 event: 'processed',
67 // @ts-ignore
68 imageData: imageWrapper.data,
69 result,
70 // @ts-ignore
71 }, [imageWrapper.data.buffer])
72 }
73
74 function workerInterfaceReady() {
75 self.postMessage({
76 event: 'initialized',
77 // @ts-ignore
78 imageData: imageWrapper.data,
79 // @ts-ignore
80 }, [imageWrapper.data.buffer])
81 }
82
83 // @ts-ignore
84 self.onmessage = function (e) {
85 if (e.data.cmd === 'init') {
86 const config = e.data.config
87 config.numOfWorkers = 0
88 imageWrapper = new Quagga.ImageWrapper({
89 x: e.data.size.x,
90 y: e.data.size.y,
91 }, new Uint8Array(e.data.imageData))
92 Quagga.init(config, workerInterfaceReady, imageWrapper)
93 Quagga.onProcessed(onProcessed)
94 }
95 else if (e.data.cmd === 'process') {
96 // @ts-ignore
97 imageWrapper.data = new Uint8Array(e.data.imageData)
98 Quagga.start()
99 }
100 else if (e.data.cmd === 'setReaders') {
101 Quagga.setReaders(e.data.readers)
102 }
103 else if (e.data.cmd === 'registerReader') {
104 Quagga.registerReader(e.data.name, e.data.reader)
105 }
106 }
107}
108
109function generateWorkerBlob() {
110 let blob,
111 factorySource
112
113 /* jshint ignore:start */
114 // @ts-ignore
115 if (typeof __factorySource__ !== 'undefined') {
116 // @ts-ignore
117 factorySource = __factorySource__
118 }
119 /* jshint ignore:end */
120
121 blob = new Blob([`(${workerInterface.toString()})(${factorySource});`], { type: 'text/javascript' })
122
123 return window.URL.createObjectURL(blob)
124}
125
126export function initWorker(config: QuaggaJSConfigObject, inputStream: any, cb: Function) {
127 const blobURL = generateWorkerBlob()
128 const worker = new Worker(blobURL)
129
130 const workerThread: QWorkerThread = {
131 worker,
132 imageData: new Uint8Array(inputStream.getWidth() * inputStream.getHeight()),
133 busy: true,
134 }
135
136 workerThread.worker.onmessage = function (e) {
137 if (e.data.event === 'initialized') {
138 URL.revokeObjectURL(blobURL)
139 workerThread.busy = false
140 workerThread.imageData = new Uint8Array(e.data.imageData)
141 if (ENV.development) {
142 console.log('Worker initialized')
143 }
144 cb(workerThread)
145 }
146 else if (e.data.event === 'processed') {
147 workerThread.imageData = new Uint8Array(e.data.imageData)
148 workerThread.busy = false
149 // TODO: how to thread publishResult into here? TypeScript says it's not here. https://github.com/ericblade/quagga2/issues/466#issuecomment-1724248080 says it's necessary?
150 // @ts-ignore
151 if (typeof publishResult !== 'undefined') {
152 // @ts-ignore
153 publishResult(e.data.result, workerThread.imageData)
154 }
155 }
156 else if (e.data.event === 'error') {
157 if (ENV.development) {
158 console.log(`Worker error: ${e.data.message}`)
159 }
160 }
161 }
162
163 workerThread.worker.postMessage({
164 cmd: 'init',
165 size: { x: inputStream.getWidth(), y: inputStream.getHeight() },
166 imageData: workerThread.imageData,
167 config: configForWorker(config),
168 }, [workerThread.imageData.buffer])
169}
170
171export function adjustWorkerPool(capacity: number, config?: QuaggaJSConfigObject, inputStream?: any, cb?: Function) {
172 const increaseBy = capacity - workerPool.length
173 if (increaseBy === 0 && cb) {
174 cb()
175 }
176 else if (increaseBy < 0) {
177 const workersToTerminate = workerPool.slice(increaseBy)
178 workersToTerminate.forEach((workerThread) => {
179 workerThread.worker.terminate()
180 if (ENV.development) {
181 console.log('Worker terminated!')
182 }
183 })
184 workerPool = workerPool.slice(0, increaseBy)
185 if (cb) {
186 cb()
187 }
188 }
189 else {
190 const workerInitialized = (workerThread: QWorkerThread) => {
191 workerPool.push(workerThread)
192 if (workerPool.length >= capacity && cb) {
193 cb()
194 }
195 }
196
197 if (config) {
198 for (let i = 0; i < increaseBy; i++) {
199 initWorker(config, inputStream, workerInitialized)
200 }
201 }
202 }
203}
204
205export function setReaders(readers: Array<QuaggaJSReaderConfig>) {
206 workerPool.forEach(workerThread => workerThread.worker.postMessage({ cmd: 'setReaders', readers }))
207}
208
209export function registerReader(name: string, reader: any) {
210 workerPool.forEach(workerThread => workerThread.worker.postMessage({ cmd: 'registerReader', name, reader }))
211}
packages/qrx/src/quagga/quagga/setupInputStream.tsadded+34-0
Changes to packages/qrx/src/quagga/quagga/setupInputStream.ts
@@ -0,0 +1,34 @@
1import type { InputStreamFactory } from 'input/input_stream/input_stream.d'
2import type { InputStreamType } from '../../type-definitions/quagga.d'
3
4// TODO: need to create an InputStream typescript interface, so we don't have an "any" in the next line
5export default function setupInputStream(type: InputStreamType = 'LiveStream', viewport: Element | null, inputStreamFactory: InputStreamFactory) {
6 switch (type) {
7 case 'VideoStream': {
8 const video = document.createElement('video')
9 return {
10 video,
11 inputStream: inputStreamFactory.createVideoStream(video),
12 }
13 }
14 case 'ImageStream':
15 return { inputStream: inputStreamFactory.createImageStream() }
16 case 'LiveStream': {
17 let video: HTMLVideoElement | null = null
18 if (viewport) {
19 video = viewport.querySelector('video')
20 if (!video) {
21 video = document.createElement('video')
22 viewport.appendChild(video)
23 }
24 }
25 return {
26 video,
27 inputStream: inputStreamFactory.createLiveStream(video as HTMLVideoElement),
28 }
29 }
30 default:
31 console.error(`* setupInputStream invalid type ${type}`)
32 return { video: null, inputStream: null }
33 }
34}
packages/qrx/src/quagga/quagga/transform.tsadded+20-0
Changes to packages/qrx/src/quagga/quagga/transform.ts
@@ -0,0 +1,20 @@
1import type { Point } from '../../type-definitions/quagga.d'
2
3export type Box = Array<[ number, number ]>
4
5export type Line = [ Point, Point ]
6
7export function moveBox(box: Box, xOffset: number, yOffset: number): void {
8 let corner = box.length
9 while (corner--) {
10 box[corner][0] += xOffset
11 box[corner][1] += yOffset
12 }
13}
14
15export function moveLine(line: Line, xOffset: number, yOffset: number): void {
16 line[0].x += xOffset
17 line[0].y += yOffset
18 line[1].x += xOffset
19 line[1].y += yOffset
20}
packages/qrx/src/quagga/reader/2of5_reader.tsadded+230-0
Changes to packages/qrx/src/quagga/reader/2of5_reader.ts
@@ -0,0 +1,230 @@
1import type { Barcode, BarcodeInfo, BarcodePosition } from './barcode_reader'
2import BarcodeReader from './barcode_reader'
3
4const N = 1
5const W = 3
6const START_PATTERN = [W, N, W, N, N, N]
7const STOP_PATTERN = [W, N, N, N, W]
8const CODE_PATTERN = [
9 [N, N, W, W, N],
10 [W, N, N, N, W],
11 [N, W, N, N, W],
12 [W, W, N, N, N],
13 [N, N, W, N, W],
14 [W, N, W, N, N],
15 [N, W, W, N, N],
16 [N, N, N, W, W],
17 [W, N, N, W, N],
18 [N, W, N, W, N],
19]
20const START_PATTERN_LENGTH = START_PATTERN.reduce((sum, val) => sum + val, 0)
21
22class TwoOfFiveReader extends BarcodeReader {
23 barSpaceRatio = [1, 1]
24
25 FORMAT = '2of5'
26
27 SINGLE_CODE_ERROR = 0.78
28
29 AVG_CODE_ERROR = 0.30
30
31 protected _findPattern(pattern: ReadonlyArray<number>, offset: number, isWhite = false, tryHarder = false): BarcodeInfo | null {
32 const counter = []
33 let counterPos = 0
34 const bestMatch = {
35 error: Number.MAX_VALUE,
36 code: -1,
37 start: 0,
38 end: 0,
39 }
40 let sum = 0
41 let error = 0
42 const epsilon = this.AVG_CODE_ERROR
43
44 if (!offset) {
45 offset = this._nextSet(this._row)
46 }
47
48 for (let i = 0; i < pattern.length; i++) {
49 counter[i] = 0
50 }
51
52 for (let i = offset; i < this._row.length; i++) {
53 if (this._row[i] ^ (isWhite ? 1 : 0)) {
54 counter[counterPos]++
55 }
56 else {
57 if (counterPos === counter.length - 1) {
58 sum = 0
59 for (let j = 0; j < counter.length; j++) {
60 sum += counter[j]
61 }
62 error = this._matchPattern(counter, pattern)
63 if (error < epsilon) {
64 bestMatch.error = error
65 bestMatch.start = i - sum
66 bestMatch.end = i
67 return bestMatch
68 }
69 if (tryHarder) {
70 for (let j = 0; j < counter.length - 2; j++) {
71 counter[j] = counter[j + 2]
72 }
73 counter[counter.length - 2] = 0
74 counter[counter.length - 1] = 0
75 counterPos--
76 }
77 else {
78 return null
79 }
80 }
81 else {
82 counterPos++
83 }
84 counter[counterPos] = 1
85 isWhite = !isWhite
86 }
87 }
88 return null
89 }
90
91 protected _findStart(): BarcodePosition | null {
92 let startInfo = null
93 let offset = this._nextSet(this._row)
94 let narrowBarWidth = 1
95 let leadingWhitespaceStart = 0
96
97 while (!startInfo) {
98 startInfo = this._findPattern(START_PATTERN, offset, false, true)
99 if (!startInfo) {
100 return null
101 }
102 narrowBarWidth = Math.floor((startInfo.end - startInfo.start) / START_PATTERN_LENGTH)
103 leadingWhitespaceStart = startInfo.start - narrowBarWidth * 5
104 if (leadingWhitespaceStart >= 0) {
105 if (this._matchRange(leadingWhitespaceStart, startInfo.start, 0)) {
106 return startInfo
107 }
108 }
109 offset = startInfo.end
110 startInfo = null
111 }
112 return startInfo
113 }
114
115 protected _verifyTrailingWhitespace(endInfo: BarcodeInfo): BarcodePosition | null {
116 const trailingWhitespaceEnd = endInfo.end + ((endInfo.end - endInfo.start) / 2)
117 if (trailingWhitespaceEnd < this._row.length) {
118 if (this._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
119 return endInfo
120 }
121 }
122 return null
123 }
124
125 protected _findEnd(): BarcodePosition | null {
126 // TODO: reverse, followed by some calcs, followed by another reverse? really?
127 this._row.reverse()
128 const offset = this._nextSet(this._row)
129 const endInfo = this._findPattern(STOP_PATTERN, offset, false, true)
130 this._row.reverse()
131
132 if (endInfo === null) {
133 return null
134 }
135
136 // reverse numbers
137 const tmp = endInfo.start
138 endInfo.start = this._row.length - endInfo.end
139 endInfo.end = this._row.length - tmp
140
141 return endInfo !== null ? this._verifyTrailingWhitespace(endInfo) : null
142 }
143
144 protected _verifyCounterLength(counters: Array<number>) {
145 return (counters.length % 10 === 0)
146 }
147
148 protected _decodeCode(counter: ReadonlyArray<number>): BarcodeInfo | null {
149 const epsilon = this.AVG_CODE_ERROR
150 const bestMatch = {
151 error: Number.MAX_VALUE,
152 code: -1,
153 start: 0,
154 end: 0,
155 }
156
157 for (let code = 0; code < CODE_PATTERN.length; code++) {
158 const error = this._matchPattern(counter, CODE_PATTERN[code])
159 if (error < bestMatch.error!) {
160 bestMatch.code = code
161 bestMatch.error = error
162 }
163 }
164 if (bestMatch.error! < epsilon) {
165 return bestMatch
166 }
167 return null
168 }
169
170 protected _decodePayload(counters: ReadonlyArray<number>, result: Array<string>, decodedCodes: Array<BarcodeInfo | BarcodePosition>): BarcodeInfo | null {
171 let pos = 0
172 const counterLength = counters.length
173 const counter = [0, 0, 0, 0, 0]
174 let code: BarcodeInfo | null = null
175
176 while (pos < counterLength) {
177 for (let i = 0; i < 5; i++) {
178 counter[i] = counters[pos] * this.barSpaceRatio[0]
179 pos += 2
180 }
181 code = this._decodeCode(counter)
182 if (!code) {
183 return null
184 }
185 result.push(`${code.code}`)
186 decodedCodes.push(code)
187 }
188 return code
189 }
190
191 public decode(row?: Array<number>, start?: BarcodePosition): Barcode | null {
192 const startInfo = this._findStart()
193 if (!startInfo) {
194 return null
195 }
196
197 const endInfo = this._findEnd()
198 if (!endInfo) {
199 return null
200 }
201
202 const counters = this._fillCounters(startInfo.end, endInfo.start, false)
203 if (!this._verifyCounterLength(counters)) {
204 return null
205 }
206 const decodedCodes = []
207 decodedCodes.push(startInfo)
208
209 const result: Array<string> = []
210 const code = this._decodePayload(counters, result, decodedCodes)
211 if (!code) {
212 return null
213 }
214 if (result.length < 5) {
215 return null
216 }
217
218 decodedCodes.push(endInfo)
219 return {
220 code: result.join(''),
221 start: startInfo.start,
222 end: endInfo.end,
223 startInfo,
224 decodedCodes,
225 format: this.FORMAT,
226 }
227 }
228}
229
230export default TwoOfFiveReader
packages/qrx/src/quagga/reader/barcode_reader.tsadded+231-0
Changes to packages/qrx/src/quagga/reader/barcode_reader.ts
@@ -0,0 +1,231 @@
1import type { ImageWrapper } from 'quagga'
2import type { QuaggaJSResultObject } from '../../type-definitions/quagga'
3import ArrayHelper from '../common/array_helper'
4
5// for some reason this throws a shadow error on itself?!
6
7export enum BarcodeDirection {
8 Forward = 1,
9 Reverse = -1,
10}
11
12export type BarcodeReaderType = string
13export type BarcodeFormat = string
14
15export interface BarcodeReaderConfig {
16 normalizeBarSpaceWidth?: boolean
17 supplements?: Array<BarcodeReaderType>
18}
19
20export interface BarcodeCorrection {
21 bar: number
22 space: number
23}
24
25export interface BarcodePosition {
26 end: number
27 endCounter?: number
28 error?: number
29 start: number
30 startCounter?: number
31}
32
33export interface BarcodeInfo extends BarcodePosition {
34 code: number
35 correction?: BarcodeCorrection
36}
37
38export interface Barcode {
39 code: string
40 codeset?: number
41 correction?: BarcodeCorrection
42 decodedCodes?: Array<string | BarcodeInfo | BarcodePosition>
43 direction?: BarcodeDirection
44 end: number
45 endInfo?: BarcodePosition
46 format: BarcodeFormat
47 start: number
48 startInfo: BarcodePosition
49 supplement?: Barcode
50}
51
52export abstract class BarcodeReader {
53 _row: Array<number> = []
54
55 config: BarcodeReaderConfig = {}
56
57 supplements: Array<BarcodeReader> = []
58
59 SINGLE_CODE_ERROR = 0
60
61 FORMAT: BarcodeFormat = 'unknown'
62
63 CONFIG_KEYS: BarcodeReaderConfig = {}
64 // TODO: should add ALPHABETH_STRING, ALPHABET, CHARACTER_ENCODINGS to base class, if they
65 // are useful in most readers.
66
67 public abstract decode(row?: Array<number>, start?: BarcodePosition | number): Barcode | null
68
69 static get Exception() {
70 return {
71 StartNotFoundException: 'Start-Info was not found!',
72 CodeNotFoundException: 'Code could not be found!',
73 PatternNotFoundException: 'Pattern could not be found!',
74 }
75 }
76
77 constructor(config: BarcodeReaderConfig, supplements?: Array<BarcodeReader>) {
78 this._row = []
79 this.config = config || {}
80 if (supplements) {
81 this.supplements = supplements
82 }
83 }
84
85 protected _nextUnset(line: ReadonlyArray<number>, start = 0): number {
86 for (let i = start; i < line.length; i++) {
87 if (!line[i])
88 return i
89 }
90 return line.length
91 }
92
93 protected _matchPattern(counter: ReadonlyArray<number>, code: ReadonlyArray<number>, maxSingleError = this.SINGLE_CODE_ERROR || 1): number {
94 let error = 0
95 let singleError = 0
96 let sum = 0
97 let modulo = 0
98 let barWidth = 0
99 let count = 0
100 let scaled = 0
101
102 for (let i = 0; i < counter.length; i++) {
103 sum += counter[i]
104 modulo += code[i]
105 }
106 if (sum < modulo) {
107 return Number.MAX_VALUE
108 }
109
110 barWidth = sum / modulo
111
112 maxSingleError *= barWidth
113 for (let i = 0; i < counter.length; i++) {
114 count = counter[i]
115 scaled = code[i] * barWidth
116 singleError = Math.abs(count - scaled) / scaled
117 if (singleError > maxSingleError) {
118 return Number.MAX_VALUE
119 }
120 error += singleError
121 }
122 return error / modulo
123 }
124
125 protected _nextSet(line: ReadonlyArray<number>, offset = 0) {
126 for (let i = offset; i < line.length; i++) {
127 if (line[i])
128 return i
129 }
130 return line.length
131 }
132
133 protected _correctBars(counter: Array<number>, correction: number, indices: Array<number>) {
134 let { length } = indices
135 let tmp = 0
136 while (length--) {
137 tmp = counter[indices[length]] * (1 - ((1 - correction) / 2))
138 if (tmp > 1) {
139 counter[indices[length]] = tmp
140 }
141 }
142 }
143
144 public decodePattern(pattern: Array<number>) {
145 // console.warn('* decodePattern', pattern);
146 this._row = pattern
147 // console.warn('* decodePattern calling decode', typeof this, this.constructor, this.FORMAT, JSON.stringify(this));
148 let result = this.decode()
149 // console.warn('* first result=', result);
150 if (result === null) {
151 this._row.reverse()
152 result = this.decode()
153 // console.warn('* reversed result=', result);
154 if (result) {
155 result.direction = BarcodeDirection.Reverse
156 result.start = this._row.length - result.start
157 result.end = this._row.length - result.end
158 }
159 }
160 else {
161 result.direction = BarcodeDirection.Forward
162 }
163 if (result) {
164 result.format = this.FORMAT
165 }
166 // console.warn('* returning', result);
167 return result
168 }
169
170 protected _matchRange(start: number, end: number, value: number) {
171 start = start < 0 ? 0 : start
172 let i
173 for (i = start; i < end; i++) {
174 if (this._row[i] !== value) {
175 return false
176 }
177 }
178 return true
179 }
180
181 protected _fillCounters(offset: number = this._nextUnset(this._row), end: number = this._row.length, isWhite = true) {
182 const counters: Array<number> = []
183 let counterPos = 0
184 counters[counterPos] = 0
185 for (let i = offset; i < end; i++) {
186 if (this._row[i] ^ (isWhite ? 1 : 0)) {
187 counters[counterPos]++
188 }
189 else {
190 counterPos++
191 counters[counterPos] = 1
192
193 isWhite = !isWhite
194 }
195 }
196 return counters
197 }
198
199 protected _toCounters(start: number, counters: Uint16Array | Array<number>) {
200 const numCounters = counters.length
201 const end = this._row.length
202 let isWhite = !this._row[start]
203 let counterPos = 0
204
205 ArrayHelper.init(counters, 0)
206 for (let i = start; i < end; i++) {
207 if (this._row[i] ^ (isWhite ? 1 : 0)) {
208 counters[counterPos]++
209 }
210 else {
211 counterPos++
212 if (counterPos === numCounters) {
213 break
214 }
215 else {
216 counters[counterPos] = 1
217 isWhite = !isWhite
218 }
219 }
220 }
221 return counters
222 }
223
224 // override/implement this in your custom readers.
225 protected decodeImage(imageWrapper: ImageWrapper): QuaggaJSResultObject | null {
226 void imageWrapper
227 return null
228 }
229}
230
231export default BarcodeReader
packages/qrx/src/quagga/reader/codabar_reader.tsadded+271-0
Changes to packages/qrx/src/quagga/reader/codabar_reader.ts
@@ -0,0 +1,271 @@
1import type { Barcode, BarcodePosition } from './barcode_reader'
2import BarcodeReader from './barcode_reader'
3
4// const ALPHABETH_STRING = '0123456789-$:/.+ABCD';
5const ALPHABET = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 45, 36, 58, 47, 46, 43, 65, 66, 67, 68]
6const CHARACTER_ENCODINGS
7 = [0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, 0x00C, 0x018, 0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E]
8const START_END = [0x01A, 0x029, 0x00B, 0x00E]
9const MIN_ENCODED_CHARS = 4
10const MAX_ACCEPTABLE = 2.0
11const PADDING = 1.5
12
13interface ThresholdSize {
14 size: number
15 counts: number
16 min: number
17 max: number
18};
19
20interface Threshold {
21 space: {
22 narrow: ThresholdSize
23 wide: ThresholdSize
24 }
25 bar: {
26 narrow: ThresholdSize
27 wide: ThresholdSize
28 }
29};
30
31class NewCodabarReader extends BarcodeReader {
32 _counters: Array<number> = []
33 FORMAT = 'codabar'
34
35 protected _computeAlternatingThreshold(offset: number, end: number) {
36 let min = Number.MAX_VALUE
37 let max = 0
38 let counter = 0
39
40 for (let i = offset; i < end; i += 2) {
41 counter = this._counters[i]
42 if (counter > max) {
43 max = counter
44 }
45 if (counter < min) {
46 min = counter
47 }
48 }
49
50 return ((min + max) / 2.0) | 0
51 };
52
53 protected _toPattern(offset: number) {
54 const numCounters = 7
55 const end = offset + numCounters
56
57 if (end > this._counters.length) {
58 return -1
59 }
60
61 const barThreshold = this._computeAlternatingThreshold(offset, end)
62 const spaceThreshold = this._computeAlternatingThreshold(offset + 1, end)
63
64 let bitmask = 1 << (numCounters - 1)
65 let threshold = 0
66 let pattern = 0
67
68 for (let i = 0; i < numCounters; i++) {
69 threshold = (i & 1) === 0 ? barThreshold : spaceThreshold
70 if (this._counters[offset + i] > threshold) {
71 pattern |= bitmask
72 }
73 bitmask >>= 1
74 }
75
76 return pattern
77 };
78
79 protected _isStartEnd(pattern: number) {
80 for (let i = 0; i < START_END.length; i++) {
81 if (START_END[i] === pattern) {
82 return true
83 }
84 }
85 return false
86 };
87
88 protected _sumCounters(start: number, end: number) {
89 let sum = 0
90
91 for (let i = start; i < end; i++) {
92 sum += this._counters[i]
93 }
94 return sum
95 };
96
97 protected _findStart(): BarcodePosition | null {
98 let start = this._nextUnset(this._row)
99 let end = start
100
101 for (let i = 1; i < this._counters.length; i++) {
102 const pattern = this._toPattern(i)
103 if (pattern !== -1 && this._isStartEnd(pattern)) {
104 // TODO: Look for whitespace ahead
105 start += this._sumCounters(0, i)
106 end = start + this._sumCounters(i, i + 8)
107 return {
108 start,
109 end,
110 startCounter: i,
111 endCounter: i + 8,
112 }
113 }
114 }
115 return null
116 }
117
118 protected _patternToChar(pattern: number) {
119 for (let i = 0; i < CHARACTER_ENCODINGS.length; i++) {
120 if (CHARACTER_ENCODINGS[i] === pattern) {
121 return String.fromCharCode(ALPHABET[i])
122 }
123 }
124 return null
125 };
126
127 protected _calculatePatternLength(offset: number) {
128 let sum = 0
129
130 for (let i = offset; i < offset + 7; i++) {
131 sum += this._counters[i]
132 }
133
134 return sum
135 };
136
137 protected _verifyWhitespace(startCounter: number, endCounter: number) {
138 if ((startCounter - 1 <= 0)
139 || this._counters[startCounter - 1] >= (this._calculatePatternLength(startCounter) / 2.0)) {
140 if ((endCounter + 8 >= this._counters.length)
141 || this._counters[endCounter + 7] >= (this._calculatePatternLength(endCounter) / 2.0)) {
142 return true
143 }
144 }
145 return false
146 };
147
148 protected _charToPattern(char: string) {
149 const charCode = char.charCodeAt(0)
150
151 for (let i = 0; i < ALPHABET.length; i++) {
152 if (ALPHABET[i] === charCode) {
153 return CHARACTER_ENCODINGS[i]
154 }
155 }
156 return 0x0
157 };
158
159 protected _thresholdResultPattern(result: ReadonlyArray<string>, startCounter: number) {
160 const categorization: Threshold = {
161 space: {
162 narrow: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE },
163 wide: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE },
164 },
165 bar: {
166 narrow: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE },
167 wide: { size: 0, counts: 0, min: 0, max: Number.MAX_VALUE },
168 },
169 }
170 let pos = startCounter
171 let pattern: number
172
173 for (let i = 0; i < result.length; i++) {
174 pattern = this._charToPattern(result[i])
175 for (let j = 6; j >= 0; j--) {
176 const kind = (j & 1) === 2 ? categorization.bar : categorization.space
177 const cat = (pattern & 1) === 1 ? kind.wide : kind.narrow
178 cat.size += this._counters[pos + j]
179 cat.counts++
180 pattern >>= 1
181 }
182 pos += 8
183 }
184
185 (['space', 'bar'] as const).forEach((key) => {
186 const newkind = categorization[key]
187 newkind.wide.min
188 = Math.floor((newkind.narrow.size / newkind.narrow.counts + newkind.wide.size / newkind.wide.counts) / 2)
189 newkind.narrow.max = Math.ceil(newkind.wide.min)
190 newkind.wide.max = Math.ceil((newkind.wide.size * MAX_ACCEPTABLE + PADDING) / newkind.wide.counts)
191 })
192
193 return categorization
194 };
195
196 protected _validateResult(result: ReadonlyArray<string>, startCounter: number) {
197 const thresholds = this._thresholdResultPattern(result, startCounter)
198 let pos = startCounter
199 let pattern: number
200
201 for (let i = 0; i < result.length; i++) {
202 pattern = this._charToPattern(result[i])
203 for (let j = 6; j >= 0; j--) {
204 const kind = (j & 1) === 0 ? thresholds.bar : thresholds.space
205 const cat = (pattern & 1) === 1 ? kind.wide : kind.narrow
206 const size = this._counters[pos + j]
207 if (size < cat.min || size > cat.max) {
208 return false
209 }
210 pattern >>= 1
211 }
212 pos += 8
213 }
214 return true
215 };
216
217 public decode(row?: Array<number>, start?: BarcodePosition | number | null): Barcode | null {
218 this._counters = this._fillCounters()
219 start = this._findStart()
220 if (!start) {
221 return null
222 }
223 let nextStart = start.startCounter as number
224
225 const result: Array<string> = []
226 let pattern: number
227 do {
228 pattern = this._toPattern(nextStart)
229 if (pattern < 0) {
230 return null
231 }
232 const decodedChar = this._patternToChar(pattern)
233 if (decodedChar === null) {
234 return null
235 }
236 result.push(decodedChar)
237 nextStart += 8
238 if (result.length > 1 && this._isStartEnd(pattern)) {
239 break
240 }
241 } while (nextStart < this._counters.length)
242
243 // verify end
244 if ((result.length - 2) < MIN_ENCODED_CHARS || !this._isStartEnd(pattern)) {
245 return null
246 }
247
248 // verify end white space
249 if (!this._verifyWhitespace(start.startCounter as number, nextStart - 8)) {
250 return null
251 }
252
253 if (!this._validateResult(result, start.startCounter as number)) {
254 return null
255 }
256
257 nextStart = nextStart > this._counters.length ? this._counters.length : nextStart
258 const end = start.start + this._sumCounters(start.startCounter as number, nextStart - 8)
259
260 return {
261 code: result.join(''),
262 start: start.start,
263 end,
264 startInfo: start,
265 decodedCodes: result,
266 format: this.FORMAT, // TODO: i think it should not be required to return format from this, as barcode_reader force sets the format anyway
267 }
268 };
269}
270
271export default NewCodabarReader
packages/qrx/src/quagga/reader/code_128_reader.tsadded+468-0
Changes to packages/qrx/src/quagga/reader/code_128_reader.ts
@@ -0,0 +1,468 @@
1import type { Barcode, BarcodeCorrection, BarcodeInfo, BarcodePosition } from './barcode_reader'
2import BarcodeReader from './barcode_reader'
3
4class Code128Reader extends BarcodeReader {
5 CODE_SHIFT = 98
6 CODE_C = 99
7 CODE_B = 100
8 CODE_A = 101
9 START_CODE_A = 103
10 START_CODE_B = 104
11 START_CODE_C = 105
12 STOP_CODE = 106
13 CODE_PATTERN = [
14 [2, 1, 2, 2, 2, 2],
15 [2, 2, 2, 1, 2, 2],
16 [2, 2, 2, 2, 2, 1],
17 [1, 2, 1, 2, 2, 3],
18 [1, 2, 1, 3, 2, 2],
19 [1, 3, 1, 2, 2, 2],
20 [1, 2, 2, 2, 1, 3],
21 [1, 2, 2, 3, 1, 2],
22 [1, 3, 2, 2, 1, 2],
23 [2, 2, 1, 2, 1, 3],
24 [2, 2, 1, 3, 1, 2],
25 [2, 3, 1, 2, 1, 2],
26 [1, 1, 2, 2, 3, 2],
27 [1, 2, 2, 1, 3, 2],
28 [1, 2, 2, 2, 3, 1],
29 [1, 1, 3, 2, 2, 2],
30 [1, 2, 3, 1, 2, 2],
31 [1, 2, 3, 2, 2, 1],
32 [2, 2, 3, 2, 1, 1],
33 [2, 2, 1, 1, 3, 2],
34 [2, 2, 1, 2, 3, 1],
35 [2, 1, 3, 2, 1, 2],
36 [2, 2, 3, 1, 1, 2],
37 [3, 1, 2, 1, 3, 1],
38 [3, 1, 1, 2, 2, 2],
39 [3, 2, 1, 1, 2, 2],
40 [3, 2, 1, 2, 2, 1],
41 [3, 1, 2, 2, 1, 2],
42 [3, 2, 2, 1, 1, 2],
43 [3, 2, 2, 2, 1, 1],
44 [2, 1, 2, 1, 2, 3],
45 [2, 1, 2, 3, 2, 1],
46 [2, 3, 2, 1, 2, 1],
47 [1, 1, 1, 3, 2, 3],
48 [1, 3, 1, 1, 2, 3],
49 [1, 3, 1, 3, 2, 1],
50 [1, 1, 2, 3, 1, 3],
51 [1, 3, 2, 1, 1, 3],
52 [1, 3, 2, 3, 1, 1],
53 [2, 1, 1, 3, 1, 3],
54 [2, 3, 1, 1, 1, 3],
55 [2, 3, 1, 3, 1, 1],
56 [1, 1, 2, 1, 3, 3],
57 [1, 1, 2, 3, 3, 1],
58 [1, 3, 2, 1, 3, 1],
59 [1, 1, 3, 1, 2, 3],
60 [1, 1, 3, 3, 2, 1],
61 [1, 3, 3, 1, 2, 1],
62 [3, 1, 3, 1, 2, 1],
63 [2, 1, 1, 3, 3, 1],
64 [2, 3, 1, 1, 3, 1],
65 [2, 1, 3, 1, 1, 3],
66 [2, 1, 3, 3, 1, 1],
67 [2, 1, 3, 1, 3, 1],
68 [3, 1, 1, 1, 2, 3],
69 [3, 1, 1, 3, 2, 1],
70 [3, 3, 1, 1, 2, 1],
71 [3, 1, 2, 1, 1, 3],
72 [3, 1, 2, 3, 1, 1],
73 [3, 3, 2, 1, 1, 1],
74 [3, 1, 4, 1, 1, 1],
75 [2, 2, 1, 4, 1, 1],
76 [4, 3, 1, 1, 1, 1],
77 [1, 1, 1, 2, 2, 4],
78 [1, 1, 1, 4, 2, 2],
79 [1, 2, 1, 1, 2, 4],
80 [1, 2, 1, 4, 2, 1],
81 [1, 4, 1, 1, 2, 2],
82 [1, 4, 1, 2, 2, 1],
83 [1, 1, 2, 2, 1, 4],
84 [1, 1, 2, 4, 1, 2],
85 [1, 2, 2, 1, 1, 4],
86 [1, 2, 2, 4, 1, 1],
87 [1, 4, 2, 1, 1, 2],
88 [1, 4, 2, 2, 1, 1],
89 [2, 4, 1, 2, 1, 1],
90 [2, 2, 1, 1, 1, 4],
91 [4, 1, 3, 1, 1, 1],
92 [2, 4, 1, 1, 1, 2],
93 [1, 3, 4, 1, 1, 1],
94 [1, 1, 1, 2, 4, 2],
95 [1, 2, 1, 1, 4, 2],
96 [1, 2, 1, 2, 4, 1],
97 [1, 1, 4, 2, 1, 2],
98 [1, 2, 4, 1, 1, 2],
99 [1, 2, 4, 2, 1, 1],
100 [4, 1, 1, 2, 1, 2],
101 [4, 2, 1, 1, 1, 2],
102 [4, 2, 1, 2, 1, 1],
103 [2, 1, 2, 1, 4, 1],
104 [2, 1, 4, 1, 2, 1],
105 [4, 1, 2, 1, 2, 1],
106 [1, 1, 1, 1, 4, 3],
107 [1, 1, 1, 3, 4, 1],
108 [1, 3, 1, 1, 4, 1],
109 [1, 1, 4, 1, 1, 3],
110 [1, 1, 4, 3, 1, 1],
111 [4, 1, 1, 1, 1, 3],
112 [4, 1, 1, 3, 1, 1],
113 [1, 1, 3, 1, 4, 1],
114 [1, 1, 4, 1, 3, 1],
115 [3, 1, 1, 1, 4, 1],
116 [4, 1, 1, 1, 3, 1],
117 [2, 1, 1, 4, 1, 2],
118 [2, 1, 1, 2, 1, 4],
119 [2, 1, 1, 2, 3, 2],
120 [2, 3, 3, 1, 1, 1, 2],
121 ]
122
123 SINGLE_CODE_ERROR = 0.64
124 AVG_CODE_ERROR = 0.30
125 FORMAT = 'code_128'
126 MODULE_INDICES = { bar: [0, 2, 4], space: [1, 3, 5] }
127
128 protected _decodeCode(start: number, correction?: BarcodeCorrection): BarcodeInfo | null {
129 const bestMatch = {
130 error: Number.MAX_VALUE,
131 code: -1,
132 start,
133 end: start,
134 correction: {
135 bar: 1,
136 space: 1,
137 },
138 }
139 const counter = [0, 0, 0, 0, 0, 0]
140 const offset = start
141 let isWhite = !this._row[offset]
142 let counterPos = 0
143
144 for (let i = offset; i < this._row.length; i++) {
145 if (this._row[i] ^ (isWhite ? 1 : 0)) {
146 counter[counterPos]++
147 }
148 else {
149 if (counterPos === counter.length - 1) {
150 if (correction) {
151 this._correct(counter, correction)
152 }
153 for (let code = 0; code < this.CODE_PATTERN.length; code++) {
154 const error = this._matchPattern(counter, this.CODE_PATTERN[code])
155 if (error < bestMatch.error) {
156 bestMatch.code = code
157 bestMatch.error = error
158 }
159 }
160 bestMatch.end = i
161 if (bestMatch.code === -1 || bestMatch.error > this.AVG_CODE_ERROR) {
162 return null
163 }
164 if (this.CODE_PATTERN[bestMatch.code]) {
165 bestMatch.correction.bar = this.calculateCorrection(
166 this.CODE_PATTERN[bestMatch.code],
167 counter,
168 this.MODULE_INDICES.bar,
169 )
170 bestMatch.correction.space = this.calculateCorrection(
171 this.CODE_PATTERN[bestMatch.code],
172 counter,
173 this.MODULE_INDICES.space,
174 )
175 }
176 return bestMatch
177 }
178 else {
179 counterPos++
180 }
181 counter[counterPos] = 1
182 isWhite = !isWhite
183 }
184 }
185 return null
186 };
187
188 protected _correct(counter: Array<number>, correction: BarcodeCorrection) {
189 this._correctBars(counter, correction.bar, this.MODULE_INDICES.bar)
190 this._correctBars(counter, correction.space, this.MODULE_INDICES.space)
191 };
192
193 // TODO: _findStart and decodeCode share similar code, can we re-use some?
194 protected _findStart(): BarcodeInfo | null {
195 const counter = [0, 0, 0, 0, 0, 0]
196 const offset = this._nextSet(this._row)
197 const bestMatch = {
198 error: Number.MAX_VALUE,
199 code: -1,
200 start: 0,
201 end: 0,
202 correction: {
203 bar: 1,
204 space: 1,
205 },
206 }
207 let isWhite = false
208 let counterPos = 0
209
210 for (let i = offset; i < this._row.length; i++) {
211 if (this._row[i] ^ (isWhite ? 1 : 0)) {
212 counter[counterPos]++
213 }
214 else {
215 if (counterPos === counter.length - 1) {
216 const sum = counter.reduce((prev, next) => prev + next, 0)
217 for (let code = this.START_CODE_A; code <= this.START_CODE_C; code++) {
218 const error = this._matchPattern(counter, this.CODE_PATTERN[code])
219 if (error < bestMatch.error) {
220 bestMatch.code = code
221 bestMatch.error = error
222 }
223 }
224 if (bestMatch.error < this.AVG_CODE_ERROR) {
225 bestMatch.start = i - sum
226 bestMatch.end = i
227 bestMatch.correction.bar = this.calculateCorrection(
228 this.CODE_PATTERN[bestMatch.code],
229 counter,
230 this.MODULE_INDICES.bar,
231 )
232 bestMatch.correction.space = this.calculateCorrection(
233 this.CODE_PATTERN[bestMatch.code],
234 counter,
235 this.MODULE_INDICES.space,
236 )
237 return bestMatch
238 }
239
240 for (let j = 0; j < 4; j++) {
241 counter[j] = counter[j + 2]
242 }
243 counter[4] = 0
244 counter[5] = 0
245 counterPos--
246 }
247 else {
248 counterPos++
249 }
250 counter[counterPos] = 1
251 isWhite = !isWhite
252 }
253 }
254 return null
255 };
256
257 public decode(row?: Array<number>, start?: BarcodePosition): Barcode | null {
258 const startInfo = this._findStart()
259 if (startInfo === null) {
260 return null
261 }
262 // var self = this,
263 // done = false,
264 // result = [],
265 // multiplier = 0,
266 // checksum = 0,
267 // codeset,
268 // rawResult = [],
269 // decodedCodes = [],
270 // shiftNext = false,
271 // unshift,
272 // removeLastCharacter = true;
273
274 let code: BarcodeInfo | null = {
275 code: startInfo.code,
276 start: startInfo.start,
277 end: startInfo.end,
278 correction: {
279 bar: startInfo.correction!.bar,
280 space: startInfo.correction!.space,
281 },
282 }
283 const decodedCodes = []
284 decodedCodes.push(code)
285 let checksum = code.code
286 let codeset = ((c: number) => {
287 switch (c) {
288 case this.START_CODE_A:
289 return this.CODE_A
290 case this.START_CODE_B:
291 return this.CODE_B
292 case this.START_CODE_C:
293 return this.CODE_C
294 default:
295 return null
296 }
297 })(code.code)
298 let done = false
299 let shiftNext = false
300 let unshift = shiftNext
301 let removeLastCharacter = true
302 let multiplier = 0
303 const rawResult: Array<number> = []
304 const result: Array<string | number> = [] // TODO: i think this should be string only, but it creates problems if it is
305
306 while (!done) {
307 unshift = shiftNext
308 shiftNext = false
309 code = this._decodeCode(code!.end, code!.correction)
310 if (code !== null) {
311 if (code.code !== this.STOP_CODE) {
312 removeLastCharacter = true
313 }
314
315 if (code.code !== this.STOP_CODE) {
316 rawResult.push(code.code)
317 multiplier++
318 checksum += multiplier * code.code
319 }
320 decodedCodes.push(code)
321
322 switch (codeset) {
323 case this.CODE_A:
324 if (code.code < 64) {
325 result.push(String.fromCharCode(32 + code.code))
326 }
327 else if (code.code < 96) {
328 result.push(String.fromCharCode(code.code - 64))
329 }
330 else {
331 if (code.code !== this.STOP_CODE) {
332 removeLastCharacter = false
333 }
334 switch (code.code) {
335 case this.CODE_SHIFT:
336 shiftNext = true
337 codeset = this.CODE_B
338 break
339 case this.CODE_B:
340 codeset = this.CODE_B
341 break
342 case this.CODE_C:
343 codeset = this.CODE_C
344 break
345 case this.STOP_CODE:
346 done = true
347 break
348 }
349 }
350 break
351 case this.CODE_B:
352 if (code.code < 96) {
353 result.push(String.fromCharCode(32 + code.code))
354 }
355 else {
356 if (code.code !== this.STOP_CODE) {
357 removeLastCharacter = false
358 }
359 switch (code.code) {
360 case this.CODE_SHIFT:
361 shiftNext = true
362 codeset = this.CODE_A
363 break
364 case this.CODE_A:
365 codeset = this.CODE_A
366 break
367 case this.CODE_C:
368 codeset = this.CODE_C
369 break
370 case this.STOP_CODE:
371 done = true
372 break
373 }
374 }
375 break
376 case this.CODE_C:
377 if (code.code < 100) {
378 result.push(code.code < 10 ? `0${code.code}` : code.code)
379 }
380 else {
381 if (code.code !== this.STOP_CODE) {
382 removeLastCharacter = false
383 }
384 switch (code.code) {
385 case this.CODE_A:
386 codeset = this.CODE_A
387 break
388 case this.CODE_B:
389 codeset = this.CODE_B
390 break
391 case this.STOP_CODE:
392 done = true
393 break
394 }
395 }
396 break
397 }
398 }
399 else {
400 done = true
401 }
402 if (unshift) {
403 codeset = codeset === this.CODE_A ? this.CODE_B : this.CODE_A
404 }
405 }
406
407 if (code === null) {
408 return null
409 }
410
411 code.end = this._nextUnset(this._row, code.end)
412 if (!this._verifyTrailingWhitespace(code)) {
413 return null
414 }
415
416 checksum -= multiplier * rawResult[rawResult.length - 1]
417 if (checksum % 103 !== rawResult[rawResult.length - 1]) {
418 return null
419 }
420
421 if (!result.length) {
422 return null
423 }
424
425 // remove last code from result (checksum)
426 if (removeLastCharacter) {
427 result.splice(result.length - 1, 1)
428 }
429
430 return {
431 code: result.join(''),
432 start: startInfo.start,
433 end: code.end,
434 codeset: codeset as number,
435 startInfo,
436 decodedCodes,
437 endInfo: code,
438 format: this.FORMAT,
439 }
440 };
441
442 protected _verifyTrailingWhitespace(endInfo: BarcodeInfo): BarcodeInfo | null {
443 const self = this
444 let trailingWhitespaceEnd
445
446 trailingWhitespaceEnd = endInfo.end + ((endInfo.end - endInfo.start) / 2)
447 if (trailingWhitespaceEnd < self._row.length) {
448 if (self._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
449 return endInfo
450 }
451 }
452 return null
453 };
454
455 public calculateCorrection(expected: ReadonlyArray<number>, normalized: ReadonlyArray<number>, indices: ReadonlyArray<number>): number {
456 let length = indices.length
457 let sumNormalized = 0
458 let sumExpected = 0
459
460 while (length--) {
461 sumExpected += expected[indices[length]]
462 sumNormalized += normalized[indices[length]]
463 }
464 return sumExpected / sumNormalized
465 }
466}
467
468export default Code128Reader
packages/qrx/src/quagga/reader/code_32_reader.tsadded+62-0
Changes to packages/qrx/src/quagga/reader/code_32_reader.ts
@@ -0,0 +1,62 @@
1import Code39Reader from './code_39_reader'
2
3const patterns = {
4 AEIO: /[AEIO]/g,
5 AZ09: /[A-Z0-9]/,
6}
7
8const code32set = '0123456789BCDFGHJKLMNPQRSTUVWXYZ'
9
10class Code32Reader extends Code39Reader {
11 FORMAT = 'code_32_reader'
12
13 protected _decodeCode32(code: string) {
14 if (/[^0-9BCDFGHJ-NP-Z]/.test(code)) {
15 return null
16 }
17 let res = 0
18 for (let i = 0; i < code.length; i++) {
19 res = res * 32 + code32set.indexOf(code[i])
20 }
21 let code32 = `${res}`
22 if (code32.length < 9) {
23 code32 = (`000000000${code32}`).slice(-9)
24 }
25 return `A${code32}`
26 }
27
28 // TODO (this was todo in original repo, no text was there. sorry.)
29 protected _checkChecksum(code: string): boolean {
30 return !!code
31 }
32
33 public decode() {
34 const result = super.decode()
35 if (!result) {
36 return null
37 }
38
39 let code = result.code
40
41 if (!code) {
42 return null
43 }
44
45 code = code.replace(patterns.AEIO, '')
46
47 if (!this._checkChecksum(code)) {
48 return null
49 }
50
51 const code32 = this._decodeCode32(code)
52
53 if (!code32) {
54 return null
55 }
56
57 result.code = code32
58 return result
59 }
60}
61
62export default Code32Reader
packages/qrx/src/quagga/reader/code_39_reader.tsadded+211-0
Changes to packages/qrx/src/quagga/reader/code_39_reader.ts
@@ -0,0 +1,211 @@
1import ArrayHelper from '../common/array_helper'
2import BarcodeReader, { type Barcode, type BarcodePosition } from './barcode_reader'
3
4const ALPHABETH_STRING = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. *$/+%'
5const ALPHABET = new Uint16Array([...ALPHABETH_STRING].map(char => char.charCodeAt(0)))
6const CHARACTER_ENCODINGS = new Uint16Array([
7 0x034,
8 0x121,
9 0x061,
10 0x160,
11 0x031,
12 0x130,
13 0x070,
14 0x025,
15 0x124,
16 0x064,
17 0x109,
18 0x049,
19 0x148,
20 0x019,
21 0x118,
22 0x058,
23 0x00D,
24 0x10C,
25 0x04C,
26 0x01C,
27 0x103,
28 0x043,
29 0x142,
30 0x013,
31 0x112,
32 0x052,
33 0x007,
34 0x106,
35 0x046,
36 0x016,
37 0x181,
38 0x0C1,
39 0x1C0,
40 0x091,
41 0x190,
42 0x0D0,
43 0x085,
44 0x184,
45 0x0C4,
46 0x094,
47 0x0A8,
48 0x0A2,
49 0x08A,
50 0x02A,
51])
52const ASTERISK = 0x094
53
54class Code39Reader extends BarcodeReader {
55 FORMAT = 'code_39'
56
57 protected _findStart(): BarcodePosition | null {
58 const offset = this._nextSet(this._row)
59 let patternStart = offset
60 const counter = new Uint16Array([0, 0, 0, 0, 0, 0, 0, 0, 0])
61 let counterPos = 0
62 let isWhite = false
63
64 for (let i = offset; i < this._row.length; i++) {
65 if (this._row[i] ^ (isWhite ? 1 : 0)) {
66 counter[counterPos]++
67 }
68 else {
69 if (counterPos === counter.length - 1) {
70 // find start pattern
71 if (this._toPattern(counter) === ASTERISK) {
72 const whiteSpaceMustStart = Math.floor(Math.max(0, patternStart - ((i - patternStart) / 4)))
73 if (this._matchRange(whiteSpaceMustStart, patternStart, 0)) {
74 return {
75 start: patternStart,
76 end: i,
77 }
78 }
79 }
80
81 patternStart += counter[0] + counter[1]
82 for (let j = 0; j < 7; j++) {
83 counter[j] = counter[j + 2]
84 }
85 counter[7] = 0
86 counter[8] = 0
87 counterPos--
88 }
89 else {
90 counterPos++
91 }
92 counter[counterPos] = 1
93 isWhite = !isWhite
94 }
95 }
96 return null
97 }
98
99 protected _toPattern(counters: Uint16Array): number {
100 const numCounters = counters.length
101 let maxNarrowWidth = 0
102 let numWideBars = numCounters
103 let wideBarWidth = 0
104
105 while (numWideBars > 3) {
106 maxNarrowWidth = this._findNextWidth(counters, maxNarrowWidth)
107 numWideBars = 0
108 let pattern = 0
109 for (let i = 0; i < numCounters; i++) {
110 if (counters[i] > maxNarrowWidth) {
111 pattern |= 1 << (numCounters - 1 - i)
112 numWideBars++
113 wideBarWidth += counters[i]
114 }
115 }
116
117 if (numWideBars === 3) {
118 for (let i = 0; i < numCounters && numWideBars > 0; i++) {
119 if (counters[i] > maxNarrowWidth) {
120 numWideBars--
121 if ((counters[i] * 2) >= wideBarWidth) {
122 return -1
123 }
124 }
125 }
126 return pattern
127 }
128 }
129 return -1
130 }
131
132 protected _findNextWidth(counters: Uint16Array, current: number): number {
133 let minWidth = Number.MAX_VALUE
134
135 for (let i = 0; i < counters.length; i++) {
136 if (counters[i] < minWidth && counters[i] > current) {
137 minWidth = counters[i]
138 }
139 }
140
141 return minWidth
142 }
143
144 protected _patternToChar(pattern: number): string | null {
145 for (let i = 0; i < CHARACTER_ENCODINGS.length; i++) {
146 if (CHARACTER_ENCODINGS[i] === pattern) {
147 return String.fromCharCode(ALPHABET[i])
148 }
149 }
150 return null
151 }
152
153 protected _verifyTrailingWhitespace(lastStart: number, nextStart: number, counters: Uint16Array): boolean {
154 const patternSize = ArrayHelper.sum(counters)
155
156 const trailingWhitespaceEnd = nextStart - lastStart - patternSize
157 if ((trailingWhitespaceEnd * 3) >= patternSize) {
158 return true
159 }
160 return false
161 }
162
163 public decode(): Barcode | null {
164 let counters = new Uint16Array([0, 0, 0, 0, 0, 0, 0, 0, 0])
165 const result: Array<string> = []
166 const start = this._findStart()
167
168 if (!start) {
169 return null
170 }
171 let nextStart = this._nextSet(this._row, start.end)
172
173 let decodedChar
174 let lastStart: number
175 do {
176 counters = this._toCounters(nextStart, counters) as Uint16Array
177 const pattern = this._toPattern(counters)
178 if (pattern < 0) {
179 return null
180 }
181 decodedChar = this._patternToChar(pattern)
182 if (decodedChar === null) {
183 return null
184 }
185 result.push(decodedChar)
186 lastStart = nextStart
187 nextStart += ArrayHelper.sum(counters)
188 nextStart = this._nextSet(this._row, nextStart)
189 } while (decodedChar !== '*')
190 result.pop()
191
192 if (!result.length) {
193 return null
194 }
195
196 if (!this._verifyTrailingWhitespace(lastStart, nextStart, counters)) {
197 return null
198 }
199
200 return {
201 code: result.join(''),
202 start: start.start,
203 end: nextStart,
204 startInfo: start,
205 decodedCodes: result,
206 format: this.FORMAT,
207 }
208 }
209}
210
211export default Code39Reader
packages/qrx/src/quagga/reader/code_39_vin_reader.tsadded+49-0
Changes to packages/qrx/src/quagga/reader/code_39_vin_reader.ts
@@ -0,0 +1,49 @@
1import type { Barcode } from './barcode_reader'
2import Code39Reader from './code_39_reader'
3
4const patterns = {
5 IOQ: /[IOQ]/g,
6 AZ09: /[A-Z0-9]{17}/,
7}
8
9class Code39VINReader extends Code39Reader {
10 FORMAT = 'code_39_vin'
11
12 // TODO (this was todo in original repo, no text was there. sorry.)
13 protected _checkChecksum(code: string): boolean {
14 return !!code
15 }
16
17 // Cribbed from:
18 // https://github.com/zxing/zxing/blob/master/core/src/main/java/com/google/zxing/client/result/VINResultParser.java
19 public decode(): Barcode | null {
20 const result = super.decode()
21 if (!result) {
22 return null
23 }
24
25 let { code } = result
26
27 if (!code) {
28 return null
29 }
30
31 code = code.replace(patterns.IOQ, '')
32
33 if (!code.match(patterns.AZ09)) {
34 if (ENV.development) {
35 console.log('Failed AZ09 pattern code:', code)
36 }
37 return null
38 }
39
40 if (!this._checkChecksum(code)) {
41 return null
42 }
43
44 result.code = code
45 return result
46 }
47}
48
49export default Code39VINReader
packages/qrx/src/quagga/reader/code_93_reader.tsadded+289-0
Changes to packages/qrx/src/quagga/reader/code_93_reader.ts
@@ -0,0 +1,289 @@
1import type { Barcode, BarcodePosition } from './barcode_reader'
2import ArrayHelper from '../common/array_helper'
3import BarcodeReader from './barcode_reader'
4
5const ALPHABETH_STRING = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%abcd*'
6const ALPHABET = new Uint16Array([...ALPHABETH_STRING].map(char => char.charCodeAt(0)))
7const CHARACTER_ENCODINGS = new Uint16Array([
8 0x114,
9 0x148,
10 0x144,
11 0x142,
12 0x128,
13 0x124,
14 0x122,
15 0x150,
16 0x112,
17 0x10A,
18 0x1A8,
19 0x1A4,
20 0x1A2,
21 0x194,
22 0x192,
23 0x18A,
24 0x168,
25 0x164,
26 0x162,
27 0x134,
28 0x11A,
29 0x158,
30 0x14C,
31 0x146,
32 0x12C,
33 0x116,
34 0x1B4,
35 0x1B2,
36 0x1AC,
37 0x1A6,
38 0x196,
39 0x19A,
40 0x16C,
41 0x166,
42 0x136,
43 0x13A,
44 0x12E,
45 0x1D4,
46 0x1D2,
47 0x1CA,
48 0x16E,
49 0x176,
50 0x1AE,
51 0x126,
52 0x1DA,
53 0x1D6,
54 0x132,
55 0x15E,
56])
57const ASTERISK = 0x15E
58
59class Code93Reader extends BarcodeReader {
60 FORMAT = 'code_93'
61 protected _patternToChar(pattern: number): string | null {
62 for (let i = 0; i < CHARACTER_ENCODINGS.length; i++) {
63 if (CHARACTER_ENCODINGS[i] === pattern) {
64 return String.fromCharCode(ALPHABET[i])
65 }
66 }
67 return null
68 };
69
70 protected _toPattern(counters: Uint16Array): number {
71 const numCounters = counters.length
72 const sum = counters.reduce((prev, next) => prev + next, 0)
73 let pattern = 0
74
75 for (let i = 0; i < numCounters; i++) {
76 const normalized = Math.round(counters[i] * 9 / sum)
77 if (normalized < 1 || normalized > 4) {
78 return -1
79 }
80 if ((i & 1) === 0) {
81 for (let j = 0; j < normalized; j++) {
82 pattern = (pattern << 1) | 1
83 }
84 }
85 else {
86 pattern <<= normalized
87 }
88 }
89 return pattern
90 };
91
92 protected _findStart(): BarcodePosition | null {
93 const offset = this._nextSet(this._row)
94 let patternStart = offset
95 const counter = new Uint16Array([0, 0, 0, 0, 0, 0])
96 let counterPos = 0
97 let isWhite = false
98
99 for (let i = offset; i < this._row.length; i++) {
100 if (this._row[i] ^ (isWhite ? 1 : 0)) {
101 counter[counterPos]++
102 }
103 else {
104 if (counterPos === counter.length - 1) {
105 // find start pattern
106 if (this._toPattern(counter) === ASTERISK) {
107 const whiteSpaceMustStart = Math.floor(Math.max(0, patternStart - ((i - patternStart) / 4)))
108 if (this._matchRange(whiteSpaceMustStart, patternStart, 0)) {
109 return {
110 start: patternStart,
111 end: i,
112 }
113 }
114 }
115
116 patternStart += counter[0] + counter[1]
117 for (let j = 0; j < 4; j++) {
118 counter[j] = counter[j + 2]
119 }
120 counter[4] = 0
121 counter[5] = 0
122 counterPos--
123 }
124 else {
125 counterPos++
126 }
127 counter[counterPos] = 1
128 isWhite = !isWhite
129 }
130 }
131 return null
132 };
133
134 protected _verifyEnd(lastStart: number, nextStart: number): boolean {
135 if (lastStart === nextStart || !this._row[nextStart]) {
136 return false
137 }
138 return true
139 };
140
141 protected _decodeExtended(charArray: Array<string>): string[] | null {
142 const length = charArray.length
143 const result: Array<string> = []
144 for (let i = 0; i < length; i++) {
145 const char = charArray[i]
146 if (char >= 'a' && char <= 'd') {
147 if (i > (length - 2)) {
148 return null
149 }
150 const nextChar = charArray[++i]
151 const nextCharCode = nextChar.charCodeAt(0)
152 let decodedChar
153 switch (char) {
154 case 'a':
155 if (nextChar >= 'A' && nextChar <= 'Z') {
156 decodedChar = String.fromCharCode(nextCharCode - 64)
157 }
158 else {
159 return null
160 }
161 break
162 case 'b':
163 if (nextChar >= 'A' && nextChar <= 'E') {
164 decodedChar = String.fromCharCode(nextCharCode - 38)
165 }
166 else if (nextChar >= 'F' && nextChar <= 'J') {
167 decodedChar = String.fromCharCode(nextCharCode - 11)
168 }
169 else if (nextChar >= 'K' && nextChar <= 'O') {
170 decodedChar = String.fromCharCode(nextCharCode + 16)
171 }
172 else if (nextChar >= 'P' && nextChar <= 'S') {
173 decodedChar = String.fromCharCode(nextCharCode + 43)
174 }
175 else if (nextChar >= 'T' && nextChar <= 'Z') {
176 decodedChar = String.fromCharCode(127)
177 }
178 else {
179 return null
180 }
181 break
182 case 'c':
183 if (nextChar >= 'A' && nextChar <= 'O') {
184 decodedChar = String.fromCharCode(nextCharCode - 32)
185 }
186 else if (nextChar === 'Z') {
187 decodedChar = ':'
188 }
189 else {
190 return null
191 }
192 break
193 case 'd':
194 if (nextChar >= 'A' && nextChar <= 'Z') {
195 decodedChar = String.fromCharCode(nextCharCode + 32)
196 }
197 else {
198 return null
199 }
200 break
201 default:
202 console.warn('* code_93_reader _decodeExtended hit default case, this may be an error', decodedChar)
203 return null
204 }
205 result.push(decodedChar)
206 }
207 else {
208 result.push(char)
209 }
210 }
211 return result
212 };
213
214 protected _matchCheckChar(charArray: Array<string>, index: number, maxWeight: number): boolean {
215 const arrayToCheck = charArray.slice(0, index)
216 const length = arrayToCheck.length
217 const weightedSums = arrayToCheck.reduce((sum, char, i) => {
218 const weight = (((i * -1) + (length - 1)) % maxWeight) + 1
219 const value = ALPHABET.indexOf(char.charCodeAt(0))
220 return sum + (weight * value)
221 }, 0)
222
223 const checkChar = ALPHABET[(weightedSums % 47)]
224 return checkChar === charArray[index].charCodeAt(0)
225 };
226
227 protected _verifyChecksums(charArray: Array<string>): boolean {
228 return this._matchCheckChar(charArray, charArray.length - 2, 20)
229 && this._matchCheckChar(charArray, charArray.length - 1, 15)
230 };
231
232 public decode(row?: Array<number>, start?: BarcodePosition | number | null): Barcode | null {
233 start = this._findStart()
234 if (!start) {
235 return null
236 }
237
238 let counters = new Uint16Array([0, 0, 0, 0, 0, 0])
239 let result: Array<string> | null = []
240 let nextStart = this._nextSet(this._row, start.end)
241 let lastStart
242 let decodedChar: string | null
243 do {
244 counters = this._toCounters(nextStart, counters) as Uint16Array
245 const pattern = this._toPattern(counters)
246 if (pattern < 0) {
247 return null
248 }
249 decodedChar = this._patternToChar(pattern)
250 if (decodedChar === null) {
251 return null
252 }
253 result.push(decodedChar)
254 lastStart = nextStart
255 nextStart += ArrayHelper.sum(counters)
256 nextStart = this._nextSet(this._row, nextStart)
257 } while (decodedChar !== '*')
258 result.pop()
259
260 if (!result.length) {
261 return null
262 }
263
264 if (!this._verifyEnd(lastStart, nextStart)) {
265 return null
266 }
267
268 if (!this._verifyChecksums(result)) {
269 return null
270 }
271
272 result = result.slice(0, result.length - 2)
273 // yes, this is an assign inside an if.
274 if ((result = this._decodeExtended(result)) === null) {
275 return null
276 }
277
278 return {
279 code: result.join(''),
280 start: start.start,
281 end: nextStart,
282 startInfo: start,
283 decodedCodes: result,
284 format: this.FORMAT,
285 }
286 }
287}
288
289export default Code93Reader
packages/qrx/src/quagga/reader/ean_2_reader.tsadded+56-0
Changes to packages/qrx/src/quagga/reader/ean_2_reader.ts
@@ -0,0 +1,56 @@
1import EANReader, { CODE_G_START } from './ean_reader';
2import { BarcodePosition, Barcode, BarcodeInfo } from './barcode_reader';
3
4
5class EAN2Reader extends EANReader {
6 FORMAT = 'ean_2';
7 public decode(row?: Array<number>, start?: number): Barcode | null {
8 if (row) {
9 this._row = row;
10 }
11
12 let codeFrequency = 0;
13 let offset = start;
14 const end = this._row.length;
15 const result = [];
16 const decodedCodes = [];
17 let code: BarcodeInfo | null = null;
18
19 if (offset === undefined){
20 return null;
21 }
22
23 for (let i = 0; i < 2 && offset < end; i++) {
24 code = this._decodeCode(offset);
25 if (!code) {
26 return null;
27 }
28 decodedCodes.push(code);
29 result.push(code.code % 10);
30 if (code.code >= CODE_G_START) {
31 codeFrequency |= 1 << (1 - i);
32 }
33 if (i !== 1) {
34 offset = this._nextSet(this._row, code.end);
35 offset = this._nextUnset(this._row, offset);
36 }
37 }
38
39 if (result.length !== 2 || (parseInt(result.join('')) % 4) !== codeFrequency) {
40 return null;
41 }
42
43 const startInfo = this._findStart();
44
45 return {
46 code: result.join(''),
47 decodedCodes,
48 end: (code as BarcodeInfo).end,
49 format: this.FORMAT,
50 startInfo: startInfo as BarcodePosition,
51 start: (startInfo as BarcodePosition).start,
52 };
53 }
54};
55
56export default EAN2Reader;
packages/qrx/src/quagga/reader/ean_5_reader.tsadded+85-0
Changes to packages/qrx/src/quagga/reader/ean_5_reader.ts
@@ -0,0 +1,85 @@
1import type { Barcode, BarcodeInfo, BarcodePosition } from './barcode_reader'
2import EANReader, { CODE_G_START } from './ean_reader'
3
4const CHECK_DIGIT_ENCODINGS = [24, 20, 18, 17, 12, 6, 3, 10, 9, 5]
5
6function determineCheckDigit(codeFrequency: number) {
7 for (let i = 0; i < 10; i++) {
8 if (codeFrequency === CHECK_DIGIT_ENCODINGS[i]) {
9 return i
10 }
11 }
12 return null
13}
14
15function extensionChecksum(result: Array<number>) {
16 const length = result.length
17 let sum = 0
18
19 for (let i = length - 2; i >= 0; i -= 2) {
20 sum += result[i]
21 }
22 sum *= 3
23 for (let i = length - 1; i >= 0; i -= 2) {
24 sum += result[i]
25 }
26 sum *= 3
27 return sum % 10
28}
29
30class EAN5Reader extends EANReader {
31 FORMAT = 'ean_5'
32 public decode(row?: Array<number>, start?: number): Barcode | null {
33 if (start === undefined) {
34 return null
35 }
36
37 if (row) {
38 this._row = row
39 }
40
41 let codeFrequency = 0
42 let offset = start
43 const end = this._row.length
44 let code: BarcodeInfo | null = null
45 const result = []
46 const decodedCodes = []
47
48 for (let i = 0; i < 5 && offset < end; i++) {
49 code = this._decodeCode(offset)
50 if (!code) {
51 return null
52 }
53 decodedCodes.push(code)
54 result.push(code.code % 10)
55 if (code.code >= CODE_G_START) {
56 codeFrequency |= 1 << (4 - i)
57 }
58 if (i !== 4) {
59 offset = this._nextSet(this._row, code.end)
60 offset = this._nextUnset(this._row, offset)
61 }
62 }
63
64 if (result.length !== 5) {
65 return null
66 }
67
68 if (extensionChecksum(result) !== determineCheckDigit(codeFrequency)) {
69 return null
70 }
71
72 const startInfo = this._findStart()
73
74 return {
75 code: result.join(''),
76 decodedCodes,
77 end: (code as BarcodeInfo).end,
78 format: this.FORMAT,
79 startInfo: startInfo as BarcodePosition,
80 start: (startInfo as BarcodePosition).start,
81 }
82 }
83};
84
85export default EAN5Reader
packages/qrx/src/quagga/reader/ean_8_reader.tsadded+37-0
Changes to packages/qrx/src/quagga/reader/ean_8_reader.ts
@@ -0,0 +1,37 @@
1import type { BarcodeInfo, BarcodePosition } from './barcode_reader'
2import EANReader, { CODE_G_START, MIDDLE_PATTERN } from './ean_reader'
3
4class EAN8Reader extends EANReader {
5 FORMAT = 'ean_8'
6 protected _decodePayload(inCode: BarcodePosition, result: Array<number>, decodedCodes: Array<BarcodePosition>): BarcodeInfo | null {
7 let code: BarcodeInfo | BarcodePosition | null = inCode
8
9 for (let i = 0; i < 4; i++) {
10 code = this._decodeCode(code.end, CODE_G_START)
11 if (!code) {
12 return null
13 }
14 result.push((code as BarcodeInfo).code)
15 decodedCodes.push(code)
16 }
17
18 code = this._findPattern(MIDDLE_PATTERN, code.end, true, false)
19 if (code === null) {
20 return null
21 }
22 decodedCodes.push(code)
23
24 for (let i = 0; i < 4; i++) {
25 code = this._decodeCode(code.end, CODE_G_START)
26 if (!code) {
27 return null
28 }
29 decodedCodes.push(code)
30 result.push((code as BarcodeInfo).code)
31 }
32
33 return code as BarcodeInfo
34 };
35}
36
37export default EAN8Reader
packages/qrx/src/quagga/reader/ean_reader.tsadded+408-0
Changes to packages/qrx/src/quagga/reader/ean_reader.ts
@@ -0,0 +1,408 @@
1import type { Barcode, BarcodeInfo, BarcodePosition, BarcodeReaderConfig } from './barcode_reader'
2import merge from 'lodash/merge'
3import BarcodeReader from './barcode_reader'
4
5// const CODE_L_START = 0;
6const CODE_G_START = 10
7export { CODE_G_START }
8const START_PATTERN = [1, 1, 1]
9const MIDDLE_PATTERN = [1, 1, 1, 1, 1]
10export { MIDDLE_PATTERN }
11const EXTENSION_START_PATTERN = [1, 1, 2]
12const CODE_PATTERN = [
13 [3, 2, 1, 1],
14 [2, 2, 2, 1],
15 [2, 1, 2, 2],
16 [1, 4, 1, 1],
17 [1, 1, 3, 2],
18 [1, 2, 3, 1],
19 [1, 1, 1, 4],
20 [1, 3, 1, 2],
21 [1, 2, 1, 3],
22 [3, 1, 1, 2],
23 [1, 1, 2, 3],
24 [1, 2, 2, 2],
25 [2, 2, 1, 2],
26 [1, 1, 4, 1],
27 [2, 3, 1, 1],
28 [1, 3, 2, 1],
29 [4, 1, 1, 1],
30 [2, 1, 3, 1],
31 [3, 1, 2, 1],
32 [2, 1, 1, 3],
33]
34const CODE_FREQUENCY = [0, 11, 13, 14, 19, 25, 28, 21, 22, 26]
35// const SINGLE_CODE_ERROR = 0.70;
36const AVG_CODE_ERROR = 0.48
37
38class EANReader extends BarcodeReader {
39 FORMAT = 'ean_13'
40 SINGLE_CODE_ERROR = 0.70
41 STOP_PATTERN = [1, 1, 1] // TODO: does this need to be in the class?
42
43 constructor(config?: BarcodeReaderConfig, supplements?: Array<BarcodeReader>) {
44 super(merge({ supplements: [] }, config), supplements)
45 }
46
47 protected _findPattern(pattern: ReadonlyArray<number>, offset: number, isWhite: boolean, tryHarder: boolean): BarcodePosition | null {
48 const counter = Array.from({ length: pattern.length }).fill(0)
49 const bestMatch: BarcodePosition = {
50 error: Number.MAX_VALUE,
51 start: 0,
52 end: 0,
53 }
54 const epsilon = AVG_CODE_ERROR
55 // console.warn('* findPattern', pattern, offset, isWhite, tryHarder, epsilon);
56 let counterPos = 0
57 if (!offset) {
58 offset = this._nextSet(this._row)
59 }
60 let found = false
61 for (let i = offset; i < this._row.length; i++) {
62 // console.warn(`* loop i=${offset} len=${this._row.length} isWhite=${isWhite} counterPos=${counterPos}`);
63 if (this._row[i] ^ (isWhite ? 1 : 0)) {
64 counter[counterPos] += 1
65 }
66 else {
67 if (counterPos === counter.length - 1) {
68 const error = this._matchPattern(counter, pattern)
69 // console.warn('* matchPattern', error, counter, pattern);
70 if (error < epsilon && bestMatch.error && error < bestMatch.error) {
71 found = true
72 bestMatch.error = error
73 bestMatch.start = i - counter.reduce((sum, value) => sum + value, 0)
74 bestMatch.end = i
75 // console.warn('* return bestMatch', JSON.stringify(bestMatch));
76 return bestMatch
77 }
78 if (tryHarder) {
79 for (let j = 0; j < counter.length - 2; j++) {
80 counter[j] = counter[j + 2]
81 }
82 counter[counter.length - 2] = 0
83 counter[counter.length - 1] = 0
84 counterPos--
85 }
86 }
87 else {
88 counterPos++
89 }
90 counter[counterPos] = 1
91 isWhite = !isWhite
92 }
93 }
94 if (found) {
95 // console.warn('* return bestMatch', JSON.stringify(bestMatch));
96 }
97 else {
98 // console.warn('* return null');
99 }
100 return found ? bestMatch : null
101 }
102
103 // TODO: findPattern and decodeCode appear to share quite similar code, can it be reduced?
104 protected _decodeCode(start: number, coderange?: number): BarcodeInfo | null {
105 // console.warn('* decodeCode', start, coderange);
106 const counter = [0, 0, 0, 0]
107 const offset = start
108 const bestMatch: BarcodeInfo = {
109 error: Number.MAX_VALUE,
110 code: -1,
111 start,
112 end: start,
113 }
114 const epsilon = AVG_CODE_ERROR
115 let isWhite = !this._row[offset]
116 let counterPos = 0
117
118 if (!coderange) {
119 // console.warn('* decodeCode before length');
120 coderange = CODE_PATTERN.length
121 // console.warn('* decodeCode after length');
122 }
123
124 const found = false
125 for (let i = offset; i < this._row.length; i++) {
126 if (this._row[i] ^ (isWhite ? 1 : 0)) {
127 counter[counterPos]++
128 }
129 else {
130 if (counterPos === counter.length - 1) {
131 for (let code = 0; code < coderange; code++) {
132 const error = this._matchPattern(counter, CODE_PATTERN[code])
133 bestMatch.end = i
134 if (error < bestMatch.error!) {
135 bestMatch.code = code
136 bestMatch.error = error
137 }
138 }
139 if (bestMatch.error! > epsilon) {
140 // console.warn('* return null');
141 return null
142 }
143 // console.warn('* return bestMatch', JSON.stringify(bestMatch));
144 return bestMatch
145 }
146 else {
147 counterPos++
148 }
149 counter[counterPos] = 1
150 isWhite = !isWhite
151 }
152 }
153 return found ? bestMatch : null
154 }
155
156 protected _findStart(): BarcodePosition | null {
157 // console.warn('* findStart');
158 let offset = this._nextSet(this._row)
159 let startInfo: BarcodePosition | null = null
160
161 while (!startInfo) {
162 startInfo = this._findPattern(START_PATTERN, offset, false, true)
163 // console.warn('* startInfo=', JSON.stringify(startInfo));
164 if (!startInfo) {
165 return null
166 }
167
168 const leadingWhitespaceStart = startInfo.start - (startInfo.end - startInfo.start)
169
170 if (leadingWhitespaceStart >= 0) {
171 if (this._matchRange(leadingWhitespaceStart, startInfo.start, 0)) {
172 // console.warn('* returning startInfo');
173 return startInfo
174 }
175 }
176
177 offset = startInfo.end
178 startInfo = null
179 }
180 // console.warn('* returning null');
181 return null
182 }
183
184 private _calculateFirstDigit(codeFrequency: number): number | null {
185 // console.warn('* calculateFirstDigit', codeFrequency);
186 for (let i = 0; i < CODE_FREQUENCY.length; i++) {
187 if (codeFrequency === CODE_FREQUENCY[i]) {
188 // console.warn('* returning', i);
189 return i
190 }
191 }
192 // console.warn('* return null');
193 return null
194 }
195
196 protected _decodePayload(inCode: BarcodePosition, result: Array<number>, decodedCodes: Array<BarcodePosition>): BarcodeInfo | null {
197 // console.warn('* decodePayload', inCode, result, decodedCodes);
198 let outCode: BarcodeInfo | BarcodePosition | null = { ...inCode }
199 let codeFrequency = 0x0
200
201 for (let i = 0; i < 6; i++) {
202 outCode = this._decodeCode(outCode.end)
203 // console.warn('* decodeCode=', outCode);
204 if (!outCode) {
205 // console.warn('* return null');
206 return null
207 }
208 if ((outCode as BarcodeInfo).code >= CODE_G_START) {
209 (outCode as BarcodeInfo).code -= CODE_G_START
210 codeFrequency |= 1 << (5 - i)
211 }
212 else {
213 codeFrequency |= 0 << (5 - i)
214 }
215 result.push((outCode as BarcodeInfo).code)
216 decodedCodes.push(outCode)
217 }
218
219 const firstDigit = this._calculateFirstDigit(codeFrequency)
220 // console.warn('* firstDigit=', firstDigit);
221 if (firstDigit === null) {
222 // console.warn('* return null');
223 return null
224 }
225
226 result.unshift(firstDigit)
227
228 let middlePattern = this._findPattern(MIDDLE_PATTERN, outCode.end, true, false)
229 // console.warn('* findPattern=', JSON.stringify(middlePattern));
230
231 if (middlePattern === null || !middlePattern.end) {
232 // console.warn('* return null');
233 return null
234 }
235
236 decodedCodes.push(middlePattern)
237
238 for (let i = 0; i < 6; i++) {
239 middlePattern = this._decodeCode(middlePattern!.end, CODE_G_START)
240 // console.warn('* decodeCode=', JSON.stringify(middlePattern));
241
242 if (!middlePattern) {
243 // console.warn('* return null');
244 return null
245 }
246
247 decodedCodes.push(middlePattern)
248 result.push((middlePattern as BarcodeInfo).code)
249 }
250
251 // console.warn('* end code=', JSON.stringify(middlePattern));
252 // console.warn('* end result=', JSON.stringify(result));
253 // console.warn('* end decodedCodes=', decodedCodes);
254 return middlePattern as BarcodeInfo
255 }
256
257 protected _verifyTrailingWhitespace(endInfo: BarcodePosition): BarcodePosition | null {
258 // console.warn('* verifyTrailingWhitespace', JSON.stringify(endInfo));
259 const trailingWhitespaceEnd = endInfo.end + (endInfo.end - endInfo.start)
260
261 if (trailingWhitespaceEnd < this._row.length) {
262 if (this._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
263 // console.warn('* returning', JSON.stringify(endInfo));
264 return endInfo
265 }
266 }
267 // console.warn('* return null');
268 return null
269 }
270
271 protected _findEnd(offset: number, isWhite: boolean): BarcodePosition | null {
272 // console.warn('* findEnd', offset, isWhite);
273 const endInfo = this._findPattern(this.STOP_PATTERN, offset, isWhite, false)
274
275 return endInfo !== null ? this._verifyTrailingWhitespace(endInfo) : null
276 }
277
278 protected _checksum(result: Array<number>): boolean {
279 // console.warn('* _checksum', result);
280 let sum = 0
281
282 for (let i = result.length - 2; i >= 0; i -= 2) {
283 sum += result[i]
284 }
285
286 sum *= 3
287
288 for (let i = result.length - 1; i >= 0; i -= 2) {
289 sum += result[i]
290 }
291
292 // console.warn('* end checksum', sum % 10 === 0);
293 return sum % 10 === 0
294 }
295
296 private _decodeExtensions(offset: number): Barcode | null {
297 const start = this._nextSet(this._row, offset)
298 const startInfo = this._findPattern(EXTENSION_START_PATTERN, start, false, false)
299
300 if (startInfo === null) {
301 return null
302 }
303
304 // console.warn('* decodeExtensions', this.supplements);
305 // console.warn('* there are ', this.supplements.length, ' supplements');
306 for (let i = 0; i < this.supplements.length; i++) {
307 // console.warn('* extensions loop', i, this.supplements[i], this.supplements[i]._decode);
308 try {
309 const result = this.supplements[i].decode(this._row, startInfo.end)
310 // console.warn('* decode result=', result);
311 if (result !== null) {
312 return {
313 code: result.code,
314 start,
315 startInfo,
316 end: result.end,
317 decodedCodes: result.decodedCodes,
318 format: this.supplements[i].FORMAT,
319 }
320 }
321 }
322 catch (err) {
323 console.error('* decodeExtensions error in ', this.supplements[i], ': ', err)
324 }
325 }
326
327 // console.warn('* end decodeExtensions');
328 return null
329 }
330
331 public decode(row?: Array<number>, start?: BarcodePosition | number): Barcode | null {
332 // console.warn('* decode', row);
333 // console.warn('* decode', start);
334 const result = new Array<number>()
335 const decodedCodes = new Array<BarcodeInfo | BarcodePosition>()
336 let resultInfo: Barcode | {} = {}
337 const startInfo = this._findStart()
338
339 if (!startInfo) {
340 return null
341 }
342
343 let code: BarcodePosition | BarcodeInfo | null = {
344 start: startInfo.start,
345 end: startInfo.end,
346 }
347 decodedCodes.push(code)
348
349 code = this._decodePayload(code, result, decodedCodes)
350
351 if (!code) {
352 return null
353 }
354
355 code = this._findEnd(code.end, false)
356
357 if (!code) {
358 return null
359 }
360
361 decodedCodes.push(code)
362
363 // Checksum
364 if (!this._checksum(result)) {
365 return null
366 }
367
368 // console.warn('* this.supplements=', this.supplements);
369 if (this.supplements.length > 0) {
370 const supplement = this._decodeExtensions(code.end)
371 // console.warn('* decodeExtensions returns', supplement);
372 if (!supplement) {
373 return null
374 }
375
376 if (!supplement.decodedCodes) {
377 return null
378 }
379
380 const lastCode = supplement.decodedCodes[supplement.decodedCodes.length - 1] as BarcodeInfo
381 const endInfo = {
382 start: lastCode.start + (((lastCode.end - lastCode.start) / 2) | 0),
383 end: lastCode.end,
384 }
385
386 if (!this._verifyTrailingWhitespace(endInfo)) {
387 return null
388 }
389
390 resultInfo = {
391 supplement,
392 code: result.join('') + supplement.code,
393 }
394 }
395
396 return {
397 code: result.join(''),
398 start: startInfo.start,
399 end: code.end,
400 startInfo,
401 decodedCodes,
402 ...resultInfo,
403 format: this.FORMAT,
404 }
405 }
406}
407
408export default EANReader
packages/qrx/src/quagga/reader/i2of5_reader.tsadded+277-0
Changes to packages/qrx/src/quagga/reader/i2of5_reader.ts
@@ -0,0 +1,277 @@
1// TODO: i2of5_reader and 2of5_reader share very similar code, make use of that
2
3import type { Barcode, BarcodeInfo, BarcodePosition, BarcodeReaderConfig } from './barcode_reader'
4import merge from 'lodash/merge'
5import BarcodeReader from './barcode_reader'
6
7const N = 1
8const W = 3
9
10class I2of5Reader extends BarcodeReader {
11 private barSpaceRatio = [1, 1]
12 SINGLE_CODE_ERROR = 0.78
13 AVG_CODE_ERROR = 0.38
14
15 START_PATTERN = [N, N, N, N]
16 STOP_PATTERN = [N, N, W]
17 CODE_PATTERN = [
18 [N, N, W, W, N],
19 [W, N, N, N, W],
20 [N, W, N, N, W],
21 [W, W, N, N, N],
22 [N, N, W, N, W],
23 [W, N, W, N, N],
24 [N, W, W, N, N],
25 [N, N, N, W, W],
26 [W, N, N, W, N],
27 [N, W, N, W, N],
28 ]
29
30 MAX_CORRECTION_FACTOR = 5
31 FORMAT = 'i2of5'
32
33 constructor(opts: BarcodeReaderConfig) {
34 super(merge({ normalizeBarSpaceWidth: false }, opts))
35 if (opts.normalizeBarSpaceWidth) {
36 this.SINGLE_CODE_ERROR = 0.38
37 this.AVG_CODE_ERROR = 0.09
38 }
39 this.config = opts
40 return this
41 }
42
43 protected _matchPattern(counter: Array<number>, code: ReadonlyArray<number>): number {
44 if (this.config.normalizeBarSpaceWidth) {
45 const counterSum = [0, 0]
46 const codeSum = [0, 0]
47 const correction = [0, 0]
48 const correctionRatio = this.MAX_CORRECTION_FACTOR
49 const correctionRatioInverse = 1 / correctionRatio
50
51 for (let i = 0; i < counter.length; i++) {
52 counterSum[i % 2] += counter[i]
53 codeSum[i % 2] += code[i]
54 }
55 correction[0] = codeSum[0] / counterSum[0]
56 correction[1] = codeSum[1] / counterSum[1]
57
58 correction[0] = Math.max(Math.min(correction[0], correctionRatio), correctionRatioInverse)
59 correction[1] = Math.max(Math.min(correction[1], correctionRatio), correctionRatioInverse)
60 this.barSpaceRatio = correction
61 for (let i = 0; i < counter.length; i++) {
62 counter[i] *= this.barSpaceRatio[i % 2]
63 }
64 }
65 return super._matchPattern(counter, code)
66 };
67
68 protected _findPattern(pattern: ReadonlyArray<number>, offset?: number, isWhite: boolean = false, tryHarder: boolean = false): BarcodePosition | null {
69 const counter = Array.from({ length: pattern.length }).fill(0)
70 let counterPos = 0
71 const bestMatch: BarcodePosition = {
72 error: Number.MAX_VALUE,
73 start: 0,
74 end: 0,
75 }
76
77 const epsilon = this.AVG_CODE_ERROR
78
79 isWhite = isWhite || false
80 tryHarder = tryHarder || false
81
82 if (!offset) {
83 offset = this._nextSet(this._row)
84 }
85
86 for (let i = offset; i < this._row.length; i++) {
87 if (this._row[i] ^ (isWhite ? 1 : 0)) {
88 counter[counterPos]++
89 }
90 else {
91 if (counterPos === counter.length - 1) {
92 const sum = counter.reduce((prev, next) => prev + next, 0)
93 const error = this._matchPattern(counter, pattern)
94 if (error < epsilon) {
95 bestMatch.error = error
96 bestMatch.start = i - sum
97 bestMatch.end = i
98 return bestMatch
99 }
100 if (tryHarder) {
101 for (let j = 0; j < counter.length - 2; j++) {
102 counter[j] = counter[j + 2]
103 }
104 counter[counter.length - 2] = 0
105 counter[counter.length - 1] = 0
106 counterPos--
107 }
108 else {
109 return null
110 }
111 }
112 else {
113 counterPos++
114 }
115 counter[counterPos] = 1
116 isWhite = !isWhite
117 }
118 }
119 return null
120 };
121
122 protected _findStart(): BarcodePosition | null {
123 let leadingWhitespaceStart = 0
124 let offset = this._nextSet(this._row)
125 let startInfo: BarcodePosition | null = null
126 let narrowBarWidth = 1
127
128 while (!startInfo) {
129 startInfo = this._findPattern(this.START_PATTERN, offset, false, true)
130 if (!startInfo) {
131 return null
132 }
133 narrowBarWidth = Math.floor((startInfo.end - startInfo.start) / 4)
134 leadingWhitespaceStart = startInfo.start - narrowBarWidth * 10
135 if (leadingWhitespaceStart >= 0) {
136 if (this._matchRange(leadingWhitespaceStart, startInfo.start, 0)) {
137 return startInfo
138 }
139 }
140 offset = startInfo.end
141 startInfo = null
142 }
143 return null
144 };
145
146 protected _verifyTrailingWhitespace(endInfo: BarcodePosition): BarcodePosition | null {
147 const trailingWhitespaceEnd = endInfo.end + ((endInfo.end - endInfo.start) / 2)
148 if (trailingWhitespaceEnd < this._row.length) {
149 if (this._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
150 return endInfo
151 }
152 }
153 return null
154 };
155
156 protected _findEnd(): BarcodePosition | null {
157 this._row.reverse()
158 const endInfo = this._findPattern(this.STOP_PATTERN)
159 this._row.reverse()
160
161 if (endInfo === null) {
162 return null
163 }
164
165 // reverse numbers
166 const tmp = endInfo.start
167 endInfo.start = this._row.length - endInfo.end
168 endInfo.end = this._row.length - tmp
169
170 return endInfo !== null ? this._verifyTrailingWhitespace(endInfo) : null
171 };
172
173 protected _decodePair(counterPair: Array<Array<number>>): Array<BarcodeInfo> | null {
174 const codes: Array<BarcodeInfo> = []
175
176 for (let i = 0; i < counterPair.length; i++) {
177 const code = this._decodeCode(counterPair[i])
178 if (!code) {
179 return null
180 }
181 codes.push(code)
182 }
183 return codes
184 };
185
186 protected _decodeCode(counter: Array<number>): BarcodeInfo | null {
187 const epsilon = this.AVG_CODE_ERROR
188
189 const bestMatch: BarcodeInfo = {
190 error: Number.MAX_VALUE,
191 code: -1,
192 start: 0,
193 end: 0,
194 }
195
196 for (let code = 0; code < this.CODE_PATTERN.length; code++) {
197 const error = this._matchPattern(counter, this.CODE_PATTERN[code])
198 if (error < bestMatch.error!) {
199 bestMatch.code = code
200 bestMatch.error = error
201 }
202 }
203 if (bestMatch.error! < epsilon) {
204 return bestMatch
205 }
206 return null
207 };
208
209 protected _decodePayload(counters: ReadonlyArray<number>, result: Array<string>, decodedCodes: Array<BarcodeInfo | BarcodePosition>): Array<BarcodeInfo> | null {
210 let pos = 0
211 const counterLength = counters.length
212 const counterPair = [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]
213 let codes: BarcodeInfo[] | null = null
214
215 while (pos < counterLength) {
216 for (let i = 0; i < 5; i++) {
217 counterPair[0][i] = counters[pos] * this.barSpaceRatio[0]
218 counterPair[1][i] = counters[pos + 1] * this.barSpaceRatio[1]
219 pos += 2
220 }
221 codes = this._decodePair(counterPair)
222 if (!codes) {
223 return null
224 }
225 for (let i = 0; i < codes.length; i++) {
226 result.push(`${codes[i].code}`)
227 decodedCodes.push(codes[i])
228 }
229 }
230 return codes
231 };
232
233 protected _verifyCounterLength(counters: Array<number>) {
234 return (counters.length % 10 === 0)
235 };
236
237 public decode(row?: Array<number>, start?: BarcodePosition | number): Barcode | null {
238 const result = new Array<string>()
239 const decodedCodes = new Array<BarcodePosition>()
240
241 const startInfo = this._findStart()
242 if (!startInfo) {
243 return null
244 }
245 decodedCodes.push(startInfo)
246
247 const endInfo = this._findEnd()
248 if (!endInfo) {
249 return null
250 }
251
252 const counters = this._fillCounters(startInfo.end, endInfo.start, false)
253 if (!this._verifyCounterLength(counters)) {
254 return null
255 }
256 const code = this._decodePayload(counters, result, decodedCodes)
257 if (!code) {
258 return null
259 }
260 if (result.length % 2 !== 0
261 || result.length < 6) {
262 return null
263 }
264
265 decodedCodes.push(endInfo)
266 return {
267 code: result.join(''),
268 start: startInfo.start,
269 end: endInfo.end,
270 startInfo,
271 decodedCodes,
272 format: this.FORMAT,
273 }
274 };
275}
276
277export default I2of5Reader
packages/qrx/src/quagga/reader/index.tsadded+33-0
Changes to packages/qrx/src/quagga/reader/index.ts
@@ -0,0 +1,33 @@
1import TwoOfFiveReader from './2of5_reader'
2import BarcodeReader from './barcode_reader'
3import NewCodabarReader from './codabar_reader'
4import Code32Reader from './code_32_reader'
5import Code39Reader from './code_39_reader'
6import Code39VINReader from './code_39_vin_reader'
7import Code93Reader from './code_93_reader'
8import Code128Reader from './code_128_reader'
9import EAN2Reader from './ean_2_reader'
10import EAN5Reader from './ean_5_reader'
11import EAN8Reader from './ean_8_reader'
12import EANReader from './ean_reader'
13import I2of5Reader from './i2of5_reader'
14import UPCEReader from './upc_e_reader'
15import UPCReader from './upc_reader'
16
17export {
18 BarcodeReader,
19 Code32Reader,
20 Code39Reader,
21 Code39VINReader,
22 Code93Reader,
23 Code128Reader,
24 EAN2Reader,
25 EAN5Reader,
26 EAN8Reader,
27 EANReader,
28 I2of5Reader,
29 NewCodabarReader,
30 TwoOfFiveReader,
31 UPCEReader,
32 UPCReader,
33}
packages/qrx/src/quagga/reader/upc_e_reader.tsadded+93-0
Changes to packages/qrx/src/quagga/reader/upc_e_reader.ts
@@ -0,0 +1,93 @@
1import type { BarcodeInfo, BarcodePosition } from './barcode_reader'
2import EANReader, { CODE_G_START } from './ean_reader'
3
4class UPCEReader extends EANReader {
5 CODE_FREQUENCY = [
6 [56, 52, 50, 49, 44, 38, 35, 42, 41, 37],
7 [7, 11, 13, 14, 19, 25, 28, 21, 22, 26],
8 ]
9
10 STOP_PATTERN = [1 / 6 * 7, 1 / 6 * 7, 1 / 6 * 7, 1 / 6 * 7, 1 / 6 * 7, 1 / 6 * 7]
11 FORMAT = 'upc_e'
12 protected _decodePayload(inCode: BarcodePosition, result: Array<number>, decodedCodes: Array<BarcodePosition>): BarcodeInfo | null {
13 let outCode: BarcodeInfo | BarcodePosition | null = { ...inCode }
14 let codeFrequency = 0x0
15
16 for (let i = 0; i < 6; i++) {
17 outCode = this._decodeCode(outCode.end)
18 if (!outCode) {
19 return null
20 }
21 if ((outCode as BarcodeInfo).code >= CODE_G_START) {
22 (outCode as BarcodeInfo).code = (outCode as BarcodeInfo).code - CODE_G_START
23 codeFrequency |= (1 << (5 - i))
24 }
25 result.push((outCode as BarcodeInfo).code)
26 decodedCodes.push(outCode)
27 }
28 if (!this._determineParity(codeFrequency, result)) {
29 return null
30 }
31 return outCode as BarcodeInfo
32 };
33
34 protected _determineParity(codeFrequency: number, result: Array<number>) {
35 for (let nrSystem = 0; nrSystem < this.CODE_FREQUENCY.length; nrSystem++) {
36 for (let i = 0; i < this.CODE_FREQUENCY[nrSystem].length; i++) {
37 if (codeFrequency === this.CODE_FREQUENCY[nrSystem][i]) {
38 result.unshift(nrSystem)
39 result.push(i)
40 return true
41 }
42 }
43 }
44 return false
45 };
46
47 protected _convertToUPCA(result: Array<number>) {
48 let upca = [result[0]]
49 const lastDigit = result[result.length - 2]
50
51 if (lastDigit <= 2) {
52 upca = upca.concat(result.slice(1, 3))
53 .concat([lastDigit, 0, 0, 0, 0])
54 .concat(result.slice(3, 6))
55 }
56 else if (lastDigit === 3) {
57 upca = upca.concat(result.slice(1, 4))
58 .concat([0, 0, 0, 0, 0])
59 .concat(result.slice(4, 6))
60 }
61 else if (lastDigit === 4) {
62 upca = upca.concat(result.slice(1, 5))
63 .concat([0, 0, 0, 0, 0, result[5]])
64 }
65 else {
66 upca = upca.concat(result.slice(1, 6))
67 .concat([0, 0, 0, 0, lastDigit])
68 }
69
70 upca.push(result[result.length - 1])
71 return upca
72 };
73
74 protected _checksum(result: Array<number>): boolean {
75 return super._checksum(this._convertToUPCA(result))
76 }
77
78 protected _findEnd(offset: number, isWhite: boolean): BarcodePosition | null {
79 return super._findEnd(offset, true)
80 }
81
82 protected _verifyTrailingWhitespace(endInfo: BarcodePosition): BarcodePosition | null {
83 const trailingWhitespaceEnd = endInfo.end + ((endInfo.end - endInfo.start) / 2)
84 if (trailingWhitespaceEnd < this._row.length) {
85 if (this._matchRange(endInfo.end, trailingWhitespaceEnd, 0)) {
86 return endInfo
87 }
88 }
89 return null
90 };
91}
92
93export default UPCEReader
packages/qrx/src/quagga/reader/upc_reader.tsadded+18-0
Changes to packages/qrx/src/quagga/reader/upc_reader.ts
@@ -0,0 +1,18 @@
1import type { Barcode, BarcodePosition } from './barcode_reader'
2import EANReader from './ean_reader'
3
4class UPCReader extends EANReader {
5 FORMAT = 'upc_a'
6 decode(row?: Array<number>, start?: BarcodePosition | number): Barcode | null {
7 const result = EANReader.prototype.decode.call(this)
8
9 if (result && result.code && result.code.length === 13 && result.code.charAt(0) === '0') {
10 result.code = result.code.substring(1)
11 return result
12 }
13
14 return null
15 }
16}
17
18export default UPCReader
packages/qrx/src/quagga/types.tsadded+1020-0
Changes to packages/qrx/src/quagga/types.ts
@@ -0,0 +1,1020 @@
1// Type definitions for QuaggaJS v0.12.1 (2017-10-19)
2// Project: http://serratus.github.io/quaggaJS/
3// Definitions by: Cam Birch, Peter Horwood aka Madman Pierre, Dan Manastireanu <https://github.com/danmana>
4
5import type { vec2 } from 'gl-matrix'
6
7// import SubImage from '../src/common/subImage';
8// import ImageWrapper from '../src/common/image_wrapper';
9// export { SubImage, ImageWrapper };
10
11declare const Quagga: QuaggaJSStatic
12export default Quagga
13
14// There are many different spots inside Quagga where we refer to an X/Y position of something, but it has entirely different
15// contextual meaning. This allows us to create a type that is branded by name, and therefore these variables cannot be directly
16// mixed up with each other, without explicitly forcing it to happen. Good.
17export interface XYObject<T extends string> {
18 type: T
19 x: number
20 y: number
21}
22
23// TODO: fill this in from cv_utils#imageRef
24export type ImageRef = XYObject<'ImageRef'>
25
26export type XYSize = XYObject<'XYSize'>
27
28export type Point = XYObject<'Point'>
29
30export interface SparseImageWrapper {
31 data: TypedArray | Array<number> | null
32 size: XYSize
33}
34
35export interface WrapperIndexMapping {
36 x: Array<number>
37 y: Array<number>
38}
39
40// eslint-disable-next-line @typescript-eslint/class-name-casing
41export interface Moment {
42 m00: number
43 m01: number
44 m02: number
45 m10: number
46 m11: number
47 m20: number
48 rad: number
49 theta: number
50 vec?: vec2
51}
52
53export class ImageWrapper {
54 data: TypedArray | Array<number>
55
56 size: XYSize
57
58 indexMapping?: WrapperIndexMapping
59
60 constructor(
61 size: XYSize,
62 data?: TypedArray | Array<number>,
63 ArrayType?: TypedArrayConstructor | ArrayConstructor,
64 initialize?: boolean
65 )
66
67 inImageWithBorder(imgRef: ImageRef, border: number): boolean
68
69 subImageAsCopy(imageWrapper: ImageWrapper, from: XYSize): ImageWrapper
70
71 get(x: number, y: number): number
72
73 getSafe(x: number, y: number): number
74
75 set(x: number, y: number, value: number): ImageWrapper
76
77 zeroBorder(): ImageWrapper
78
79 moments(labelCount: any): Array<Moment>
80
81 getAsRGBA(scale?: number): Uint8ClampedArray
82
83 show(canvas: HTMLCanvasElement, scale?: number): void
84
85 overlay(canvas: HTMLCanvasElement, scale: number, from: XYSize): void
86}
87
88export class SubImage {
89 I: ImageWrapper | SparseImageWrapper
90
91 data: ImageWrapper['data']
92
93 originalSize: ImageRef
94
95 from: ImageRef
96
97 size: ImageRef
98
99 constructor(from: ImageRef, size: ImageRef, I: SparseImageWrapper)
100
101 get(x: number, y: number): number
102
103 show(canvas: HTMLCanvasElement, scale: number): void
104
105 updateData(image: ImageWrapper): void
106
107 updateFrom(from: ImageRef): void
108}
109
110export type QuaggaImageData = Array<number>
111
112export type BarcodeReaderType = string
113
114export interface BarcodeReaderConfig {
115 normalizeBarSpaceWidth?: boolean
116 supplements?: Array<BarcodeReaderType>
117}
118
119export enum BarcodeDirection {
120 Forward = 1,
121 Reverse = -1,
122}
123type BarcodeFormat = string
124
125export interface BarcodeCorrection {
126 bar: number
127 space: number
128}
129
130export interface BarcodePosition {
131 end: number
132 endCounter?: number
133 error?: number
134 start: number
135 startCounter?: number
136}
137
138export interface BarcodeInfo extends BarcodePosition {
139 code: number
140 correction?: BarcodeCorrection
141}
142
143export interface Barcode {
144 code: string
145 codeset?: number
146 correction?: BarcodeCorrection
147 decodedCodes?: Array<string | BarcodeInfo | BarcodePosition>
148 direction?: BarcodeDirection
149 end: number
150 endInfo?: BarcodePosition
151 format: BarcodeFormat
152 start: number
153 startInfo: BarcodePosition
154 supplement?: Barcode
155}
156
157export interface ThresholdSize {
158 counts: number
159 max: number
160 min: number
161 size: number
162}
163
164export interface Threshold {
165 bar: {
166 narrow: ThresholdSize
167 wide: ThresholdSize
168 }
169 space: {
170 narrow: ThresholdSize
171 wide: ThresholdSize
172 }
173}
174
175export declare namespace Readers {
176 export abstract class BarcodeReader {
177 _row: Array<number>
178
179 SINGLE_CODE_ERROR: number
180
181 FORMAT: BarcodeFormat
182
183 CONFIG_KEYS: BarcodeReaderConfig
184
185 static get Exception(): {
186 CodeNotFoundException: string
187 PatternNotFoundException: string
188 StartNotFoundException: string
189 }
190
191 constructor(config: BarcodeReaderConfig, supplements?: Array<BarcodeReader>)
192
193 abstract decode(row?: Array<number>, start?: BarcodePosition | number): Barcode | null
194
195 decodePattern(pattern: Array<number>): Barcode | null
196
197 protected _nextUnset(line: ReadonlyArray<number>, start?: number): number
198
199 protected _matchPattern(counter: ReadonlyArray<number>, code: ReadonlyArray<number>, maxSingleError?: number): number
200
201 protected _nextSet(line: ReadonlyArray<number>, offset?: number): number
202
203 protected _correctBars(counter: Array<number>, correction: number, indices: Array<number>): void
204
205 protected _matchRange(start: number, end: number, value: number): boolean
206
207 protected _fillCounters(offset?: number, end?: number, isWhite?: boolean): number[]
208
209 protected _toCounters(start: number, counters: Uint16Array | Array<number>): number[] | Uint16Array
210 }
211
212 export class TwoOfFiveReader extends BarcodeReader {
213 FORMAT: string
214
215 SINGLE_CODE_ERROR: number
216
217 AVG_CODE_ERROR: number
218
219 decode(row?: Array<number>, start?: BarcodePosition): Barcode | null
220
221 protected _findPattern(pattern: ReadonlyArray<number>, offset: number, isWhite?: boolean, tryHarder?: boolean): BarcodeInfo | null
222
223 protected _findStart(): BarcodePosition | null
224
225 protected _verifyTrailingWhitespace(endInfo: BarcodeInfo): BarcodePosition | null
226
227 protected _findEnd(): BarcodePosition | null
228
229 protected _verifyCounterLength(counters: Array<number>): boolean
230
231 protected _decodeCode(counter: ReadonlyArray<number>): BarcodeInfo | null
232
233 protected _decodePayload(counters: ReadonlyArray<number>, result: Array<string>, decodedCodes: Array<BarcodeInfo | BarcodePosition>): BarcodeInfo | null
234 }
235
236 export class NewCodabarReader extends BarcodeReader {
237 FORMAT: string
238
239 decode(row?: Array<number>, start?: BarcodePosition | number | null): Barcode | null
240
241 protected _computeAlternatingThreshold(offset: number, end: number): number
242
243 protected _toPattern(offset: number): number
244
245 protected _isStartEnd(pattern: number): boolean
246
247 protected _sumCounters(start: number, end: number): number
248
249 protected _findStart(): BarcodePosition | null
250
251 protected _patternToChar(pattern: number): string | null
252
253 protected _calculatePatternLength(offset: number): number
254
255 protected _verifyWhitespace(startCounter: number, endCounter: number): boolean
256
257 protected _charToPattern(char: string): number
258
259 protected _thresholdResultPattern(result: ReadonlyArray<string>, startCounter: number): Threshold
260
261 protected _validateResult(result: ReadonlyArray<string>, startCounter: number): boolean
262 }
263
264 export class Code128Reader extends BarcodeReader {
265 CODE_SHIFT: number
266
267 CODE_C: number
268
269 CODE_B: number
270
271 CODE_A: number
272
273 START_CODE_A: number
274
275 START_CODE_B: number
276
277 START_CODE_C: number
278
279 STOP_CODE: number
280
281 CODE_PATTERN: number[][]
282
283 SINGLE_CODE_ERROR: number
284
285 AVG_CODE_ERROR: number
286
287 FORMAT: string
288
289 MODULE_INDICES: {
290 bar: number[]
291 space: number[]
292 }
293
294 decode(row?: Array<number>, start?: BarcodePosition): Barcode | null
295
296 calculateCorrection(expected: ReadonlyArray<number>, normalized: ReadonlyArray<number>, indices: ReadonlyArray<number>): number
297
298 protected _decodeCode(start: number, correction?: BarcodeCorrection): BarcodeInfo | null
299
300 protected _correct(counter: Array<number>, correction: BarcodeCorrection): void
301
302 protected _findStart(): BarcodeInfo | null
303
304 protected _verifyTrailingWhitespace(endInfo: BarcodeInfo): BarcodeInfo | null
305 }
306
307 export class Code39Reader extends BarcodeReader {
308 FORMAT: string
309
310 decode(row?: Array<number>, start?: BarcodePosition | number | null): Barcode | null
311
312 protected _findStart(): BarcodePosition | null
313
314 protected _toPattern(counters: Uint16Array): number
315
316 protected _findNextWidth(counters: Uint16Array, current: number): number
317
318 protected _patternToChar(pattern: number): string | null
319
320 protected _verifyTrailingWhitespace(lastStart: number, nextStart: number, counters: Uint16Array): boolean
321 }
322
323 export class Code32Reader extends Code39Reader {
324 FORMAT: string
325
326 decode(row?: Array<number>, start?: BarcodePosition): Barcode | null
327
328 protected _decodeCode32(code: string): string | null
329
330 protected _checkChecksum(code: string): boolean
331 }
332
333 export class Code39VINReader extends Code39Reader {
334 FORMAT: string
335
336 decode(row?: Array<number>, start?: BarcodePosition): Barcode | null
337
338 protected _checkChecksum(code: string): boolean
339 }
340
341 export class Code93Reader extends BarcodeReader {
342 FORMAT: string
343
344 decode(row?: Array<number>, start?: BarcodePosition | number | null): Barcode | null
345
346 protected _patternToChar(pattern: number): string | null
347
348 protected _toPattern(counters: Uint16Array): number
349
350 protected _findStart(): BarcodePosition | null
351
352 protected _verifyEnd(lastStart: number, nextStart: number): boolean
353
354 protected _decodeExtended(charArray: Array<string>): string[] | null
355
356 protected _matchCheckChar(charArray: Array<string>, index: number, maxWeight: number): boolean
357
358 protected _verifyChecksums(charArray: Array<string>): boolean
359 }
360
361 export class EANReader extends BarcodeReader {
362 FORMAT: string
363 SINGLE_CODE_ERROR: number
364 STOP_PATTERN: number[]
365
366 constructor(config?: BarcodeReaderConfig, supplements?: Array<BarcodeReader>)
367
368 decode(row?: Array<number>, start?: BarcodePosition | number): Barcode | null
369
370 protected _findPattern(pattern: ReadonlyArray<number>, offset: number, isWhite: boolean, tryHarder: boolean): BarcodePosition | null
371
372 protected _decodeCode(start: number, coderange?: number): BarcodeInfo | null
373
374 protected _findStart(): BarcodePosition | null
375
376 protected _decodePayload(inCode: BarcodePosition, result: Array<number>, decodedCodes: Array<BarcodePosition>): BarcodeInfo | null
377
378 protected _verifyTrailingWhitespace(endInfo: BarcodePosition): BarcodePosition | null
379
380 protected _findEnd(offset: number, isWhite: boolean): BarcodePosition | null
381
382 protected _checksum(result: Array<number>): boolean
383 }
384
385 export class EAN2Reader extends EANReader {
386 FORMAT: string
387
388 decode(row?: Array<number>, start?: number): Barcode | null
389 }
390
391 export class EAN5Reader extends EANReader {
392 FORMAT: string
393
394 decode(row?: Array<number>, start?: number): Barcode | null
395 }
396
397 export class EAN8Reader extends EANReader {
398 FORMAT: string
399
400 protected _decodePayload(inCode: BarcodePosition, result: Array<number>, decodedCodes: Array<BarcodePosition>): BarcodeInfo | null
401 }
402
403 export class I2of5Reader extends BarcodeReader {
404 SINGLE_CODE_ERROR: number
405
406 AVG_CODE_ERROR: number
407
408 START_PATTERN: number[]
409
410 STOP_PATTERN: number[]
411
412 CODE_PATTERN: number[][]
413
414 MAX_CORRECTION_FACTOR: number
415
416 FORMAT: string
417
418 constructor(opts: BarcodeReaderConfig)
419
420 decode(row?: Array<number>, start?: BarcodePosition | number): Barcode | null
421
422 protected _matchPattern(counter: Array<number>, code: ReadonlyArray<number>): number
423
424 protected _findPattern(pattern: ReadonlyArray<number>, offset?: number, isWhite?: boolean, tryHarder?: boolean): BarcodePosition | null
425
426 protected _findStart(): BarcodePosition | null
427
428 protected _verifyTrailingWhitespace(endInfo: BarcodePosition): BarcodePosition | null
429
430 protected _findEnd(): BarcodePosition | null
431
432 protected _decodePair(counterPair: Array<Array<number>>): Array<BarcodeInfo> | null
433
434 protected _decodeCode(counter: Array<number>): BarcodeInfo | null
435
436 protected _decodePayload(counters: ReadonlyArray<number>, result: Array<string>, decodedCodes: Array<BarcodeInfo | BarcodePosition>): Array<BarcodeInfo> | null
437
438 protected _verifyCounterLength(counters: Array<number>): boolean
439 }
440
441 export class UPCEReader extends EANReader {
442 CODE_FREQUENCY: number[][]
443
444 STOP_PATTERN: number[]
445
446 FORMAT: string
447
448 protected _decodePayload(inCode: BarcodePosition, result: Array<number>, decodedCodes: Array<BarcodePosition>): BarcodeInfo | null
449
450 protected _determineParity(codeFrequency: number, result: Array<number>): boolean
451
452 protected _convertToUPCA(result: Array<number>): number[]
453
454 protected _checksum(result: Array<number>): boolean
455
456 protected _findEnd(offset: number, isWhite: boolean): BarcodePosition | null
457
458 protected _verifyTrailingWhitespace(endInfo: BarcodePosition): BarcodePosition | null
459 }
460
461 export class UPCReader extends EANReader {
462 FORMAT: string
463
464 decode(row?: Array<number>, start?: BarcodePosition | number): Barcode | null
465 }
466
467}
468
469export interface QuaggaJSStatic {
470 /**
471 * This method initializes the library for a given
472 * configuration config (see below) and invokes the callback when Quagga is
473 * ready to start. The initialization process also requests for camera
474 * access if real-time detection is configured.
475 */
476 init: ((
477 config: QuaggaJSConfigObject,
478 callback?: (err: any) => void
479 ) => Promise<void>) & ((
480 config: QuaggaJSConfigObject,
481 callback: (err: any) => void,
482 imageWrapper: ImageWrapper,
483 ) => Promise<void>)
484
485 /**
486 * When the library is initialized, the start()
487 * method starts the video-stream and begins locating and decoding the
488 * images.
489 */
490 start: () => void
491
492 /**
493 * If the decoder is currently running, after calling
494 * stop() the decoder does not process any more images.
495 * Additionally, if a camera-stream was requested upon initialization,
496 * this operation also disconnects the camera.
497 */
498 stop: () => Promise<void>
499
500 /**
501 * Pauses processing, but does not release any handlers
502 */
503 pause: () => void
504
505 /**
506 * This method registers a callback(data) function that is
507 * called for each frame after the processing is done. The data object
508 * contains detailed information about the success/failure of the operation.
509 * The output varies, depending whether the detection and/or decoding were
510 * successful or not.
511 */
512 onProcessed: (callback: QuaggaJSResultCallbackFunction) => void
513
514 /**
515 * Removes a callback that was previously registered with @see onProcessed
516 */
517 offProcessed: (callback?: QuaggaJSResultCallbackFunction) => void
518
519 /**
520 * Registers a callback(data) function which is triggered whenever a
521 * barcode- pattern has been located and decoded successfully. The passed
522 * data object contains information about the decoding process including the
523 * detected code which can be obtained by calling data.codeResult.code.
524 */
525 onDetected: (callback: QuaggaJSResultCallbackFunction) => void
526
527 /**
528 * Removes a callback that was previously registered with @see onDetected
529 */
530 offDetected: (callback?: QuaggaJSResultCallbackFunction) => void
531
532 ResultCollector: QuaggaJSResultCollector
533 registerResultCollector: (resultCollector: QuaggaJSResultCollector) => void
534 setReaders: (readers: (QuaggaJSReaderConfig | string)[]) => void
535 registerReader: (name: string, reader: object) => void
536
537 /**
538 * In contrast to the calls described
539 * above, this method does not rely on getUserMedia and operates on a single
540 * image instead. The provided callback is the same as in onDetected and
541 * contains the result data object.
542 */
543 decodeSingle: (
544 config: QuaggaJSConfigObject,
545 resultCallback?: QuaggaJSResultCallbackFunction
546 ) => Promise<QuaggaJSResultObject>
547
548 /**
549 * Constructs used for debugging purposes
550 */
551 ImageDebug: {
552 drawPath: QuaggaJSDebugDrawPath
553 drawRect: QuaggaJSDebugDrawRect
554 }
555 ImageWrapper: ImageWrapper
556
557 /**
558 * an object Quagga uses for drawing and processing, useful for calling code
559 * when debugging
560 */
561 canvas: {
562 ctx: {
563 image: CanvasRenderingContext2D
564 overlay: CanvasRenderingContext2D
565 }
566 dom: {
567 image: HTMLCanvasElement
568 overlay: HTMLCanvasElement
569 }
570 }
571
572 CameraAccess: QuaggaJSCameraAccess
573}
574
575/**
576 * Used for accessing information about the active stream track and available video devices.
577 */
578export interface QuaggaJSCameraAccess {
579 disableTorch: () => Promise<void>
580 enableTorch: () => Promise<void>
581 enumerateVideoDevices: () => Promise<MediaDeviceInfo[]> | never
582 getActiveStreamLabel: () => string
583 getActiveTrack: () => MediaStreamTrack | null
584 release: () => Promise<void>
585 request: (
586 video: HTMLVideoElement | null,
587 videoConstraints?: MediaTrackConstraintsWithDeprecated
588 ) => Promise<void> | never
589 requestedVideoElement: HTMLVideoElement | null
590}
591
592/**
593 * Called whenever an item is detected or a process step has been completed.
594 */
595export interface QuaggaJSResultCallbackFunction {
596 (
597 data: QuaggaJSResultObject
598 ): void
599}
600
601/**
602 * Called to draw debugging path. The path is an array of array of 2 numbers.
603 * The def.x specifies element in the sub array is the x, and similarly the def.y
604 * defines the y.
605 * typical values 0, 1, 'x', 'y'
606 */
607export interface QuaggaJSDebugDrawPath {
608 (
609 path: any[],
610 def: QuaggaJSxyDef,
611 ctx: CanvasRenderingContext2D,
612 style: QuaggaJSStyle
613 ): void
614}
615
616/**
617 * Called to draw debugging Rectangle
618 */
619export interface QuaggaJSDebugDrawRect {
620 (
621 pos: any[],
622 size: QuaggaJSRectSize,
623 ctx: CanvasRenderingContext2D,
624 style: QuaggaJSStyle
625 ): void
626}
627
628/**
629 * an object with an x and a y value, the x and y specify which element in
630 * another array is the x or y value.
631 * typical values 0, 1, 'x', 'y'
632 */
633// TODO: remove QuaggaJSxyDef from global type-defs (it's only used in image_debug, i think, which has a local definition)
634export interface QuaggaJSxyDef {
635 x: any
636 y: any
637}
638
639/**
640 * an object with an x and a y value
641 */
642export interface QuaggaJSxy {
643 x: number
644 y: number
645}
646
647/**
648 * an object with a pair of x and a y values.
649 * Used for giving a html canvas context.strokeRect function it's x, y, width
650 * and height values.
651 */
652export interface QuaggaJSRectSize {
653 pos: QuaggaJSxy
654 size: QuaggaJSxy
655}
656
657/**
658 * an object with the styles, color can actually be a color, a gradient or a
659 * pattern (see definitions for context.strokeStyle. But is most commonly a
660 * colour.
661 */
662export interface QuaggaJSStyle {
663 color: string
664
665 /* http://www.w3schools.com/tags/canvas_linewidth.asp */
666 lineWidth: number
667}
668
669/**
670 * Pass when creating a ResultCollector
671 */
672export interface QuaggaJSResultCollector {
673 /**
674 * keep track of the image producing this result
675 */
676 capture?: boolean
677
678 /**
679 * maximum number of results to store
680 */
681 capacity?: number
682
683 /**
684 * a list of codes that should not be recorded. This is effectively a list
685 * of filters that return false.
686 */
687 blacklist?: Array<QuaggaJSCodeResult>
688
689 /**
690 * passed a QuaggaJSCodeResult, return true if you want this to be stored,
691 * false if you don't. Note: The black list is effectively a list of filters
692 * that return false. So if you only want to store results that are ean_13,
693 * you would say return codeResult.format==="ean_13"
694 */
695 filter?: QuaggaJSResultCollectorFilterFunction
696
697 /*
698 * a static function that returns you a ResultCollector
699 */
700 create?: (param: QuaggaJSResultCollector) => QuaggaJSResultCollector
701
702 getResults?: () => QuaggaJSCodeResult[]
703 willReadFrequently?: boolean
704}
705
706/**
707 * used for ResultCollector blacklists and filters
708 */
709export interface QuaggaJSCodeResult {
710 code?: string
711 format?: string
712}
713
714/**
715 * Called to filter which Results to collect in ResultCollector
716 */
717export interface QuaggaJSResultCollectorFilterFunction {
718 (
719 data: QuaggaJSCodeResult
720 ): boolean
721}
722
723/**
724 * The callbacks passed into onProcessed, onDetected and decodeSingle receive a
725 * data object upon execution. The data object contains the following
726 * information. Depending on the success, some fields may be undefined or just
727 * empty.
728 */
729export interface QuaggaJSResultObject {
730 // eslint-disable-next-line @typescript-eslint/camelcase
731 codeResult: QuaggaJSResultObject_CodeResult
732 barcodes?: Array<QuaggaJSResultObject>
733 line: {
734 x: number
735 y: number
736 }[]
737 angle: number
738 pattern: number[]
739 box: number[][]
740 boxes: number[][][]
741 frame?: string
742}
743
744// eslint-disable-next-line @typescript-eslint/camelcase,@typescript-eslint/class-name-casing
745export interface QuaggaJSResultObject_CodeResult {
746 code: string | null
747 start: number
748 end: number
749 codeset: number
750 startInfo: {
751 error: number
752 code: number
753 start: number
754 end: number
755 }
756 decodedCodes: {
757 error?: number
758 code: number
759 start: number
760 end: number
761 }[]
762
763 endInfo: {
764 error: number
765 code: number
766 start: number
767 end: number
768 }
769 direction: number
770 format: string
771}
772
773export type InputStreamType = 'VideoStream' | 'ImageStream' | 'LiveStream'
774
775export interface QuaggaJSConfigObject {
776 /**
777 * The image path to load from, or a data url
778 * Ex: '/test/fixtures/code_128/image-001.jpg'
779 * or: 'data:image/jpg;base64,' + data
780 */
781 src?: string | Uint8Array
782
783 inputStream?: {
784 /**
785 * @default "Live"
786 */
787 name?: string
788
789 /**
790 * @default "LiveStream"
791 */
792 type?: InputStreamType
793
794 /**
795 * Use canvas.getContext('2d', { willReadFrequently: true }) for browser frame grabber operations
796 * @default false
797 * ... defaulting false because historically this wasn't an option, so i don't want to change behavior
798 */
799 willReadFrequently?: boolean
800
801 target?: Element | string
802
803 constraints?: MediaTrackConstraints
804
805 /**
806 * defines rectangle of the detection/localization area. Useful when you
807 * KNOW that certain parts of the image will not contain a barcode, also
808 * useful when you have multiple barcodes in a row and you want to make
809 * sure that only a code in, say the middle quarter is read not codes
810 * above or below
811 */
812 area?: {
813 /**
814 * @default "0%", set this and bottom to 25% if you only want to
815 * read a 'line' that is in the middle quarter
816 */
817 top?: string
818
819 /**
820 * @default "0%"
821 */
822 right?: string
823
824 /**
825 * @default "0%"
826 */
827 left?: string
828
829 /**
830 * @default "0%", set this and top to 50% if you only want to read a
831 * 'line' that is in the middle half
832 */
833 bottom?: string
834 }
835
836 mime?: string
837
838 singleChannel?: boolean
839 size?: number
840 sequence?: boolean
841 }
842
843 /**
844 * @default false
845 */
846 debug?: boolean
847
848 /**
849 * @default true
850 */
851 locate?: boolean
852
853 /**
854 * @default 4
855 */
856 numOfWorkers?: number
857
858 /**
859 * This top-level property controls the scan-frequency of the video-stream.
860 * It’s optional and defines the maximum number of scans per second.
861 * This renders useful for cases where the scan-session is long-running and
862 * resources such as CPU power are of concern.
863 */
864 frequency?: number
865
866 decoder?: {
867 /**
868 * @default [ "code_128_reader" ]
869 */
870 readers?: (QuaggaJSReaderConfig | QuaggaJSCodeReader)[]
871
872 debug?: {
873 /**
874 * @default false
875 */
876 drawBoundingBox?: boolean
877
878 /**
879 * @default false
880 */
881 showFrequency?: boolean
882
883 /**
884 * @default false
885 */
886 drawScanline?: boolean
887
888 /**
889 * @default false
890 */
891 showPattern?: boolean
892 }
893
894 /**
895 * The multiple property tells the decoder if it should continue decoding after finding a valid barcode.
896 * If multiple is set to true, the results will be returned as an array of result objects.
897 * Each object in the array will have a box, and may have a codeResult
898 * depending on the success of decoding the individual box.
899 */
900 multiple?: boolean
901 }
902
903 locator?: {
904 /**
905 * @default true
906 */
907 halfSample?: boolean
908
909 /**
910 * @default "medium"
911 * Available values: x-small, small, medium, large, x-large
912 */
913 patchSize?: string
914 willReadFrequently?: boolean
915 debug?: {
916 /**
917 * @default false
918 */
919 showCanvas?: boolean
920
921 /**
922 * @default false
923 */
924 showPatches?: boolean
925
926 /**
927 * @default false
928 */
929 showFoundPatches?: boolean
930
931 /**
932 * @default false
933 */
934 showSkeleton?: boolean
935
936 /**
937 * @default false
938 */
939 showLabels?: boolean
940
941 /**
942 * @default false
943 */
944 showPatchLabels?: boolean
945
946 /**
947 * @default false
948 */
949 showRemainingPatchLabels?: boolean
950
951 boxFromPatches?: {
952 /**
953 * @default false
954 */
955 showTransformed?: boolean
956
957 /**
958 * @default false
959 */
960 showTransformedBox?: boolean
961
962 /**
963 * @default false
964 */
965 showBB?: boolean
966 }
967 }
968 }
969}
970
971export interface QuaggaJSReaderConfig {
972 config: {
973 supplements: string[]
974 }
975 format: string
976}
977
978export interface MediaTrackConstraintsWithDeprecated extends MediaTrackConstraints {
979 facing?: string
980 maxAspectRatio?: number // i don't see this in the documentation anywhere, but it's in the original test suite...
981 minAspectRatio?: number
982}
983
984export type TypedArrayConstructor =
985 Int8ArrayConstructor
986 | Uint8ArrayConstructor
987 | Uint8ClampedArrayConstructor
988 | Int16ArrayConstructor
989 | Uint16ArrayConstructor
990 | Int32ArrayConstructor
991 | Uint32ArrayConstructor
992 | Float32ArrayConstructor
993 | Float64ArrayConstructor
994
995export type TypedArray =
996 Int8Array
997 | Uint8Array
998 | Uint8ClampedArray
999 | Int16Array
1000 | Uint16Array
1001 | Int32Array
1002 | Uint32Array
1003 | Float32Array
1004 | Float64Array
1005
1006export type QuaggaJSCodeReader =
1007 | 'code_128_reader'
1008 | 'ean_reader'
1009 | 'ean_5_reader'
1010 | 'ean_2_reader'
1011 | 'ean_8_reader'
1012 | 'code_39_reader'
1013 | 'code_39_vin_reader'
1014 | 'codabar_reader'
1015 | 'upc_reader'
1016 | 'upc_e_reader'
1017 | 'i2of5_reader'
1018 | '2of5_reader'
1019 | 'code_93_reader'
1020 | 'code_32_reader'
packages/qrx/src/quagga/vendor.d.tsadded+6-0
Changes to packages/qrx/src/quagga/vendor.d.ts
@@ -0,0 +1,6 @@
1// typescript definitions for modules that we use that don't provide their own defs
2
3declare module 'get-pixels' {
4 function GetPixels(url: string, type: string | undefined, callback: (err: any, pixels: any) => void): void
5 export = GetPixels
6}
test/quagga/array_helper.spec.tsadded+63-0
Changes to test/quagga/array_helper.spec.ts
@@ -0,0 +1,63 @@
1import { expect } from 'chai'
2import { after, before, describe, it } from 'mocha'
3import sinon from 'sinon'
4import ArrayHelper from '../array_helper'
5
6describe('Array Helper', () => {
7 describe('init', () => {
8 it('initializes an array with the given value', () => {
9 const input = [0, 0, 0]
10 ArrayHelper.init(input, 5)
11 expect(input).to.deep.equal([5, 5, 5])
12 })
13 })
14
15 describe('shuffle', () => {
16 let MathStub: sinon.SinonStub
17 before(() => {
18 // presumably, this makes the shuffle deterministic so it can be tested
19 MathStub = sinon.stub(Math as any, 'random').returns(0.5) // TODO: remove as any, and fix this type def issue
20 })
21
22 after(() => {
23 MathStub.restore()
24 })
25 it('shuffles the content of an array', () => {
26 const input = [1, 2, 3]
27 const output = ArrayHelper.shuffle(input)
28 expect(output).to.be.an('Array').with.length(3)
29 expect(output).to.not.deep.equal([1, 2, 3])
30 expect(output).to.be.an('Array').that.contains(1)
31 expect(output).to.be.an('Array').that.contains(2)
32 expect(output).to.be.an('Array').that.contains(3)
33 })
34 })
35
36 describe('toPointList', () => {
37 it('converts an Array to a List of points', () => {
38 const input = [[1, 2], [2, 2], [3, 2]]
39 expect(ArrayHelper.toPointList(input)).to.equal('[[1,2],\r\n[2,2],\r\n[3,2]]')
40 })
41 })
42
43 describe('threshold', () => {
44 it('returns all elements above the given threshold', () => {
45 const input = [1, 2, 3]
46 expect(ArrayHelper.threshold(input, 2, score => score * 1.5)).to.deep.equal([2, 3])
47 })
48 })
49
50 describe('maxIndex', () => {
51 it('gets the index of the biggest element in the array', () => {
52 const input = [1, 2, 3]
53 expect(ArrayHelper.maxIndex(input)).to.equal(2)
54 })
55 })
56
57 describe('max', () => {
58 it('gets the biggest element in the array', () => {
59 const input = [1, 3, 2]
60 expect(ArrayHelper.max(input)).to.equal(3)
61 })
62 })
63})
test/quagga/barcode_locator.spec.tsadded+170-0
Changes to test/quagga/barcode_locator.spec.ts
@@ -0,0 +1,170 @@
1import type { SinonSpy } from 'sinon'
2import type { QuaggaJSConfigObject } from '../../../type-definitions/quagga'
3import { expect } from 'chai'
4import merge from 'lodash/merge'
5import sinon from 'sinon'
6import QuaggaConfig from '../../config/config'
7import BarcodeLocator from '../barcode_locator'
8
9if (typeof (globalThis as any).ENV === 'undefined') {
10 (globalThis as any).ENV = { production: true, development: false, node: true }
11}
12
13declare interface AreaConfig {
14 area: {
15 top: string
16 right: string
17 bottom: string
18 left: string
19 }
20};
21
22describe('barcode Locator', () => {
23 describe('checkImageConstraints', () => {
24 let config: QuaggaJSConfigObject
25 let imageSize = { x: 0, y: 0 }
26 let inputStream = {
27 getWidth() {
28 return imageSize.x
29 },
30 getHeight() {
31 return imageSize.y
32 },
33 setWidth(width: number) {},
34 setHeight(height: number) {},
35 setTopRight() {},
36 setCanvasSize() {},
37 getConfig() {
38 return streamConfig
39 },
40 }
41
42 let streamConfig: AreaConfig | {} = { }
43
44 beforeEach(() => {
45 imageSize = {
46 x: 640,
47 y: 480,
48 }
49 config = merge({}, QuaggaConfig)
50 inputStream = {
51 getWidth() {
52 return imageSize.x
53 },
54 getHeight() {
55 return imageSize.y
56 },
57 setWidth() {},
58 setHeight() {},
59 setTopRight() {},
60 setCanvasSize() {},
61 getConfig() {
62 return streamConfig
63 },
64 }
65 sinon.stub(inputStream, 'setWidth').callsFake((width) => {
66 imageSize.x = width
67 })
68 sinon.stub(inputStream, 'setHeight').callsFake((height) => {
69 imageSize.y = height
70 })
71 sinon.stub(inputStream, 'setTopRight')
72 sinon.stub(inputStream, 'setCanvasSize')
73 })
74
75 afterEach(() => {
76 ((inputStream.setWidth) as SinonSpy).restore();
77 ((inputStream.setHeight) as SinonSpy).restore()
78 })
79
80 it('should not adjust the image-size if not needed', () => {
81 // console.warn('* image size=', JSON.stringify(imageSize));
82 const expected = { x: imageSize.x, y: imageSize.y }
83 // console.warn('* inputStream before=', inputStream.getWidth(), inputStream.getHeight());
84 BarcodeLocator.checkImageConstraints(inputStream, config.locator)
85 // console.warn('* inputStream after=', inputStream.getWidth(), inputStream.getHeight());
86 expect(inputStream.getWidth()).to.be.equal(expected.x)
87 expect(inputStream.getHeight()).to.be.equal(expected.y)
88 })
89
90 it('should adjust the image-size', () => {
91 const expected = { x: imageSize.x, y: imageSize.y }
92
93 config.locator!.halfSample = true
94 imageSize.y += 1
95 BarcodeLocator.checkImageConstraints(inputStream, config.locator)
96 expect(inputStream.getWidth()).to.be.equal(expected.x)
97 expect(inputStream.getHeight()).to.be.equal(expected.y)
98 })
99
100 it('should adjust the image-size', () => {
101 const expected = { x: imageSize.x, y: imageSize.y }
102
103 imageSize.y += 1
104 config.locator!.halfSample = false
105 BarcodeLocator.checkImageConstraints(inputStream, config.locator)
106 expect(inputStream.getHeight()).to.be.equal(expected.y)
107 expect(inputStream.getWidth()).to.be.equal(expected.x)
108 })
109
110 it('should take the defined area into account', () => {
111 const expectedSize = {
112 x: 420,
113 y: 315,
114 }
115 const expectedTopRight = {
116 x: 115,
117 y: 52,
118 }
119 const expectedCanvasSize = {
120 x: 640,
121 y: 480,
122 }
123
124 // @ts-ignore
125 streamConfig.area = {
126 top: '11%',
127 right: '15%',
128 bottom: '20%',
129 left: '18%',
130 }
131
132 config.locator!.halfSample = false
133 BarcodeLocator.checkImageConstraints(inputStream, config.locator)
134 expect(inputStream.getHeight()).to.be.equal(expectedSize.y)
135 expect(inputStream.getWidth()).to.be.equal(expectedSize.x)
136 expect(((inputStream.setTopRight) as SinonSpy).getCall(0).args[0]).to.deep.equal(expectedTopRight)
137 expect(((inputStream.setCanvasSize) as SinonSpy).getCall(0).args[0]).to.deep.equal(expectedCanvasSize)
138 })
139
140 it('should return the original size if set to full image', () => {
141 const expectedSize = {
142 x: 640,
143 y: 480,
144 }
145 const expectedTopRight = {
146 x: 0,
147 y: 0,
148 }
149 const expectedCanvasSize = {
150 x: 640,
151 y: 480,
152 }
153
154 // @ts-ignore
155 streamConfig.area = {
156 top: '0%',
157 right: '0%',
158 bottom: '0%',
159 left: '0%',
160 }
161
162 config.locator!.halfSample = false
163 BarcodeLocator.checkImageConstraints(inputStream, config.locator)
164 expect(inputStream.getHeight()).to.be.equal(expectedSize.y)
165 expect(inputStream.getWidth()).to.be.equal(expectedSize.x)
166 expect(((inputStream.setTopRight) as SinonSpy).getCall(0).args[0]).to.deep.equal(expectedTopRight)
167 expect(((inputStream.setCanvasSize) as SinonSpy).getCall(0).args[0]).to.deep.equal(expectedCanvasSize)
168 })
169 })
170})
test/quagga/browser/camera_access.spec.tsadded+144-0
Changes to test/quagga/browser/camera_access.spec.ts
@@ -0,0 +1,144 @@
1import type { MediaTrackConstraintsWithDeprecated } from '../../../../type-definitions/quagga.d'
2import { expect } from 'chai'
3import { after, describe, it } from 'mocha'
4import CameraAccess, { pickConstraints } from '../../camera_access'
5
6const Quagga = { CameraAccess }
7
8describe('CameraAccess (browser)', () => {
9 // TODO: move pickConstraints tests into "universal" test, no reason it shouldn't work in node, even if you wouldn't use it.
10 // TODO: consider moving the entire CameraAccess section to a separate library
11 describe('pickConstraints', () => {
12 it('should return the given constraints if no facingMode is defined', async () => {
13 const givenConstraints = { width: 180 }
14 const actualConstraints = await pickConstraints(givenConstraints)
15 expect(actualConstraints.video).to.deep.equal(givenConstraints)
16 })
17
18 it('should return the given constraints if deviceId is defined', async () => {
19 const givenConstraints = { width: 180, deviceId: '4343' }
20 const actualConstraints = await pickConstraints(givenConstraints)
21 expect(actualConstraints.video).to.deep.equal(givenConstraints)
22 })
23
24 it('should remove facingMode if deviceId is defined', async () => {
25 const givenConstraints = { deviceId: 'dummy', facingMode: 'user' }
26 const actualConstraints = await pickConstraints(givenConstraints)
27 expect(actualConstraints.video).to.deep.equal({ deviceId: 'dummy' })
28 })
29 })
30
31 describe('enumerateVideoDevices', () => {
32 it('works', async () => {
33 // TODO: if someone runs this test in live Chrome with no video devices, it should
34 // fail .. hmm...
35 const v = await Quagga.CameraAccess.enumerateVideoDevices()
36 expect(v).to.be.an('Array').of.length(1)
37 expect(v[0]).to.be.an.instanceof(InputDeviceInfo)
38 expect(v[0].deviceId).to.be.a('string')
39 expect(v[0].groupId).to.be.a('string')
40 expect(v[0].kind).to.equal('videoinput')
41 expect(v[0].label).to.equal('fake_device_0')
42 })
43 })
44
45 describe('request', () => {
46 const video = document.createElement('video')
47 after(() => {
48 video.pause()
49 Quagga.CameraAccess.release()
50 })
51 it('works', async () => {
52 await Quagga.CameraAccess.request(video, {})
53 expect(video.srcObject).to.not.equal(null)
54 // "as any" here to prevent typescript blowing up because it doesn't understand 'id' and
55 // 'active' as members of MediaStream | MediaSource | Blob .. why?
56 expect(((video?.srcObject) as any)?.id).to.be.a('string')
57 expect(((video?.srcObject) as any)?.active).to.equal(true)
58 })
59
60 it('should allow deprecated constraints to be used', async () => {
61 await Quagga.CameraAccess.request(video, {
62 width: 320,
63 height: 240,
64 facing: 'user',
65 minAspectRatio: 2,
66 maxAspectRatio: 100,
67 })
68 const activeTrack = Quagga.CameraAccess.getActiveTrack()
69 const constraints = activeTrack?.getConstraints() as MediaTrackConstraintsWithDeprecated
70 if (!constraints) {
71 throw new Error('no active track constraints')
72 }
73 expect(constraints.width).to.equal(320)
74 expect(constraints.height).to.equal(240)
75 expect(constraints.facingMode).to.equal('user')
76 expect(constraints.aspectRatio).to.equal(2)
77
78 /* eslint-disable @typescript-eslint/no-unused-expressions */
79 expect(constraints.facing).to.not.exist
80 expect(constraints.minAspectRatio).to.not.exist
81 expect(constraints.maxAspectRatio).to.not.exist
82
83 /* eslint-enable @typescript-eslint/no-unused-expressions */
84 })
85
86 it('will fail on NotAllowedError', async () => {
87 cy.stub(navigator.mediaDevices, 'getUserMedia').rejects(new DOMException('Not Allowed', 'NotAllowedError'))
88 const video = document.createElement('video')
89 try {
90 const x = await Quagga.CameraAccess.request(video, { width: 320, height: 240 })
91 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
92 expect(x).to.not.exist
93 }
94 catch (err) {
95 expect(err).to.be.an.instanceOf(DOMException)
96 expect(err.name).to.equal('NotAllowedError')
97 }
98 })
99
100 it('fails eventually on unacceptable video size', async function () {
101 this.timeout(10000)
102 const video = document.createElement('video')
103 try {
104 const x = await Quagga.CameraAccess.request(video, { width: 5, height: 5 })
105 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
106 expect(x).to.not.exist
107 }
108 catch (err) {
109 expect(err.message).to.equal('Unable to play video stream. Is webcam working?')
110 }
111 })
112 // TODO: need to add a test for no support in browser to straight up fail
113 })
114
115 describe('release', () => {
116 it('works', async () => {
117 const video = document.createElement('video')
118 await Quagga.CameraAccess.request(video, {})
119 await Quagga.CameraAccess.release()
120 expect(((video?.srcObject) as any)?.active).to.equal(false)
121 })
122 })
123
124 describe('getActiveStreamLabel', () => {
125 after(() => Quagga.CameraAccess.release())
126 it('no active stream', () => {
127 const x = Quagga.CameraAccess.getActiveStreamLabel()
128 expect(x).to.equal('')
129 })
130
131 it('with active stream', async () => {
132 const video = document.createElement('video')
133 await Quagga.CameraAccess.request(video, {})
134 const x = Quagga.CameraAccess.getActiveStreamLabel()
135 const v = await Quagga.CameraAccess.enumerateVideoDevices()
136 expect(x).to.equal(v[0].label)
137 })
138 })
139
140 // TODO: can this be tested?
141 describe('enableTorch', () => {})
142 // TODO: can this be tested?
143 describe('disableTorch', () => {})
144})
test/quagga/browser/exif_helper.spec.jsadded+59-0
Changes to test/quagga/browser/exif_helper.spec.js
@@ -0,0 +1,59 @@
1import {
2 base64ToArrayBuffer,
3 findTagsInBuffer,
4 findTagsInObjectURL,
5} from '../../exif_helper'
6
7const fixtures = {
8 orientation: {
9 none: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QQuRXhpZgAASUkqAAgAAAALAAMBAwABAAAABgAAAA8BAgAIAAAAkgAAABABAgAJAAAAmgAAABoBBQABAAAApAAAABsBBQABAAAArAAAACgBAwABAAAAAgAAADEBAgAMAAAAtAAAADIBAgAUAAAAwAAAABMCAwABAAAAAQAAAGmHBAABAAAA1AAAACWIBAABAAAA7gIAALwDAABzYW1zdW5nAFNNLUc5MjBGAABIAAAAAQAAAEgAAAABAAAAR0lNUCAyLjguMTAAMjAxNjowODoxNSAwMDoyMjoxNQAZAJqCBQABAAAABgIAAJ2CBQABAAAADgIAACKIAwABAAAAAgAAACeIAwABAAAAZAAAAACQBwAEAAAAMDIyMAOQAgAUAAAAFgIAAASQAgAUAAAAKgIAAAGSCgABAAAAPgIAAAKSBQABAAAARgIAAAOSCgABAAAATgIAAASSCgABAAAAVgIAAAWSBQABAAAAXgIAAAeSAwABAAAAAgAAAAmSAwABAAAAAAAAAAqSBQABAAAAZgIAAHySBwBiAAAAbgIAAACgBwAEAAAAMDEwMAGgAwABAAAAAQAAAAKgBAABAAAAEAAAAAOgBAABAAAACQAAAAKkAwABAAAAAAAAAAOkAwABAAAAAAAAAAWkAwABAAAAHAAAAAakAwABAAAAAAAAACCkAgAeAAAA0AIAAAAAAAABAAAAMgAAABMAAAAKAAAAMjAxNjowODoxNSAwMDowNDoxNwAyMDE2OjA4OjE1IDAwOjA0OjE3ACMsAADSBwAAOTEAAJQaAAAMAQAAZAAAAAAAAAABAAAAOTEAAJQaAAArAAAACgAAAAcAAQAHAAQAAAAwMTAwAgAEAAEAAAAAIAEADAAEAAEAAAAAAAAAEAAFAAEAAABaAAAAQAAEAAEAAAAAAAAAUAAEAAEAAAABAAAAAAEDAAEAAAAAAAAAAAAAAAAAAAAAAAAAQTE2TExJQzA4U00gQTE2TExJTDAyR00KAAAAAAAACQAAAAEABAAAAAICAAABAAIAAgAAAE4AAAACAAUAAwAAAGADAAADAAIAAgAAAEUAAAAEAAUAAwAAAHgDAAAFAAEAAQAAAAEAAAAGAAUAAQAAAJADAAAHAAUAAwAAAJgDAAAdAAIACwAAALADAAAAAAAALwAAAAEAAAAGAAAAAQAAABAAAAABAAAADwAAAAEAAAAZAAAAAQAAABYAAAABAAAAAAAAAAEAAAAWAAAAAQAAAAQAAAABAAAADQAAAAEAAAAyMDE2OjA4OjE0AAAHAAABBAABAAAAAAIAAAEBBAABAAAAIAEAAAMBAwABAAAABgAAABIBAwABAAAABgAAABoBBQABAAAAFgQAABsBBQABAAAAHgQAACgBAwABAAAAAgAAAAAAAABIAAAAAQAAAEgAAAABAAAA/9sAQwAQCwwODAoQDg0OEhEQExgoGhgWFhgxIyUdKDozPTw5Mzg3QEhcTkBEV0U3OFBtUVdfYmdoZz5NcXlwZHhcZWdj/9sAQwEREhIYFRgvGhovY0I4QmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Nj/8AAEQgACQAQAwEhAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A57SbCC+aRZmdQoXleoJYD+tV9Vshp2tSWiuXWM8E/Sot7lzS/v2Lvh7/AFlx/ur/AOhiq+uf8hp/r/jVP4BL4z//2Q==',
10 6: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QQ6RXhpZgAASUkqAAgAAAAMAAMBAwABAAAABgAAAA8BAgAIAAAAngAAABABAgAJAAAApgAAABIBAwABAAAABgAAABoBBQABAAAAsAAAABsBBQABAAAAuAAAACgBAwABAAAAAgAAADEBAgAMAAAAwAAAADIBAgAUAAAAzAAAABMCAwABAAAAAQAAAGmHBAABAAAA4AAAACWIBAABAAAA+gIAAMgDAABzYW1zdW5nAFNNLUc5MjBGAABIAAAAAQAAAEgAAAABAAAAR0lNUCAyLjguMTAAMjAxNjowODoxNSAwMDoyMjoxNQAZAJqCBQABAAAAEgIAAJ2CBQABAAAAGgIAACKIAwABAAAAAgAAACeIAwABAAAAZAAAAACQBwAEAAAAMDIyMAOQAgAUAAAAIgIAAASQAgAUAAAANgIAAAGSCgABAAAASgIAAAKSBQABAAAAUgIAAAOSCgABAAAAWgIAAASSCgABAAAAYgIAAAWSBQABAAAAagIAAAeSAwABAAAAAgAAAAmSAwABAAAAAAAAAAqSBQABAAAAcgIAAHySBwBiAAAAegIAAACgBwAEAAAAMDEwMAGgAwABAAAAAQAAAAKgBAABAAAAEAAAAAOgBAABAAAACQAAAAKkAwABAAAAAAAAAAOkAwABAAAAAAAAAAWkAwABAAAAHAAAAAakAwABAAAAAAAAACCkAgAeAAAA3AIAAAAAAAABAAAAMgAAABMAAAAKAAAAMjAxNjowODoxNSAwMDowNDoxNwAyMDE2OjA4OjE1IDAwOjA0OjE3ACMsAADSBwAAOTEAAJQaAAAMAQAAZAAAAAAAAAABAAAAOTEAAJQaAAArAAAACgAAAAcAAQAHAAQAAAAwMTAwAgAEAAEAAAAAIAEADAAEAAEAAAAAAAAAEAAFAAEAAABaAAAAQAAEAAEAAAAAAAAAUAAEAAEAAAABAAAAAAEDAAEAAAAAAAAAAAAAAAAAAAAAAAAAQTE2TExJQzA4U00gQTE2TExJTDAyR00KAAAAAAAACQAAAAEABAAAAAICAAABAAIAAgAAAE4AAAACAAUAAwAAAGwDAAADAAIAAgAAAEUAAAAEAAUAAwAAAIQDAAAFAAEAAQAAAAEAAAAGAAUAAQAAAJwDAAAHAAUAAwAAAKQDAAAdAAIACwAAALwDAAAAAAAALwAAAAEAAAAGAAAAAQAAABAAAAABAAAADwAAAAEAAAAZAAAAAQAAABYAAAABAAAAAAAAAAEAAAAWAAAAAQAAAAQAAAABAAAADQAAAAEAAAAyMDE2OjA4OjE0AAAHAAABBAABAAAAAAIAAAEBBAABAAAAIAEAAAMBAwABAAAABgAAABIBAwABAAAABgAAABoBBQABAAAAIgQAABsBBQABAAAAKgQAACgBAwABAAAAAgAAAAAAAABIAAAAAQAAAEgAAAABAAAA/9sAQwAQCwwODAoQDg0OEhEQExgoGhgWFhgxIyUdKDozPTw5Mzg3QEhcTkBEV0U3OFBtUVdfYmdoZz5NcXlwZHhcZWdj/9sAQwEREhIYFRgvGhovY0I4QmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Nj/8AAEQgACQAQAwEhAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A57SbCC+aRZmdQoXleoJYD+tV9Vshp2tSWiuXWM8E/Sot7lzS/v2Lvh7/AFlx/ur/AOhiq+uf8hp/r/jVP4BL4z//2Q==',
11 3: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QQ6RXhpZgAASUkqAAgAAAAMAAMBAwABAAAABgAAAA8BAgAIAAAAngAAABABAgAJAAAApgAAABIBAwABAAAAAwAAABoBBQABAAAAsAAAABsBBQABAAAAuAAAACgBAwABAAAAAgAAADEBAgAMAAAAwAAAADIBAgAUAAAAzAAAABMCAwABAAAAAQAAAGmHBAABAAAA4AAAACWIBAABAAAA+gIAAMgDAABzYW1zdW5nAFNNLUc5MjBGAABIAAAAAQAAAEgAAAABAAAAR0lNUCAyLjguMTAAMjAxNjowODoxNSAwMDoyMjoxNQAZAJqCBQABAAAAEgIAAJ2CBQABAAAAGgIAACKIAwABAAAAAgAAACeIAwABAAAAZAAAAACQBwAEAAAAMDIyMAOQAgAUAAAAIgIAAASQAgAUAAAANgIAAAGSCgABAAAASgIAAAKSBQABAAAAUgIAAAOSCgABAAAAWgIAAASSCgABAAAAYgIAAAWSBQABAAAAagIAAAeSAwABAAAAAgAAAAmSAwABAAAAAAAAAAqSBQABAAAAcgIAAHySBwBiAAAAegIAAACgBwAEAAAAMDEwMAGgAwABAAAAAQAAAAKgBAABAAAAEAAAAAOgBAABAAAACQAAAAKkAwABAAAAAAAAAAOkAwABAAAAAAAAAAWkAwABAAAAHAAAAAakAwABAAAAAAAAACCkAgAeAAAA3AIAAAAAAAABAAAAMgAAABMAAAAKAAAAMjAxNjowODoxNSAwMDowNDoxNwAyMDE2OjA4OjE1IDAwOjA0OjE3ACMsAADSBwAAOTEAAJQaAAAMAQAAZAAAAAAAAAABAAAAOTEAAJQaAAArAAAACgAAAAcAAQAHAAQAAAAwMTAwAgAEAAEAAAAAIAEADAAEAAEAAAAAAAAAEAAFAAEAAABaAAAAQAAEAAEAAAAAAAAAUAAEAAEAAAABAAAAAAEDAAEAAAAAAAAAAAAAAAAAAAAAAAAAQTE2TExJQzA4U00gQTE2TExJTDAyR00KAAAAAAAACQAAAAEABAAAAAICAAABAAIAAgAAAE4AAAACAAUAAwAAAGwDAAADAAIAAgAAAEUAAAAEAAUAAwAAAIQDAAAFAAEAAQAAAAEAAAAGAAUAAQAAAJwDAAAHAAUAAwAAAKQDAAAdAAIACwAAALwDAAAAAAAALwAAAAEAAAAGAAAAAQAAABAAAAABAAAADwAAAAEAAAAZAAAAAQAAABYAAAABAAAAAAAAAAEAAAAWAAAAAQAAAAQAAAABAAAADQAAAAEAAAAyMDE2OjA4OjE0AAAHAAABBAABAAAAAAIAAAEBBAABAAAAIAEAAAMBAwABAAAABgAAABIBAwABAAAABgAAABoBBQABAAAAIgQAABsBBQABAAAAKgQAACgBAwABAAAAAgAAAAAAAABIAAAAAQAAAEgAAAABAAAA/9sAQwAQCwwODAoQDg0OEhEQExgoGhgWFhgxIyUdKDozPTw5Mzg3QEhcTkBEV0U3OFBtUVdfYmdoZz5NcXlwZHhcZWdj/9sAQwEREhIYFRgvGhovY0I4QmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Nj/8AAEQgACQAQAwEhAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A57SbCC+aRZmdQoXleoJYD+tV9Vshp2tSWiuXWM8E/Sot7lzS/v2Lvh7/AFlx/ur/AOhiq+uf8hp/r/jVP4BL4z//2Q==',
12 8: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QQ6RXhpZgAASUkqAAgAAAAMAAMBAwABAAAABgAAAA8BAgAIAAAAngAAABABAgAJAAAApgAAABIBAwABAAAACAAAABoBBQABAAAAsAAAABsBBQABAAAAuAAAACgBAwABAAAAAgAAADEBAgAMAAAAwAAAADIBAgAUAAAAzAAAABMCAwABAAAAAQAAAGmHBAABAAAA4AAAACWIBAABAAAA+gIAAMgDAABzYW1zdW5nAFNNLUc5MjBGAABIAAAAAQAAAEgAAAABAAAAR0lNUCAyLjguMTAAMjAxNjowODoxNSAwMDoyMjoxNQAZAJqCBQABAAAAEgIAAJ2CBQABAAAAGgIAACKIAwABAAAAAgAAACeIAwABAAAAZAAAAACQBwAEAAAAMDIyMAOQAgAUAAAAIgIAAASQAgAUAAAANgIAAAGSCgABAAAASgIAAAKSBQABAAAAUgIAAAOSCgABAAAAWgIAAASSCgABAAAAYgIAAAWSBQABAAAAagIAAAeSAwABAAAAAgAAAAmSAwABAAAAAAAAAAqSBQABAAAAcgIAAHySBwBiAAAAegIAAACgBwAEAAAAMDEwMAGgAwABAAAAAQAAAAKgBAABAAAAEAAAAAOgBAABAAAACQAAAAKkAwABAAAAAAAAAAOkAwABAAAAAAAAAAWkAwABAAAAHAAAAAakAwABAAAAAAAAACCkAgAeAAAA3AIAAAAAAAABAAAAMgAAABMAAAAKAAAAMjAxNjowODoxNSAwMDowNDoxNwAyMDE2OjA4OjE1IDAwOjA0OjE3ACMsAADSBwAAOTEAAJQaAAAMAQAAZAAAAAAAAAABAAAAOTEAAJQaAAArAAAACgAAAAcAAQAHAAQAAAAwMTAwAgAEAAEAAAAAIAEADAAEAAEAAAAAAAAAEAAFAAEAAABaAAAAQAAEAAEAAAAAAAAAUAAEAAEAAAABAAAAAAEDAAEAAAAAAAAAAAAAAAAAAAAAAAAAQTE2TExJQzA4U00gQTE2TExJTDAyR00KAAAAAAAACQAAAAEABAAAAAICAAABAAIAAgAAAE4AAAACAAUAAwAAAGwDAAADAAIAAgAAAEUAAAAEAAUAAwAAAIQDAAAFAAEAAQAAAAEAAAAGAAUAAQAAAJwDAAAHAAUAAwAAAKQDAAAdAAIACwAAALwDAAAAAAAALwAAAAEAAAAGAAAAAQAAABAAAAABAAAADwAAAAEAAAAZAAAAAQAAABYAAAABAAAAAAAAAAEAAAAWAAAAAQAAAAQAAAABAAAADQAAAAEAAAAyMDE2OjA4OjE0AAAHAAABBAABAAAAAAIAAAEBBAABAAAAIAEAAAMBAwABAAAABgAAABIBAwABAAAABgAAABoBBQABAAAAIgQAABsBBQABAAAAKgQAACgBAwABAAAAAgAAAAAAAABIAAAAAQAAAEgAAAABAAAA/9sAQwAQCwwODAoQDg0OEhEQExgoGhgWFhgxIyUdKDozPTw5Mzg3QEhcTkBEV0U3OFBtUVdfYmdoZz5NcXlwZHhcZWdj/9sAQwEREhIYFRgvGhovY0I4QmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Nj/8AAEQgACQAQAwEhAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A57SbCC+aRZmdQoXleoJYD+tV9Vshp2tSWiuXWM8E/Sot7lzS/v2Lvh7/AFlx/ur/AOhiq+uf8hp/r/jVP4BL4z//2Q==',
13 1: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QQ6RXhpZgAASUkqAAgAAAAMAAMBAwABAAAABgAAAA8BAgAIAAAAngAAABABAgAJAAAApgAAABIBAwABAAAAAQAAABoBBQABAAAAsAAAABsBBQABAAAAuAAAACgBAwABAAAAAgAAADEBAgAMAAAAwAAAADIBAgAUAAAAzAAAABMCAwABAAAAAQAAAGmHBAABAAAA4AAAACWIBAABAAAA+gIAAMgDAABzYW1zdW5nAFNNLUc5MjBGAABIAAAAAQAAAEgAAAABAAAAR0lNUCAyLjguMTAAMjAxNjowODoxNSAwMDoyMjoxNQAZAJqCBQABAAAAEgIAAJ2CBQABAAAAGgIAACKIAwABAAAAAgAAACeIAwABAAAAZAAAAACQBwAEAAAAMDIyMAOQAgAUAAAAIgIAAASQAgAUAAAANgIAAAGSCgABAAAASgIAAAKSBQABAAAAUgIAAAOSCgABAAAAWgIAAASSCgABAAAAYgIAAAWSBQABAAAAagIAAAeSAwABAAAAAgAAAAmSAwABAAAAAAAAAAqSBQABAAAAcgIAAHySBwBiAAAAegIAAACgBwAEAAAAMDEwMAGgAwABAAAAAQAAAAKgBAABAAAAEAAAAAOgBAABAAAACQAAAAKkAwABAAAAAAAAAAOkAwABAAAAAAAAAAWkAwABAAAAHAAAAAakAwABAAAAAAAAACCkAgAeAAAA3AIAAAAAAAABAAAAMgAAABMAAAAKAAAAMjAxNjowODoxNSAwMDowNDoxNwAyMDE2OjA4OjE1IDAwOjA0OjE3ACMsAADSBwAAOTEAAJQaAAAMAQAAZAAAAAAAAAABAAAAOTEAAJQaAAArAAAACgAAAAcAAQAHAAQAAAAwMTAwAgAEAAEAAAAAIAEADAAEAAEAAAAAAAAAEAAFAAEAAABaAAAAQAAEAAEAAAAAAAAAUAAEAAEAAAABAAAAAAEDAAEAAAAAAAAAAAAAAAAAAAAAAAAAQTE2TExJQzA4U00gQTE2TExJTDAyR00KAAAAAAAACQAAAAEABAAAAAICAAABAAIAAgAAAE4AAAACAAUAAwAAAGwDAAADAAIAAgAAAEUAAAAEAAUAAwAAAIQDAAAFAAEAAQAAAAEAAAAGAAUAAQAAAJwDAAAHAAUAAwAAAKQDAAAdAAIACwAAALwDAAAAAAAALwAAAAEAAAAGAAAAAQAAABAAAAABAAAADwAAAAEAAAAZAAAAAQAAABYAAAABAAAAAAAAAAEAAAAWAAAAAQAAAAQAAAABAAAADQAAAAEAAAAyMDE2OjA4OjE0AAAHAAABBAABAAAAAAIAAAEBBAABAAAAIAEAAAMBAwABAAAABgAAABIBAwABAAAABgAAABoBBQABAAAAIgQAABsBBQABAAAAKgQAACgBAwABAAAAAgAAAAAAAABIAAAAAQAAAEgAAAABAAAA/9sAQwAQCwwODAoQDg0OEhEQExgoGhgWFhgxIyUdKDozPTw5Mzg3QEhcTkBEV0U3OFBtUVdfYmdoZz5NcXlwZHhcZWdj/9sAQwEREhIYFRgvGhovY0I4QmNjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2Nj/8AAEQgACQAQAwEhAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A57SbCC+aRZmdQoXleoJYD+tV9Vshp2tSWiuXWM8E/Sot7lzS/v2Lvh7/AFlx/ur/AOhiq+uf8hp/r/jVP4BL4z//2Q==',
14 },
15}
16
17describe('eXIF Helper', () => {
18 describe('findTagsInObjectURL', () => {
19 it('should return null if the src type is not supported', (done) => {
20 findTagsInObjectURL('blabla').then((tags) => {
21 expect(tags === null).to.equal(true)
22 done()
23 })
24 })
25
26 it('should fail for a invalid blob type', (done) => {
27 findTagsInObjectURL('blob:balbla')
28 .then(() => {})
29 .catch((err) => {
30 console.log(err)
31 expect(typeof err !== 'undefined')
32 })
33 .then(done)
34 })
35 })
36
37 describe('findTagsInBuffer', () => {
38 it('should result in a rotation value of 1', () => {
39 const result = findTagsInBuffer(base64ToArrayBuffer(fixtures.orientation['1']))
40 expect(result).to.deep.equal({ orientation: 1 })
41 })
42 it('should result in a rotation value of 6', () => {
43 const result = findTagsInBuffer(base64ToArrayBuffer(fixtures.orientation['6']))
44 expect(result).to.deep.equal({ orientation: 6 })
45 })
46 it('should result in a rotation value of 3', () => {
47 const result = findTagsInBuffer(base64ToArrayBuffer(fixtures.orientation['3']))
48 expect(result).to.deep.equal({ orientation: 3 })
49 })
50 it('should result in a rotation value of 4', () => {
51 const result = findTagsInBuffer(base64ToArrayBuffer(fixtures.orientation['8']))
52 expect(result).to.deep.equal({ orientation: 8 })
53 })
54 it('should return nothing if orientation is not found', () => {
55 const result = findTagsInBuffer(base64ToArrayBuffer(fixtures.orientation.none))
56 expect(result).to.deep.equal({})
57 })
58 })
59})
test/quagga/browser/mediaDevices.spec.tsadded+24-0
Changes to test/quagga/browser/mediaDevices.spec.ts
@@ -0,0 +1,24 @@
1import { expect } from 'chai'
2import { describe, it } from 'mocha'
3import { enumerateDevices, getUserMedia } from '../../mediaDevices'
4
5describe('mediaDevices (browser)', () => {
6 describe('enumerateDevices', () => {
7 it('TODO: rejects with an error if enumerateDevices is not supported in browser version')
8 it('returns a Promise', () => {
9 expect(enumerateDevices()).to.be.a('Promise')
10 })
11 it('resolves with an Array of InputDeviceInfo', async () => {
12 const d = await enumerateDevices()
13 expect(d).to.be.an('Array').of.length.greaterThan(1)
14 console.warn('* d=', d)
15 expect(d[0]).to.be.an.instanceof(InputDeviceInfo)
16 })
17 })
18 describe('getUserMedia', () => {
19 it('TODO: rejects with an error if getUserMedia is not supported')
20 it('returns a Promise', () => {
21 expect(getUserMedia({})).to.be.a('Promise')
22 })
23 })
24})
test/quagga/browser/result_collector.spec.tsadded+121-0
Changes to test/quagga/browser/result_collector.spec.ts
@@ -0,0 +1,121 @@
1import type { SinonSpy } from 'sinon'
2import type { QuaggaJSCodeResult, QuaggaJSResultCollector, XYSize } from '../../../../type-definitions/quagga.d'
3import { expect } from 'chai'
4import {
5 afterEach,
6 beforeEach,
7 describe,
8 it,
9} from 'mocha'
10import sinon from 'sinon'
11import ImageDebug from '../../../common/image_debug'
12import ResultCollector from '../../result_collector'
13
14interface MockCanvas {
15 getContext: () => {}
16 toDataURL: sinon.SinonSpy
17 width: number
18 height: number
19}
20
21let canvasMock: MockCanvas
22let imageSize: XYSize
23let config: QuaggaJSResultCollector
24
25describe('Result Collector', () => {
26 beforeEach(() => {
27 imageSize = { x: 320, y: 240, type: 'XYSize' }
28 config = {
29 capture: true,
30 capacity: 20,
31 blacklist: [{ code: '3574660239843', format: 'ean_13' }],
32 filter(): boolean {
33 return true
34 },
35 }
36 canvasMock = {
37 getContext(): {} {
38 return {}
39 },
40 toDataURL: sinon.spy(),
41 width: 0,
42 height: 0,
43 }
44 sinon.stub(document, 'createElement').callsFake((type) => {
45 if (type === 'canvas') {
46 return canvasMock as unknown as HTMLElement // forcing type, eh
47 }
48 return undefined as unknown as HTMLElement
49 })
50 })
51
52 afterEach(() => {
53 (document.createElement as SinonSpy).restore()
54 })
55
56 describe('create', () => {
57 it('should return a new collector', () => {
58 ResultCollector.create(config)
59 expect((document.createElement as SinonSpy).calledOnce).to.be.equal(true)
60 expect((document.createElement as SinonSpy).getCall(0).args[0]).to.equal('canvas')
61 })
62 })
63
64 describe('addResult', () => {
65 beforeEach(() => {
66 sinon.stub(ImageDebug, 'drawImage').callsFake(() => true)
67 })
68
69 afterEach(() => {
70 (ImageDebug.drawImage as SinonSpy).restore()
71 })
72
73 it('should not add result if capacity is full', () => {
74 config.capacity = 1
75 const collector = ResultCollector.create(config)
76 collector.addResult([], imageSize, {})
77 collector.addResult([], imageSize, {})
78 collector.addResult([], imageSize, {})
79 expect(collector.getResults()).to.have.length(1)
80 })
81
82 it('should only add results which match constraints', () => {
83 const collector = ResultCollector.create(config)
84
85 collector.addResult([], imageSize, { code: '423423443', format: 'ean_13' })
86 collector.addResult([], imageSize, { code: '3574660239843', format: 'ean_13' })
87 collector.addResult([], imageSize, { code: '3574660239843', format: 'code_128' })
88
89 const results = collector.getResults()
90 expect(results).to.have.length(2)
91
92 results.forEach((result: QuaggaJSCodeResult) => {
93 expect(result).not.to.deep.equal(config.blacklist?.[0])
94 })
95 })
96
97 it('should add result if no filter is set', () => {
98 delete config.filter
99 const collector = ResultCollector.create(config)
100
101 collector.addResult([], imageSize, { code: '423423443', format: 'ean_13' })
102 expect(collector.getResults()).to.have.length(1)
103 })
104
105 it('should not add results if filter returns false', () => {
106 config.filter = (): boolean => (false)
107 const collector = ResultCollector.create(config)
108
109 collector.addResult([], imageSize, { code: '423423443', format: 'ean_13' })
110 expect(collector.getResults()).to.have.length(0)
111 })
112
113 it('should add result if no blacklist is set', () => {
114 delete config.blacklist
115 const collector = ResultCollector.create(config)
116
117 collector.addResult([], imageSize, { code: '3574660239843', format: 'ean_13' })
118 expect(collector.getResults()).to.have.length(1)
119 })
120 })
121})
test/quagga/cluster.spec.jsadded+11-0
Changes to test/quagga/cluster.spec.js
@@ -0,0 +1,11 @@
1import cluster from '../cluster';
2
3describe('cluster module', () => {
4 it('cluster.create');
5 it('cluster.createPoint');
6 describe('cluster object', () => {
7 let testCluster;
8 beforeEach(() => testCluster = cluster.create());
9 it('i have no idea what im testing here');
10 });
11});
test/quagga/cv_utils.spec.jsadded+155-0
Changes to test/quagga/cv_utils.spec.js
@@ -0,0 +1,155 @@
1
2const { describe, it, beforeEach } = require('mocha');
3const { expect } = require('chai');
4const {
5 imageRef,
6 calculatePatchSize,
7 _parseCSSDimensionValues,
8 _dimensionsConverters,
9 computeImageArea,
10} = require('../cv_utils');
11
12describe('CV Utils', () => {
13 describe('imageRef', () => {
14 it('gets the image Reference for a coordinate', () => {
15 const res = imageRef(1, 2);
16 expect(res.x).to.equal(1);
17 expect(res.y).to.equal(2);
18 expect(res.toVec2()[0]).to.equal(1);
19 });
20 });
21
22 describe('calculatePatchSize', () => {
23 it('should not throw an error in case of valid image size', () => {
24 const expected = { x: 32, y: 32 };
25 const patchSize = calculatePatchSize('medium', { x: 640, y: 480 });
26
27 expect(patchSize).to.deep.equal(expected);
28 });
29
30 it('should thow an error if image size it not valid', () => {
31 const expected = { x: 32, y: 32 };
32 const patchSize = calculatePatchSize('medium', { x: 640, y: 480 });
33
34 expect(patchSize).to.deep.equal(expected);
35 });
36 });
37
38 describe('_parseCSSDimensionValues', () => {
39 it('should convert a percentual value correctly', () => {
40 const expected = {
41 value: 10,
42 unit: '%',
43 };
44 const result = _parseCSSDimensionValues('10%');
45
46 expect(result).to.deep.equal(expected);
47 });
48
49 it('should convert a 0% value correctly', () => {
50 const expected = {
51 value: 100,
52 unit: '%',
53 };
54 const result = _parseCSSDimensionValues('100%');
55
56 expect(result).to.deep.equal(expected);
57 });
58
59 it('should convert a 100% value correctly', () => {
60 const expected = {
61 value: 0,
62 unit: '%',
63 };
64 const result = _parseCSSDimensionValues('0%');
65
66 expect(result).to.deep.equal(expected);
67 });
68
69 it('should convert a pixel value to percentage', () => {
70 const expected = {
71 value: 26.3,
72 unit: '%',
73 };
74 const result = _parseCSSDimensionValues('26.3px');
75
76 // console.log(result);
77 expect(result).to.deep.equal(expected);
78 });
79 });
80
81 describe('_dimensionsConverters', () => {
82 let context;
83
84 beforeEach(() => {
85 context = {
86 width: 640,
87 height: 480,
88 };
89 });
90
91 it('should convert a top-value correclty', () => {
92 const expected = 48;
93 const result = _dimensionsConverters.top({ value: 10, unit: '%' }, context);
94
95 expect(result).to.be.equal(expected);
96 });
97
98 it('should convert a right-value correclty', () => {
99 const expected = 640 - 128;
100 const result = _dimensionsConverters.right({ value: 20, unit: '%' }, context);
101
102 expect(result).to.be.equal(expected);
103 });
104
105 it('should convert a bottom-value correclty', () => {
106 const expected = 480 - 77;
107 const result = _dimensionsConverters.bottom({ value: 16, unit: '%' }, context);
108
109 expect(result).to.be.equal(expected);
110 });
111
112 it('should convert a left-value correclty', () => {
113 const expected = 57;
114 const result = _dimensionsConverters.left({ value: 9, unit: '%' }, context);
115
116 expect(result).to.be.equal(expected);
117 });
118 });
119
120 describe('computeImageArea', () => {
121 it('should calculate an image-area', () => {
122 const expected = {
123 sx: 115,
124 sy: 48,
125 sw: 429,
126 sh: 336,
127 };
128 const result = computeImageArea(640, 480, {
129 top: '10%',
130 right: '15%',
131 bottom: '20%',
132 left: '18%',
133 });
134
135 expect(result).to.deep.equal(expected);
136 });
137
138 it('should calculate full image-area', () => {
139 const expected = {
140 sx: 0,
141 sy: 0,
142 sw: 640,
143 sh: 480,
144 };
145 const result = computeImageArea(640, 480, {
146 top: '0%',
147 right: '0%',
148 bottom: '0%',
149 left: '0%',
150 });
151
152 expect(result).to.deep.equal(expected);
153 });
154 });
155});
test/quagga/events.spec.tsadded+119-0
Changes to test/quagga/events.spec.ts
@@ -0,0 +1,119 @@
1import { describe, it, beforeEach } from 'mocha';
2import { expect } from 'chai';
3
4import sinon, { SinonSpy } from 'sinon';
5import Events from '../events';
6
7describe('Events', () => {
8 beforeEach(() => {
9 Events.unsubscribe();
10 });
11
12 describe('subscribe', () => {
13 it('should call one callback for a single event', () => {
14 const callbackA = sinon.stub();
15 const callbackB = sinon.stub();
16
17 Events.subscribe('test', callbackA);
18 Events.subscribe('test', callbackB);
19 Events.publish('test');
20
21 expect(callbackA.calledOnce).to.be.equal(true);
22 expect(callbackB.calledOnce).to.be.equal(true);
23 });
24
25 it('should call one callback twice if published twice', () => {
26 const callback = sinon.stub();
27
28 Events.subscribe('test', callback);
29
30 Events.publish('test');
31 Events.publish('test');
32
33 expect(callback.calledTwice).to.be.equal(true);
34 });
35
36 it('should call the callback asynchronously', (done) => {
37 const test = {
38 callback: (): void => {
39
40 },
41 };
42
43 sinon.stub(test, 'callback').callsFake(() => {
44 expect((test.callback as SinonSpy).calledOnce).to.equal(true);
45 done();
46 });
47 Events.subscribe('test', test.callback, true);
48 Events.publish('test');
49 expect((test.callback as SinonSpy).called).to.equal(false);
50 });
51 });
52
53 describe('once', () => {
54 it('should call the callback once, even when published twice', () => {
55 const callbackA = sinon.stub();
56 const callbackB = sinon.stub();
57
58 Events.once('test', callbackA);
59 Events.subscribe('test', callbackB);
60
61 Events.publish('test');
62 Events.publish('test');
63
64 expect(callbackA.calledOnce).to.be.equal(true);
65 expect(callbackB.calledTwice).to.be.equal(true);
66 });
67 });
68
69 describe('unsubscribe', () => {
70 it('should unsubscribe all callbacks from a single event', () => {
71 const callbackA = sinon.stub();
72 const callbackB = sinon.stub();
73 const callbackC = sinon.stub();
74
75 Events.subscribe('test', callbackA);
76 Events.subscribe('test', callbackB);
77 Events.subscribe('testC', callbackC);
78
79 Events.publish('test');
80
81 expect(callbackC.called).to.be.equal(false);
82 expect(callbackA.calledOnce).to.be.equal(true);
83 expect(callbackB.calledOnce).to.be.equal(true);
84
85 Events.publish('testC');
86
87 expect(callbackC.calledOnce).to.be.equal(true);
88 expect(callbackA.calledOnce).to.be.equal(true);
89 expect(callbackB.calledOnce).to.be.equal(true);
90
91 Events.unsubscribe('test');
92 Events.publish('test');
93
94 expect(callbackC.calledOnce).to.be.equal(true);
95 expect(callbackA.calledOnce).to.be.equal(true);
96 expect(callbackB.calledOnce).to.be.equal(true);
97 });
98
99 it('should unsubscribe a single callback from a single event', () => {
100 const callbackA = sinon.stub();
101 const callbackB = sinon.stub();
102
103 Events.subscribe('test', callbackA);
104 Events.subscribe('test', callbackB);
105
106 Events.publish('test');
107
108 expect(callbackA.calledOnce).to.be.equal(true);
109 expect(callbackB.calledOnce).to.be.equal(true);
110
111
112 Events.unsubscribe('test', callbackB);
113 Events.publish('test');
114
115 expect(callbackA.calledTwice).to.be.equal(true);
116 expect(callbackB.calledOnce).to.be.equal(true);
117 });
118 });
119});
test/quagga/image_wrapper.spec.tsadded+104-0
Changes to test/quagga/image_wrapper.spec.ts
@@ -0,0 +1,104 @@
1import { describe, it, beforeEach } from 'mocha';
2import { expect } from 'chai';
3import ImageWrapper from '../image_wrapper';
4
5describe('Image Wrapper', () => {
6 it('constructs', () => {
7 const iw = new ImageWrapper({ x: 320, y: 240, type: 'XYSize' }, undefined, Uint8Array, true);
8 expect(iw).to.be.an.instanceOf(ImageWrapper);
9 expect(iw.data).to.be.a('Uint8Array').with.lengthOf(320 * 240);
10 expect(iw.size).to.deep.equal({ x: 320, y: 240, type: 'XYSize' });
11 });
12 describe('inImageWithBorder', () => {
13 let iw: ImageWrapper;
14 // eslint-disable-next-line no-return-assign
15 beforeEach(() => iw = new ImageWrapper({ x: 320, y: 240, type: 'XYSize' }, undefined, Uint8Array, true));
16 it('returns true for location inside image', () => {
17 expect(iw.inImageWithBorder({ x: 100, y: 100, type: 'XYSize' })).to.equal(true);
18 });
19 it('returns false for location outside image', () => {
20 expect(iw.inImageWithBorder({ x: 1000, y: 1000, type: 'XYSize' })).to.equal(false);
21 });
22 it('returns true for locations inside border area', () => {
23 expect(iw.inImageWithBorder({ x: 325, y: 245, type: 'XYSize' }, 15)).to.equal(true);
24 });
25 it('returns false for location outside image and border', () => {
26 expect(iw.inImageWithBorder({ x: 330, y: 250, type: 'XYSize' }, 5)).to.equal(false);
27 });
28 it('throws with negative borders', () => {
29 expect(iw.inImageWithBorder.bind(iw, { x: 320, y: 240, type: 'XYSize' }, -240)).to.throw();
30 });
31 });
32 describe('subImageAsCopy', () => {
33 let iw: ImageWrapper;
34 // eslint-disable-next-line no-return-assign
35 beforeEach(() => iw = new ImageWrapper({ x: 320, y: 240, type: 'XYSize' }, undefined, Uint8Array, true));
36 it('returns a correctly sized image', () => {
37 const testIW = iw.subImageAsCopy(new ImageWrapper({ x: 220, y: 140, type: 'XYSize' }), { x: 100, y: 100, type: 'XYSize' });
38 expect(testIW).to.be.an.instanceOf(ImageWrapper);
39 expect(testIW.data).to.be.a('Uint8Array').with.lengthOf(220 * 140);
40 expect(testIW.size).to.deep.equal({ x: 220, y: 140, type: 'XYSize' });
41 });
42 it('TODO: write a test that does this with a real image and compares');
43 });
44 describe('set/get/getSafe', () => {
45 let iw: ImageWrapper;
46 // eslint-disable-next-line no-return-assign
47 beforeEach(() => iw = new ImageWrapper({ x: 320, y: 240, type: 'XYSize' }, undefined, Uint8Array, true));
48 it('set and get', () => {
49 expect(iw.get(100, 100)).to.equal(0);
50 iw.set(100, 100, 255);
51 expect(iw.get(100, 100)).to.equal(255);
52 });
53 it('set and getSafe', () => {
54 expect(iw.getSafe(100, 100)).to.equal(0);
55 iw.set(100, 100, 255);
56 expect(iw.getSafe(100, 100)).to.equal(255);
57 });
58 // Today, I learned that Uint8Array will silently ignore
59 // out of bound sets, and return undefined for out of bounds
60 // queries.
61 it('get returns undefined for out-of-bounds', () => {
62 expect(iw.get(5000, 5000)).to.equal(undefined);
63 });
64 it('set does nothing for out-of-bounds', () => {
65 expect(iw.set.bind(iw, 5000, 5000, 5000)).to.not.throw();
66 });
67 it('getSafe returns undefined for out-of-bounds', () => {
68 expect(iw.getSafe(5000, 5000)).to.equal(undefined);
69 });
70 });
71 describe.skip('moments', () => {
72 it('TODO');
73 });
74 describe('getAsRGBA', () => {
75 let iw: ImageWrapper;
76 // eslint-disable-next-line no-return-assign
77 beforeEach(() => iw = new ImageWrapper({ x: 320, y: 240, type: 'XYSize' }, undefined, Uint8Array, true));
78 it('returns a 1:1 copy by default', () => {
79 iw.set(0, 0, 128);
80 iw.set(200, 200, 227);
81 const image = iw.getAsRGBA();
82 // cy.log(`${JSON.stringify(image)}`);
83 expect(image).to.be.a('Uint8ClampedArray').with.lengthOf(4 * 320 * 240);
84 expect(image[0]).to.equal(128);
85 expect(image[1]).to.equal(128);
86 expect(image[2]).to.equal(128);
87 expect(image[3]).to.equal(255);
88 expect(image[4]).to.equal(0);
89 const pixel = (200 * 320 + 200) * 4;
90 expect(image[pixel + 0]).to.equal(227);
91 expect(image[pixel + 1]).to.equal(227);
92 expect(image[pixel + 2]).to.equal(227);
93 expect(image[pixel + 3]).to.equal(255);
94 expect(image[pixel + 4]).to.equal(0);
95 });
96 it('TODO: test rescaling');
97 });
98 describe('show', () => {
99 it('TODO: create a CanvasElement and load an image into it and test this. expect it to blow up righteously in node');
100 });
101 describe('overlay', () => {
102 it('TODO: same as show but for overlay');
103 });
104});
test/quagga/server/camera_access.spec.tsadded+47-0
Changes to test/quagga/server/camera_access.spec.ts
@@ -0,0 +1,47 @@
1import { describe, it } from 'mocha';
2import { expect } from 'chai';
3import CameraAccess from '../../camera_access';
4
5const Quagga = { CameraAccess };
6
7describe('CameraAccess (node)', () => {
8 describe('enumerateVideoDevices', () => {
9 it('rejects', async () => {
10 try {
11 const x = await Quagga.CameraAccess.enumerateVideoDevices();
12 // eslint-disable-next-line @typescript-eslint/no-unused-expressions,no-unused-expressions
13 expect(x).to.not.exist;
14 } catch (err: any) { // TODO: error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
15 // expect(err.message).to.equal('enumerateDevices is not defined');
16 expect(err.code).to.equal(-1);
17 }
18 });
19 });
20
21 describe('request', () => {
22 it('rejects', async () => {
23 try {
24 const x = await Quagga.CameraAccess.request(null, {});
25 // eslint-disable-next-line @typescript-eslint/no-unused-expressions,no-unused-expressions
26 expect(x).to.not.exist;
27 } catch (err: any) { // TODO: error TS1196: Catch clause variable type annotation must be 'any' or 'unknown' if specified.
28 // expect(err.message).to.equal('getUserMedia is not defined');
29 expect(err.code).to.equal(-1);
30 }
31 });
32 });
33
34 describe('release', () => {
35 it('works (no-op)', () => {
36 // eslint-disable-next-line @typescript-eslint/no-unused-expressions,no-unused-expressions
37 expect(Quagga.CameraAccess.release).to.not.throw;
38 });
39 });
40
41 describe('getActiveStreamLabel', () => {
42 it('no active stream', () => {
43 const x = Quagga.CameraAccess.getActiveStreamLabel();
44 expect(x).to.equal('');
45 });
46 });
47});
test/quagga/server/mediaDevices.spec.tsadded+28-0
Changes to test/quagga/server/mediaDevices.spec.ts
@@ -0,0 +1,28 @@
1import { expect } from 'chai'
2import { describe, it } from 'mocha'
3import { enumerateDevices, getUserMedia } from '../../mediaDevices'
4
5describe('mediaDevices (node)', () => {
6 it('enumerateDevices rejects', async () => {
7 try {
8 const x = await enumerateDevices()
9 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
10 expect(x).to.not.exist
11 }
12 catch (err: any) { // TODO: TS 4.4.4 gives us the welcome 'unknown' default for catches, but doesn't allow us to specify anything other than 'any'!! ?????
13 // expect(err.message).to.equal('enumerateDevices is not defined');
14 expect(err.code).to.equal(-1)
15 }
16 })
17 it('getUserMedia rejects', async () => {
18 try {
19 const x = await getUserMedia({})
20 // eslint-disable-next-line @typescript-eslint/no-unused-expressions
21 expect(x).to.not.exist
22 }
23 catch (err: any) { // TODO: TS 4.4.4+ improvement?
24 // expect(err.message).to.equal('getUserMedia is not defined');
25 expect(err.code).to.equal(-1)
26 }
27 })
28})