1 | import { EventEmitter, ListenerFn } from "eventemitter3";
|
2 |
|
3 | import { engineWorkerBlob } from "./workers/engineWorker";
|
4 |
|
5 | import { deviceId, scanditEngineLocation, userLicenseKey } from "../index";
|
6 | import { Barcode, BarcodeWASMResult } from "./barcode";
|
7 | import { BrowserCompatibility } from "./browserCompatibility";
|
8 | import { BrowserHelper } from "./browserHelper";
|
9 | import { CustomError } from "./customError";
|
10 | import { ImageSettings } from "./imageSettings";
|
11 | import { Parser } from "./parser";
|
12 | import { ParserField } from "./parserField";
|
13 | import { ParserResult } from "./parserResult";
|
14 | import { ScanResult } from "./scanResult";
|
15 | import { ScanSettings } from "./scanSettings";
|
16 | import { UnsupportedBrowserError } from "./unsupportedBrowserError";
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | type EventName = "ready" | "licenseFeaturesReady" | "newScanSettings";
|
22 |
|
23 |
|
24 |
|
25 |
|
26 | class ScannerEventEmitter extends EventEmitter<string> {}
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | export 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 |
|
55 |
|
56 |
|
57 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
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 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 |
|
119 | public destroy(): void {
|
120 | if (this.engineWorker != null) {
|
121 | this.engineWorker.terminate();
|
122 | }
|
123 | this.eventEmitter.removeAllListeners();
|
124 | }
|
125 |
|
126 | |
127 |
|
128 |
|
129 |
|
130 |
|
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 |
|
145 |
|
146 |
|
147 |
|
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 |
|
161 |
|
162 |
|
163 |
|
164 |
|
165 |
|
166 |
|
167 | public clearSession(): Scanner {
|
168 | this.engineWorker.postMessage({
|
169 | type: "clear-session"
|
170 | });
|
171 |
|
172 | return this;
|
173 | }
|
174 |
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 |
|
184 |
|
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
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
|
552 | export 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 |