From e3650b714875ba52dd2e9058e35935525f3bcdf2 Mon Sep 17 00:00:00 2001
From: Christoph Reiter <reiter.christoph@gmail.com>
Date: Thu, 19 May 2022 12:54:40 +0200
Subject: [PATCH] qr-code-scanner: Export an API for scanning single images

In greenlight we copied this code for scanning images, either from
real image files, or from PDF files converted to images via pdf.js.

To avoid duplication clean up the internal API and expose it.

Also write some unit tests for the scan engine while at it.
---
 packages/qr-code-scanner/src/engine.js        | 48 +++++++++++++++++++
 packages/qr-code-scanner/src/index.js         |  5 +-
 .../qr-code-scanner/src/qr-code-scanner.js    | 43 +++++------------
 packages/qr-code-scanner/test/unit.js         | 28 +++++++++++
 4 files changed, 89 insertions(+), 35 deletions(-)
 create mode 100644 packages/qr-code-scanner/src/engine.js

diff --git a/packages/qr-code-scanner/src/engine.js b/packages/qr-code-scanner/src/engine.js
new file mode 100644
index 00000000..ff4dbce2
--- /dev/null
+++ b/packages/qr-code-scanner/src/engine.js
@@ -0,0 +1,48 @@
+export class ScanResult {
+    constructor() {
+        this.data = null;
+        this.cornerPoints = null;
+    }
+}
+
+export class QrCodeScannerEngine {
+    constructor() {
+        this._engine = null;
+        this._canvas = document.createElement('canvas');
+        this._scanner = null;
+    }
+
+    /**
+     * Scan am image like thing for a QR code. Returns null if none is found.
+     * The region to scan in can be restricted via "options".
+     *
+     * @param {*} image
+     * @param {?object} options
+     * @param {number} options.x
+     * @param {number} options.y
+     * @param {number} options.width
+     * @param {number} options.height
+     * @returns {?ScanResult}
+     */
+    async scanImage(image, options = null) {
+        if (this._scanner === null) {
+            this._scanner = (await import('qr-scanner')).default;
+        }
+        if (this._engine === null) {
+            this._engine = await this._scanner.createQrEngine();
+        }
+        try {
+            let tmp = await this._scanner.scanImage(image, {
+                scanRegion: options ?? null,
+                qrEngine: this._engine,
+                canvas: this._canvas,
+            });
+            let res = new ScanResult();
+            res.data = tmp.data;
+            res.cornerPoints = tmp.cornerPoints;
+            return res;
+        } catch (e) {
+            return null;
+        }
+    }
+}
diff --git a/packages/qr-code-scanner/src/index.js b/packages/qr-code-scanner/src/index.js
index 4e722c43..887a4cc3 100644
--- a/packages/qr-code-scanner/src/index.js
+++ b/packages/qr-code-scanner/src/index.js
@@ -1,3 +1,2 @@
-import {QrCodeScanner} from './qr-code-scanner.js';
-
-export {QrCodeScanner};
+export {QrCodeScanner} from './qr-code-scanner.js';
+export {QrCodeScannerEngine, ScanResult} from './engine.js';
diff --git a/packages/qr-code-scanner/src/qr-code-scanner.js b/packages/qr-code-scanner/src/qr-code-scanner.js
index be3080b5..e1ffcd07 100644
--- a/packages/qr-code-scanner/src/qr-code-scanner.js
+++ b/packages/qr-code-scanner/src/qr-code-scanner.js
@@ -7,6 +7,7 @@ import {Icon, MiniSpinner} from '@dbp-toolkit/common';
 import {classMap} from 'lit/directives/class-map.js';
 import {Mutex} from 'async-mutex';
 import {getIconSVGURL} from '@dbp-toolkit/common';
+import {QrCodeScannerEngine, ScanResult} from './engine.js';
 
 /**
  * Returns the ID for the most important device
@@ -116,34 +117,6 @@ async function createVideoElement(deviceId) {
     return null;
 }
 
-class QRScanner {
-    constructor() {
-        this._engine = null;
-        this._canvas = document.createElement('canvas');
-        this._scanner = null;
-    }
-
-    async scan(canvas, x, y, width, height) {
-        if (this._scanner === null) {
-            this._scanner = (await import('qr-scanner')).default;
-        }
-        if (this._engine === null) {
-            this._engine = await this._scanner.createQrEngine();
-        }
-        try {
-            return {
-                data: await this._scanner.scanImage(canvas, {
-                    scanRegion: {x: x, y: y, width: width, height: height},
-                    qrEngine: this._engine,
-                    canvas: this._canvas,
-                }),
-            };
-        } catch (e) {
-            return null;
-        }
-    }
-}
-
 export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
     constructor() {
         super();
@@ -265,10 +238,11 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
         }
         this._askPermission = false;
 
+        /** @type {?ScanResult} */
         let lastCode = null;
         let lastSentData = null;
 
