/** * Core Code come from https://github.com/schmich/instascan * Now I just rewrite again by typescript * ZXing ("zebra crossing") is an open-source, multi-format 1D/2D barcode image processing library implemented in Java, with ports to other languages. * You can get everything here https://github.com/zxing/zxing */ import { EventEmitter } from "events"; import * as Visibility from "visibilityjs"; const StateMachine = require("fsm-as-promised"); const ZXing = require("./zxing")(); class ScanProvider { _active: boolean; scanPeriod: number; captureImage: boolean; refractoryPeriod: number; _emitter: Scanner; _frameCount: number; _analyzer: Analyzer; _lastResult: any; refractoryTimeout: NodeJS.Timeout; constructor( emitter: Scanner, analyzer: Analyzer, captureImage: boolean, scanPeriod: number, refractoryPeriod: number ) { this.scanPeriod = scanPeriod; this.captureImage = captureImage; this.refractoryPeriod = refractoryPeriod; this._emitter = emitter; this._frameCount = 0; this._analyzer = analyzer; this._lastResult = null; this._active = false; } start() { this._active = true; requestAnimationFrame(() => this._scan()); } stop() { this._active = false; } scan() { return this._analyze(false); } _analyze(skipDups: boolean): { content: string; image?: string } | null { let analysis = this._analyzer.analyze(); if (!analysis) { return null; } let { result, canvas } = analysis; if (!result) { return null; } if (skipDups && result === this._lastResult) { return null; } clearTimeout(this.refractoryTimeout); this.refractoryTimeout = setTimeout(() => { this._lastResult = null; }, this.refractoryPeriod); let image = this.captureImage ? canvas.toDataURL("image/webp", 0.8) : null; this._lastResult = result; let payload: { content: string; image?: string } = { content: result }; if (image) { payload.image = image; } return payload; } _scan() { if (!this._active) { return; } requestAnimationFrame(() => this._scan()); if (++this._frameCount !== this.scanPeriod) { return; } else { this._frameCount = 0; } let result = this._analyze(true); if (result) { setTimeout(() => { this._emitter.emit("scan", result?.content, result?.image || null); }, 0); } } } class Analyzer { video: HTMLVideoElement; sensorLeft: number; sensorTop: number; sensorWidth: number; sensorHeight: number; canvas: HTMLCanvasElement; canvasContext: CanvasRenderingContext2D; decodeCallback: any; imageBuffer: any; constructor(video: HTMLVideoElement) { this.video = video; this.imageBuffer = null; this.canvas = document.createElement("canvas"); this.canvas.style.display = "none"; this.decodeCallback = ZXing.Runtime.addFunction(function ( ptr: any, len: number, resultIndex: number ) { let result = new Uint8Array(ZXing.HEAPU8.buffer, ptr, len); let str = String.fromCharCode.apply(null, result); if (resultIndex === 0) { window.zxDecodeResult = ""; } window.zxDecodeResult += str; }); } analyze() { if (!this.video.videoWidth) { return null; } if (!this.imageBuffer) { let videoWidth = this.video.videoWidth; let videoHeight = this.video.videoHeight; this.sensorWidth = videoWidth; this.sensorHeight = videoHeight; this.sensorLeft = Math.floor(videoWidth / 2 - this.sensorWidth / 2); this.sensorTop = Math.floor(videoHeight / 2 - this.sensorHeight / 2); this.canvas.width = this.sensorWidth; this.canvas.height = this.sensorHeight; this.canvasContext = this.canvas.getContext("2d")!; this.imageBuffer = ZXing._resize(this.sensorWidth, this.sensorHeight); return null; } this.canvasContext.drawImage( this.video, this.sensorLeft, this.sensorTop, this.sensorWidth, this.sensorHeight ); let data = this.canvasContext.getImageData(0, 0, this.sensorWidth, this.sensorHeight).data; for (let i = 0, j = 0; i < data.length; i += 4, j++) { let [r, g, b] = [data[i], data[i + 1], data[i + 2]]; ZXing.HEAPU8[this.imageBuffer + j] = Math.trunc((r + g + b) / 3); } let err = ZXing._decode_qr(this.decodeCallback); if (err) { return null; } let result = window.zxDecodeResult; if (result != null) { return { result: result, canvas: this.canvas }; } return null; } } export interface ScanOptions { // The HTML element to use for the camera"s video preview. Must be a