diff --git a/src/html5-qrcode.ts b/src/html5-qrcode.ts index b3fcbdab..30245a1a 100644 --- a/src/html5-qrcode.ts +++ b/src/html5-qrcode.ts @@ -268,6 +268,8 @@ export class Html5Qrcode { private readonly qrcode: RobustQrcodeDecoderAsync; private shouldScan: boolean; + private flippedScan: boolean; + private invertedScan: boolean; // Nullable elements // TODO(mebjas): Reduce the state-fulness of this mammoth class, by splitting @@ -316,7 +318,7 @@ export class Html5Qrcode { this.elementId = elementId; this.verbose = false; - + let experimentalFeatureConfig : ExperimentalFeaturesConfig | undefined; let configObject: Html5QrcodeFullConfig | undefined; if (typeof configOrVerbosityFlag == "boolean") { @@ -336,6 +338,8 @@ export class Html5Qrcode { this.foreverScanTimeout; this.shouldScan = true; + this.flippedScan = false; + this.invertedScan = false; this.stateManagerProxy = StateManagerFactory.create(); } @@ -1211,20 +1215,37 @@ export class Html5Qrcode { internalConfig, qrCodeSuccessCallback, qrCodeErrorCallback); }, this.getTimeoutFps(internalConfig.fps)); }; - + if(this.context) { + // apply invert function that replace this.context.filter = 'invert(X)' + this.context = this.invert(this.context, (this.invertedScan) ? '1' : '0'); + } // Try scanning normal frame and in case of failure, scan // the inverted context if not explictly disabled. // TODO(mebjas): Move this logic to decoding library. this.scanContext(qrCodeSuccessCallback, qrCodeErrorCallback) .then((isSuccessfull) => { - // Previous scan failed and disableFlip is off. - if (!isSuccessfull && internalConfig.disableFlip !== true) { - this.context!.translate(this.context!.canvas.width, 0); - this.context!.scale(-1, 1); - this.scanContext(qrCodeSuccessCallback, qrCodeErrorCallback) - .finally(() => { - triggerNextScan(); - }); + // Previous scan failed + if (!isSuccessfull) { + // disableFlip is off and is not flipped + if (internalConfig.disableFlip !== true && this.flippedScan === false) { + this.context!.translate(this.context!.canvas.width, 0); + this.context!.scale(-1, 1); + this.flippedScan = true; + } else { + // disableFlip is off + if (internalConfig.disableFlip !== true) { + this.context!.translate(this.context!.canvas.width, 0); + this.context!.scale(-1, 1); + } + this.flippedScan = false; + } + // previous scan failed, check if next time I shouldapply invert function with amount parameter set to 1 or 0 + if (this.invertedScan === false && this.flippedScan === false) { + this.invertedScan = true; + } else if (this.invertedScan === true && this.flippedScan === false) { + this.invertedScan = false; + } + triggerNextScan(); } else { triggerNextScan(); } @@ -1330,6 +1351,69 @@ export class Html5Qrcode { const type = (typeof cameraIdOrConfig); throw `Invalid type of 'cameraIdOrConfig' = ${type}`; } + + /** + * Method that replace CanvasRenderingContext2D.filter that is not supported from Safari + * https://github.com/davidenke/context-filter-polyfill/blob/main/src/filters/invert.filter.ts + * @param context + * @param amount from 0 to 1 + */ + private invert( + context: CanvasRenderingContext2D, + amount: any = '0' + ) { + amount = this.normalizeNumberPercentage(amount); + // do not manipulate without proper amount + if (amount <= 0) { + return context; + } + // a maximum of 100% + if (amount > 1) { + amount = 1; + } + const { height, width } = context.canvas; + const imageData = context.getImageData(0, 0, width, height); + const { data } = imageData; + const { length } = data; + + // in rgba world, every + // n * 4 + 0 is red, + // n * 4 + 1 green and + // n * 4 + 2 is blue + // the fourth can be skipped as it's the alpha channel + for (let i = 0; i < length; i += 4) { + data[i + 0] = Math.abs(data[i + 0] - 255 * amount); + data[i + 1] = Math.abs(data[i + 1] - 255 * amount); + data[i + 2] = Math.abs(data[i + 2] - 255 * amount); + } + + // set back image data to context + context.putImageData(imageData, 0, 0); + + // return the context itself + return context; + } + + /** + * filter options are often represented as number-percentage, + * means that they'll be percentages like `50%` or floating + * in-between 0 and 1 like `.5`, so we normalize them. + * https://developer.mozilla.org/en-US/docs/Web/CSS/filter#number-percentage + * https://github.com/davidenke/context-filter-polyfill/blob/main/src/utils/filter.utils.ts + * @param percentage + * @returns + */ + private normalizeNumberPercentage( + percentage: string + ) { + let normalized = parseFloat(percentage); + // check for percentages and divide by a hundred + if (/%\s*?$/i.test(percentage)) { + normalized /= 100; + } + return normalized; + } + //#endregion //#region Documented private methods for file based scanner.