scandit-sdk
Version:
Scandit Barcode Scanner SDK for the Web
850 lines (776 loc) • 26.5 kB
text/typescript
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"
}
);