also looking at this
chore(deps): update dependency react to 19.2.7
#2137
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.
| @@ -296,8 +296,9 @@ We would like to extend our thanks to the following sponsors for funding Stacks | ||
| 296 | 296 | |
| 297 | 297 | Many thanks for the libraries that laid the groundwork: |
| 298 | 298 | |
| 299 | - **QRCode.js**: <https://github.com/davidshimjs/qrcodejs> | |
| 300 | 299 | - **JsBarcode**: <https://github.com/lindell/JsBarcode> |
| 300 | - **Quagga**: <https://github.com/ericblade/quagga2> | |
| 301 | - **QRCode.js**: <https://github.com/davidshimjs/qrcodejs> | |
| 301 | 302 | |
| 302 | 303 | ## License |
| 303 | 304 | |
| @@ -20,6 +20,13 @@ | ||
| 20 | 20 | "qr", |
| 21 | 21 | "bar", |
| 22 | 22 | "code", |
| 23 | "ean", | |
| 24 | "code128", | |
| 25 | "code39", | |
| 26 | "codabar", | |
| 27 | "i2of5", | |
| 28 | "upc", | |
| 29 | "getUserMedia", | |
| 23 | 30 | "generate", |
| 24 | 31 | "read", |
| 25 | 32 | "lightweight", |
| @@ -64,6 +71,13 @@ | ||
| 64 | 71 | "test": "bun test", |
| 65 | 72 | "typecheck": "bun --bun tsc --noEmit" |
| 66 | 73 | }, |
| 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 | }, | |
| 67 | 81 | "devDependencies": { |
| 68 | 82 | "@types/bun": "^1.1.14", |
| 69 | 83 | "bun-plugin-dtsx": "^0.21.9", |
| @@ -0,0 +1,41 @@ | ||
| 1 | import type ImageWrapper from 'common/image_wrapper' | |
| 2 | import type { QuaggaJSConfigObject } from '../type-definitions/quagga' | |
| 3 | ||
| 4 | export 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 | ||
| 28 | export class CanvasInfo { | |
| 29 | image: any | |
| 30 | overlay: any | |
| 31 | } | |
| 32 | ||
| 33 | export 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 | } | |
| @@ -0,0 +1,64 @@ | ||
| 1 | import type { | |
| 2 | QuaggaImageData, | |
| 3 | QuaggaJSCodeResult, | |
| 4 | QuaggaJSResultCollector, | |
| 5 | QuaggaJSResultCollectorFilterFunction, | |
| 6 | XYSize, | |
| 7 | } from '../../type-definitions/quagga.d' | |
| 8 | import ImageDebug from '../common/image_debug' | |
| 9 | ||
| 10 | function 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 | ||
| 17 | function passesFilter( | |
| 18 | codeResult: QuaggaJSCodeResult, | |
| 19 | filter: QuaggaJSResultCollectorFilterFunction | undefined, | |
| 20 | ): boolean { | |
| 21 | return typeof filter === 'function' ? filter(codeResult) : true | |
| 22 | } | |
| 23 | ||
| 24 | interface ResultCollector { | |
| 25 | addResult: (data: QuaggaImageData, imageSize: XYSize, codeResult: QuaggaJSCodeResult) => void | |
| 26 | getResults: () => Array<QuaggaJSCodeResult> | |
| 27 | } | |
| 28 | ||
| 29 | export 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 | } | |
| @@ -0,0 +1,74 @@ | ||
| 1 | /* eslint-disable no-param-reassign */ | |
| 2 | import { TypedArray } from '../../type-definitions/quagga'; | |
| 3 | ||
| 4 | export 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 | }; | |
| @@ -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 | ||
| 4 | import { glMatrix, vec2 } from 'gl-matrix'; | |
| 5 | ||
| 6 | glMatrix.setMatrixArrayType(Array); | |
| 7 | /** | |
| 8 | * Creates a cluster for grouping similar orientations of datapoints | |
| 9 | */ | |
| 10 | export 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 | }; | |
| @@ -0,0 +1,773 @@ | ||
| 1 | /* eslint-disable no-mixed-operators */ | |
| 2 | /* eslint-disable no-bitwise */ | |
| 3 | import { glMatrix, vec2, vec3 } from 'gl-matrix'; | |
| 4 | import ArrayHelper from './array_helper'; | |
| 5 | import Cluster2 from './cluster'; | |
| 6 | ||
| 7 | glMatrix.setMatrixArrayType(Array); | |
| 8 | ||
| 9 | /** | |
| 10 | * @param x x-coordinate | |
| 11 | * @param y y-coordinate | |
| 12 | * @return ImageReference {x,y} Coordinate | |
| 13 | */ | |
| 14 | export 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 | */ | |
| 37 | export 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 | ||
| 81 | export 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 | ||
| 103 | export 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 | ||
| 116 | export 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 | ||
| 133 | export 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 | ||
| 151 | export 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 | ||
| 203 | export function otsuThreshold(imageWrapper, targetWrapper) { | |
| 204 | const threshold = determineOtsuThreshold(imageWrapper); | |
| 205 | ||
| 206 | thresholdImage(imageWrapper, threshold, targetWrapper); | |
| 207 | return threshold; | |
| 208 | } | |
| 209 | ||
| 210 | // local thresholding | |
| 211 | export 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 | ||
| 255 | export 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 | ||
| 286 | export 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 | ||
| 368 | export const DILATE = 1; | |
| 369 | export const ERODE = 2; | |
| 370 | ||
| 371 | export 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 | ||
| 398 | export 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 | ||
| 425 | export 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 | ||
| 440 | export 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 | ||
| 455 | export 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 | ||
| 466 | export 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 | ||
| 496 | export 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 | ||
| 502 | export 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 | ||
| 507 | export 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 | ||
| 541 | export 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 | ||
| 558 | export 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 | */ | |
| 585 | export 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 | ||
| 608 | export 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 | ||
| 647 | export 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 | ||
| 662 | function _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 | ||
| 681 | export 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 | ||
| 730 | export function _parseCSSDimensionValues(value) { | |
| 731 | const dimension = { | |
| 732 | value: parseFloat(value), | |
| 733 | unit: value.indexOf('%') === value.length - 1 ? '%' : '%', | |
| 734 | }; | |
| 735 | ||
| 736 | return dimension; | |
| 737 | } | |
| 738 | ||
| 739 | export 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 | ||
| 754 | export 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 | } | |
| @@ -0,0 +1,109 @@ | ||
| 1 | type EventName = string; | |
| 2 | ||
| 3 | interface Subscription { | |
| 4 | async?: boolean; | |
| 5 | callback: Function; | |
| 6 | once?: boolean; | |
| 7 | } | |
| 8 | ||
| 9 | interface EventData { | |
| 10 | subscribers: Array<Subscription>; | |
| 11 | } | |
| 12 | ||
| 13 | interface Events { | |
| 14 | [key: string]: EventData; | |
| 15 | } | |
| 16 | ||
| 17 | interface 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 | ||
| 24 | export 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 | }()); | |
| @@ -0,0 +1,62 @@ | ||
| 1 | import { XYSize } from '../../type-definitions/quagga.d'; | |
| 2 | ||
| 3 | // TODO: XYPosition should be an XYObject, but that breaks XYDefinition, which breaks drawPath() below. | |
| 4 | declare interface XYPosition { | |
| 5 | x: number; | |
| 6 | y: number; | |
| 7 | } | |
| 8 | ||
| 9 | declare 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. | |
| 16 | declare interface XYDefinition { | |
| 17 | x: keyof XYPosition; | |
| 18 | y: keyof XYPosition; | |
| 19 | } | |
| 20 | ||
| 21 | declare type Path = Array<XYPosition>; | |
| 22 | ||
| 23 | export 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 | }; | |
| @@ -0,0 +1,272 @@ | ||
| 1 | import { glMatrix, vec2 } from 'gl-matrix'; | |
| 2 | import { | |
| 3 | XYSize, | |
| 4 | TypedArrayConstructor, | |
| 5 | TypedArray, | |
| 6 | WrapperIndexMapping, | |
| 7 | Moment, | |
| 8 | SparseImageWrapper, | |
| 9 | } from '../../type-definitions/quagga.d'; | |
| 10 | import ArrayHelper from './array_helper'; | |
| 11 | import { hsv2rgb } from './cv_utils'; | |
| 12 | ||
| 13 | glMatrix.setMatrixArrayType(Array); | |
| 14 | ||
| 15 | type PositiveNumber = number; | |
| 16 | function assertNumberPositive(val: number): asserts val is PositiveNumber { | |
| 17 | if (val < 0) { | |
| 18 | throw new Error(`expected positive number, received ${val}`); | |
| 19 | } | |
| 20 | } | |
| 21 | ||
| 22 | class 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 | ||
| 272 | export default ImageWrapper; | |
| @@ -0,0 +1,21 @@ | ||
| 1 | import Exception from '../quagga/Exception'; | |
| 2 | ||
| 3 | const 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 | ||
| 5 | export 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 | ||
| 14 | export 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 | } | |
| @@ -0,0 +1,72 @@ | ||
| 1 | import type { ImageRef, SparseImageWrapper, XYSize } from '../../type-definitions/quagga.d'; | |
| 2 | import ImageWrapper from './image_wrapper'; | |
| 3 | ||
| 4 | // A window into a specific area of an ImageWrapper | |
| 5 | export 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 | ||
| 72 | export default SubImage; | |
| @@ -0,0 +1,61 @@ | ||
| 1 | /* | |
| 2 | * typedefs.js | |
| 3 | * Normalizes browser-specific prefixes and provide some basic polyfills | |
| 4 | */ | |
| 5 | ||
| 6 | if (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 | ||
| 20 | if (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 | ||
| 34 | if (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 | } | |
| @@ -0,0 +1,55 @@ | ||
| 1 | import type { QuaggaJSConfigObject } from '../../type-definitions/quagga.d' | |
| 2 | ||
| 3 | const 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 | ||
| 55 | export default DevConfig | |
| @@ -0,0 +1,29 @@ | ||
| 1 | import type { QuaggaJSConfigObject } from '../../type-definitions/quagga.d' | |
| 2 | ||
| 3 | const 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 | ||
| 29 | export default NodeConfig | |
| @@ -0,0 +1,35 @@ | ||
| 1 | import type { QuaggaJSConfigObject } from '../../type-definitions/quagga.d' | |
| 2 | ||
| 3 | const 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 | ||
| 35 | export default ProdConfig | |
| @@ -0,0 +1,20 @@ | ||
| 1 | import type { QuaggaJSConfigObject } from '../../type-definitions/quagga.d' | |
| 2 | import DevConfig from './config.dev' | |
| 3 | import NodeConfig from './config.node' | |
| 4 | import ProdConfig from './config.prod' | |
| 5 | ||
| 6 | const 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 | ||
| 20 | export default ExportConfig | |
| @@ -0,0 +1,359 @@ | ||
| 1 | import ImageDebug from '../common/image_debug' | |
| 2 | import TwoOfFiveReader from '../reader/2of5_reader' | |
| 3 | import CodabarReader from '../reader/codabar_reader' | |
| 4 | import Code32Reader from '../reader/code_32_reader' | |
| 5 | import Code39Reader from '../reader/code_39_reader' | |
| 6 | import Code39VINReader from '../reader/code_39_vin_reader' | |
| 7 | import Code93Reader from '../reader/code_93_reader' | |
| 8 | import Code128Reader from '../reader/code_128_reader' | |
| 9 | import EAN2Reader from '../reader/ean_2_reader' | |
| 10 | import EAN5Reader from '../reader/ean_5_reader' | |
| 11 | import EAN8Reader from '../reader/ean_8_reader' | |
| 12 | import EANReader from '../reader/ean_reader' | |
| 13 | import I2of5Reader from '../reader/i2of5_reader' | |
| 14 | import UPCEReader from '../reader/upc_e_reader' | |
| 15 | import UPCReader from '../reader/upc_reader' | |
| 16 | import Bresenham from './bresenham' | |
| 17 | ||
| 18 | const 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 | ||
| 35 | export 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 | } | |
| @@ -0,0 +1,225 @@ | ||
| 1 | const Slope = { | |
| 2 | DIR: { | |
| 3 | UP: 1, | |
| 4 | DOWN: -1, | |
| 5 | }, | |
| 6 | } | |
| 7 | ||
| 8 | export 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 | ||
| 51 | const 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 | ||
| 225 | export default Bresenham | |
| @@ -0,0 +1,170 @@ | ||
| 1 | import 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. | |
| 8 | import pick from 'lodash/pick' | |
| 9 | import { enumerateDevices, getUserMedia } from '../common/mediaDevices' | |
| 10 | import Exception from '../quagga/Exception' | |
| 11 | ||
| 12 | let streamRef: MediaStream | null | |
| 13 | ||
| 14 | function 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 | */ | |
| 45 | async 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 | ||
| 64 | function 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. | |
| 82 | export 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 | ||
| 91 | async function enumerateVideoDevices(): Promise<Array<MediaDeviceInfo>> { | |
| 92 | const devices = await enumerateDevices() | |
| 93 | return devices.filter((device: MediaDeviceInfo) => device.kind === 'videoinput') | |
| 94 | } | |
| 95 | ||
| 96 | function 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 | */ | |
| 107 | const 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 | ||
| 170 | export default QuaggaJSCameraAccess | |
| @@ -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 | |
| 6 | const ExifTags = { 0x0112: 'orientation' } | |
| 7 | export const AvailableTags = Object.keys(ExifTags).map(key => ExifTags[key]) | |
| 8 | ||
| 9 | export 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 | ||
| 18 | export 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 | ||
| 31 | function 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 | ||
| 41 | function 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 | ||
| 56 | export 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 | ||
| 88 | function 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 | ||
| 119 | function 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 | ||
| 133 | function 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 | ||
| 147 | function 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 | } | |
| @@ -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 | ||
| 4 | const Ndarray = require('ndarray') | |
| 5 | const CVUtils = require('../common/cv_utils') | |
| 6 | const Interp2D = require('ndarray-linear-interpolate').d2 | |
| 7 | ||
| 8 | const FrameGrabber = {} | |
| 9 | ||
| 10 | FrameGrabber.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 | ||
| 100 | module.exports = FrameGrabber | |
| @@ -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 | ||
| 4 | import { | |
| 5 | computeGray, | |
| 6 | grayAndHalfSampleFromCanvasData, | |
| 7 | imageRef, | |
| 8 | } from '../common/cv_utils' | |
| 9 | ||
| 10 | const TO_RADIANS = Math.PI / 180 | |
| 11 | ||
| 12 | function 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 | ||
| 27 | const FrameGrabber = {} | |
| 28 | ||
| 29 | FrameGrabber.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 | ||
| 129 | export default FrameGrabber | |
| @@ -0,0 +1,74 @@ | ||
| 1 | import { findTagsInObjectURL } from './exif_helper' | |
| 2 | ||
| 3 | const ImageLoader = {} | |
| 4 | ImageLoader.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 | ||
| 68 | function addOnloadHandler(img, htmlImagesArray) { | |
| 69 | img.onload = function () { | |
| 70 | htmlImagesArray.loaded(this) | |
| 71 | } | |
| 72 | } | |
| 73 | ||
| 74 | export default (ImageLoader) | |
| @@ -0,0 +1,41 @@ | ||
| 1 | import type { Point, XYSize } from '../../../type-definitions/quagga.d' | |
| 2 | ||
| 3 | type EventHandler = (arg?: any) => void | |
| 4 | ||
| 5 | export interface EventHandlerList { | |
| 6 | [index: string]: Array<EventHandler> | |
| 7 | } | |
| 8 | ||
| 9 | export 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 | ||
| 33 | type VideoStreamFactory = (video: HTMLVideoElement) => InputStream | |
| 34 | type LiveStreamFactory = (video: HTMLVideoElement) => InputStream | |
| 35 | type ImageStreamFactory = () => InputStream | |
| 36 | ||
| 37 | export interface InputStreamFactory { | |
| 38 | createImageStream: ImageStreamFactory | |
| 39 | createLiveStream: LiveStreamFactory | |
| 40 | createVideoStream: VideoStreamFactory | |
| 41 | } | |
| @@ -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 | ||
| 5 | import type { Point, XYSize } from '../../../type-definitions/quagga.d' | |
| 6 | import type { EventHandlerList, InputStream, InputStreamFactory } from './input_stream' | |
| 7 | import GetPixels from 'get-pixels' | |
| 8 | ||
| 9 | const 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 | ||
| 194 | export default inputStreamFactory | |
| @@ -0,0 +1,363 @@ | ||
| 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ | |
| 2 | ||
| 3 | import type { Point, XYSize } from '../../../type-definitions/quagga.d' | |
| 4 | import type { EventHandlerList, InputStream, InputStreamFactory } from './input_stream' | |
| 5 | import ImageLoader from '../image_loader' | |
| 6 | ||
| 7 | const 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 | ||
| 363 | export default inputStreamFactory | |
| @@ -0,0 +1,589 @@ | ||
| 1 | import { glMatrix, mat2, vec2 } from 'gl-matrix' | |
| 2 | import ArrayHelper from '../common/array_helper' | |
| 3 | import { | |
| 4 | calculatePatchSize, | |
| 5 | cluster, | |
| 6 | computeImageArea, | |
| 7 | halfSample, | |
| 8 | hsv2rgb, | |
| 9 | imageRef, | |
| 10 | otsuThreshold, | |
| 11 | topGeneric, | |
| 12 | } from '../common/cv_utils' | |
| 13 | import ImageDebug from '../common/image_debug' | |
| 14 | import ImageWrapper from '../common/image_wrapper' | |
| 15 | import Rasterizer from './rasterizer' | |
| 16 | import skeletonizer from './skeletonizer' | |
| 17 | import Tracer from './tracer' | |
| 18 | ||
| 19 | glMatrix.setMatrixArrayType(Array) | |
| 20 | ||
| 21 | let _config | |
| 22 | let _currentImageWrapper | |
| 23 | let _skelImageWrapper | |
| 24 | let _subImageWrapper | |
| 25 | let _labelImageWrapper | |
| 26 | let _patchGrid | |
| 27 | let _patchLabelGrid | |
| 28 | let _imageToPatchGrid | |
| 29 | let _binaryImageWrapper | |
| 30 | let _patchSize | |
| 31 | const _canvasContainer = { | |
| 32 | ctx: { | |
| 33 | binary: null, | |
| 34 | }, | |
| 35 | dom: { | |
| 36 | binary: null, | |
| 37 | }, | |
| 38 | } | |
| 39 | const _numPatches = { x: 0, y: 0 } | |
| 40 | let _inputImageWrapper | |
| 41 | let _skeletonizer | |
| 42 | ||
| 43 | function 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 | ||
| 85 | function 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 | */ | |
| 105 | function 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 | */ | |
| 195 | function 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 | */ | |
| 207 | function 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 | */ | |
| 258 | function 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 | */ | |
| 290 | function 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 | */ | |
| 332 | function 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 | ||
| 346 | function 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 | */ | |
| 364 | function 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 | */ | |
| 423 | function 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 | ||
| 515 | export 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 | } | |
| @@ -0,0 +1,204 @@ | ||
| 1 | import Tracer from './tracer' | |
| 2 | ||
| 3 | /** | |
| 4 | * http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization | |
| 5 | */ | |
| 6 | const 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 | ||
| 204 | export default Rasterizer | |
| @@ -0,0 +1,206 @@ | ||
| 1 | /* eslint-disable eqeqeq */ | |
| 2 | ||
| 3 | /* @preserve ASM BEGIN */ | |
| 4 | function 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 */ | |
| 205 | export default Skeletonizer | |
| 206 | /* eslint-enable eqeqeq */ | |
| @@ -0,0 +1,104 @@ | ||
| 1 | /** | |
| 2 | * http://www.codeproject.com/Tips/407172/Connected-Component-Labeling-and-Vectorization | |
| 3 | */ | |
| 4 | const 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 | ||
| 104 | export default (Tracer) | |
| @@ -0,0 +1,161 @@ | ||
| 1 | import merge from 'lodash/merge' | |
| 2 | import ResultCollector from './analytics/result_collector' | |
| 3 | import Events from './common/events' | |
| 4 | import ImageDebug from './common/image_debug' | |
| 5 | import ImageWrapper from './common/image_wrapper' | |
| 6 | import TypeDefs from './common/typedefs' // eslint-disable-line no-unused-vars | |
| 7 | import Config from './config/config' | |
| 8 | import BarcodeDecoder from './decoder/barcode_decoder' | |
| 9 | import CameraAccess from './input/camera_access' | |
| 10 | import Quagga from './quagga/quagga' | |
| 11 | ||
| 12 | import * as Readers from './reader/index' | |
| 13 | ||
| 14 | const instance = new Quagga() | |
| 15 | const _context = instance.context | |
| 16 | ||
| 17 | const 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 | ||
| 152 | export default QuaggaJSStaticInterface | |
| 153 | // export BarcodeReader and other utilities for external plugins | |
| 154 | export { | |
| 155 | BarcodeDecoder, | |
| 156 | CameraAccess, | |
| 157 | ImageDebug, | |
| 158 | ImageWrapper, | |
| 159 | Readers, | |
| 160 | ResultCollector, | |
| 161 | } | |
| @@ -0,0 +1,13 @@ | ||
| 1 | export 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 | } | |
| @@ -0,0 +1,31 @@ | ||
| 1 | import type { InputStream } from '../input/input_stream/input_stream' | |
| 2 | import { glMatrix, vec2 } from 'gl-matrix' | |
| 3 | import ImageWrapper from '../common/image_wrapper' | |
| 4 | import BarcodeLocator from '../locator/barcode_locator' | |
| 5 | ||
| 6 | glMatrix.setMatrixArrayType(Array) | |
| 7 | ||
| 8 | // TODO: need typescript def for BarcodeLocator | |
| 9 | export 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 | } | |
| @@ -0,0 +1,65 @@ | ||
| 1 | import type { CanvasContainer, QuaggaContext } from 'QuaggaContext' | |
| 2 | import type { XYSize } from '../../type-definitions/quagga.d' | |
| 3 | import getViewPort from './getViewPort' | |
| 4 | ||
| 5 | function 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 | ||
| 14 | function 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 | ||
| 21 | function 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 | ||
| 44 | export 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 | } | |
| @@ -0,0 +1,294 @@ | ||
| 1 | import type { QuaggaJSReaderConfig, QuaggaJSResultObject } from '../../type-definitions/quagga.d' | |
| 2 | import type ImageWrapper from '../common/image_wrapper.js' | |
| 3 | import type { BarcodeInfo } from '../reader/barcode_reader.js' | |
| 4 | import { glMatrix, vec2 } from 'gl-matrix' | |
| 5 | import Events from '../common/events.js' | |
| 6 | import BarcodeDecoder from '../decoder/barcode_decoder.js' | |
| 7 | import CameraAccess from '../input/camera_access.js' | |
| 8 | import FrameGrabber from '../input/frame_grabber.js' | |
| 9 | import InputStream from '../input/input_stream/input_stream.js' | |
| 10 | import BarcodeLocator from '../locator/barcode_locator.js' | |
| 11 | import { QuaggaContext } from '../QuaggaContext.js' | |
| 12 | import _getViewPort from './getViewPort.js' | |
| 13 | import _initBuffers from './initBuffers.js' | |
| 14 | import _initCanvas from './initCanvas.js' | |
| 15 | import * as QWorkers from './qworker.js' | |
| 16 | import setupInputStream from './setupInputStream.js' | |
| 17 | import { moveBox, moveLine } from './transform.js' | |
| 18 | ||
| 19 | glMatrix.setMatrixArrayType(Array) | |
| 20 | ||
| 21 | export 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 | } | |
| @@ -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 | ||
| 6 | import type { QuaggaJSConfigObject, QuaggaJSReaderConfig } from '../../type-definitions/quagga' | |
| 7 | ||
| 8 | // TODO: need a typescript interface for FrameGrabber | |
| 9 | ||
| 10 | interface QWorkerThread { | |
| 11 | imageData: Uint8Array | |
| 12 | busy: boolean | |
| 13 | worker: Worker | |
| 14 | } | |
| 15 | ||
| 16 | let workerPool: Array<QWorkerThread> = [] | |
| 17 | ||
| 18 | export 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 | ||
| 40 | function configForWorker(config: QuaggaJSConfigObject) { | |
| 41 | return { | |
| 42 | ...config, | |
| 43 | inputStream: { | |
| 44 | ...config.inputStream, | |
| 45 | target: null, | |
| 46 | }, | |
| 47 | } | |
| 48 | } | |
| 49 | ||
| 50 | // @ts-ignore | |
| 51 | function 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 | ||
| 109 | function 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 | ||
| 126 | export 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 | ||
| 171 | export 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 | ||
| 205 | export function setReaders(readers: Array<QuaggaJSReaderConfig>) { | |
| 206 | workerPool.forEach(workerThread => workerThread.worker.postMessage({ cmd: 'setReaders', readers })) | |
| 207 | } | |
| 208 | ||
| 209 | export function registerReader(name: string, reader: any) { | |
| 210 | workerPool.forEach(workerThread => workerThread.worker.postMessage({ cmd: 'registerReader', name, reader })) | |
| 211 | } | |
| @@ -0,0 +1,34 @@ | ||
| 1 | import type { InputStreamFactory } from 'input/input_stream/input_stream.d' | |
| 2 | import 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 | |
| 5 | export 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 | } | |
| @@ -0,0 +1,20 @@ | ||
| 1 | import type { Point } from '../../type-definitions/quagga.d' | |
| 2 | ||
| 3 | export type Box = Array<[ number, number ]> | |
| 4 | ||
| 5 | export type Line = [ Point, Point ] | |
| 6 | ||
| 7 | export 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 | ||
| 15 | export 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 | } | |
| @@ -0,0 +1,230 @@ | ||
| 1 | import type { Barcode, BarcodeInfo, BarcodePosition } from './barcode_reader' | |
| 2 | import BarcodeReader from './barcode_reader' | |
| 3 | ||
| 4 | const N = 1 | |
| 5 | const W = 3 | |
| 6 | const START_PATTERN = [W, N, W, N, N, N] | |
| 7 | const STOP_PATTERN = [W, N, N, N, W] | |
| 8 | const 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 | ] | |
| 20 | const START_PATTERN_LENGTH = START_PATTERN.reduce((sum, val) => sum + val, 0) | |
| 21 | ||
| 22 | class 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 | ||
| 230 | export default TwoOfFiveReader | |
| @@ -0,0 +1,231 @@ | ||
| 1 | import type { ImageWrapper } from 'quagga' | |
| 2 | import type { QuaggaJSResultObject } from '../../type-definitions/quagga' | |
| 3 | import ArrayHelper from '../common/array_helper' | |
| 4 | ||
| 5 | // for some reason this throws a shadow error on itself?! | |
| 6 | ||
| 7 | export enum BarcodeDirection { | |
| 8 | Forward = 1, | |
| 9 | Reverse = -1, | |
| 10 | } | |
| 11 | ||
| 12 | export type BarcodeReaderType = string | |
| 13 | export type BarcodeFormat = string | |
| 14 | ||
| 15 | export interface BarcodeReaderConfig { | |
| 16 | normalizeBarSpaceWidth?: boolean | |
| 17 | supplements?: Array<BarcodeReaderType> | |
| 18 | } | |
| 19 | ||
| 20 | export interface BarcodeCorrection { | |
| 21 | bar: number | |
| 22 | space: number | |
| 23 | } | |
| 24 | ||
| 25 | export interface BarcodePosition { | |
| 26 | end: number | |
| 27 | endCounter?: number | |
| 28 | error?: number | |
| 29 | start: number | |
| 30 | startCounter?: number | |
| 31 | } | |
| 32 | ||
| 33 | export interface BarcodeInfo extends BarcodePosition { | |
| 34 | code: number | |
| 35 | correction?: BarcodeCorrection | |
| 36 | } | |
| 37 | ||
| 38 | export 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 | ||
| 52 | export 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 | ||
| 231 | export default BarcodeReader | |
| @@ -0,0 +1,271 @@ | ||
| 1 | import type { Barcode, BarcodePosition } from './barcode_reader' | |
| 2 | import BarcodeReader from './barcode_reader' | |
| 3 | ||
| 4 | // const ALPHABETH_STRING = '0123456789-$:/.+ABCD'; | |
| 5 | const ALPHABET = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 45, 36, 58, 47, 46, 43, 65, 66, 67, 68] | |
| 6 | const CHARACTER_ENCODINGS | |
| 7 | = [0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, 0x00C, 0x018, 0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E] | |
| 8 | const START_END = [0x01A, 0x029, 0x00B, 0x00E] | |
| 9 | const MIN_ENCODED_CHARS = 4 | |
| 10 | const MAX_ACCEPTABLE = 2.0 | |
| 11 | const PADDING = 1.5 | |
| 12 | ||
| 13 | interface ThresholdSize { | |
| 14 | size: number | |
| 15 | counts: number | |
| 16 | min: number | |
| 17 | max: number | |
| 18 | }; | |
| 19 | ||
| 20 | interface Threshold { | |
| 21 | space: { | |
| 22 | narrow: ThresholdSize | |
| 23 | wide: ThresholdSize | |
| 24 | } | |
| 25 | bar: { | |
| 26 | narrow: ThresholdSize | |
| 27 | wide: ThresholdSize | |
| 28 | } | |
| 29 | }; | |
| 30 | ||
| 31 | class 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 | ||
| 271 | export default NewCodabarReader | |
| @@ -0,0 +1,468 @@ | ||
| 1 | import type { Barcode, BarcodeCorrection, BarcodeInfo, BarcodePosition } from './barcode_reader' | |
| 2 | import BarcodeReader from './barcode_reader' | |
| 3 | ||
| 4 | class 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 | ||
| 468 | export default Code128Reader | |
| @@ -0,0 +1,62 @@ | ||
| 1 | import Code39Reader from './code_39_reader' | |
| 2 | ||
| 3 | const patterns = { | |
| 4 | AEIO: /[AEIO]/g, | |
| 5 | AZ09: /[A-Z0-9]/, | |
| 6 | } | |
| 7 | ||
| 8 | const code32set = '0123456789BCDFGHJKLMNPQRSTUVWXYZ' | |
| 9 | ||
| 10 | class 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 | ||
| 62 | export default Code32Reader | |
| @@ -0,0 +1,211 @@ | ||
| 1 | import ArrayHelper from '../common/array_helper' | |
| 2 | import BarcodeReader, { type Barcode, type BarcodePosition } from './barcode_reader' | |
| 3 | ||
| 4 | const ALPHABETH_STRING = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. *$/+%' | |
| 5 | const ALPHABET = new Uint16Array([...ALPHABETH_STRING].map(char => char.charCodeAt(0))) | |
| 6 | const 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 | ]) | |
| 52 | const ASTERISK = 0x094 | |
| 53 | ||
| 54 | class 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 | ||
| 211 | export default Code39Reader | |
| @@ -0,0 +1,49 @@ | ||
| 1 | import type { Barcode } from './barcode_reader' | |
| 2 | import Code39Reader from './code_39_reader' | |
| 3 | ||
| 4 | const patterns = { | |
| 5 | IOQ: /[IOQ]/g, | |
| 6 | AZ09: /[A-Z0-9]{17}/, | |
| 7 | } | |
| 8 | ||
| 9 | class 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 | ||
| 49 | export default Code39VINReader | |
| @@ -0,0 +1,289 @@ | ||
| 1 | import type { Barcode, BarcodePosition } from './barcode_reader' | |
| 2 | import ArrayHelper from '../common/array_helper' | |
| 3 | import BarcodeReader from './barcode_reader' | |
| 4 | ||
| 5 | const ALPHABETH_STRING = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%abcd*' | |
| 6 | const ALPHABET = new Uint16Array([...ALPHABETH_STRING].map(char => char.charCodeAt(0))) | |
| 7 | const 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 | ]) | |
| 57 | const ASTERISK = 0x15E | |
| 58 | ||
| 59 | class 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 | ||
| 289 | export default Code93Reader | |
| @@ -0,0 +1,56 @@ | ||
| 1 | import EANReader, { CODE_G_START } from './ean_reader'; | |
| 2 | import { BarcodePosition, Barcode, BarcodeInfo } from './barcode_reader'; | |
| 3 | ||
| 4 | ||
| 5 | class 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 | ||
| 56 | export default EAN2Reader; | |
| @@ -0,0 +1,85 @@ | ||
| 1 | import type { Barcode, BarcodeInfo, BarcodePosition } from './barcode_reader' | |
| 2 | import EANReader, { CODE_G_START } from './ean_reader' | |
| 3 | ||
| 4 | const CHECK_DIGIT_ENCODINGS = [24, 20, 18, 17, 12, 6, 3, 10, 9, 5] | |
| 5 | ||
| 6 | function 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 | ||
| 15 | function 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 | ||
| 30 | class 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 | ||
| 85 | export default EAN5Reader | |
| @@ -0,0 +1,37 @@ | ||
| 1 | import type { BarcodeInfo, BarcodePosition } from './barcode_reader' | |
| 2 | import EANReader, { CODE_G_START, MIDDLE_PATTERN } from './ean_reader' | |
| 3 | ||
| 4 | class 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 | ||
| 37 | export default EAN8Reader | |
| @@ -0,0 +1,408 @@ | ||
| 1 | import type { Barcode, BarcodeInfo, BarcodePosition, BarcodeReaderConfig } from './barcode_reader' | |
| 2 | import merge from 'lodash/merge' | |
| 3 | import BarcodeReader from './barcode_reader' | |
| 4 | ||
| 5 | // const CODE_L_START = 0; | |
| 6 | const CODE_G_START = 10 | |
| 7 | export { CODE_G_START } | |
| 8 | const START_PATTERN = [1, 1, 1] | |
| 9 | const MIDDLE_PATTERN = [1, 1, 1, 1, 1] | |
| 10 | export { MIDDLE_PATTERN } | |
| 11 | const EXTENSION_START_PATTERN = [1, 1, 2] | |
| 12 | const 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 | ] | |
| 34 | const CODE_FREQUENCY = [0, 11, 13, 14, 19, 25, 28, 21, 22, 26] | |
| 35 | // const SINGLE_CODE_ERROR = 0.70; | |
| 36 | const AVG_CODE_ERROR = 0.48 | |
| 37 | ||
| 38 | class 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 | ||
| 408 | export default EANReader | |
| @@ -0,0 +1,277 @@ | ||
| 1 | // TODO: i2of5_reader and 2of5_reader share very similar code, make use of that | |
| 2 | ||
| 3 | import type { Barcode, BarcodeInfo, BarcodePosition, BarcodeReaderConfig } from './barcode_reader' | |
| 4 | import merge from 'lodash/merge' | |
| 5 | import BarcodeReader from './barcode_reader' | |
| 6 | ||
| 7 | const N = 1 | |
| 8 | const W = 3 | |
| 9 | ||
| 10 | class 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 | ||
| 277 | export default I2of5Reader | |
| @@ -0,0 +1,33 @@ | ||
| 1 | import TwoOfFiveReader from './2of5_reader' | |
| 2 | import BarcodeReader from './barcode_reader' | |
| 3 | import NewCodabarReader from './codabar_reader' | |
| 4 | import Code32Reader from './code_32_reader' | |
| 5 | import Code39Reader from './code_39_reader' | |
| 6 | import Code39VINReader from './code_39_vin_reader' | |
| 7 | import Code93Reader from './code_93_reader' | |
| 8 | import Code128Reader from './code_128_reader' | |
| 9 | import EAN2Reader from './ean_2_reader' | |
| 10 | import EAN5Reader from './ean_5_reader' | |
| 11 | import EAN8Reader from './ean_8_reader' | |
| 12 | import EANReader from './ean_reader' | |
| 13 | import I2of5Reader from './i2of5_reader' | |
| 14 | import UPCEReader from './upc_e_reader' | |
| 15 | import UPCReader from './upc_reader' | |
| 16 | ||
| 17 | export { | |
| 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 | } | |
| @@ -0,0 +1,93 @@ | ||
| 1 | import type { BarcodeInfo, BarcodePosition } from './barcode_reader' | |
| 2 | import EANReader, { CODE_G_START } from './ean_reader' | |
| 3 | ||
| 4 | class 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 | ||
| 93 | export default UPCEReader | |
| @@ -0,0 +1,18 @@ | ||
| 1 | import type { Barcode, BarcodePosition } from './barcode_reader' | |
| 2 | import EANReader from './ean_reader' | |
| 3 | ||
| 4 | class 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 | ||
| 18 | export default UPCReader | |
| @@ -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 | ||
| 5 | import 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 | ||
| 11 | declare const Quagga: QuaggaJSStatic | |
| 12 | export 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. | |
| 17 | export 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 | |
| 24 | export type ImageRef = XYObject<'ImageRef'> | |
| 25 | ||
| 26 | export type XYSize = XYObject<'XYSize'> | |
| 27 | ||
| 28 | export type Point = XYObject<'Point'> | |
| 29 | ||
| 30 | export interface SparseImageWrapper { | |
| 31 | data: TypedArray | Array<number> | null | |
| 32 | size: XYSize | |
| 33 | } | |
| 34 | ||
| 35 | export interface WrapperIndexMapping { | |
| 36 | x: Array<number> | |
| 37 | y: Array<number> | |
| 38 | } | |
| 39 | ||
| 40 | // eslint-disable-next-line @typescript-eslint/class-name-casing | |
| 41 | export 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 | ||
| 53 | export 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 | ||
| 88 | export 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 | ||
| 110 | export type QuaggaImageData = Array<number> | |
| 111 | ||
| 112 | export type BarcodeReaderType = string | |
| 113 | ||
| 114 | export interface BarcodeReaderConfig { | |
| 115 | normalizeBarSpaceWidth?: boolean | |
| 116 | supplements?: Array<BarcodeReaderType> | |
| 117 | } | |
| 118 | ||
| 119 | export enum BarcodeDirection { | |
| 120 | Forward = 1, | |
| 121 | Reverse = -1, | |
| 122 | } | |
| 123 | type BarcodeFormat = string | |
| 124 | ||
| 125 | export interface BarcodeCorrection { | |
| 126 | bar: number | |
| 127 | space: number | |
| 128 | } | |
| 129 | ||
| 130 | export interface BarcodePosition { | |
| 131 | end: number | |
| 132 | endCounter?: number | |
| 133 | error?: number | |
| 134 | start: number | |
| 135 | startCounter?: number | |
| 136 | } | |
| 137 | ||
| 138 | export interface BarcodeInfo extends BarcodePosition { | |
| 139 | code: number | |
| 140 | correction?: BarcodeCorrection | |
| 141 | } | |
| 142 | ||
| 143 | export 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 | ||
| 157 | export interface ThresholdSize { | |
| 158 | counts: number | |
| 159 | max: number | |
| 160 | min: number | |
| 161 | size: number | |
| 162 | } | |
| 163 | ||
| 164 | export interface Threshold { | |
| 165 | bar: { | |
| 166 | narrow: ThresholdSize | |
| 167 | wide: ThresholdSize | |
| 168 | } | |
| 169 | space: { | |
| 170 | narrow: ThresholdSize | |
| 171 | wide: ThresholdSize | |
| 172 | } | |
| 173 | } | |
| 174 | ||
| 175 | export 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 | ||
| 469 | export 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 | */ | |
| 578 | export 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 | */ | |
| 595 | export 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 | */ | |
| 607 | export 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 | */ | |
| 619 | export 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) | |
| 634 | export interface QuaggaJSxyDef { | |
| 635 | x: any | |
| 636 | y: any | |
| 637 | } | |
| 638 | ||
| 639 | /** | |
| 640 | * an object with an x and a y value | |
| 641 | */ | |
| 642 | export 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 | */ | |
| 652 | export 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 | */ | |
| 662 | export 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 | */ | |
| 672 | export 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 | */ | |
| 709 | export interface QuaggaJSCodeResult { | |
| 710 | code?: string | |
| 711 | format?: string | |
| 712 | } | |
| 713 | ||
| 714 | /** | |
| 715 | * Called to filter which Results to collect in ResultCollector | |
| 716 | */ | |
| 717 | export 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 | */ | |
| 729 | export 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 | |
| 745 | export 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 | ||
| 773 | export type InputStreamType = 'VideoStream' | 'ImageStream' | 'LiveStream' | |
| 774 | ||
| 775 | export 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 | ||
| 971 | export interface QuaggaJSReaderConfig { | |
| 972 | config: { | |
| 973 | supplements: string[] | |
| 974 | } | |
| 975 | format: string | |
| 976 | } | |
| 977 | ||
| 978 | export 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 | ||
| 984 | export type TypedArrayConstructor = | |
| 985 | Int8ArrayConstructor | |
| 986 | | Uint8ArrayConstructor | |
| 987 | | Uint8ClampedArrayConstructor | |
| 988 | | Int16ArrayConstructor | |
| 989 | | Uint16ArrayConstructor | |
| 990 | | Int32ArrayConstructor | |
| 991 | | Uint32ArrayConstructor | |
| 992 | | Float32ArrayConstructor | |
| 993 | | Float64ArrayConstructor | |
| 994 | ||
| 995 | export type TypedArray = | |
| 996 | Int8Array | |
| 997 | | Uint8Array | |
| 998 | | Uint8ClampedArray | |
| 999 | | Int16Array | |
| 1000 | | Uint16Array | |
| 1001 | | Int32Array | |
| 1002 | | Uint32Array | |
| 1003 | | Float32Array | |
| 1004 | | Float64Array | |
| 1005 | ||
| 1006 | export 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' | |
| @@ -0,0 +1,6 @@ | ||
| 1 | // typescript definitions for modules that we use that don't provide their own defs | |
| 2 | ||
| 3 | declare module 'get-pixels' { | |
| 4 | function GetPixels(url: string, type: string | undefined, callback: (err: any, pixels: any) => void): void | |
| 5 | export = GetPixels | |
| 6 | } | |
| @@ -0,0 +1,63 @@ | ||
| 1 | import { expect } from 'chai' | |
| 2 | import { after, before, describe, it } from 'mocha' | |
| 3 | import sinon from 'sinon' | |
| 4 | import ArrayHelper from '../array_helper' | |
| 5 | ||
| 6 | describe('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 | }) | |
| @@ -0,0 +1,170 @@ | ||
| 1 | import type { SinonSpy } from 'sinon' | |
| 2 | import type { QuaggaJSConfigObject } from '../../../type-definitions/quagga' | |
| 3 | import { expect } from 'chai' | |
| 4 | import merge from 'lodash/merge' | |
| 5 | import sinon from 'sinon' | |
| 6 | import QuaggaConfig from '../../config/config' | |
| 7 | import BarcodeLocator from '../barcode_locator' | |
| 8 | ||
| 9 | if (typeof (globalThis as any).ENV === 'undefined') { | |
| 10 | (globalThis as any).ENV = { production: true, development: false, node: true } | |
| 11 | } | |
| 12 | ||
| 13 | declare interface AreaConfig { | |
| 14 | area: { | |
| 15 | top: string | |
| 16 | right: string | |
| 17 | bottom: string | |
| 18 | left: string | |
| 19 | } | |
| 20 | }; | |
| 21 | ||
| 22 | describe('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 | }) | |
| @@ -0,0 +1,144 @@ | ||
| 1 | import type { MediaTrackConstraintsWithDeprecated } from '../../../../type-definitions/quagga.d' | |
| 2 | import { expect } from 'chai' | |
| 3 | import { after, describe, it } from 'mocha' | |
| 4 | import CameraAccess, { pickConstraints } from '../../camera_access' | |
| 5 | ||
| 6 | const Quagga = { CameraAccess } | |
| 7 | ||
| 8 | describe('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 | }) | |
| @@ -0,0 +1,59 @@ | ||
| 1 | import { | |
| 2 | base64ToArrayBuffer, | |
| 3 | findTagsInBuffer, | |
| 4 | findTagsInObjectURL, | |
| 5 | } from '../../exif_helper' | |
| 6 | ||
| 7 | const 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 | ||
| 17 | describe('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 | }) | |
| @@ -0,0 +1,24 @@ | ||
| 1 | import { expect } from 'chai' | |
| 2 | import { describe, it } from 'mocha' | |
| 3 | import { enumerateDevices, getUserMedia } from '../../mediaDevices' | |
| 4 | ||
| 5 | describe('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 | }) | |
| @@ -0,0 +1,121 @@ | ||
| 1 | import type { SinonSpy } from 'sinon' | |
| 2 | import type { QuaggaJSCodeResult, QuaggaJSResultCollector, XYSize } from '../../../../type-definitions/quagga.d' | |
| 3 | import { expect } from 'chai' | |
| 4 | import { | |
| 5 | afterEach, | |
| 6 | beforeEach, | |
| 7 | describe, | |
| 8 | it, | |
| 9 | } from 'mocha' | |
| 10 | import sinon from 'sinon' | |
| 11 | import ImageDebug from '../../../common/image_debug' | |
| 12 | import ResultCollector from '../../result_collector' | |
| 13 | ||
| 14 | interface MockCanvas { | |
| 15 | getContext: () => {} | |
| 16 | toDataURL: sinon.SinonSpy | |
| 17 | width: number | |
| 18 | height: number | |
| 19 | } | |
| 20 | ||
| 21 | let canvasMock: MockCanvas | |
| 22 | let imageSize: XYSize | |
| 23 | let config: QuaggaJSResultCollector | |
| 24 | ||
| 25 | describe('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 | }) | |
| @@ -0,0 +1,11 @@ | ||
| 1 | import cluster from '../cluster'; | |
| 2 | ||
| 3 | describe('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 | }); | |
| @@ -0,0 +1,155 @@ | ||
| 1 | ||
| 2 | const { describe, it, beforeEach } = require('mocha'); | |
| 3 | const { expect } = require('chai'); | |
| 4 | const { | |
| 5 | imageRef, | |
| 6 | calculatePatchSize, | |
| 7 | _parseCSSDimensionValues, | |
| 8 | _dimensionsConverters, | |
| 9 | computeImageArea, | |
| 10 | } = require('../cv_utils'); | |
| 11 | ||
| 12 | describe('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 | }); | |
| @@ -0,0 +1,119 @@ | ||
| 1 | import { describe, it, beforeEach } from 'mocha'; | |
| 2 | import { expect } from 'chai'; | |
| 3 | ||
| 4 | import sinon, { SinonSpy } from 'sinon'; | |
| 5 | import Events from '../events'; | |
| 6 | ||
| 7 | describe('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 | }); | |
| @@ -0,0 +1,104 @@ | ||
| 1 | import { describe, it, beforeEach } from 'mocha'; | |
| 2 | import { expect } from 'chai'; | |
| 3 | import ImageWrapper from '../image_wrapper'; | |
| 4 | ||
| 5 | describe('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 | }); | |
| @@ -0,0 +1,47 @@ | ||
| 1 | import { describe, it } from 'mocha'; | |
| 2 | import { expect } from 'chai'; | |
| 3 | import CameraAccess from '../../camera_access'; | |
| 4 | ||
| 5 | const Quagga = { CameraAccess }; | |
| 6 | ||
| 7 | describe('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 | }); | |
| @@ -0,0 +1,28 @@ | ||
| 1 | import { expect } from 'chai' | |
| 2 | import { describe, it } from 'mocha' | |
| 3 | import { enumerateDevices, getUserMedia } from '../../mediaDevices' | |
| 4 | ||
| 5 | describe('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 | }) | |