UNPKG

21.9 kBPlain TextView Raw
1import { EventEmitter, ListenerFn } from "eventemitter3";
2
3import { engineWorkerBlob } from "./workers/engineWorker";
4
5import { deviceId, scanditEngineLocation, userLicenseKey } from "../index";
6import { Barcode, BarcodeWASMResult } from "./barcode";
7import { BrowserCompatibility } from "./browserCompatibility";
8import { BrowserHelper } from "./browserHelper";
9import { CustomError } from "./customError";
10import { ImageSettings } from "./imageSettings";
11import { Parser } from "./parser";
12import { ParserField } from "./parserField";
13import { ParserResult } from "./parserResult";
14import { ScanResult } from "./scanResult";
15import { ScanSettings } from "./scanSettings";
16import { UnsupportedBrowserError } from "./unsupportedBrowserError";
17
18/**
19 * @hidden
20 */
21type EventName = "ready" | "licenseFeaturesReady" | "newScanSettings";
22
23/**
24 * @hidden
25 */
26class ScannerEventEmitter extends EventEmitter<string> {}
27
28/**
29 * A low-level scanner interacting with the external Scandit Engine library.
30 * Used to set up scan / image settings and to process single image frames.
31 *
32 * The loading of the external Scandit Engine library which takes place on creation can take some time,
33 * the [[on]] method targeting the [[ready]] event can be used to set up a listener function to be called when the
34 * library is loaded and the [[isReady]] method can return the current status. The scanner will be ready to start
35 * scanning when the library is fully loaded.
36 *
37 * In the special case where a single [[Scanner]] instance is shared between multiple active [[BarcodePicker]]
38 * instances, the fairness in resource allocation for processing images between the different pickers is not guaranteed.
39 */
40export class Scanner {
41 private readonly engineWorker: Worker;
42 private readonly eventEmitter: ScannerEventEmitter;
43
44 private scanSettings: ScanSettings;
45 private imageSettings?: ImageSettings;
46 private workerParseRequestId: number;
47 private workerScanRequestId: number;
48 private workerScanQueueLength: number;
49 private isReadyToWork: boolean;
50 private licenseFeatures?: object;
51 private imageDataConversionContext?: CanvasRenderingContext2D;
52
53 /**
54 * Create a Scanner instance.
55 *
56 * It is required to having configured the library via [[configure]] before this object can be created.
57 *
58 * Before processing an image the relative settings must also have been set.
59 *
60 * If the library has not been correctly configured yet a `LibraryNotConfiguredError` error is thrown.
61 *
62 * If a browser is incompatible a `UnsupportedBrowserError` error is thrown.
63 *
64 * @param scanSettings <div class="tsd-signature-symbol">Default =&nbsp;new ScanSettings()</div>
65 * The configuration object for scanning options.
66 * @param imageSettings <div class="tsd-signature-symbol">Default =&nbsp;undefined</div>
67 * The configuration object to define the properties of an image to be scanned.
68 */
69 constructor({
70 scanSettings = new ScanSettings(),
71 imageSettings
72 }: {
73 scanSettings?: ScanSettings;
74 imageSettings?: ImageSettings;
75 } = {}) {
76 const browserCompatibility: BrowserCompatibility = BrowserHelper.checkBrowserCompatibility();
77 if (!browserCompatibility.scannerSupport) {
78 throw new UnsupportedBrowserError(browserCompatibility);
79 }
80 if (userLicenseKey == null) {
81 throw new CustomError({
82 name: "LibraryNotConfiguredError",
83 message: "The library has not correctly been configured yet, please call 'configure' with valid parameters"
84 });
85 }
86
87 this.isReadyToWork = false;
88 this.workerScanQueueLength = 0;
89
90 this.engineWorker = new Worker(URL.createObjectURL(engineWorkerBlob));
91 this.engineWorker.onmessage = this.engineWorkerOnMessage.bind(this);
92 this.engineWorker.postMessage({
93 type: "load-library",
94 deviceId,
95 libraryLocation: scanditEngineLocation,
96 path: self.location.pathname,
97 deviceModelName: BrowserHelper.userAgentInfo.getDevice().model,
98 uaBrowserName: BrowserHelper.userAgentInfo.getBrowser().name
99 });
100
101 this.eventEmitter = new EventEmitter();
102 this.workerParseRequestId = 0;
103 this.workerScanRequestId = 0;
104
105 this.applyLicenseKey(userLicenseKey);
106 this.applyScanSettings(scanSettings);
107 if (imageSettings != null) {
108 this.applyImageSettings(imageSettings);
109 }
110 }
111
112 /**
113 * Stop the internal `WebWorker` and destroy the scanner itself; ensuring complete cleanup.
114 *
115 * This method should be called after you don't plan to use the scanner anymore,
116 * before the object is automatically cleaned up by JavaScript.
117 * The barcode picker must not be used in any way after this call.
118 */
119 public destroy(): void {
120 if (this.engineWorker != null) {
121 this.engineWorker.terminate();
122 }
123 this.eventEmitter.removeAllListeners();
124 }
125
126 /**
127 * Apply a new set of scan settings to the scanner (replacing old settings).
128 *
129 * @param scanSettings The scan configuration object to be applied to the scanner.
130 * @returns The updated [[Scanner]] object.
131 */
132 public applyScanSettings(scanSettings: ScanSettings): Scanner {
133 this.scanSettings = scanSettings;
134 this.engineWorker.postMessage({
135 type: "settings",
136 settings: this.scanSettings.toJSONString()
137 });
138 this.eventEmitter.emit("newScanSettings", this.scanSettings);
139
140 return this;
141 }
142
143 /**
144 * Apply a new set of image settings to the scanner (replacing old settings).
145 *
146 * @param imageSettings The image configuration object to be applied to the scanner.
147 * @returns The updated [[Scanner]] object.
148 */
149 public applyImageSettings(imageSettings: ImageSettings): Scanner {
150 this.imageSettings = imageSettings;
151 this.engineWorker.postMessage({
152 type: "image-settings",
153 imageSettings
154 });
155
156 return this;
157 }
158
159 /**
160 * Clear the scanner session.
161 *
162 * This removes all recognized barcodes from the scanner session and allows them to be scanned again in case a custom
163 * *codeDuplicateFilter* was set in the [[ScanSettings]].
164 *
165 * @returns The updated [[Scanner]] object.
166 */
167 public clearSession(): Scanner {
168 this.engineWorker.postMessage({
169 type: "clear-session"
170 });
171
172 return this;
173 }
174
175 /**
176 * Process a given image using the previously set scanner and image settings,
177 * recognizing codes and retrieving the result as a list of barcodes (if any).
178 *
179 * Multiple requests done without waiting for previous results will be queued and handled in order.
180 *
181 * If *highQualitySingleFrameMode* is enabled the image will be processed with really accurate internal settings,
182 * resulting in much slower but more precise scanning results. This should be used only for single images not part
183 * of a continuous video stream.
184 *
185 * Passing image data as a *Uint8ClampedArray* is the fastest option, passing a *HTMLImageElement* will incur
186 * in additional operations.
187 *
188 * Depending on the current image settings, given *imageData* and scanning execution, any of the following errors
189 * could be the rejected result of the returned promise:
190 * - `NoImageSettings`
191 * - `ImageSettingsDataMismatch`
192 * - `ScanditEngineError`
193 *
194 * @param imageData The image data given as byte array or image element, complying with the previously set
195 * image settings.
196 * @param highQualitySingleFrameMode Whether to process the image as a high quality single frame.
197 * @returns A promise resolving to the [[ScanResult]] object.
198 */
199 public processImage(
200 imageData: Uint8ClampedArray | HTMLImageElement,
201 highQualitySingleFrameMode: boolean = false
202 ): Promise<ScanResult> {
203 if (this.imageSettings == null) {
204 return Promise.reject(
205 new CustomError({ name: "NoImageSettings", message: "No image settings set up in the scanner" })
206 );
207 }
208
209 if (imageData instanceof HTMLImageElement) {
210 if (this.imageDataConversionContext == null) {
211 this.imageDataConversionContext = <CanvasRenderingContext2D>document.createElement("canvas").getContext("2d");
212 }
213 this.imageDataConversionContext.canvas.width = imageData.naturalWidth;
214 this.imageDataConversionContext.canvas.height = imageData.naturalHeight;
215 this.imageDataConversionContext.drawImage(imageData, 0, 0, imageData.naturalWidth, imageData.naturalHeight);
216 imageData = this.imageDataConversionContext.getImageData(0, 0, imageData.naturalWidth, imageData.naturalHeight)
217 .data;
218 }
219
220 let channels: number;
221 switch (this.imageSettings.format.valueOf()) {
222 case ImageSettings.Format.GRAY_8U:
223 channels = 1;
224 break;
225 case ImageSettings.Format.RGB_8U:
226 channels = 3;
227 break;
228 case ImageSettings.Format.RGBA_8U:
229 channels = 4;
230 break;
231 default:
232 channels = 1;
233 break;
234 }
235
236 if (this.imageSettings.width * this.imageSettings.height * channels !== imageData.length) {
237 return Promise.reject(
238 new CustomError({
239 name: "ImageSettingsDataMismatch",
240 message: "The provided image data doesn't match the previously set image settings"
241 })
242 );
243 }
244
245 this.workerScanRequestId++;
246 this.workerScanQueueLength++;
247
248 const originalImageData: Uint8ClampedArray = imageData.slice();
249
250 return new Promise<ScanResult>((resolve, reject) => {
251 const workResultEvent: string = `workResult-${this.workerScanRequestId}`;
252 const workErrorEvent: string = `workError-${this.workerScanRequestId}`;
253 this.eventEmitter.once(workResultEvent, (workResult: { scanResult: BarcodeWASMResult[] }) => {
254 this.eventEmitter.removeAllListeners(workErrorEvent);
255 resolve(
256 new ScanResult(workResult.scanResult.map(Barcode.createFromWASMResult), originalImageData, <ImageSettings>(
257 this.imageSettings
258 ))
259 );
260 });
261 this.eventEmitter.once(workErrorEvent, (error: { errorCode: number; errorMessage: string }) => {
262 console.error(`Scandit Engine error (${error.errorCode}):`, error.errorMessage);
263 this.eventEmitter.removeAllListeners(workResultEvent);
264 const errorObject: Error = new CustomError({
265 name: "ScanditEngineError",
266 message: `${error.errorMessage} (${error.errorCode})`
267 });
268 reject(errorObject);
269 });
270 this.engineWorker.postMessage(
271 {
272 type: "work",
273 requestId: this.workerScanRequestId,
274 data: <Uint8ClampedArray>imageData,
275 highQualitySingleFrameMode
276 },
277 [(<Uint8ClampedArray>imageData).buffer]
278 );
279 });
280 }
281
282 /**
283 * @returns Whether the scanner is currently busy processing an image.
284 */
285 public isBusyProcessing(): boolean {
286 return this.workerScanQueueLength !== 0;
287 }
288
289 /**
290 * @returns Whether the scanner has loaded the external Scandit Engine library and is ready to scan.
291 */
292 public isReady(): boolean {
293 return this.isReadyToWork;
294 }
295
296 /**
297 * Add the listener function to the listeners array for an event.
298 *
299 * No checks are made to see if the listener has already been added.
300 * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
301 *
302 * @param eventName The name of the event to listen to.
303 * @param listener The listener function.
304 * @returns The updated [[Scanner]] object.
305 */
306 // tslint:disable-next-line:bool-param-default
307 public on(eventName: EventName, listener: ListenerFn): Scanner;
308 /**
309 * Add the listener function to the listeners array for the [[ready]] event, fired only once when the external
310 * Scandit Engine library has been loaded and the scanner can thus start to scan barcodes.
311 * If the external Scandit Engine library has already been loaded the listener is called immediately.
312 *
313 * No checks are made to see if the listener has already been added.
314 * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
315 *
316 * @param eventName The name of the event to listen to.
317 * @param listener The listener function.
318 * @returns The updated [[Scanner]] object.
319 */
320 public on(eventName: "ready", listener: () => void): Scanner;
321 public on(eventName: EventName, listener: ListenerFn): Scanner {
322 if (eventName === "ready") {
323 if (this.isReadyToWork) {
324 listener();
325 } else {
326 this.eventEmitter.once(eventName, listener, this);
327 }
328 } else if (eventName === "licenseFeaturesReady") {
329 if (this.licenseFeatures != null) {
330 listener(this.licenseFeatures);
331 } else {
332 this.eventEmitter.once(eventName, listener, this);
333 }
334 } else {
335 this.eventEmitter.on(eventName, listener, this);
336 }
337
338 return this;
339 }
340
341 /**
342 * Add the listener function to the listeners array for the [[ready]] event, fired only once when the external
343 * Scandit Engine library has been loaded and the scanner can thus start to scan barcodes.
344 * If the external Scandit Engine library has already been loaded the listener is called immediately.
345 *
346 * No checks are made to see if the listener has already been added.
347 * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
348 *
349 * @deprecated Use the [[on]] method instead.
350 *
351 * @param listener The listener function.
352 * @returns The updated [[Scanner]] object.
353 */
354 public onReady(listener: () => void): Scanner {
355 console.warn(
356 "The onReady(<listener>) method is deprecated and will be removed in the next major library version." +
357 'Please use on("ready", <listener>) instead.'
358 );
359
360 return this.on("ready", listener);
361 }
362
363 /**
364 * *See the [[on]] method.*
365 *
366 * @param eventName The name of the event to listen to.
367 * @param listener The listener function.
368 * @returns The updated [[Scanner]] object.
369 */
370 public addListener(eventName: EventName, listener: ListenerFn): Scanner {
371 return this.on(eventName, listener);
372 }
373
374 /**
375 * Create a new parser object.
376 *
377 * @param dataFormat The format of the input data for the parser.
378 * @returns The newly created parser.
379 */
380 public createParserForFormat(dataFormat: Parser.DataFormat): Parser {
381 return new Parser(this, dataFormat);
382 }
383
384 /**
385 * Return the current image settings.
386 *
387 * Note that modifying this object won't directly apply these settings: the [[applyImageSettings]] method must be
388 * called with the updated object.
389 *
390 * @returns The current image settings.
391 */
392 public getImageSettings(): ImageSettings | undefined {
393 return this.imageSettings;
394 }
395
396 /**
397 * Return the current scan settings.
398 *
399 * Note that modifying this object won't directly apply these settings: the [[applyScanSettings]] method must be
400 * called with the updated object.
401 *
402 * @returns The current scan settings.
403 */
404 public getScanSettings(): ScanSettings {
405 return this.scanSettings;
406 }
407
408 /**
409 * @hidden
410 *
411 * Process a given string using the Scandit Parser library,
412 * parsing the data in the given format and retrieving the result as a [[ParserResult]] object.
413 *
414 * Multiple requests done without waiting for previous results will be queued and handled in order.
415 *
416 * If parsing of the data fails the returned promise is rejected with a `ScanditEngineError` error.
417 *
418 * @param dataFormat The format of the given data.
419 * @param dataString The string containing the data to be parsed.
420 * @param options Options for the specific data format parser.
421 * @returns A promise resolving to the [[ParserResult]] object.
422 */
423 public parseString(dataFormat: Parser.DataFormat, dataString: string, options?: object): Promise<ParserResult> {
424 this.workerParseRequestId++;
425
426 return new Promise<ParserResult>((resolve, reject) => {
427 const parseStringResultEvent: string = `parseStringResult-${this.workerParseRequestId}`;
428 const parseStringErrorEvent: string = `parseStringError-${this.workerParseRequestId}`;
429 this.eventEmitter.once(parseStringResultEvent, (result: string) => {
430 this.eventEmitter.removeAllListeners(parseStringErrorEvent);
431 const parserResult: ParserResult = {
432 jsonString: result,
433 fields: [],
434 fieldsByName: {}
435 };
436 (<ParserField[]>JSON.parse(result)).forEach(parserField => {
437 parserResult.fields.push(parserField);
438 parserResult.fieldsByName[parserField.name] = parserField;
439 });
440 resolve(parserResult);
441 });
442 this.eventEmitter.once(parseStringErrorEvent, (error: { errorCode: number; errorMessage: string }) => {
443 console.error(`Scandit Engine error (${error.errorCode}):`, error.errorMessage);
444 this.eventEmitter.removeAllListeners(parseStringResultEvent);
445 const errorObject: Error = new CustomError({
446 name: "ScanditEngineError",
447 message: `${error.errorMessage} (${error.errorCode})`
448 });
449 reject(errorObject);
450 });
451 this.engineWorker.postMessage({
452 type: "parse-string",
453 requestId: this.workerParseRequestId,
454 dataFormat,
455 dataString,
456 options: options == null ? "{}" : JSON.stringify(options)
457 });
458 });
459 }
460
461 /**
462 * @hidden
463 *
464 * Add the listener function to the listeners array for the "licenseFeaturesReady" event, fired only once
465 * when the external Scandit Engine library has verified and loaded the license key and parsed its features.
466 * If the license features are already available the listener is called immediately.
467 *
468 * No checks are made to see if the listener has already been added.
469 * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
470 *
471 * @param listener The listener function, which will be invoked with a *licenseFeatures* object.
472 * @returns The updated [[Scanner]] object.
473 */
474 public onLicenseFeaturesReady(listener: (licenseFeatures: object) => void): Scanner {
475 return this.on("licenseFeaturesReady", listener);
476 }
477
478 /**
479 * @hidden
480 *
481 * Add the listener function to the listeners array for the "newScanSettings" event, fired when new a new
482 * [[ScanSettings]] object is applied via the [[applyScanSettings]] method.
483 *
484 * No checks are made to see if the listener has already been added.
485 * Multiple calls passing the same listener will result in the listener being added, and called, multiple times.
486 *
487 * @param listener The listener function, which will be invoked with a [[ScanSettings]] object.
488 * @returns The updated [[Scanner]] object.
489 */
490 public onNewScanSettings(listener: (scanSettings: ScanSettings) => void): Scanner {
491 return this.on("newScanSettings", listener);
492 }
493
494 /**
495 * @hidden
496 *
497 * Remove the specified listener from the given event's listener array.
498 *
499 * @param eventName The name of the event from which to remove the listener.
500 * @param listener The listener function to be removed.
501 * @returns The updated [[Scanner]] object.
502 */
503 public removeListener(eventName: string, listener: ListenerFn): Scanner {
504 this.eventEmitter.removeListener(eventName, listener);
505
506 return this;
507 }
508
509 private applyLicenseKey(licenseKey: string): Scanner {
510 this.engineWorker.postMessage({
511 type: "license-key",
512 licenseKey
513 });
514
515 return this;
516 }
517
518 private engineWorkerOnMessage(ev: MessageEvent): void {
519 // tslint:disable:max-union-size
520 const data:
521 | ["status", string]
522 | ["license-features", object]
523 | ["work-result", { requestId: number; result: { scanResult: BarcodeWASMResult[] } }]
524 | ["work-error", { requestId: number; error: { errorCode: number; errorMessage: string } }]
525 | ["parse-string-result", { requestId: number; result: string }]
526 | ["parse-string-error", { requestId: number; error: { errorCode: number; errorMessage: string } }] = ev.data;
527 // tslint:enable:max-union-size
528
529 if (data[0] === "status" && data[1] === "ready") {
530 this.isReadyToWork = true;
531 this.eventEmitter.emit("ready");
532 } else if (data[1] != null) {
533 if (data[0] === "license-features") {
534 this.licenseFeatures = data[1];
535 this.eventEmitter.emit("licenseFeaturesReady", this.licenseFeatures);
536 } else if (data[0] === "work-result") {
537 this.eventEmitter.emit(`workResult-${data[1].requestId}`, data[1].result);
538 this.workerScanQueueLength--;
539 } else if (data[0] === "work-error") {
540 this.eventEmitter.emit(`workError-${data[1].requestId}`, data[1].error);
541 this.workerScanQueueLength--;
542 } else if (data[0] === "parse-string-result") {
543 this.eventEmitter.emit(`parseStringResult-${data[1].requestId}`, data[1].result);
544 } else if (data[0] === "parse-string-error") {
545 this.eventEmitter.emit(`parseStringError-${data[1].requestId}`, data[1].error);
546 }
547 }
548 }
549}
550
551// istanbul ignore next
552export namespace Scanner {
553 /**
554 * Fired when the external Scandit Engine library has been loaded and the scanner can thus start to scan barcodes.
555 *
556 * @asMemberOf Scanner
557 * @event
558 */
559 // @ts-ignore
560 declare function ready(): void;
561 /**
562 * @hidden
563 *
564 * Fired when the external Scandit Engine library has verified and loaded the license key and parsed its features.
565 *
566 * @asMemberOf Scanner
567 * @event
568 * @param licenseFeatures The features of the used license key.
569 */
570 // @ts-ignore
571 // declare function licenseFeaturesReady(licenseFeatures: any): void;
572 /**
573 * @hidden
574 *
575 * Fired when new a new [[ScanSettings]] object is applied via the [[applyScanSettings]] method.
576 *
577 * @asMemberOf Scanner
578 * @event
579 * @param newScanSettings The features of the used license key.
580 */
581 // @ts-ignore
582 // declare function newScanSettings(newScanSettings: any): void;
583}
584
\No newline at end of file