import { EventEmitter, ListenerFn } from "eventemitter3"; import { engineWorkerBlob } from "./workers/engineWorker"; import { deviceId, scanditEngineLocation, userLicenseKey } from "../index"; import { Barcode, BarcodeWASMResult } from "./barcode"; import { BrowserCompatibility } from "./browserCompatibility"; import { BrowserHelper } from "./browserHelper"; import { CustomError } from "./customError"; import { ImageSettings } from "./imageSettings"; import { Parser } from "./parser"; import { ParserField } from "./parserField"; import { ParserResult } from "./parserResult"; import { ScanResult } from "./scanResult"; import { ScanSettings } from "./scanSettings"; import { UnsupportedBrowserError } from "./unsupportedBrowserError"; /** * @hidden */ type EventName = "ready" | "licenseFeaturesReady" | "newScanSettings"; /** * @hidden */ class ScannerEventEmitter extends EventEmitter {} /** * A low-level scanner interacting with the external Scandit Engine library. * Used to set up scan / image settings and to process single image frames. * * The loading of the external Scandit Engine library which takes place on creation can take some time, * the [[on]] method targeting the [[ready]] event can be used to set up a listener function to be called when the * library is loaded and the [[isReady]] method can return the current status. The scanner will be ready to start * scanning when the library is fully loaded. * * In the special case where a single [[Scanner]] instance is shared between multiple active [[BarcodePicker]] * instances, the fairness in resource allocation for processing images between the different pickers is not guaranteed. */ export class Scanner { private readonly engineWorker: Worker; private readonly eventEmitter: ScannerEventEmitter; private scanSettings: ScanSettings; private imageSettings?: ImageSettings; private workerParseRequestId: number; private workerScanRequestId: number; private workerScanQueueLength: number; private isReadyToWork: boolean; private licenseFeatures?: object; private imageDataConversionContext?: CanvasRenderingContext2D; /** * Create a Scanner instance. * * It is required to having configured the library via [[configure]] before this object can be created. * * Before processing an image the relative settings must also have been set. * * If the library has not been correctly configured yet a `LibraryNotConfiguredError` error is thrown. * * If a browser is incompatible a `UnsupportedBrowserError` error is thrown. * * @param scanSettings
Default = new ScanSettings()
* The configuration object for scanning options. * @param imageSettings
Default = undefined
* The configuration object to define the properties of an image to be scanned. */ constructor({ scanSettings = new ScanSettings(), imageSettings }: { scanSettings?: ScanSettings; imageSettings?: ImageSettings; } = {}) { const browserCompatibility: BrowserCompatibility = BrowserHelper.checkBrowserCompatibility(); if (!browserCompatibility.scannerSupport) { throw new UnsupportedBrowserError(browserCompatibility); } if (userLicenseKey == null) { throw new CustomError({ name: "LibraryNotConfiguredError", message: "The library has not correctly been configured yet, please call 'configure' with valid parameters" }); } this.isReadyToWork = false; this.workerScanQueueLength = 0; this.engineWorker = new Worker(URL.createObjectURL(engineWorkerBlob)); this.engineWorker.onmessage = this.engineWorkerOnMessage.bind(this); this.engineWorker.postMessage({ type: "load-library", deviceId, libraryLocation: scanditEngineLocation, path: self.location.pathname, deviceModelName: BrowserHelper.userAgentInfo.getDevice().model, uaBrowserName: BrowserHelper.userAgentInfo.getBrowser().name }); this.eventEmitter = new EventEmitter(); this.workerParseRequestId = 0; this.workerScanRequestId = 0; this.applyLicenseKey(userLicenseKey); this.applyScanSettings(scanSettings); if (imageSettings != null) { this.applyImageSettings(imageSettings); } } /** * Stop the internal `WebWorker` and destroy the scanner itself; ensuring complete cleanup. * * This method should be called after you don't plan to use the scanner anymore, * before the object is automatically cleaned up by JavaScript. * The barcode picker must not be used in any way after this call. */ public destroy(): void { if (this.engineWorker != null) { this.engineWorker.terminate(); } this.eventEmitter.removeAllListeners(); } /** * Apply a new set of scan settings to the scanner (replacing old settings). * * @param scanSettings The scan configuration object to be applied to the scanner. * @returns The updated [[Scanner]] object. */ public applyScanSettings(scanSettings: ScanSettings): Scanner { this.scanSettings = scanSettings; this.engineWorker.postMessage({ type: "settings", settings: this.scanSettings.toJSONString() }); this.eventEmitter.emit("newScanSettings", this.scanSettings); return this; } /** * Apply a new set of image settings to the scanner (replacing old settings). * * @param imageSettings The image configuration object to be applied to the scanner. * @returns The updated [[Scanner]] object. */ public applyImageSettings(imageSettings: ImageSettings): Scanner { this.imageSettings = imageSettings; this.engineWorker.postMessage({ type: "image-settings", imageSettings }); return this; } /** * Clear the scanner session. * * This removes all recognized barcodes from the scanner session and allows them to be scanned again in case a custom * *codeDuplicateFilter* was set in the [[ScanSettings]]. * * @returns The updated [[Scanner]] object. */ public clearSession(): Scanner { this.engineWorker.postMessage({ type: "clear-session" }); return this; } /** * Process a given image using the previously set scanner and image settings, * recognizing codes and retrieving the result as a list of barcodes (if any). * * Multiple requests done without waiting for previous results will be queued and handled in order. * * If *highQualitySingleFrameMode* is enabled the image will be processed with really accurate internal settings, * resulting in much slower but more precise scanning results. This should be used only for single images not part * of a continuous video stream. * * Passing image data as a *Uint8ClampedArray* is the fastest option, passing a *HTMLImageElement* will incur * in additional operations. * * Depending on the current image settings, given *imageData* and scanning execution, any of the following errors * could be the rejected result of the returned promise: * - `NoImageSettings` * - `ImageSettingsDataMismatch` * - `ScanditEngineError` * * @param imageData The image data given as byte array or image element, complying with the previously set * image settings. * @param highQualitySingleFrameMode Whether to process the image as a high quality single frame. * @returns A promise resolving to the [[ScanResult]] object. */ public processImage( imageData: Uint8ClampedArray | HTMLImageElement, highQualitySingleFrameMode: boolean = false ): Promise { if (this.imageSettings == null) { return Promise.reject( new CustomError({ name: "NoImageSettings", message: "No image settings set up in the scanner" }) ); } if (imageData instanceof HTMLImageElement) { if (this.imageDataConversionContext == null) { this.imageDataConversionContext = document.createElement("canvas").getContext("2d"); } this.imageDataConversionContext.canvas.width = imageData.naturalWidth; this.imageDataConversionContext.canvas.height = imageData.naturalHeight; this.imageDataConversionContext.drawImage(imageData, 0, 0, imageData.naturalWidth, imageData.naturalHeight); imageData = this.imageDataConversionContext.getImageData(0, 0, imageData.naturalWidth, imageData.naturalHeight) .data; } let channels: number; switch (this.imageSettings.format.valueOf()) { case ImageSettings.Format.GRAY_8U: channels = 1; break; case ImageSettings.Format.RGB_8U: channels = 3; break; case ImageSettings.Format.RGBA_8U: channels = 4; break; default: channels = 1; break; } if (this.imageSettings.width * this.imageSettings.height * channels !== imageData.length) { return Promise.reject( new CustomError({ name: "ImageSettingsDataMismatch", message: "The provided image data doesn't match the previously set image settings" }) ); } this.workerScanRequestId++; this.workerScanQueueLength++; const originalImageData: Uint8ClampedArray = imageData.slice(); return new Promise((resolve, reject) => { const workResultEvent: string = `workResult-${this.workerScanRequestId}`; const workErrorEvent: string = `workError-${this.workerScanRequestId}`; this.eventEmitter.once(workResultEvent, (workResult: { scanResult: BarcodeWASMResult[] }) => { this.eventEmitter.removeAllListeners(workErrorEvent); resolve( new ScanResult(workResult.scanResult.map(Barcode.createFromWASMResult), originalImageData, ( this.imageSettings )) ); }); this.eventEmitter.once(workErrorEvent, (error: { errorCode: number; errorMessage: string }) => { console.error(`Scandit Engine error (${error.errorCode}):`, error.errorMessage); this.eventEmitter.removeAllListeners(workResultEvent); const errorObject: Error = new CustomError({ name: "ScanditEngineError", message: `${error.errorMessage} (${error.errorCode})` }); reject(errorObject); }); this.engineWorker.postMessage( { type: "work", requestId: this.workerScanRequestId, data: imageData, highQualitySingleFrameMode }, [(imageData).buffer] ); }); } /** * @returns Whether the scanner is currently busy processing an image. */ public isBusyProcessing(): boolean { return this.workerScanQueueLength !== 0; } /** * @returns Whether the scanner has loaded the external Scandit Engine library and is ready to scan. */ public isReady(): boolean { return this.isReadyToWork; } /** * Add the listener function to the listeners array for an event. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param eventName The name of the event to listen to. * @param listener The listener function. * @returns The updated [[Scanner]] object. */ // tslint:disable-next-line:bool-param-default public on(eventName: EventName, listener: ListenerFn): Scanner; /** * Add the listener function to the listeners array for the [[ready]] event, fired only once when the external * Scandit Engine library has been loaded and the scanner can thus start to scan barcodes. * If the external Scandit Engine library has already been loaded the listener is called immediately. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param eventName The name of the event to listen to. * @param listener The listener function. * @returns The updated [[Scanner]] object. */ public on(eventName: "ready", listener: () => void): Scanner; public on(eventName: EventName, listener: ListenerFn): Scanner { if (eventName === "ready") { if (this.isReadyToWork) { listener(); } else { this.eventEmitter.once(eventName, listener, this); } } else if (eventName === "licenseFeaturesReady") { if (this.licenseFeatures != null) { listener(this.licenseFeatures); } else { this.eventEmitter.once(eventName, listener, this); } } else { this.eventEmitter.on(eventName, listener, this); } return this; } /** * Add the listener function to the listeners array for the [[ready]] event, fired only once when the external * Scandit Engine library has been loaded and the scanner can thus start to scan barcodes. * If the external Scandit Engine library has already been loaded the listener is called immediately. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @deprecated Use the [[on]] method instead. * * @param listener The listener function. * @returns The updated [[Scanner]] object. */ public onReady(listener: () => void): Scanner { console.warn( "The onReady() method is deprecated and will be removed in the next major library version." + 'Please use on("ready", ) instead.' ); return this.on("ready", listener); } /** * *See the [[on]] method.* * * @param eventName The name of the event to listen to. * @param listener The listener function. * @returns The updated [[Scanner]] object. */ public addListener(eventName: EventName, listener: ListenerFn): Scanner { return this.on(eventName, listener); } /** * Create a new parser object. * * @param dataFormat The format of the input data for the parser. * @returns The newly created parser. */ public createParserForFormat(dataFormat: Parser.DataFormat): Parser { return new Parser(this, dataFormat); } /** * Return the current image settings. * * Note that modifying this object won't directly apply these settings: the [[applyImageSettings]] method must be * called with the updated object. * * @returns The current image settings. */ public getImageSettings(): ImageSettings | undefined { return this.imageSettings; } /** * Return the current scan settings. * * Note that modifying this object won't directly apply these settings: the [[applyScanSettings]] method must be * called with the updated object. * * @returns The current scan settings. */ public getScanSettings(): ScanSettings { return this.scanSettings; } /** * @hidden * * Process a given string using the Scandit Parser library, * parsing the data in the given format and retrieving the result as a [[ParserResult]] object. * * Multiple requests done without waiting for previous results will be queued and handled in order. * * If parsing of the data fails the returned promise is rejected with a `ScanditEngineError` error. * * @param dataFormat The format of the given data. * @param dataString The string containing the data to be parsed. * @param options Options for the specific data format parser. * @returns A promise resolving to the [[ParserResult]] object. */ public parseString(dataFormat: Parser.DataFormat, dataString: string, options?: object): Promise { this.workerParseRequestId++; return new Promise((resolve, reject) => { const parseStringResultEvent: string = `parseStringResult-${this.workerParseRequestId}`; const parseStringErrorEvent: string = `parseStringError-${this.workerParseRequestId}`; this.eventEmitter.once(parseStringResultEvent, (result: string) => { this.eventEmitter.removeAllListeners(parseStringErrorEvent); const parserResult: ParserResult = { jsonString: result, fields: [], fieldsByName: {} }; (JSON.parse(result)).forEach(parserField => { parserResult.fields.push(parserField); parserResult.fieldsByName[parserField.name] = parserField; }); resolve(parserResult); }); this.eventEmitter.once(parseStringErrorEvent, (error: { errorCode: number; errorMessage: string }) => { console.error(`Scandit Engine error (${error.errorCode}):`, error.errorMessage); this.eventEmitter.removeAllListeners(parseStringResultEvent); const errorObject: Error = new CustomError({ name: "ScanditEngineError", message: `${error.errorMessage} (${error.errorCode})` }); reject(errorObject); }); this.engineWorker.postMessage({ type: "parse-string", requestId: this.workerParseRequestId, dataFormat, dataString, options: options == null ? "{}" : JSON.stringify(options) }); }); } /** * @hidden * * Add the listener function to the listeners array for the "licenseFeaturesReady" event, fired only once * when the external Scandit Engine library has verified and loaded the license key and parsed its features. * If the license features are already available the listener is called immediately. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param listener The listener function, which will be invoked with a *licenseFeatures* object. * @returns The updated [[Scanner]] object. */ public onLicenseFeaturesReady(listener: (licenseFeatures: object) => void): Scanner { return this.on("licenseFeaturesReady", listener); } /** * @hidden * * Add the listener function to the listeners array for the "newScanSettings" event, fired when new a new * [[ScanSettings]] object is applied via the [[applyScanSettings]] method. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param listener The listener function, which will be invoked with a [[ScanSettings]] object. * @returns The updated [[Scanner]] object. */ public onNewScanSettings(listener: (scanSettings: ScanSettings) => void): Scanner { return this.on("newScanSettings", listener); } /** * @hidden * * Remove the specified listener from the given event's listener array. * * @param eventName The name of the event from which to remove the listener. * @param listener The listener function to be removed. * @returns The updated [[Scanner]] object. */ public removeListener(eventName: string, listener: ListenerFn): Scanner { this.eventEmitter.removeListener(eventName, listener); return this; } private applyLicenseKey(licenseKey: string): Scanner { this.engineWorker.postMessage({ type: "license-key", licenseKey }); return this; } private engineWorkerOnMessage(ev: MessageEvent): void { // tslint:disable:max-union-size const data: | ["status", string] | ["license-features", object] | ["work-result", { requestId: number; result: { scanResult: BarcodeWASMResult[] } }] | ["work-error", { requestId: number; error: { errorCode: number; errorMessage: string } }] | ["parse-string-result", { requestId: number; result: string }] | ["parse-string-error", { requestId: number; error: { errorCode: number; errorMessage: string } }] = ev.data; // tslint:enable:max-union-size if (data[0] === "status" && data[1] === "ready") { this.isReadyToWork = true; this.eventEmitter.emit("ready"); } else if (data[1] != null) { if (data[0] === "license-features") { this.licenseFeatures = data[1]; this.eventEmitter.emit("licenseFeaturesReady", this.licenseFeatures); } else if (data[0] === "work-result") { this.eventEmitter.emit(`workResult-${data[1].requestId}`, data[1].result); this.workerScanQueueLength--; } else if (data[0] === "work-error") { this.eventEmitter.emit(`workError-${data[1].requestId}`, data[1].error); this.workerScanQueueLength--; } else if (data[0] === "parse-string-result") { this.eventEmitter.emit(`parseStringResult-${data[1].requestId}`, data[1].result); } else if (data[0] === "parse-string-error") { this.eventEmitter.emit(`parseStringError-${data[1].requestId}`, data[1].error); } } } } // istanbul ignore next export namespace Scanner { /** * Fired when the external Scandit Engine library has been loaded and the scanner can thus start to scan barcodes. * * @asMemberOf Scanner * @event */ // @ts-ignore declare function ready(): void; /** * @hidden * * Fired when the external Scandit Engine library has verified and loaded the license key and parsed its features. * * @asMemberOf Scanner * @event * @param licenseFeatures The features of the used license key. */ // @ts-ignore // declare function licenseFeaturesReady(licenseFeatures: any): void; /** * @hidden * * Fired when new a new [[ScanSettings]] object is applied via the [[applyScanSettings]] method. * * @asMemberOf Scanner * @event * @param newScanSettings The features of the used license key. */ // @ts-ignore // declare function newScanSettings(newScanSettings: any): void; }