diff --git a/README.md b/README.md index 9e8df9f6..f6facaba 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ This data is in the same form as the [`ImageData`](https://developer.mozilla.org - `height` - The height of the image you wish to decode. - `options` (optional) - Additional options. - `inversionAttempts` - (`attemptBoth` (default), `dontInvert`, `onlyInvert`, or `invertFirst`) - Should jsQR attempt to invert the image to find QR codes with white modules on black backgrounds instead of the black modules on white background. This option defaults to `attemptBoth` for backwards compatibility but causes a ~50% performance hit, and will probably be default to `dontInvert` in future versions. + - `canOverwriteImage` - (`true` (default) or `false`) - Specifies whether the image data can be overwritten for performance improvements or whether it should be kept untouched. If `true` the image buffer will be used internally to reduce additional memory allocation. ### Return value If a QR is able to be decoded the library will return an object with the following keys. diff --git a/src/binarizer/index.ts b/src/binarizer/index.ts index 9f230653..6c401dc9 100644 --- a/src/binarizer/index.ts +++ b/src/binarizer/index.ts @@ -11,9 +11,13 @@ function numBetween(value: number, min: number, max: number): number { class Matrix { private data: Uint8ClampedArray; private width: number; - constructor(width: number, height: number) { + constructor(width: number, height: number, buffer?: Uint8ClampedArray) { this.width = width; - this.data = new Uint8ClampedArray(width * height); + const bufferSize = width * height; + if (buffer && buffer.length !== bufferSize) { + throw new Error("Wrong buffer size"); + } + this.data = buffer || new Uint8ClampedArray(bufferSize); } public get(x: number, y: number) { return this.data[y * this.width + x]; @@ -23,24 +27,40 @@ class Matrix { } } -export function binarize(data: Uint8ClampedArray, width: number, height: number, returnInverted: boolean) { - if (data.length !== width * height * 4) { +export function binarize(data: Uint8ClampedArray, width: number, height: number, returnInverted: boolean, + canOverwriteImage: boolean) { + const pixelCount = width * height; + if (data.length !== pixelCount * 4) { throw new Error("Malformed data passed to binarizer."); } + // assign the greyscale and binary image within the rgba buffer as the rgba image will not be needed after conversion + let bufferOffset = 0; // Convert image to greyscale - const greyscalePixels = new Matrix(width, height); - for (let x = 0; x < width; x++) { - for (let y = 0; y < height; y++) { - const r = data[((y * width + x) * 4) + 0]; - const g = data[((y * width + x) * 4) + 1]; - const b = data[((y * width + x) * 4) + 2]; + let greyscaleBuffer: Uint8ClampedArray; + if (canOverwriteImage) { + greyscaleBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount); + bufferOffset += pixelCount; + } + const greyscalePixels = new Matrix(width, height, greyscaleBuffer); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const pixelPosition = (y * width + x) * 4; + const r = data[pixelPosition]; + const g = data[pixelPosition + 1]; + const b = data[pixelPosition + 2]; greyscalePixels.set(x, y, 0.2126 * r + 0.7152 * g + 0.0722 * b); } } const horizontalRegionCount = Math.ceil(width / REGION_SIZE); const verticalRegionCount = Math.ceil(height / REGION_SIZE); + const blackPointsCount = horizontalRegionCount * verticalRegionCount; - const blackPoints = new Matrix(horizontalRegionCount, verticalRegionCount); + let blackPointsBuffer: Uint8ClampedArray; + if (canOverwriteImage) { + blackPointsBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, blackPointsCount); + bufferOffset += blackPointsCount; + } + const blackPoints = new Matrix(horizontalRegionCount, verticalRegionCount, blackPointsBuffer); for (let verticalRegion = 0; verticalRegion < verticalRegionCount; verticalRegion++) { for (let hortizontalRegion = 0; hortizontalRegion < horizontalRegionCount; hortizontalRegion++) { let sum = 0; @@ -87,11 +107,25 @@ export function binarize(data: Uint8ClampedArray, width: number, height: number, } } - const binarized = BitMatrix.createEmpty(width, height); + let binarized: BitMatrix; + if (canOverwriteImage) { + const binarizedBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount); + bufferOffset += pixelCount; + binarized = new BitMatrix(binarizedBuffer, width); + } else { + binarized = BitMatrix.createEmpty(width, height); + } + let inverted: BitMatrix = null; if (returnInverted) { - inverted = BitMatrix.createEmpty(width, height); + if (canOverwriteImage) { + const invertedBuffer = new Uint8ClampedArray(data.buffer, bufferOffset, pixelCount); + inverted = new BitMatrix(invertedBuffer, width); + } else { + inverted = BitMatrix.createEmpty(width, height); + } } + for (let verticalRegion = 0; verticalRegion < verticalRegionCount; verticalRegion++) { for (let hortizontalRegion = 0; hortizontalRegion < horizontalRegionCount; hortizontalRegion++) { const left = numBetween(hortizontalRegion, 2, horizontalRegionCount - 3); diff --git a/src/index.ts b/src/index.ts index 65116049..d5dd94df 100644 --- a/src/index.ts +++ b/src/index.ts @@ -56,10 +56,12 @@ function scan(matrix: BitMatrix): QRCode | null { export interface Options { inversionAttempts?: "dontInvert" | "onlyInvert" | "attemptBoth" | "invertFirst"; + canOverwriteImage?: boolean; } const defaultOptions: Options = { inversionAttempts: "attemptBoth", + canOverwriteImage: true, }; function jsQR(data: Uint8ClampedArray, width: number, height: number, providedOptions: Options = {}): QRCode | null { @@ -71,7 +73,7 @@ function jsQR(data: Uint8ClampedArray, width: number, height: number, providedOp const shouldInvert = options.inversionAttempts === "attemptBoth" || options.inversionAttempts === "invertFirst"; const tryInvertedFirst = options.inversionAttempts === "onlyInvert" || options.inversionAttempts === "invertFirst"; - const {binarized, inverted} = binarize(data, width, height, shouldInvert); + const {binarized, inverted} = binarize(data, width, height, shouldInvert, options.canOverwriteImage); let result = scan(tryInvertedFirst ? inverted : binarized); if (!result && (options.inversionAttempts === "attemptBoth" || options.inversionAttempts === "invertFirst")) { result = scan(tryInvertedFirst ? binarized : inverted);