Skip to content
Snippets Groups Projects
Commit e3650b71 authored by Reiter, Christoph's avatar Reiter, Christoph :snake:
Browse files

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.
parent 0987064d
No related branches found
No related tags found
No related merge requests found
Pipeline #127389 failed
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;
}
}
}
import {QrCodeScanner} from './qr-code-scanner.js'; export {QrCodeScanner} from './qr-code-scanner.js';
export {QrCodeScannerEngine, ScanResult} from './engine.js';
export {QrCodeScanner};
...@@ -7,6 +7,7 @@ import {Icon, MiniSpinner} from '@dbp-toolkit/common'; ...@@ -7,6 +7,7 @@ import {Icon, MiniSpinner} from '@dbp-toolkit/common';
import {classMap} from 'lit/directives/class-map.js'; import {classMap} from 'lit/directives/class-map.js';
import {Mutex} from 'async-mutex'; import {Mutex} from 'async-mutex';
import {getIconSVGURL} from '@dbp-toolkit/common'; import {getIconSVGURL} from '@dbp-toolkit/common';
import {QrCodeScannerEngine, ScanResult} from './engine.js';
/** /**
* Returns the ID for the most important device * Returns the ID for the most important device
...@@ -116,34 +117,6 @@ async function createVideoElement(deviceId) { ...@@ -116,34 +117,6 @@ async function createVideoElement(deviceId) {
return null; 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) { export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
constructor() { constructor() {
super(); super();
...@@ -265,10 +238,11 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) { ...@@ -265,10 +238,11 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
} }
this._askPermission = false; this._askPermission = false;
/** @type {?ScanResult} */
let lastCode = null; let lastCode = null;
let lastSentData = null; let lastSentData = null;
let detector = new QRScanner(); let detector = new QrCodeScannerEngine();
let detectorRunning = false; let detectorRunning = false;
const tick = () => { const tick = () => {
...@@ -300,7 +274,12 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) { ...@@ -300,7 +274,12 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
if (!detectorRunning) { if (!detectorRunning) {
detectorRunning = true; detectorRunning = true;
detector detector
.scan(canvasElement, maskStartX, maskStartY, maskWidth, maskHeight) .scanImage(canvasElement, {
x: maskStartX,
y: maskStartY,
width: maskWidth,
height: maskHeight,
})
.then((code) => { .then((code) => {
detectorRunning = false; detectorRunning = false;
// if we got restarted then the video element is new, so stop then. // if we got restarted then the video element is new, so stop then.
...@@ -308,7 +287,7 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) { ...@@ -308,7 +287,7 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) {
lastCode = code; lastCode = code;
if (code) { if (code) {
let currentData = code.data.data; let currentData = code.data;
if (lastSentData !== currentData) { if (lastSentData !== currentData) {
this._outputData = currentData; this._outputData = currentData;
this.dispatchEvent( this.dispatchEvent(
...@@ -327,7 +306,7 @@ export class QrCodeScanner extends ScopedElementsMixin(DBPLitElement) { ...@@ -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 //draw mask
canvas.beginPath(); canvas.beginPath();
......
import {assert} from '@esm-bundle/chai'; import {assert} from '@esm-bundle/chai';
import '../src/dbp-qr-code-scanner'; import '../src/dbp-qr-code-scanner';
import {QrCodeScannerEngine} from '../src';
suite('dbp-qr-code-scanner basics', () => { suite('dbp-qr-code-scanner basics', () => {
let node; let node;
...@@ -19,3 +20,30 @@ suite('dbp-qr-code-scanner basics', () => { ...@@ -19,3 +20,30 @@ suite('dbp-qr-code-scanner basics', () => {
assert.isNotNull(node.shadowRoot); 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');
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment