UNPKG

scandit-sdk

Version:

Scandit Barcode Scanner SDK for the Web

850 lines (776 loc) 26.5 kB
import { BarcodeWASMResult } from "../barcode"; import { ImageSettings } from "../imageSettings"; import { Parser } from "../parser"; // WARNING // ========== // The "engine" function is extracted and executed in isolation as a WebWorker in the browser. // We currently cannot use too advanced language features here as the code will not get transformed/polyfilled correctly // by Rollup and Babel as it might refer to other externally defined variables/functions. // This means we also cannot import and use variables from the rest of the project. // The used language features should be compatible with (supported by) the browsers mentioned in the documentation. // See rollup.config.js and .browserslistrc.worker for more details. // TODO: This should be fixed... // tslint:disable:no-any /** * @hidden */ declare const self: any; /** * @hidden */ declare const importScripts: (...urls: string[]) => Promise<void> | undefined; // Promise is used only during testing /** * @hidden */ declare const postMessage: (message: any, transfer?: any[]) => void; /** * @hidden * * Defined here as we cannot use too recent typescript type definitions... */ declare namespace WebAssembly { interface Instance { readonly exports: any; } interface WebAssemblyInstantiatedSource { instance: Instance; // tslint:disable-next-line:no-reserved-keywords module: {}; } } /** * @hidden */ type FileSystemType = {}; /** * @hidden */ export declare type Module = { HEAPU8: Uint8Array; lengthBytesUTF8(str: string): number; UTF8ToString(ptr: number): string; stringToUTF8(str: string, outPtr: number, maxBytesToWrite: number): void; _malloc(size: number): number; _free(ptr: number): void; _create_context(ptr: number, debug: boolean): void; _scanner_settings_new_from_json( ptr: number, blurryDecodingEnabled: boolean, matrixScanEnabled: boolean, highQualitySingleFrameMode: boolean, gpuEnabled: boolean ): number; _scanner_image_settings_new(width: number, height: number, channels: number): void; _scanner_session_clear(): void; _can_hide_logo(): number; _scanner_scan(ptr: number): number; _parser_parse_string(parserType: number, ptr: number, stringDataLength: number, ptr2: number): number; canvas(): HTMLCanvasElement; instantiateWasm(importObject: object, successCallback: (instance: WebAssembly.Instance) => void): void; preRun(): void; onRuntimeInitialized(): void; callMain(): void; }; /** * @hidden */ declare let Module: Module; /** * @hidden */ declare namespace FS { function syncfs(populate: boolean, callback: (e: any) => any): void; function mount(fsType: FileSystemType, opts: any, mountpoint: string): any; function mkdir(path: string, mode?: number): any; } /** * @hidden */ declare const IDBFS: FileSystemType; // tslint:enable:no-any /** * @hidden */ declare type ScanWorkUnit = { requestId: number; data: Uint8ClampedArray; highQualitySingleFrameMode: boolean; }; /** * @hidden */ declare type ParseWorkUnit = { requestId: number; dataFormat: Parser.DataFormat; dataString: string; options: string; }; /** * @hidden */ export declare type Engine = { loadLibrary( deviceId: string, libraryLocation: string, locationPath: string, deviceModelName: string | undefined, uaBrowserName: string | undefined ): Promise<void>; createContext(newLicenseKey: string): void; setSettings(newSettings: string): void; setImageSettings(newImageSettings: ImageSettings): void; workOnScanQueue(): void; workOnParseQueue(): void; addScanWorkUnit(scanWorkUnit: ScanWorkUnit): void; addParseWorkUnit(parseWorkUnit: ParseWorkUnit): void; clearSession(): void; }; /** * @hidden * @returns Engine */ // tslint:disable-next-line:max-func-body-length export function engine(): Engine { const scanQueue: ScanWorkUnit[] = []; const parseQueue: ParseWorkUnit[] = []; const gpuAccelerationAvailable: boolean = typeof self.OffscreenCanvas === "function"; let browserName: string | undefined; let imageBufferPointer: number | undefined; let licenseKey: string; let settings: string; let imageSettings: ImageSettings; let scanWorkSubmitted: boolean = false; let fileSystemSynced: boolean = false; let runtimeLoaded: boolean = false; let wasmReady: boolean = false; let scannerSettingsReady: boolean = false; let scannerImageSettingsReady: boolean = false; let contextAvailable: boolean = false; let fsSyncInProgress: boolean | undefined; let fsSyncScheduled: boolean = false; // Public // Promise is used only during testing function loadLibrary( deviceId: string, libraryLocation: string, locationPath: string, deviceModelName: string | undefined, uaBrowserName: string | undefined ): Promise<void> { function start(): void { if (!wasmReady && fileSystemSynced && runtimeLoaded) { wasmReady = true; Module.callMain(); postMessage(["status", "ready"]); workOnScanQueue(); workOnParseQueue(); } } const { jsURI, wasmURI } = getLibraryLocationURIs(libraryLocation); Module = <Module>(<unknown>{ arguments: [deviceId], canvas: gpuAccelerationAvailable ? new self.OffscreenCanvas(32, 32) : /* istanbul ignore next */ undefined, instantiateWasm: (importObject: object, successCallback: (instance: WebAssembly.Instance) => void) => { // wasmJSVersion is globally defined inside scandit-engine-sdk.min.js let wasmJSVersion: string = self.wasmJSVersion; // istanbul ignore if if (wasmJSVersion == null) { wasmJSVersion = "undefined"; } // istanbul ignore if if (wasmJSVersion !== "%VER%") { console.error( `The Scandit SDK Engine library JS file found at ${jsURI} seems invalid: ` + `expected version doesn't match (received: ${wasmJSVersion}, expected: ${"%VER%"}). ` + `Please ensure the correct Scandit SDK Engine file (with correct version) is retrieved.` ); } if (typeof self.WebAssembly.instantiateStreaming === "function") { instantiateWebAssemblyStreaming(importObject, wasmURI, successCallback); } else { instantiateWebAssembly(importObject, wasmURI, successCallback); } return {}; }, noInitialRun: true, preRun: () => { try { FS.mkdir("/scandit_sync_folder"); } catch (error) { // istanbul ignore next if (error.code !== "EEXIST") { throw error; } } FS.mount(IDBFS, {}, "/scandit_sync_folder"); FS.syncfs(true, () => { fileSystemSynced = true; start(); }); }, onRuntimeInitialized: () => { runtimeLoaded = true; start(); } }); browserName = uaBrowserName; self.window = self.document = self; // Fix some Emscripten quirks self.path = locationPath; // Used by the Scandit SDK Engine library self.deviceModelName = deviceModelName; // Used by the Scandit SDK Engine library function tryImportScripts(): Promise<void> { try { const importScriptsResults: Promise<void> | undefined = importScripts(jsURI); // istanbul ignore else if (importScriptsResults != null) { return importScriptsResults; } else { return Promise.resolve(); } } catch (error) { return Promise.reject(error); } } return retryWithExponentialBackoff(tryImportScripts, 250, 4000, error => { console.warn(error); console.warn(`Couldn't retrieve Scandit SDK Engine library at ${jsURI}, retrying...`); }).catch(error => { console.error(error); console.error( `Couldn't retrieve Scandit SDK Engine library at ${jsURI}, did you configure the path for it correctly?` ); return Promise.resolve(error); // Promise is used only during testing }); } function createContext(newLicenseKey: string): void { licenseKey = newLicenseKey; if (contextAvailable || licenseKey == null || !wasmReady) { return; } const licenseKeyLength: number = Module.lengthBytesUTF8(licenseKey) + 1; const licenseKeyPointer: number = Module._malloc(licenseKeyLength); Module.stringToUTF8(licenseKey, licenseKeyPointer, licenseKeyLength); Module._create_context(licenseKeyPointer, false); Module._free(licenseKeyPointer); contextAvailable = true; postMessage([ "license-features", { hiddenScanditLogoAllowed: Module._can_hide_logo() === 1 } ]); } function setSettings(newSettings: string): void { settings = newSettings; applySettings(); } function setImageSettings(newImageSettings: ImageSettings): void { imageSettings = newImageSettings; applyImageSettings(); } function augmentErrorInformation(error: { errorCode: number; errorMessage: string }): void { if (error.errorCode === 260) { let hostname: string; // istanbul ignore if if (location.href != null && location.href.indexOf("blob:null/") === 0) { hostname = "localhost"; } else { hostname = new URL( location.pathname != null && location.pathname !== "" && !location.pathname.startsWith("/") ? /* istanbul ignore next */ location.pathname : location.origin ).hostname; } // istanbul ignore next if (hostname[0].startsWith("[") && hostname.endsWith("]")) { hostname = hostname.slice(1, -1); } error.errorMessage = error.errorMessage.replace("domain name", `domain name (${hostname})`); } } function processScanWorkUnit(currentScanWorkUnit: ScanWorkUnit): void { if (currentScanWorkUnit.highQualitySingleFrameMode) { applySettings(true); } const resultData: string = scanImage(currentScanWorkUnit.data); if (currentScanWorkUnit.highQualitySingleFrameMode) { applySettings(false); } const result: { scanResult?: BarcodeWASMResult[]; error?: { errorCode: number; errorMessage: string }; } = JSON.parse(resultData); // Important! We transfer data back even if we don't use it on the receiving end on Firefox. // Not doing so can result in memory and stability issues. // https://developer.mozilla.org/en-US/docs/Web/API/Transferable // https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage const postMessageTransfer: Transferable[] | undefined = browserName === "Firefox" ? [currentScanWorkUnit.data.buffer] : undefined; if (result.error != null) { augmentErrorInformation(result.error); postMessage( [ "work-error", { requestId: currentScanWorkUnit.requestId, error: result.error } ], postMessageTransfer ); } else { // istanbul ignore else if (result.scanResult != null) { if (result.scanResult.length > 0 || fsSyncInProgress == null) { syncFS(); } postMessage( [ "work-result", { requestId: currentScanWorkUnit.requestId, result } ], postMessageTransfer ); } else { console.error("Unrecognized Scandit Engine result:", result); postMessage([""], postMessageTransfer); } } } function workOnScanQueue(): void { if ((!scannerSettingsReady || !scannerImageSettingsReady) && scanQueue.length !== 0) { // First submitted work unit createContext(licenseKey); applySettings(); applyImageSettings(); } if (!scannerSettingsReady || !scannerImageSettingsReady || scanQueue.length === 0) { return; } while (scanQueue.length !== 0) { processScanWorkUnit(<ScanWorkUnit>scanQueue.shift()); } } function processParseWorkUnit(parseWorkUnit: ParseWorkUnit): void { const resultData: string = parseString(parseWorkUnit.dataFormat, parseWorkUnit.dataString, parseWorkUnit.options); const result: { result?: string; error?: { errorCode: number; errorMessage: string } } = JSON.parse(resultData); if (result.error != null) { augmentErrorInformation(result.error); postMessage([ "parse-string-error", { requestId: parseWorkUnit.requestId, error: result.error } ]); } else { // istanbul ignore else if (result.result != null) { postMessage([ "parse-string-result", { requestId: parseWorkUnit.requestId, result: result.result } ]); } else { console.error("Unrecognized Scandit Parser result:", result); postMessage([ "parse-string-error", { requestId: parseWorkUnit.requestId, error: { errorCode: -1, errorMessage: "Unknown Scandit Parser error" } } ]); } } } function workOnParseQueue(): void { if (!contextAvailable && parseQueue.length !== 0) { // First submitted work unit createContext(licenseKey); } if (!contextAvailable || !wasmReady || parseQueue.length === 0) { return; } while (parseQueue.length !== 0) { processParseWorkUnit(<ParseWorkUnit>parseQueue.shift()); } syncFS(); } function addScanWorkUnit(scanWorkUnit: ScanWorkUnit): void { scanWorkSubmitted = true; scanQueue.push(scanWorkUnit); workOnScanQueue(); } function addParseWorkUnit(parseWorkUnit: ParseWorkUnit): void { parseQueue.push(parseWorkUnit); workOnParseQueue(); } function clearSession(): void { if (scannerSettingsReady) { Module._scanner_session_clear(); } } // Private function retryWithExponentialBackoff<T>( handler: () => Promise<T>, backoffMs: number, maxBackoffMs: number, singleTryRejectionCallback: (error: Error) => void ): Promise<T> { return new Promise((resolve, reject) => { handler() .then(resolve) .catch(error => { const newBackoffMs: number = backoffMs * 2; if (newBackoffMs > maxBackoffMs) { return reject(error); } singleTryRejectionCallback(error); setTimeout(() => { retryWithExponentialBackoff(handler, newBackoffMs, maxBackoffMs, singleTryRejectionCallback) .then(resolve) .catch(reject); }, backoffMs); }); }); } function getLibraryLocationURIs(libraryLocation: string): { jsURI: string; wasmURI: string } { let cdnURI: boolean = false; if (/^https?:\/\/([^\/.]*\.)*cdn.jsdelivr.net\//.test(libraryLocation)) { libraryLocation = "https://cdn.jsdelivr.net/npm/scandit-sdk@%VER%/build/"; cdnURI = true; } else if (/^https?:\/\/([^\/.]*\.)*unpkg.com\//.test(libraryLocation)) { libraryLocation = "https://unpkg.com/scandit-sdk@%VER%/build/"; cdnURI = true; } if (cdnURI) { return { jsURI: `${libraryLocation}scandit-engine-sdk.min.js`, wasmURI: `${libraryLocation}scandit-engine-sdk.wasm` }; } return { jsURI: `${libraryLocation}scandit-engine-sdk.min.js?v=%VER%`, wasmURI: `${libraryLocation}scandit-engine-sdk.wasm?v=%VER%` }; } function arrayBufferToHexString(arrayBuffer: ArrayBuffer): string { return Array.from(new Uint8Array(arrayBuffer)) .map(byteNumber => { const byteHex: string = byteNumber.toString(16); return byteHex.length === 1 ? /* istanbul ignore next */ `0${byteHex}` : byteHex; }) .join(""); } function applySettings(highQualitySingleFrameMode: boolean = false): void { if (settings == null || !contextAvailable || !wasmReady || !scanWorkSubmitted) { return; } scannerSettingsReady = false; const parsedSettings: { matrixScanEnabled: boolean; gpuAcceleration: boolean; blurryRecognition: boolean; } = JSON.parse(settings); const settingsLength: number = Module.lengthBytesUTF8(settings) + 1; const settingsPointer: number = Module._malloc(settingsLength); Module.stringToUTF8(settings, settingsPointer, settingsLength); const resultPointer: number = Module._scanner_settings_new_from_json( settingsPointer, parsedSettings.blurryRecognition, parsedSettings.matrixScanEnabled, highQualitySingleFrameMode, gpuAccelerationAvailable && parsedSettings.gpuAcceleration ); Module._free(settingsPointer); const result: string = Module.UTF8ToString(resultPointer); if (result !== "") { scannerSettingsReady = true; console.debug(JSON.parse(result)); } } function applyImageSettings(): void { if (imageSettings == null || !wasmReady || !scanWorkSubmitted) { return; } scannerImageSettingsReady = false; let channels: number; // TODO: For now it's not possible to use imported variables as the worker doesn't have access at runtime if (imageSettings.format.valueOf() === 1) { // RGB_8U channels = 3; } else if (imageSettings.format.valueOf() === 2) { // RGBA_8U channels = 4; } else { // GRAY_8U channels = 1; } Module._scanner_image_settings_new(imageSettings.width, imageSettings.height, channels); if (imageBufferPointer != null) { Module._free(imageBufferPointer); imageBufferPointer = undefined; } imageBufferPointer = Module._malloc(imageSettings.width * imageSettings.height * channels); scannerImageSettingsReady = true; } function scanImage(imageData: Uint8ClampedArray): string { Module.HEAPU8.set(imageData, imageBufferPointer); return Module.UTF8ToString(Module._scanner_scan(<number>imageBufferPointer)); } function parseString(dataFormat: Parser.DataFormat, dataString: string, options: string): string { const dataStringLength: number = Module.lengthBytesUTF8(dataString) + 1; const dataStringPointer: number = Module._malloc(dataStringLength); Module.stringToUTF8(dataString, dataStringPointer, dataStringLength); const optionsLength: number = Module.lengthBytesUTF8(options) + 1; const optionsPointer: number = Module._malloc(optionsLength); Module.stringToUTF8(options, optionsPointer, optionsLength); const resultPointer: number = Module._parser_parse_string( dataFormat.valueOf(), dataStringPointer, dataStringLength - 1, optionsPointer ); Module._free(dataStringPointer); Module._free(optionsPointer); return Module.UTF8ToString(resultPointer); } function verifiedWasmFetch(wasmURI: string, awaitFullResponse: boolean): Promise<Response> { function verifyResponseData(responseData: ArrayBuffer): void { // istanbul ignore else if (crypto.subtle != null && typeof crypto.subtle === "object" && typeof crypto.subtle.digest === "function") { crypto.subtle.digest("SHA-256", responseData).then(hash => { const hashString: string = arrayBufferToHexString(hash); // istanbul ignore if if (hashString !== "%________________________ENGINE_WASM_HASH________________________%") { console.error( `The Scandit SDK Engine library WASM file found at ${wasmURI} seems invalid: ` + `expected file hash doesn't match (received: ${hashString}, ` + `expected: ${"%________________________ENGINE_WASM_HASH________________________%"}). ` + `Please ensure the correct Scandit SDK Engine file (with correct version) is retrieved.` ); } }); } else { console.warn( "Insecure origin (see https://goo.gl/Y0ZkNV): " + `The hash of the Scandit SDK Engine library WASM file found at ${wasmURI} could not be verified` ); } } function tryFetch(): Promise<Response> { return new Promise((resolve, reject) => { fetch(wasmURI) .then(response => { // istanbul ignore else if (response.ok) { response .clone() .arrayBuffer() .then(responseData => { if (awaitFullResponse) { resolve(response); } verifyResponseData(responseData); }) .catch( // istanbul ignore next error => { if (awaitFullResponse) { reject(error); } } ); if (!awaitFullResponse) { resolve(response); } } else { reject(new Error("HTTP status code is not ok")); } }) .catch(error => { reject(error); }); }); } return retryWithExponentialBackoff(tryFetch, 250, 4000, error => { console.warn(error); console.warn(`Couldn't retrieve Scandit SDK Engine library at ${wasmURI}, retrying...`); }).catch(error => { console.error(error); console.error( `Couldn't retrieve/instantiate Scandit SDK Engine library at ${wasmURI}, ` + "did you configure the path for it correctly?" ); return Promise.reject(error); }); } function instantiateWebAssembly( importObject: object, wasmURI: string, successCallback: (instance: WebAssembly.Instance) => void ): void { verifiedWasmFetch(wasmURI, true) .then(response => { return response.arrayBuffer(); }) .then(bytes => { return self.WebAssembly.instantiate(bytes, importObject) .then((results: WebAssembly.WebAssemblyInstantiatedSource) => { successCallback(results.instance); }) .catch((error: Error) => { console.error(error); console.error( `Couldn't instantiate Scandit SDK Engine library at ${wasmURI}, ` + "did you configure the path for it correctly?" ); }); }) .catch( /* istanbul ignore next */ () => { // Ignored } ); } function instantiateWebAssemblyStreaming( importObject: object, wasmURI: string, successCallback: (instance: WebAssembly.Instance) => void ): void { verifiedWasmFetch(wasmURI, false) .then(response => { self.WebAssembly.instantiateStreaming(response, importObject) .then((results: WebAssembly.WebAssemblyInstantiatedSource) => { successCallback(results.instance); }) .catch((error: Error) => { console.warn(error); console.warn( "WebAssembly streaming compile failed. " + "Falling back to ArrayBuffer instantiation (this will make things slower)" ); instantiateWebAssembly(importObject, wasmURI, successCallback); }); }) .catch( /* istanbul ignore next */ () => { // Ignored } ); } function syncFS(): void { // istanbul ignore if if (fsSyncInProgress === true) { fsSyncScheduled = true; } else { fsSyncInProgress = true; fsSyncScheduled = false; FS.syncfs(false, () => { fsSyncInProgress = false; // istanbul ignore if if (fsSyncScheduled) { syncFS(); } }); } } return { loadLibrary, createContext, setSettings, setImageSettings, workOnScanQueue, workOnParseQueue, addScanWorkUnit, addParseWorkUnit, clearSession }; } /** * @hidden */ // istanbul ignore next export function engineWorkerFunction(): void { const engineInstance: Engine = engine(); onmessage = e => { // Setting settings triggers license verification and activation: delay until first frame processed // tslint:disable:no-reserved-keywords max-union-size const data: | { type: "load-library"; deviceId: string; libraryLocation: string; path: string; deviceModelName?: string; uaBrowserName?: string; } | { type: "license-key"; licenseKey: string } | { type: "settings"; settings: string } | { type: "image-settings"; imageSettings: ImageSettings } | { type: "work"; requestId: number; data: Uint8ClampedArray; highQualitySingleFrameMode: boolean } | { type: "parse-string"; requestId: number; dataFormat: Parser.DataFormat; dataString: string; options: string } | { type: "clear-session" } = e.data; // tslint:enable:no-reserved-keywords max-union-size switch (data.type) { case "load-library": // tslint:disable-next-line: no-floating-promises engineInstance.loadLibrary( data.deviceId, data.libraryLocation, data.path, data.deviceModelName, data.uaBrowserName ); break; case "license-key": engineInstance.createContext(data.licenseKey); engineInstance.workOnParseQueue(); break; case "settings": engineInstance.setSettings(data.settings); engineInstance.workOnScanQueue(); break; case "image-settings": engineInstance.setImageSettings(data.imageSettings); engineInstance.workOnScanQueue(); break; case "work": engineInstance.addScanWorkUnit({ requestId: data.requestId, data: data.data, highQualitySingleFrameMode: data.highQualitySingleFrameMode }); break; case "parse-string": engineInstance.addParseWorkUnit({ requestId: data.requestId, dataFormat: data.dataFormat, dataString: data.dataString, options: data.options }); break; case "clear-session": engineInstance.clearSession(); break; default: break; } }; } /** * @hidden */ export const engineWorkerBlob: Blob = new Blob( [`var Module;${engine.toString()}(${engineWorkerFunction.toString()})()`], { type: "text/javascript" } );