-        let detector = new QRScanner();
+        let detector = new QrCodeScannerEngine();
         let detectorRunning = false;
 
         const tick = () => {
@@ -300,7 +274,12 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
                 if (!detectorRunning) {
                     detectorRunning = true;
                     detector
-                        .scan(canvasElement, maskStartX, maskStartY, maskWidth, maskHeight)
+                        .scanImage(canvasElement, {
+                            x: maskStartX,
+                            y: maskStartY,
+                            width: maskWidth,
+                            height: maskHeight,
+                        })
                         .then((code) => {
                             detectorRunning = false;
                             // if we got restarted then the video element is new, so stop then.
@@ -308,7 +287,7 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
                             lastCode = code;
 
                             if (code) {
-                                let currentData = code.data.data;
+                                let currentData = code.data;
                                 if (lastSentData !== currentData) {
                                     this._outputData = currentData;
                                     this.dispatchEvent(
@@ -327,7 +306,7 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
                         });
                 }
 
-                let matched = lastCode ? lastCode.data.data.match(this.matchRegex) !== null : false;
+                let matched = lastCode ? lastCode.data.match(this.matchRegex) !== null : false;
 
                 //draw mask
                 canvas.beginPath();
diff --git a/packages/qr-code-scanner/test/unit.js b/packages/qr-code-scanner/test/unit.js
index 2c1b2987..2d3bb796 100644
--- a/packages/qr-code-scanner/test/unit.js
+++ b/packages/qr-code-scanner/test/unit.js
@@ -1,6 +1,7 @@
 import {assert} from '@esm-bundle/chai';
 
 import '../src/dbp-qr-code-scanner';
+import {QrCodeScannerEngine} from '../src';
 
 suite('dbp-qr-code-scanner basics', () => {
     let node;
@@ -19,3 +20,30 @@ suite('dbp-qr-code-scanner basics', () => {
         assert.isNotNull(node.shadowRoot);
     });
 });
+
+suite('scan image', () => {
+    test('should detect', async () => {
+        let engine = new QrCodeScannerEngine();
+        let image = new Image();
+        image.setAttribute(
+            'src',
+            'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACYAAAAmAQMAAACS83vtAAABhGlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw0AcxV9bi1IqDu0gopChdbIgKuKoVShChVArtOpgcukXNGlIUlwcBdeCgx+LVQcXZ10dXAVB8APE0clJ0UVK/F9SaBHjwXE/3t173L0D/M0qU82ecUDVLCOTSgq5/KrQ+4ogIghhBHGJmfqcKKbhOb7u4ePrXYJneZ/7c/QrBZMBPoF4lumGRbxBPL1p6Zz3iaOsLCnE58RjBl2Q+JHrsstvnEsO+3lm1Mhm5omjxEKpi+UuZmVDJZ4ijimqRvn+nMsK5y3OarXO2vfkLwwXtJVlrtMcRgqLWIIIATLqqKAKCwlaNVJMZGg/6eEfcvwiuWRyVcDIsYAaVEiOH/wPfndrFicn3KRwEgi+2PZHHOjdBVoN2/4+tu3WCRB4Bq60jr/WBGY+SW90tNgRMLANXFx3NHkPuNwBBp90yZAcKUDTXywC72f0TXkgcguE1tze2vs4fQCy1FX6Bjg4BEZLlL3u8e6+7t7+PdPu7wdo/XKjkhoyogAAAAlwSFlzAAAN1wAADdcBQiibeAAAAAd0SU1FB+YFEwogFupCMRsAAAAGUExURQAAAP///6XZn90AAAABYktHRAH/Ai3eAAAAuUlEQVQI12P4DwR/GDDJD1KfFWwYvt/ctn4Pw5fYnRpAMrz1BZCM6wKS3y97vN/D8EHUa4ENw//PrPZ/GH7e7FwKJA19l9ow/Lv4extQzdPvjEBzBH+31jB80jyVu4fh//09JjDyn1Ef5x+Gz7caltswfM2xOruH4Y8Qd8Ueho8rnHhrGP6yWjPWAE0+uNOG4YMM+wqgaTe3WtYwfInoZQaS0b1BQDKcZRLQhZf99gJtkSieuIcBh18ArRODGZrlUXYAAAAASUVORK5CYII='
+        );
+        let res;
+
+        res = await engine.scanImage(image);
+        assert.strictEqual(res.data, 'http://en.m.wikipedia.org');
+
+        // the second time the same
+        res = await engine.scanImage(image);
+        assert.strictEqual(res.data, 'http://en.m.wikipedia.org');
+
+        // if we don't scan the whole thing then it fails
+        res = await engine.scanImage(image, {x: 0, y: 0, width: 5, height: 5});
+        assert.isNull(res);
+
+        // if we pass the right size, then everything is OK again
+        res = await engine.scanImage(image, {x: 0, y: 0, width: image.width, height: image.height});
+        assert.strictEqual(res.data, 'http://en.m.wikipedia.org');
+    });
+});
-- 
GitLab