Support highlighting the outline of detected codes

This commit is contained in:
Daniel
2022-01-28 18:24:13 +01:00
parent 16ad9b9b82
commit 0d48a807f5
10 changed files with 188 additions and 142 deletions
+3 -2
View File
@@ -6,7 +6,7 @@ In this library, several improvements have been applied over the original port:
- Web cam scanning support out of the box
- Uses the browser's native [BarcodeDetector](https://web.dev/shape-detection/) [if available](https://github.com/WICG/shape-detection-api#overview)
- Lightweight: ~56.9 kB (~15.3 kB gzipped) minified with Google's closure compiler. If the native `BarcodeDetector` is available, only ~13.0 kB (~4.7 kB gzipped) are loaded.
- Lightweight: ~58.0 kB (~15.6 kB gzipped) minified with Google's closure compiler. If the native `BarcodeDetector` is available, only ~14.1 kB (~5.1 kB gzipped) are loaded.
- Improved performance and reduced memory footprint.
- Runs in a WebWorker which keeps the main / UI thread responsive.
- Can be configured for better performance on colored QR codes.
@@ -151,7 +151,8 @@ Supported options are:
| `onDecodeError` | Handler to be invoked on decoding errors. The default is `QrScanner._onDecodeError`. |
| `preferredCamera` | Preference for the camera to be used. The preference can be either a device id as returned by `listCameras` or a facing mode specified as `'environment'` or `'user'`. The default is `'environment'`. Note that there is no guarantee that the preference can actually be fulfilled. |
| `calculateScanRegion` | A method that determines a region to which scanning should be restricted as a performance improvement. This region can optionally also be scaled down before performing the scan as an additional performance improvement. The region is specified as `x`, `y`, `width` and `height`; the dimensions for the downscaled region as `downScaledWidth` and `downScaledHeight`. Note that the aspect ratio between `width` and `height` and `downScaledWidth` and `downScaledHeight` should remain the same. By default, the scan region is restricted to a centered square of two thirds of the video width or height, whichever is smaller, and scaled down to a 400x400 square. |
| `highlightScanRegion` | Set this option for rendering an outline around the the scan region on the video stream. This creates an absolutely positioned `div` that covers the scan region. This overlay can be accessed via `qrScanner.$scanRegionHighlight` and should be placed in the DOM as a sibling of `videoElem`. It can be custom styled via CSS, e.g. by setting an outline, border, background color, etc. See the [demo](https://nimiq.github.io/qr-scanner/demo/) for examples. |
| `highlightScanRegion` | Set this option to `true` for rendering an outline around the the scan region on the video stream. This creates an absolutely positioned `div` that covers the scan region. This overlay can be accessed via `qrScanner.$highlightOverlay` and should be placed in the DOM as a sibling of `videoElem`. It can be freely styled via CSS, e.g. by setting an outline, border, background color, etc. See the [demo](https://nimiq.github.io/qr-scanner/demo/) for examples. |
| `highlightCodeOutline` | Set this option to `true` for rendering an outline around detected QR codes. This creates an absolutely positioned `div` on which an SVG for rendering the outline will be placed. This overlay can be accessed via `qrScanner.$highlightOverlay` and should be placed in the DOM as a sibling of `videoElem`. The SVG can be freely styled via CSS, e.g. by setting the fill color, stroke color, stroke width, etc. See the [demo](https://nimiq.github.io/qr-scanner/demo/) for examples. For more special needs, you can also use the `cornerPoints` directly, see below, for rendering an outline or the points yourself. |
| `returnDetailedScanResult` | Enforce reporting detailed scan results, see below. |
To use the default value for an option, omit it or supply `undefined`.
+30 -47
View File
@@ -6,17 +6,16 @@
</head>
<body>
<h1>Scan from WebCam:</h1>
<div id="video-container" class="example-scan-region-highlight-style-1">
<div id="video-container" class="example-style-1">
<video id="qr-video"></video>
<canvas id="outline-canvas"></canvas>
</div>
<div>
<label>
Scan Region Highlight Style
Highlight Style
<select id="scan-region-highlight-style-select">
<option value="example-scan-region-highlight-style-1">Example custom style 1</option>
<option value="example-scan-region-highlight-style-2">Example custom style 2</option>
<option value="default-scan-region-highlight-style">Default style</option>
<option value="example-style-1">Example custom style 1</option>
<option value="example-style-2">Example custom style 2</option>
<option value="default-style">Default style</option>
</select>
</label>
<label>
@@ -69,8 +68,6 @@
const video = document.getElementById('qr-video');
const videoContainer = document.getElementById('video-container');
const outlineCanvas = document.getElementById('outline-canvas');
const outlineCanvasContext = outlineCanvas.getContext('2d');
const camHasCamera = document.getElementById('cam-has-camera');
const camList = document.getElementById('cam-list');
const camHasFlash = document.getElementById('cam-has-flash');
@@ -87,33 +84,18 @@
camQrResultTimestamp.textContent = new Date().toString();
label.style.color = 'teal';
clearTimeout(label.highlightTimeout);
label.highlightTimeout = setTimeout(() => {
label.style.color = 'inherit';
outlineCanvasContext.clearRect(0, 0, outlineCanvas.width, outlineCanvas.height);
}, 100);
if (label === camQrResult) {
outlineCanvasContext.clearRect(0, 0, outlineCanvas.width, outlineCanvas.height);
outlineCanvasContext.beginPath();
outlineCanvasContext.moveTo(result.cornerPoints[0].x, result.cornerPoints[0].y);
outlineCanvasContext.lineTo(result.cornerPoints[1].x, result.cornerPoints[1].y);
outlineCanvasContext.lineTo(result.cornerPoints[2].x, result.cornerPoints[2].y);
outlineCanvasContext.lineTo(result.cornerPoints[3].x, result.cornerPoints[3].y);
outlineCanvasContext.closePath();
outlineCanvasContext.stroke();
}
label.highlightTimeout = setTimeout(() => label.style.color = 'inherit', 100);
}
// ####### Web Cam Scanning #######
outlineCanvasContext.strokeStyle = 'teal';
outlineCanvasContext.lineWidth = 5;
const scanner = new QrScanner(video, result => setResult(camQrResult, result), {
onDecodeError: error => {
camQrResult.textContent = error;
camQrResult.style.color = 'inherit';
},
highlightScanRegion: true,
highlightCodeOutline: true,
});
const updateFlashAvailability = () => {
@@ -123,13 +105,6 @@
});
};
video.addEventListener('loadedmetadata', () => {
outlineCanvas.width = video.videoWidth;
outlineCanvas.height = video.videoHeight;
// copy the video's mirror
outlineCanvas.style.transform = video.style.transform;
});
scanner.start().then(() => {
updateFlashAvailability();
// List cameras after the scanner started to avoid listCamera's stream and the scanner's stream being requested
@@ -151,7 +126,7 @@
document.getElementById('scan-region-highlight-style-select').addEventListener('change', (e) => {
videoContainer.className = e.target.value;
scanner._updateScanRegionHighlight(); // reposition the highlight because style 2 sets position: relative
scanner._updateHighlightOverlay(); // reposition the highlight because style 2 sets position: relative
});
document.getElementById('show-scan-region').addEventListener('change', (e) => {
@@ -189,7 +164,7 @@
scanRegionHighlightSvg.innerHTML = `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke="#e9b213" stroke-width="4"
d="M31.3 2H10a8 8 0 0 0-8 8v21.3M206.8 2H228a8 8 0 0 1 8 8v21.3m0 175.4V228a8 8 0 0 1-8 8h-21.3m-175.4 0H10a8 8 0 0 1-8-8v-21.3">
</path>`;
scanner.$scanRegionHighlight.appendChild(scanRegionHighlightSvg);
scanner.$highlightOverlay.appendChild(scanRegionHighlightSvg);
// ####### File Scanning #######
@@ -213,38 +188,46 @@
line-height: 0;
}
#video-container:not(.example-scan-region-highlight-style-1) .scan-region-highlight-svg {
#video-container:not(.example-style-1) .scan-region-highlight-svg,
#video-container.example-style-1 :not(.scan-region-highlight) .scan-region-highlight-svg {
display: none;
}
#video-container.example-scan-region-highlight-style-1 .scan-region-highlight {
#video-container.example-style-1 .scan-region-highlight {
outline: none !important;
}
#video-container.example-scan-region-highlight-style-1 .scan-region-highlight-svg {
#video-container.example-style-1 .scan-region-highlight .scan-region-highlight-svg {
position: absolute;
width: 100%;
height: 100%;
animation: example-scan-region-highlight-style-1-animation .4s infinite alternate ease-in-out;
left: 0;
top: 0;
animation: example-style-1-animation .4s infinite alternate ease-in-out;
}
@keyframes example-scan-region-highlight-style-1-animation {
@keyframes example-style-1-animation {
from { transform: scale(.98); }
to { transform: scale(1.01); }
}
#video-container.example-style-1 .code-outline-highlight {
stroke-width: 5 !important;
stroke-dasharray: 25;
stroke-linejoin: round;
stroke-linecap: round;
}
#video-container.example-scan-region-highlight-style-2 {
#video-container.example-style-2 {
position: relative;
width: max-content;
height: max-content;
overflow: hidden;
}
#video-container.example-scan-region-highlight-style-2 .scan-region-highlight {
#video-container.example-style-2 .scan-region-highlight {
border-radius: 30px;
outline: rgba(0, 0, 0, .25) solid 50vmax !important;
}
#outline-canvas {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
#video-container.example-style-2 .code-outline-highlight {
stroke: rgba(255, 255, 255, .5) !important;
stroke-width: 15 !important;
stroke-linejoin: round;
}
#flash-toggle {
+16 -14
View File
@@ -18,26 +18,28 @@ c.generator.Engine_.prototype.nextStep_=function(){for(;this.context_.nextAddres
c.generator.Generator_=function(a){this.next=function(b){return a.next_(b)};this.throw=function(b){return a.throw_(b)};this.return=function(b){return a.return_(b)};this[Symbol.iterator]=function(){return this}};c.generator.createGenerator=function(a,b){b=new c.generator.Generator_(new c.generator.Engine_(b));c.setPrototypeOf&&a.prototype&&c.setPrototypeOf(b,a.prototype);return b};
c.asyncExecutePromiseGenerator=function(a){function b(e){return a.next(e)}function d(e){return a.throw(e)}return new Promise(function(e,f){function g(l){l.done?e(l.value):Promise.resolve(l.value).then(b,d).then(g,f)}g(a.next())})};c.asyncExecutePromiseGeneratorFunction=function(a){return c.asyncExecutePromiseGenerator(a())};c.asyncExecutePromiseGeneratorProgram=function(a){return c.asyncExecutePromiseGenerator(new c.generator.Generator_(new c.generator.Engine_(a)))};
class h{constructor(a,b,d,e,f){this._legacyCanvasSize=h.DEFAULT_CANVAS_SIZE;this._preferredCamera="environment";this._destroyed=this._flashOn=this._paused=this._active=!1;this.$video=a;this.$canvas=document.createElement("canvas");d&&"object"===typeof d?this._onDecode=b:(d||e||f?console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future"):console.warn("Note that the type of the scan result passed to onDecode will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true."),
this._legacyOnDecode=b);b="object"===typeof d?d:{};this._onDecodeError=b.onDecodeError||("function"===typeof d?d:this._onDecodeError);this._calculateScanRegion=b.calculateScanRegion||("function"===typeof e?e:this._calculateScanRegion);this._preferredCamera=b.preferredCamera||f||this._preferredCamera;this._legacyCanvasSize="number"===typeof d?d:"number"===typeof e?e:this._legacyCanvasSize;b.highlightScanRegion&&(this.$scanRegionHighlight=document.createElement("div"),this.$scanRegionHighlight.classList.add("scan-region-highlight"),
this.$scanRegionHighlight.style.position="absolute",this.$scanRegionHighlight.style.display="none",this.$scanRegionHighlight.style.outline="rgba(255, 255, 255, .3) solid 7px",this.$scanRegionHighlight.style.pointerEvents="none");this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this._updateScanRegionHighlight=this._updateScanRegionHighlight.bind(this);
a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let g=!1;a.hidden&&(a.hidden=!1,g=!0);document.body.contains(a)||(document.body.appendChild(a),g=!0);let l=a.parentElement;requestAnimationFrame(()=>{let m=window.getComputedStyle(a);"none"===m.display&&(a.style.setProperty("display","block","important"),g=!0);"visible"!==m.visibility&&(a.style.setProperty("visibility","visible","important"),g=!0);g&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),
a.style.opacity="0",a.style.width="0",a.style.height="0",delete this.$scanRegionHighlight);this.$scanRegionHighlight&&(this._updateScanRegionHighlight(),l.appendChild(this.$scanRegionHighlight))});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);window.addEventListener("resize",this._updateScanRegionHighlight);this._qrEnginePromise=h.createQrEngine()}static hasCamera(){return c.asyncExecutePromiseGeneratorFunction(function*(){try{return!!(yield h.listCameras(!1)).length}catch(a){return!1}})}static listCameras(a=
this._legacyOnDecode=b);b="object"===typeof d?d:{};this._onDecodeError=b.onDecodeError||("function"===typeof d?d:this._onDecodeError);this._calculateScanRegion=b.calculateScanRegion||("function"===typeof e?e:this._calculateScanRegion);this._preferredCamera=b.preferredCamera||f||this._preferredCamera;this._legacyCanvasSize="number"===typeof d?d:"number"===typeof e?e:this._legacyCanvasSize;if(b.highlightScanRegion||b.highlightCodeOutline)this.$highlightOverlay=document.createElement("div"),d=this.$highlightOverlay.style,
d.position="absolute",d.display="none",d.pointerEvents="none",b.highlightScanRegion&&(this.$highlightOverlay.classList.add("scan-region-highlight"),d.outline="#e9b213 solid 5px"),b.highlightCodeOutline&&(this.$codeOutlineHighlight=document.createElementNS("http://www.w3.org/2000/svg","svg"),d=document.createElementNS("http://www.w3.org/2000/svg","polygon"),this.$codeOutlineHighlight.appendChild(d),this.$highlightOverlay.appendChild(this.$codeOutlineHighlight),d=this.$codeOutlineHighlight.style,d.width=
"100%",d.height="100%",this.$codeOutlineHighlight.setAttribute("preserveAspectRatio","none"),d.display="none",this.$codeOutlineHighlight.classList.add("code-outline-highlight"),d.fill="none",d.stroke="#e9b213",d.strokeWidth="4");this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this._updateHighlightOverlay=this._updateHighlightOverlay.bind(this);a.disablePictureInPicture=
!0;a.playsInline=!0;a.muted=!0;let g=!1;a.hidden&&(a.hidden=!1,g=!0);document.body.contains(a)||(document.body.appendChild(a),g=!0);let l=a.parentElement;requestAnimationFrame(()=>{let m=window.getComputedStyle(a);"none"===m.display&&(a.style.setProperty("display","block","important"),g=!0);"visible"!==m.visibility&&(a.style.setProperty("visibility","visible","important"),g=!0);g&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity=
"0",a.style.width="0",a.style.height="0",delete this.$highlightOverlay,delete this.$codeOutlineHighlight);this.$highlightOverlay&&(this._updateHighlightOverlay(),l.appendChild(this.$highlightOverlay))});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);window.addEventListener("resize",this._updateHighlightOverlay);this._qrEnginePromise=h.createQrEngine()}static hasCamera(){return c.asyncExecutePromiseGeneratorFunction(function*(){try{return!!(yield h.listCameras(!1)).length}catch(a){return!1}})}static listCameras(a=
!1){return c.asyncExecutePromiseGeneratorFunction(function*(){if(!navigator.mediaDevices)return[];let b=()=>c.asyncExecutePromiseGeneratorFunction(function*(){return(yield navigator.mediaDevices.enumerateDevices()).filter(e=>"videoinput"===e.kind)}),d;try{a&&(yield b()).every(e=>!e.label)&&(d=yield navigator.mediaDevices.getUserMedia({audio:!1,video:!0}))}catch(e){}try{return(yield b()).map((e,f)=>({id:e.deviceId,label:e.label||(0===f?"Default Camera":`Camera ${f+1}`)}))}finally{d&&(console.warn("Call listCameras after successfully starting a QR scanner to avoid creating a temporary video stream"),
h._stopVideoStream(d))}})}hasFlash(){const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){let b;try{if(a.$video.srcObject){if(!(a.$video.srcObject instanceof MediaStream))return!1;b=a.$video.srcObject}else b=(yield a._getCameraStream()).stream;return"torch"in b.getVideoTracks()[0].getSettings()}catch(d){return!1}finally{b&&b!==a.$video.srcObject&&(console.warn("Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream"),h._stopVideoStream(b))}})}isFlashOn(){return this._flashOn}toggleFlash(){const a=
this;return c.asyncExecutePromiseGeneratorFunction(function*(){a._flashOn?yield a.turnFlashOff():yield a.turnFlashOn()})}turnFlashOn(){const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){if(!a._flashOn&&!a._destroyed&&(a._flashOn=!0,a._active&&!a._paused))try{if(!(yield a.hasFlash()))throw"No flash available";yield a.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:!0}]})}catch(b){throw a._flashOn=!1,b;}})}turnFlashOff(){const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){a._flashOn&&
(a._flashOn=!1,yield a._restartVideoStream())})}destroy(){this.$video.removeEventListener("loadedmetadata",this._onLoadedMetaData);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);window.removeEventListener("resize",this._updateScanRegionHighlight);this._destroyed=!0;this._flashOn=!1;this.stop();h._postWorkerMessage(this._qrEnginePromise,"close")}start(){const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){if(!(a._active&&
(a._flashOn=!1,yield a._restartVideoStream())})}destroy(){this.$video.removeEventListener("loadedmetadata",this._onLoadedMetaData);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);window.removeEventListener("resize",this._updateHighlightOverlay);this._destroyed=!0;this._flashOn=!1;this.stop();h._postWorkerMessage(this._qrEnginePromise,"close")}start(){const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){if(!(a._active&&
!a._paused||a._destroyed||("https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https."),a._active=!0,document.hidden)))if(a._paused=!1,a.$video.srcObject)yield a.$video.play();else try{let {stream:b,facingMode:d}=yield a._getCameraStream();!a._active||a._paused?h._stopVideoStream(b):(a._setVideoMirror(d),a.$video.srcObject=b,yield a.$video.play(),a._flashOn&&(a._flashOn=!1,a.turnFlashOn().catch(()=>{})))}catch(b){if(!a._paused)throw a._active=
!1,b;}})}stop(){this.pause();this._active=!1}pause(a=!1){const b=this;return c.asyncExecutePromiseGeneratorFunction(function*(){b._paused=!0;if(!b._active)return!0;b.$video.pause();b.$scanRegionHighlight&&(b.$scanRegionHighlight.style.display="none");let d=()=>{b.$video.srcObject instanceof MediaStream&&(h._stopVideoStream(b.$video.srcObject),b.$video.srcObject=null)};if(a)return d(),!0;yield new Promise(e=>setTimeout(e,300));if(!b._paused)return!1;d();return!0})}setCamera(a){const b=this;return c.asyncExecutePromiseGeneratorFunction(function*(){a!==
!1,b;}})}stop(){this.pause();this._active=!1}pause(a=!1){const b=this;return c.asyncExecutePromiseGeneratorFunction(function*(){b._paused=!0;if(!b._active)return!0;b.$video.pause();b.$highlightOverlay&&(b.$highlightOverlay.style.display="none");let d=()=>{b.$video.srcObject instanceof MediaStream&&(h._stopVideoStream(b.$video.srcObject),b.$video.srcObject=null)};if(a)return d(),!0;yield new Promise(e=>setTimeout(e,300));if(!b._paused)return!1;d();return!0})}setCamera(a){const b=this;return c.asyncExecutePromiseGeneratorFunction(function*(){a!==
b._preferredCamera&&(b._preferredCamera=a,yield b._restartVideoStream())})}static scanImage(a,b,d,e,f=!1,g=!1){return c.asyncExecutePromiseGeneratorFunction(function*(){let l,m=!1;b&&("scanRegion"in b||"qrEngine"in b||"canvas"in b||"disallowCanvasResizing"in b||"alsoTryWithoutScanRegion"in b||"returnDetailedScanResult"in b)?(l=b.scanRegion,d=b.qrEngine,e=b.canvas,f=b.disallowCanvasResizing||!1,g=b.alsoTryWithoutScanRegion||!1,m=!0):b||d||e||f||g?console.warn("You're using a deprecated api for scanImage which will be removed in the future."):
console.warn("Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true.");let r=!!d;try{let p,q;[d,p]=yield Promise.all([d||h.createQrEngine(),h._loadImage(a)]);[e,q]=h._drawToCanvas(p,l,e,f);if(d instanceof Worker){let k=d;r||k.postMessage({type:"inversionMode",data:"both"});return yield new Promise((n,w)=>{let x,u,v;u=t=>{"qrResult"===t.data.type&&(k.removeEventListener("message",u),k.removeEventListener("error",
v),clearTimeout(x),null!==t.data.data?n(m?{data:t.data.data,cornerPoints:h._convertPoints(t.data.cornerPoints,l)}:t.data.data):w(h.NO_QR_CODE_FOUND))};v=t=>{k.removeEventListener("message",u);k.removeEventListener("error",v);clearTimeout(x);w("Scanner error: "+(t?t.message||t:"Unknown Error"))};k.addEventListener("message",u);k.addEventListener("error",v);x=setTimeout(()=>v("timeout"),1E4);let y=q.getImageData(0,0,e.width,e.height);k.postMessage({type:"decode",data:y},[y.data.buffer])})}return yield Promise.race([new Promise((k,
n)=>window.setTimeout(()=>n("Scanner error: timeout"),1E4)),(()=>c.asyncExecutePromiseGeneratorFunction(function*(){try{let [k]=yield d.detect(e);if(!k)throw h.NO_QR_CODE_FOUND;return m?{data:k.rawValue,cornerPoints:h._convertPoints(k.cornerPoints,l)}:k.rawValue}catch(k){throw`Scanner error: ${k.message||k}`;}}))()])}catch(p){if(!l||!g)throw p;let q=yield h.scanImage(a,{qrEngine:d,canvas:e,disallowCanvasResizing:f});return m?q:q.data}finally{r||h._postWorkerMessage(d,"close")}})}setGrayscaleWeights(a,
console.warn("Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true.");let t=!!d;try{let n,r;[d,n]=yield Promise.all([d||h.createQrEngine(),h._loadImage(a)]);[e,r]=h._drawToCanvas(n,l,e,f);if(d instanceof Worker){let k=d;t||k.postMessage({type:"inversionMode",data:"both"});return yield new Promise((p,w)=>{let x,v,u;v=q=>{"qrResult"===q.data.type&&(k.removeEventListener("message",v),k.removeEventListener("error",
u),clearTimeout(x),null!==q.data.data?p(m?{data:q.data.data,cornerPoints:h._convertPoints(q.data.cornerPoints,l)}:q.data.data):w(h.NO_QR_CODE_FOUND))};u=q=>{k.removeEventListener("message",v);k.removeEventListener("error",u);clearTimeout(x);w("Scanner error: "+(q?q.message||q:"Unknown Error"))};k.addEventListener("message",v);k.addEventListener("error",u);x=setTimeout(()=>u("timeout"),1E4);let y=r.getImageData(0,0,e.width,e.height);k.postMessage({type:"decode",data:y},[y.data.buffer])})}return yield Promise.race([new Promise((k,
p)=>window.setTimeout(()=>p("Scanner error: timeout"),1E4)),(()=>c.asyncExecutePromiseGeneratorFunction(function*(){try{let [k]=yield d.detect(e);if(!k)throw h.NO_QR_CODE_FOUND;return m?{data:k.rawValue,cornerPoints:h._convertPoints(k.cornerPoints,l)}:k.rawValue}catch(k){throw`Scanner error: ${k.message||k}`;}}))()])}catch(n){if(!l||!g)throw n;let r=yield h.scanImage(a,{qrEngine:d,canvas:e,disallowCanvasResizing:f});return m?r:r.data}finally{t||h._postWorkerMessage(d,"close")}})}setGrayscaleWeights(a,
b,d,e=!0){h._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,blue:d,useIntegerApproximation:e})}setInversionMode(a){h._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static createQrEngine(a=h.WORKER_PATH){return c.asyncExecutePromiseGeneratorFunction(function*(){return"BarcodeDetector"in window&&BarcodeDetector.getSupportedFormats&&(yield BarcodeDetector.getSupportedFormats()).includes("qr_code")?new BarcodeDetector({formats:["qr_code"]}):new Worker(a)})}_onPlay(){this._scanRegion=
this._calculateScanRegion(this.$video);this._updateScanRegionHighlight();this.$scanRegionHighlight&&(this.$scanRegionHighlight.style.display="");this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateScanRegionHighlight()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-
b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_updateScanRegionHighlight(){requestAnimationFrame(()=>{if(this.$scanRegionHighlight){var a=this.$video,b=a.videoWidth,d=a.videoHeight,e=a.offsetWidth,f=a.offsetHeight,g=a.offsetLeft,l=a.offsetTop,m=window.getComputedStyle(a),r=m.objectFit,p=b/d,q=e/f;switch(r){case "none":var k=b;var n=d;break;case "fill":k=e;n=f;break;default:("cover"===r?p>q:p<q)?(n=f,k=n*p):(k=e,n=k/p),"scale-down"===r&&(k=Math.min(k,
b),n=Math.min(n,d))}var [w,x]=m.objectPosition.split(" ").map((u,v)=>{const y=parseFloat(u);return u.endsWith("%")?(v?f-n:e-k)*y/100:y});m=this._scanRegion.width||b;p=this._scanRegion.height||d;r=this._scanRegion.x||0;q=this._scanRegion.y||0;this.$scanRegionHighlight.style.width=`${m/b*k}px`;this.$scanRegionHighlight.style.height=`${p/d*n}px`;this.$scanRegionHighlight.style.top=`${l+x+q/d*n}px`;a=/scaleX\(-1\)/.test(a.style.transform);this.$scanRegionHighlight.style.left=`${g+(a?e-w-k:w)+(a?b-r-m:
r)/b*k}px`}})}static _convertPoints(a,b){if(!b)return a;let d=b.x||0,e=b.y||0,f=b.width&&b.downScaledWidth?b.width/b.downScaledWidth:1;b=b.height&&b.downScaledHeight?b.height/b.downScaledHeight:1;for(let g of a)g.x=g.x*f+d,g.y=g.y*b+e;return a}_scanFrame(){!this._active||this.$video.paused||this.$video.ended||requestAnimationFrame(()=>{const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){if(!(1>=a.$video.readyState)){try{var b=yield h.scanImage(a.$video,{scanRegion:a._scanRegion,
qrEngine:a._qrEnginePromise,canvas:a.$canvas})}catch(d){if(!a._active)return;(d.message||d).includes("service unavailable")&&(a._qrEnginePromise=h.createQrEngine());a._onDecodeError(d)}b&&a._onDecode?a._onDecode(b):b&&a._legacyOnDecode&&a._legacyOnDecode(b.data)}a._scanFrame()})})}_onDecodeError(a){a!==h.NO_QR_CODE_FOUND&&console.log(a)}_getCameraStream(){const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){if(!navigator.mediaDevices)throw"Camera not found.";let b=/^(environment|user)$/.test(a._preferredCamera)?
this._calculateScanRegion(this.$video);this._updateHighlightOverlay();this.$highlightOverlay&&(this.$highlightOverlay.style.display="");this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateHighlightOverlay()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,
height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_updateHighlightOverlay(){requestAnimationFrame(()=>{if(this.$highlightOverlay){var a=this.$video,b=a.videoWidth,d=a.videoHeight,e=a.offsetWidth,f=a.offsetHeight,g=a.offsetLeft,l=a.offsetTop,m=window.getComputedStyle(a),t=m.objectFit,n=b/d,r=e/f;switch(t){case "none":var k=b;var p=d;break;case "fill":k=e;p=f;break;default:("cover"===t?n>r:n<r)?(p=f,k=p*n):(k=e,p=k/n),"scale-down"===t&&(k=Math.min(k,b),p=Math.min(p,
d))}var [w,x]=m.objectPosition.split(" ").map((u,y)=>{const q=parseFloat(u);return u.endsWith("%")?(y?f-p:e-k)*q/100:q});m=this._scanRegion.width||b;r=this._scanRegion.height||d;t=this._scanRegion.x||0;var v=this._scanRegion.y||0;n=this.$highlightOverlay.style;n.width=`${m/b*k}px`;n.height=`${r/d*p}px`;n.top=`${l+x+v/d*p}px`;d=/scaleX\(-1\)/.test(a.style.transform);n.left=`${g+(d?e-w-k:w)+(d?b-t-m:t)/b*k}px`;n.transform=a.style.transform}})}static _convertPoints(a,b){if(!b)return a;let d=b.x||0,e=
b.y||0,f=b.width&&b.downScaledWidth?b.width/b.downScaledWidth:1;b=b.height&&b.downScaledHeight?b.height/b.downScaledHeight:1;for(let g of a)g.x=g.x*f+d,g.y=g.y*b+e;return a}_scanFrame(){!this._active||this.$video.paused||this.$video.ended||requestAnimationFrame(()=>{const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){if(!(1>=a.$video.readyState)){try{var b=yield h.scanImage(a.$video,{scanRegion:a._scanRegion,qrEngine:a._qrEnginePromise,canvas:a.$canvas})}catch(d){if(!a._active)return;
(d.message||d).includes("service unavailable")&&(a._qrEnginePromise=h.createQrEngine());a._onDecodeError(d)}b?(a._onDecode?a._onDecode(b):a._legacyOnDecode&&a._legacyOnDecode(b.data),a.$codeOutlineHighlight&&(clearTimeout(a._codeOutlineHighlightRemovalTimeout),a._codeOutlineHighlightRemovalTimeout=void 0,a.$codeOutlineHighlight.setAttribute("viewBox",`${a._scanRegion.x||0} `+`${a._scanRegion.y||0} `+`${a._scanRegion.width||a.$video.videoWidth} `+`${a._scanRegion.height||a.$video.videoHeight}`),a.$codeOutlineHighlight.firstElementChild.setAttribute("points",
b.cornerPoints.map(({x:d,y:e})=>`${d},${e}`).join(" ")),a.$codeOutlineHighlight.style.display="")):a.$codeOutlineHighlight&&!a._codeOutlineHighlightRemovalTimeout&&(a._codeOutlineHighlightRemovalTimeout=setTimeout(()=>a.$codeOutlineHighlight.style.display="none",100))}a._scanFrame()})})}_onDecodeError(a){a!==h.NO_QR_CODE_FOUND&&console.log(a)}_getCameraStream(){const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){if(!navigator.mediaDevices)throw"Camera not found.";let b=/^(environment|user)$/.test(a._preferredCamera)?
"facingMode":"deviceId",d=[{width:{min:1024}},{width:{min:768}},{}],e=d.map(f=>Object.assign({},f,{[b]:{exact:a._preferredCamera}}));for(let f of[...e,...d])try{let g=yield navigator.mediaDevices.getUserMedia({video:f,audio:!1}),l=a._getFacingMode(g)||(f.facingMode?a._preferredCamera:"environment"===a._preferredCamera?"user":"environment");return{stream:g,facingMode:l}}catch(g){}throw"Camera not found.";})}_restartVideoStream(){const a=this;return c.asyncExecutePromiseGeneratorFunction(function*(){let b=
a._paused;(yield a.pause(!0))&&!b&&a._active&&(yield a.start())})}static _stopVideoStream(a){for(let b of a.getTracks())b.stop(),a.removeTrack(b)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _drawToCanvas(a,b,d,e=!1){d=d||document.createElement("canvas");let f=b&&b.x?b.x:0,g=b&&b.y?b.y:0,l=b&&b.width?b.width:
a.videoWidth||a.width,m=b&&b.height?b.height:a.videoHeight||a.height;e||(e=b&&b.downScaledWidth?b.downScaledWidth:l,b=b&&b.downScaledHeight?b.downScaledHeight:m,d.width!==e&&(d.width=e),d.height!==b&&(d.height=b));b=d.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,f,g,l,m,0,0,d.width,d.height);return[d,b]}static _loadImage(a){return c.asyncExecutePromiseGeneratorFunction(function*(){if(a instanceof Image)return yield h._awaitImageLoad(a),a;if(a instanceof HTMLVideoElement||a instanceof
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"qr-scanner.legacy.min.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
{"version":3,"file":"qr-scanner.legacy.min.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
+24 -21
View File
@@ -1,26 +1,29 @@
class e{constructor(a,b,c,d,f){this._legacyCanvasSize=e.DEFAULT_CANVAS_SIZE;this._preferredCamera="environment";this._destroyed=this._flashOn=this._paused=this._active=!1;this.$video=a;this.$canvas=document.createElement("canvas");c&&"object"===typeof c?this._onDecode=b:(c||d||f?console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future"):console.warn("Note that the type of the scan result passed to onDecode will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true."),
this._legacyOnDecode=b);b="object"===typeof c?c:{};this._onDecodeError=b.onDecodeError||("function"===typeof c?c:this._onDecodeError);this._calculateScanRegion=b.calculateScanRegion||("function"===typeof d?d:this._calculateScanRegion);this._preferredCamera=b.preferredCamera||f||this._preferredCamera;this._legacyCanvasSize="number"===typeof c?c:"number"===typeof d?d:this._legacyCanvasSize;b.highlightScanRegion&&(this.$scanRegionHighlight=document.createElement("div"),this.$scanRegionHighlight.classList.add("scan-region-highlight"),
this.$scanRegionHighlight.style.position="absolute",this.$scanRegionHighlight.style.display="none",this.$scanRegionHighlight.style.outline="rgba(255, 255, 255, .3) solid 7px",this.$scanRegionHighlight.style.pointerEvents="none");this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this._updateScanRegionHighlight=this._updateScanRegionHighlight.bind(this);
a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let h=!1;a.hidden&&(a.hidden=!1,h=!0);document.body.contains(a)||(document.body.appendChild(a),h=!0);let m=a.parentElement;requestAnimationFrame(()=>{let k=window.getComputedStyle(a);"none"===k.display&&(a.style.setProperty("display","block","important"),h=!0);"visible"!==k.visibility&&(a.style.setProperty("visibility","visible","important"),h=!0);h&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),
a.style.opacity="0",a.style.width="0",a.style.height="0",delete this.$scanRegionHighlight);this.$scanRegionHighlight&&(this._updateScanRegionHighlight(),m.appendChild(this.$scanRegionHighlight))});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);window.addEventListener("resize",this._updateScanRegionHighlight);this._qrEnginePromise=e.createQrEngine()}static async hasCamera(){try{return!!(await e.listCameras(!1)).length}catch(a){return!1}}static async listCameras(a=
this._legacyOnDecode=b);b="object"===typeof c?c:{};this._onDecodeError=b.onDecodeError||("function"===typeof c?c:this._onDecodeError);this._calculateScanRegion=b.calculateScanRegion||("function"===typeof d?d:this._calculateScanRegion);this._preferredCamera=b.preferredCamera||f||this._preferredCamera;this._legacyCanvasSize="number"===typeof c?c:"number"===typeof d?d:this._legacyCanvasSize;if(b.highlightScanRegion||b.highlightCodeOutline)this.$highlightOverlay=document.createElement("div"),c=this.$highlightOverlay.style,
c.position="absolute",c.display="none",c.pointerEvents="none",b.highlightScanRegion&&(this.$highlightOverlay.classList.add("scan-region-highlight"),c.outline="#e9b213 solid 5px"),b.highlightCodeOutline&&(this.$codeOutlineHighlight=document.createElementNS("http://www.w3.org/2000/svg","svg"),c=document.createElementNS("http://www.w3.org/2000/svg","polygon"),this.$codeOutlineHighlight.appendChild(c),this.$highlightOverlay.appendChild(this.$codeOutlineHighlight),c=this.$codeOutlineHighlight.style,c.width=
"100%",c.height="100%",this.$codeOutlineHighlight.setAttribute("preserveAspectRatio","none"),c.display="none",this.$codeOutlineHighlight.classList.add("code-outline-highlight"),c.fill="none",c.stroke="#e9b213",c.strokeWidth="4");this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this._updateHighlightOverlay=this._updateHighlightOverlay.bind(this);a.disablePictureInPicture=
!0;a.playsInline=!0;a.muted=!0;let h=!1;a.hidden&&(a.hidden=!1,h=!0);document.body.contains(a)||(document.body.appendChild(a),h=!0);let n=a.parentElement;requestAnimationFrame(()=>{let k=window.getComputedStyle(a);"none"===k.display&&(a.style.setProperty("display","block","important"),h=!0);"visible"!==k.visibility&&(a.style.setProperty("visibility","visible","important"),h=!0);h&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity=
"0",a.style.width="0",a.style.height="0",delete this.$highlightOverlay,delete this.$codeOutlineHighlight);this.$highlightOverlay&&(this._updateHighlightOverlay(),n.appendChild(this.$highlightOverlay))});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);window.addEventListener("resize",this._updateHighlightOverlay);this._qrEnginePromise=e.createQrEngine()}static async hasCamera(){try{return!!(await e.listCameras(!1)).length}catch(a){return!1}}static async listCameras(a=
!1){if(!navigator.mediaDevices)return[];let b=async()=>(await navigator.mediaDevices.enumerateDevices()).filter(d=>"videoinput"===d.kind),c;try{a&&(await b()).every(d=>!d.label)&&(c=await navigator.mediaDevices.getUserMedia({audio:!1,video:!0}))}catch(d){}try{return(await b()).map((d,f)=>({id:d.deviceId,label:d.label||(0===f?"Default Camera":`Camera ${f+1}`)}))}finally{c&&(console.warn("Call listCameras after successfully starting a QR scanner to avoid creating a temporary video stream"),e._stopVideoStream(c))}}async hasFlash(){let a;
try{if(this.$video.srcObject){if(!(this.$video.srcObject instanceof MediaStream))return!1;a=this.$video.srcObject}else a=(await this._getCameraStream()).stream;return"torch"in a.getVideoTracks()[0].getSettings()}catch(b){return!1}finally{a&&a!==this.$video.srcObject&&(console.warn("Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream"),e._stopVideoStream(a))}}isFlashOn(){return this._flashOn}async toggleFlash(){this._flashOn?await this.turnFlashOff():await this.turnFlashOn()}async turnFlashOn(){if(!this._flashOn&&
!this._destroyed&&(this._flashOn=!0,this._active&&!this._paused))try{if(!await this.hasFlash())throw"No flash available";await this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:!0}]})}catch(a){throw this._flashOn=!1,a;}}async turnFlashOff(){this._flashOn&&(this._flashOn=!1,await this._restartVideoStream())}destroy(){this.$video.removeEventListener("loadedmetadata",this._onLoadedMetaData);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",
this._onVisibilityChange);window.removeEventListener("resize",this._updateScanRegionHighlight);this._destroyed=!0;this._flashOn=!1;this.stop();e._postWorkerMessage(this._qrEnginePromise,"close")}async start(){if(!(this._active&&!this._paused||this._destroyed||("https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https."),this._active=!0,document.hidden)))if(this._paused=!1,this.$video.srcObject)await this.$video.play();else try{let {stream:a,
facingMode:b}=await this._getCameraStream();!this._active||this._paused?e._stopVideoStream(a):(this._setVideoMirror(b),this.$video.srcObject=a,await this.$video.play(),this._flashOn&&(this._flashOn=!1,this.turnFlashOn().catch(()=>{})))}catch(a){if(!this._paused)throw this._active=!1,a;}}stop(){this.pause();this._active=!1}async pause(a=!1){this._paused=!0;if(!this._active)return!0;this.$video.pause();this.$scanRegionHighlight&&(this.$scanRegionHighlight.style.display="none");let b=()=>{this.$video.srcObject instanceof
MediaStream&&(e._stopVideoStream(this.$video.srcObject),this.$video.srcObject=null)};if(a)return b(),!0;await new Promise(c=>setTimeout(c,300));if(!this._paused)return!1;b();return!0}async setCamera(a){a!==this._preferredCamera&&(this._preferredCamera=a,await this._restartVideoStream())}static async scanImage(a,b,c,d,f=!1,h=!1){let m,k=!1;b&&("scanRegion"in b||"qrEngine"in b||"canvas"in b||"disallowCanvasResizing"in b||"alsoTryWithoutScanRegion"in b||"returnDetailedScanResult"in b)?(m=b.scanRegion,
c=b.qrEngine,d=b.canvas,f=b.disallowCanvasResizing||!1,h=b.alsoTryWithoutScanRegion||!1,k=!0):b||c||d||f||h?console.warn("You're using a deprecated api for scanImage which will be removed in the future."):console.warn("Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true.");b=!!c;try{let r,n;[c,r]=await Promise.all([c||e.createQrEngine(),e._loadImage(a)]);[d,n]=e._drawToCanvas(r,m,d,f);if(c instanceof
Worker){let g=c;b||g.postMessage({type:"inversionMode",data:"both"});return await new Promise((l,p)=>{let u,v,t;v=q=>{"qrResult"===q.data.type&&(g.removeEventListener("message",v),g.removeEventListener("error",t),clearTimeout(u),null!==q.data.data?l(k?{data:q.data.data,cornerPoints:e._convertPoints(q.data.cornerPoints,m)}:q.data.data):p(e.NO_QR_CODE_FOUND))};t=q=>{g.removeEventListener("message",v);g.removeEventListener("error",t);clearTimeout(u);p("Scanner error: "+(q?q.message||q:"Unknown Error"))};
g.addEventListener("message",v);g.addEventListener("error",t);u=setTimeout(()=>t("timeout"),1E4);let w=n.getImageData(0,0,d.width,d.height);g.postMessage({type:"decode",data:w},[w.data.buffer])})}return await Promise.race([new Promise((g,l)=>window.setTimeout(()=>l("Scanner error: timeout"),1E4)),(async()=>{try{let [g]=await c.detect(d);if(!g)throw e.NO_QR_CODE_FOUND;return k?{data:g.rawValue,cornerPoints:e._convertPoints(g.cornerPoints,m)}:g.rawValue}catch(g){throw`Scanner error: ${g.message||g}`;
}})()])}catch(r){if(!m||!h)throw r;let n=await e.scanImage(a,{qrEngine:c,canvas:d,disallowCanvasResizing:f});return k?n:n.data}finally{b||e._postWorkerMessage(c,"close")}}setGrayscaleWeights(a,b,c,d=!0){e._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,blue:c,useIntegerApproximation:d})}setInversionMode(a){e._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static async createQrEngine(a=e.WORKER_PATH){return"BarcodeDetector"in window&&BarcodeDetector.getSupportedFormats&&
(await BarcodeDetector.getSupportedFormats()).includes("qr_code")?new BarcodeDetector({formats:["qr_code"]}):new Worker(a)}_onPlay(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateScanRegionHighlight();this.$scanRegionHighlight&&(this.$scanRegionHighlight.style.display="");this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateScanRegionHighlight()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=
Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_updateScanRegionHighlight(){requestAnimationFrame(()=>{if(this.$scanRegionHighlight){var a=this.$video,b=a.videoWidth,c=a.videoHeight,d=a.offsetWidth,f=a.offsetHeight,h=a.offsetLeft,m=a.offsetTop,k=window.getComputedStyle(a),r=k.objectFit,n=b/c,g=d/f;switch(r){case "none":var l=
b;var p=c;break;case "fill":l=d;p=f;break;default:("cover"===r?n>g:n<g)?(p=f,l=p*n):(l=d,p=l/n),"scale-down"===r&&(l=Math.min(l,b),p=Math.min(p,c))}var [u,v]=k.objectPosition.split(" ").map((t,w)=>{const q=parseFloat(t);return t.endsWith("%")?(w?f-p:d-l)*q/100:q});k=this._scanRegion.width||b;n=this._scanRegion.height||c;r=this._scanRegion.x||0;g=this._scanRegion.y||0;this.$scanRegionHighlight.style.width=`${k/b*l}px`;this.$scanRegionHighlight.style.height=`${n/c*p}px`;this.$scanRegionHighlight.style.top=
`${m+v+g/c*p}px`;a=/scaleX\(-1\)/.test(a.style.transform);this.$scanRegionHighlight.style.left=`${h+(a?d-u-l:u)+(a?b-r-k:r)/b*l}px`}})}static _convertPoints(a,b){if(!b)return a;let c=b.x||0,d=b.y||0,f=b.width&&b.downScaledWidth?b.width/b.downScaledWidth:1;b=b.height&&b.downScaledHeight?b.height/b.downScaledHeight:1;for(let h of a)h.x=h.x*f+c,h.y=h.y*b+d;return a}_scanFrame(){!this._active||this.$video.paused||this.$video.ended||requestAnimationFrame(async()=>{if(!(1>=this.$video.readyState)){try{var a=
await e.scanImage(this.$video,{scanRegion:this._scanRegion,qrEngine:this._qrEnginePromise,canvas:this.$canvas})}catch(b){if(!this._active)return;(b.message||b).includes("service unavailable")&&(this._qrEnginePromise=e.createQrEngine());this._onDecodeError(b)}a&&this._onDecode?this._onDecode(a):a&&this._legacyOnDecode&&this._legacyOnDecode(a.data)}this._scanFrame()})}_onDecodeError(a){a!==e.NO_QR_CODE_FOUND&&console.log(a)}async _getCameraStream(){if(!navigator.mediaDevices)throw"Camera not found.";
let a=/^(environment|user)$/.test(this._preferredCamera)?"facingMode":"deviceId",b=[{width:{min:1024}},{width:{min:768}},{}],c=b.map(d=>Object.assign({},d,{[a]:{exact:this._preferredCamera}}));for(let d of[...c,...b])try{let f=await navigator.mediaDevices.getUserMedia({video:d,audio:!1}),h=this._getFacingMode(f)||(d.facingMode?this._preferredCamera:"environment"===this._preferredCamera?"user":"environment");return{stream:f,facingMode:h}}catch(f){}throw"Camera not found.";}async _restartVideoStream(){let a=
this._paused;await this.pause(!0)&&!a&&this._active&&await this.start()}static _stopVideoStream(a){for(let b of a.getTracks())b.stop(),a.removeTrack(b)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _drawToCanvas(a,b,c,d=!1){c=c||document.createElement("canvas");let f=b&&b.x?b.x:0,h=b&&b.y?b.y:0,m=b&&b.width?
b.width:a.videoWidth||a.width,k=b&&b.height?b.height:a.videoHeight||a.height;d||(d=b&&b.downScaledWidth?b.downScaledWidth:m,b=b&&b.downScaledHeight?b.downScaledHeight:k,c.width!==d&&(c.width=d),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,f,h,m,k,0,0,c.width,c.height);return[c,b]}static async _loadImage(a){if(a instanceof Image)return await e._awaitImageLoad(a),a;if(a instanceof HTMLVideoElement||a instanceof HTMLCanvasElement||a instanceof SVGImageElement||
"OffscreenCanvas"in window&&a instanceof OffscreenCanvas||"ImageBitmap"in window&&a instanceof ImageBitmap)return a;if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a.toString();try{return await e._awaitImageLoad(b),b}finally{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src)}}else throw"Unsupported image type.";}static async _awaitImageLoad(a){a.complete&&0!==a.naturalWidth||
await new Promise((b,c)=>{let d=f=>{a.removeEventListener("load",d);a.removeEventListener("error",d);f instanceof ErrorEvent?c("Image load error"):b()};a.addEventListener("load",d);a.addEventListener("error",d)})}static async _postWorkerMessage(a,b,c){a=await a;a instanceof Worker&&a.postMessage({type:b,data:c})}}e.DEFAULT_CANVAS_SIZE=400;e.NO_QR_CODE_FOUND="No QR code found";e.WORKER_PATH="qr-scanner-worker.min.js";export default e
this._onVisibilityChange);window.removeEventListener("resize",this._updateHighlightOverlay);this._destroyed=!0;this._flashOn=!1;this.stop();e._postWorkerMessage(this._qrEnginePromise,"close")}async start(){if(!(this._active&&!this._paused||this._destroyed||("https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https."),this._active=!0,document.hidden)))if(this._paused=!1,this.$video.srcObject)await this.$video.play();else try{let {stream:a,
facingMode:b}=await this._getCameraStream();!this._active||this._paused?e._stopVideoStream(a):(this._setVideoMirror(b),this.$video.srcObject=a,await this.$video.play(),this._flashOn&&(this._flashOn=!1,this.turnFlashOn().catch(()=>{})))}catch(a){if(!this._paused)throw this._active=!1,a;}}stop(){this.pause();this._active=!1}async pause(a=!1){this._paused=!0;if(!this._active)return!0;this.$video.pause();this.$highlightOverlay&&(this.$highlightOverlay.style.display="none");let b=()=>{this.$video.srcObject instanceof
MediaStream&&(e._stopVideoStream(this.$video.srcObject),this.$video.srcObject=null)};if(a)return b(),!0;await new Promise(c=>setTimeout(c,300));if(!this._paused)return!1;b();return!0}async setCamera(a){a!==this._preferredCamera&&(this._preferredCamera=a,await this._restartVideoStream())}static async scanImage(a,b,c,d,f=!1,h=!1){let n,k=!1;b&&("scanRegion"in b||"qrEngine"in b||"canvas"in b||"disallowCanvasResizing"in b||"alsoTryWithoutScanRegion"in b||"returnDetailedScanResult"in b)?(n=b.scanRegion,
c=b.qrEngine,d=b.canvas,f=b.disallowCanvasResizing||!1,h=b.alsoTryWithoutScanRegion||!1,k=!0):b||c||d||f||h?console.warn("You're using a deprecated api for scanImage which will be removed in the future."):console.warn("Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true.");b=!!c;try{let q,l;[c,q]=await Promise.all([c||e.createQrEngine(),e._loadImage(a)]);[d,l]=e._drawToCanvas(q,n,d,f);if(c instanceof
Worker){let g=c;b||g.postMessage({type:"inversionMode",data:"both"});return await new Promise((m,p)=>{let u,v,t;v=r=>{"qrResult"===r.data.type&&(g.removeEventListener("message",v),g.removeEventListener("error",t),clearTimeout(u),null!==r.data.data?m(k?{data:r.data.data,cornerPoints:e._convertPoints(r.data.cornerPoints,n)}:r.data.data):p(e.NO_QR_CODE_FOUND))};t=r=>{g.removeEventListener("message",v);g.removeEventListener("error",t);clearTimeout(u);p("Scanner error: "+(r?r.message||r:"Unknown Error"))};
g.addEventListener("message",v);g.addEventListener("error",t);u=setTimeout(()=>t("timeout"),1E4);let w=l.getImageData(0,0,d.width,d.height);g.postMessage({type:"decode",data:w},[w.data.buffer])})}return await Promise.race([new Promise((g,m)=>window.setTimeout(()=>m("Scanner error: timeout"),1E4)),(async()=>{try{let [g]=await c.detect(d);if(!g)throw e.NO_QR_CODE_FOUND;return k?{data:g.rawValue,cornerPoints:e._convertPoints(g.cornerPoints,n)}:g.rawValue}catch(g){throw`Scanner error: ${g.message||g}`;
}})()])}catch(q){if(!n||!h)throw q;let l=await e.scanImage(a,{qrEngine:c,canvas:d,disallowCanvasResizing:f});return k?l:l.data}finally{b||e._postWorkerMessage(c,"close")}}setGrayscaleWeights(a,b,c,d=!0){e._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,blue:c,useIntegerApproximation:d})}setInversionMode(a){e._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static async createQrEngine(a=e.WORKER_PATH){return"BarcodeDetector"in window&&BarcodeDetector.getSupportedFormats&&
(await BarcodeDetector.getSupportedFormats()).includes("qr_code")?new BarcodeDetector({formats:["qr_code"]}):new Worker(a)}_onPlay(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateHighlightOverlay();this.$highlightOverlay&&(this.$highlightOverlay.style.display="");this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateHighlightOverlay()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=
Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_updateHighlightOverlay(){requestAnimationFrame(()=>{if(this.$highlightOverlay){var a=this.$video,b=a.videoWidth,c=a.videoHeight,d=a.offsetWidth,f=a.offsetHeight,h=a.offsetLeft,n=a.offsetTop,k=window.getComputedStyle(a),q=k.objectFit,l=b/c,g=d/f;switch(q){case "none":var m=b;var p=
c;break;case "fill":m=d;p=f;break;default:("cover"===q?l>g:l<g)?(p=f,m=p*l):(m=d,p=m/l),"scale-down"===q&&(m=Math.min(m,b),p=Math.min(p,c))}var [u,v]=k.objectPosition.split(" ").map((w,r)=>{const x=parseFloat(w);return w.endsWith("%")?(r?f-p:d-m)*x/100:x});k=this._scanRegion.width||b;g=this._scanRegion.height||c;q=this._scanRegion.x||0;var t=this._scanRegion.y||0;l=this.$highlightOverlay.style;l.width=`${k/b*m}px`;l.height=`${g/c*p}px`;l.top=`${n+v+t/c*p}px`;c=/scaleX\(-1\)/.test(a.style.transform);
l.left=`${h+(c?d-u-m:u)+(c?b-q-k:q)/b*m}px`;l.transform=a.style.transform}})}static _convertPoints(a,b){if(!b)return a;let c=b.x||0,d=b.y||0,f=b.width&&b.downScaledWidth?b.width/b.downScaledWidth:1;b=b.height&&b.downScaledHeight?b.height/b.downScaledHeight:1;for(let h of a)h.x=h.x*f+c,h.y=h.y*b+d;return a}_scanFrame(){!this._active||this.$video.paused||this.$video.ended||requestAnimationFrame(async()=>{if(!(1>=this.$video.readyState)){try{var a=await e.scanImage(this.$video,{scanRegion:this._scanRegion,
qrEngine:this._qrEnginePromise,canvas:this.$canvas})}catch(b){if(!this._active)return;(b.message||b).includes("service unavailable")&&(this._qrEnginePromise=e.createQrEngine());this._onDecodeError(b)}a?(this._onDecode?this._onDecode(a):this._legacyOnDecode&&this._legacyOnDecode(a.data),this.$codeOutlineHighlight&&(clearTimeout(this._codeOutlineHighlightRemovalTimeout),this._codeOutlineHighlightRemovalTimeout=void 0,this.$codeOutlineHighlight.setAttribute("viewBox",`${this._scanRegion.x||0} `+`${this._scanRegion.y||
0} `+`${this._scanRegion.width||this.$video.videoWidth} `+`${this._scanRegion.height||this.$video.videoHeight}`),this.$codeOutlineHighlight.firstElementChild.setAttribute("points",a.cornerPoints.map(({x:b,y:c})=>`${b},${c}`).join(" ")),this.$codeOutlineHighlight.style.display="")):this.$codeOutlineHighlight&&!this._codeOutlineHighlightRemovalTimeout&&(this._codeOutlineHighlightRemovalTimeout=setTimeout(()=>this.$codeOutlineHighlight.style.display="none",100))}this._scanFrame()})}_onDecodeError(a){a!==
e.NO_QR_CODE_FOUND&&console.log(a)}async _getCameraStream(){if(!navigator.mediaDevices)throw"Camera not found.";let a=/^(environment|user)$/.test(this._preferredCamera)?"facingMode":"deviceId",b=[{width:{min:1024}},{width:{min:768}},{}],c=b.map(d=>Object.assign({},d,{[a]:{exact:this._preferredCamera}}));for(let d of[...c,...b])try{let f=await navigator.mediaDevices.getUserMedia({video:d,audio:!1}),h=this._getFacingMode(f)||(d.facingMode?this._preferredCamera:"environment"===this._preferredCamera?
"user":"environment");return{stream:f,facingMode:h}}catch(f){}throw"Camera not found.";}async _restartVideoStream(){let a=this._paused;await this.pause(!0)&&!a&&this._active&&await this.start()}static _stopVideoStream(a){for(let b of a.getTracks())b.stop(),a.removeTrack(b)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:
null}static _drawToCanvas(a,b,c,d=!1){c=c||document.createElement("canvas");let f=b&&b.x?b.x:0,h=b&&b.y?b.y:0,n=b&&b.width?b.width:a.videoWidth||a.width,k=b&&b.height?b.height:a.videoHeight||a.height;d||(d=b&&b.downScaledWidth?b.downScaledWidth:n,b=b&&b.downScaledHeight?b.downScaledHeight:k,c.width!==d&&(c.width=d),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,f,h,n,k,0,0,c.width,c.height);return[c,b]}static async _loadImage(a){if(a instanceof
Image)return await e._awaitImageLoad(a),a;if(a instanceof HTMLVideoElement||a instanceof HTMLCanvasElement||a instanceof SVGImageElement||"OffscreenCanvas"in window&&a instanceof OffscreenCanvas||"ImageBitmap"in window&&a instanceof ImageBitmap)return a;if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a.toString();try{return await e._awaitImageLoad(b),b}finally{(a instanceof File||a instanceof
Blob)&&URL.revokeObjectURL(b.src)}}else throw"Unsupported image type.";}static async _awaitImageLoad(a){a.complete&&0!==a.naturalWidth||await new Promise((b,c)=>{let d=f=>{a.removeEventListener("load",d);a.removeEventListener("error",d);f instanceof ErrorEvent?c("Image load error"):b()};a.addEventListener("load",d);a.addEventListener("error",d)})}static async _postWorkerMessage(a,b,c){a=await a;a instanceof Worker&&a.postMessage({type:b,data:c})}}e.DEFAULT_CANVAS_SIZE=400;e.NO_QR_CODE_FOUND="No QR code found";
e.WORKER_PATH="qr-scanner-worker.min.js";export default e
//# sourceMappingURL=qr-scanner.min.js.map
File diff suppressed because one or more lines are too long
+23 -21
View File
@@ -1,27 +1,29 @@
'use strict';(function(e,a){"object"===typeof exports&&"undefined"!==typeof module?module.exports=a():"function"===typeof define&&define.amd?define(a):(e="undefined"!==typeof globalThis?globalThis:e||self,e.QrScanner=a())})(this,function(){class e{constructor(a,b,c,d,f){this._legacyCanvasSize=e.DEFAULT_CANVAS_SIZE;this._preferredCamera="environment";this._destroyed=this._flashOn=this._paused=this._active=!1;this.$video=a;this.$canvas=document.createElement("canvas");c&&"object"===typeof c?this._onDecode=
b:(c||d||f?console.warn("You're using a deprecated version of the QrScanner constructor which will be removed in the future"):console.warn("Note that the type of the scan result passed to onDecode will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true."),this._legacyOnDecode=b);b="object"===typeof c?c:{};this._onDecodeError=b.onDecodeError||("function"===typeof c?c:this._onDecodeError);this._calculateScanRegion=b.calculateScanRegion||("function"===
typeof d?d:this._calculateScanRegion);this._preferredCamera=b.preferredCamera||f||this._preferredCamera;this._legacyCanvasSize="number"===typeof c?c:"number"===typeof d?d:this._legacyCanvasSize;b.highlightScanRegion&&(this.$scanRegionHighlight=document.createElement("div"),this.$scanRegionHighlight.classList.add("scan-region-highlight"),this.$scanRegionHighlight.style.position="absolute",this.$scanRegionHighlight.style.display="none",this.$scanRegionHighlight.style.outline="rgba(255, 255, 255, .3) solid 7px",
this.$scanRegionHighlight.style.pointerEvents="none");this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this._updateScanRegionHighlight=this._updateScanRegionHighlight.bind(this);a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let h=!1;a.hidden&&(a.hidden=!1,h=!0);document.body.contains(a)||(document.body.appendChild(a),h=!0);let m=a.parentElement;
requestAnimationFrame(()=>{let k=window.getComputedStyle(a);"none"===k.display&&(a.style.setProperty("display","block","important"),h=!0);"visible"!==k.visibility&&(a.style.setProperty("visibility","visible","important"),h=!0);h&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity="0",a.style.width="0",a.style.height="0",delete this.$scanRegionHighlight);this.$scanRegionHighlight&&(this._updateScanRegionHighlight(),m.appendChild(this.$scanRegionHighlight))});
a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);window.addEventListener("resize",this._updateScanRegionHighlight);this._qrEnginePromise=e.createQrEngine()}static async hasCamera(){try{return!!(await e.listCameras(!1)).length}catch(a){return!1}}static async listCameras(a=!1){if(!navigator.mediaDevices)return[];let b=async()=>(await navigator.mediaDevices.enumerateDevices()).filter(d=>
typeof d?d:this._calculateScanRegion);this._preferredCamera=b.preferredCamera||f||this._preferredCamera;this._legacyCanvasSize="number"===typeof c?c:"number"===typeof d?d:this._legacyCanvasSize;if(b.highlightScanRegion||b.highlightCodeOutline)this.$highlightOverlay=document.createElement("div"),c=this.$highlightOverlay.style,c.position="absolute",c.display="none",c.pointerEvents="none",b.highlightScanRegion&&(this.$highlightOverlay.classList.add("scan-region-highlight"),c.outline="#e9b213 solid 5px"),
b.highlightCodeOutline&&(this.$codeOutlineHighlight=document.createElementNS("http://www.w3.org/2000/svg","svg"),c=document.createElementNS("http://www.w3.org/2000/svg","polygon"),this.$codeOutlineHighlight.appendChild(c),this.$highlightOverlay.appendChild(this.$codeOutlineHighlight),c=this.$codeOutlineHighlight.style,c.width="100%",c.height="100%",this.$codeOutlineHighlight.setAttribute("preserveAspectRatio","none"),c.display="none",this.$codeOutlineHighlight.classList.add("code-outline-highlight"),
c.fill="none",c.stroke="#e9b213",c.strokeWidth="4");this._scanRegion=this._calculateScanRegion(a);this._onPlay=this._onPlay.bind(this);this._onLoadedMetaData=this._onLoadedMetaData.bind(this);this._onVisibilityChange=this._onVisibilityChange.bind(this);this._updateHighlightOverlay=this._updateHighlightOverlay.bind(this);a.disablePictureInPicture=!0;a.playsInline=!0;a.muted=!0;let h=!1;a.hidden&&(a.hidden=!1,h=!0);document.body.contains(a)||(document.body.appendChild(a),h=!0);let n=a.parentElement;
requestAnimationFrame(()=>{let k=window.getComputedStyle(a);"none"===k.display&&(a.style.setProperty("display","block","important"),h=!0);"visible"!==k.visibility&&(a.style.setProperty("visibility","visible","important"),h=!0);h&&(console.warn("QrScanner has overwritten the video hiding style to avoid Safari stopping the playback."),a.style.opacity="0",a.style.width="0",a.style.height="0",delete this.$highlightOverlay,delete this.$codeOutlineHighlight);this.$highlightOverlay&&(this._updateHighlightOverlay(),
n.appendChild(this.$highlightOverlay))});a.addEventListener("play",this._onPlay);a.addEventListener("loadedmetadata",this._onLoadedMetaData);document.addEventListener("visibilitychange",this._onVisibilityChange);window.addEventListener("resize",this._updateHighlightOverlay);this._qrEnginePromise=e.createQrEngine()}static async hasCamera(){try{return!!(await e.listCameras(!1)).length}catch(a){return!1}}static async listCameras(a=!1){if(!navigator.mediaDevices)return[];let b=async()=>(await navigator.mediaDevices.enumerateDevices()).filter(d=>
"videoinput"===d.kind),c;try{a&&(await b()).every(d=>!d.label)&&(c=await navigator.mediaDevices.getUserMedia({audio:!1,video:!0}))}catch(d){}try{return(await b()).map((d,f)=>({id:d.deviceId,label:d.label||(0===f?"Default Camera":`Camera ${f+1}`)}))}finally{c&&(console.warn("Call listCameras after successfully starting a QR scanner to avoid creating a temporary video stream"),e._stopVideoStream(c))}}async hasFlash(){let a;try{if(this.$video.srcObject){if(!(this.$video.srcObject instanceof MediaStream))return!1;
a=this.$video.srcObject}else a=(await this._getCameraStream()).stream;return"torch"in a.getVideoTracks()[0].getSettings()}catch(b){return!1}finally{a&&a!==this.$video.srcObject&&(console.warn("Call hasFlash after successfully starting the scanner to avoid creating a temporary video stream"),e._stopVideoStream(a))}}isFlashOn(){return this._flashOn}async toggleFlash(){this._flashOn?await this.turnFlashOff():await this.turnFlashOn()}async turnFlashOn(){if(!this._flashOn&&!this._destroyed&&(this._flashOn=
!0,this._active&&!this._paused))try{if(!await this.hasFlash())throw"No flash available";await this.$video.srcObject.getVideoTracks()[0].applyConstraints({advanced:[{torch:!0}]})}catch(a){throw this._flashOn=!1,a;}}async turnFlashOff(){this._flashOn&&(this._flashOn=!1,await this._restartVideoStream())}destroy(){this.$video.removeEventListener("loadedmetadata",this._onLoadedMetaData);this.$video.removeEventListener("play",this._onPlay);document.removeEventListener("visibilitychange",this._onVisibilityChange);
window.removeEventListener("resize",this._updateScanRegionHighlight);this._destroyed=!0;this._flashOn=!1;this.stop();e._postWorkerMessage(this._qrEnginePromise,"close")}async start(){if(!(this._active&&!this._paused||this._destroyed||("https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https."),this._active=!0,document.hidden)))if(this._paused=!1,this.$video.srcObject)await this.$video.play();else try{let {stream:a,facingMode:b}=await this._getCameraStream();
!this._active||this._paused?e._stopVideoStream(a):(this._setVideoMirror(b),this.$video.srcObject=a,await this.$video.play(),this._flashOn&&(this._flashOn=!1,this.turnFlashOn().catch(()=>{})))}catch(a){if(!this._paused)throw this._active=!1,a;}}stop(){this.pause();this._active=!1}async pause(a=!1){this._paused=!0;if(!this._active)return!0;this.$video.pause();this.$scanRegionHighlight&&(this.$scanRegionHighlight.style.display="none");let b=()=>{this.$video.srcObject instanceof MediaStream&&(e._stopVideoStream(this.$video.srcObject),
this.$video.srcObject=null)};if(a)return b(),!0;await new Promise(c=>setTimeout(c,300));if(!this._paused)return!1;b();return!0}async setCamera(a){a!==this._preferredCamera&&(this._preferredCamera=a,await this._restartVideoStream())}static async scanImage(a,b,c,d,f=!1,h=!1){let m,k=!1;b&&("scanRegion"in b||"qrEngine"in b||"canvas"in b||"disallowCanvasResizing"in b||"alsoTryWithoutScanRegion"in b||"returnDetailedScanResult"in b)?(m=b.scanRegion,c=b.qrEngine,d=b.canvas,f=b.disallowCanvasResizing||!1,
h=b.alsoTryWithoutScanRegion||!1,k=!0):b||c||d||f||h?console.warn("You're using a deprecated api for scanImage which will be removed in the future."):console.warn("Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true.");b=!!c;try{let r,n;[c,r]=await Promise.all([c||e.createQrEngine(),e._loadImage(a)]);[d,n]=e._drawToCanvas(r,m,d,f);if(c instanceof Worker){let g=c;b||g.postMessage({type:"inversionMode",
data:"both"});return await new Promise((l,p)=>{let u,v,t;v=q=>{"qrResult"===q.data.type&&(g.removeEventListener("message",v),g.removeEventListener("error",t),clearTimeout(u),null!==q.data.data?l(k?{data:q.data.data,cornerPoints:e._convertPoints(q.data.cornerPoints,m)}:q.data.data):p(e.NO_QR_CODE_FOUND))};t=q=>{g.removeEventListener("message",v);g.removeEventListener("error",t);clearTimeout(u);p("Scanner error: "+(q?q.message||q:"Unknown Error"))};g.addEventListener("message",v);g.addEventListener("error",
t);u=setTimeout(()=>t("timeout"),1E4);let w=n.getImageData(0,0,d.width,d.height);g.postMessage({type:"decode",data:w},[w.data.buffer])})}return await Promise.race([new Promise((g,l)=>window.setTimeout(()=>l("Scanner error: timeout"),1E4)),(async()=>{try{let [g]=await c.detect(d);if(!g)throw e.NO_QR_CODE_FOUND;return k?{data:g.rawValue,cornerPoints:e._convertPoints(g.cornerPoints,m)}:g.rawValue}catch(g){throw`Scanner error: ${g.message||g}`;}})()])}catch(r){if(!m||!h)throw r;let n=await e.scanImage(a,
{qrEngine:c,canvas:d,disallowCanvasResizing:f});return k?n:n.data}finally{b||e._postWorkerMessage(c,"close")}}setGrayscaleWeights(a,b,c,d=!0){e._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,blue:c,useIntegerApproximation:d})}setInversionMode(a){e._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static async createQrEngine(a=e.WORKER_PATH){return"BarcodeDetector"in window&&BarcodeDetector.getSupportedFormats&&(await BarcodeDetector.getSupportedFormats()).includes("qr_code")?
new BarcodeDetector({formats:["qr_code"]}):new Worker(a)}_onPlay(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateScanRegionHighlight();this.$scanRegionHighlight&&(this.$scanRegionHighlight.style.display="");this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateScanRegionHighlight()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,
a.videoHeight));return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_updateScanRegionHighlight(){requestAnimationFrame(()=>{if(this.$scanRegionHighlight){var a=this.$video,b=a.videoWidth,c=a.videoHeight,d=a.offsetWidth,f=a.offsetHeight,h=a.offsetLeft,m=a.offsetTop,k=window.getComputedStyle(a),r=k.objectFit,n=b/c,g=d/f;switch(r){case "none":var l=b;var p=c;break;case "fill":l=d;p=
f;break;default:("cover"===r?n>g:n<g)?(p=f,l=p*n):(l=d,p=l/n),"scale-down"===r&&(l=Math.min(l,b),p=Math.min(p,c))}var [u,v]=k.objectPosition.split(" ").map((t,w)=>{const q=parseFloat(t);return t.endsWith("%")?(w?f-p:d-l)*q/100:q});k=this._scanRegion.width||b;n=this._scanRegion.height||c;r=this._scanRegion.x||0;g=this._scanRegion.y||0;this.$scanRegionHighlight.style.width=`${k/b*l}px`;this.$scanRegionHighlight.style.height=`${n/c*p}px`;this.$scanRegionHighlight.style.top=`${m+v+g/c*p}px`;a=/scaleX\(-1\)/.test(a.style.transform);
this.$scanRegionHighlight.style.left=`${h+(a?d-u-l:u)+(a?b-r-k:r)/b*l}px`}})}static _convertPoints(a,b){if(!b)return a;let c=b.x||0,d=b.y||0,f=b.width&&b.downScaledWidth?b.width/b.downScaledWidth:1;b=b.height&&b.downScaledHeight?b.height/b.downScaledHeight:1;for(let h of a)h.x=h.x*f+c,h.y=h.y*b+d;return a}_scanFrame(){!this._active||this.$video.paused||this.$video.ended||requestAnimationFrame(async()=>{if(!(1>=this.$video.readyState)){try{var a=await e.scanImage(this.$video,{scanRegion:this._scanRegion,
qrEngine:this._qrEnginePromise,canvas:this.$canvas})}catch(b){if(!this._active)return;(b.message||b).includes("service unavailable")&&(this._qrEnginePromise=e.createQrEngine());this._onDecodeError(b)}a&&this._onDecode?this._onDecode(a):a&&this._legacyOnDecode&&this._legacyOnDecode(a.data)}this._scanFrame()})}_onDecodeError(a){a!==e.NO_QR_CODE_FOUND&&console.log(a)}async _getCameraStream(){if(!navigator.mediaDevices)throw"Camera not found.";let a=/^(environment|user)$/.test(this._preferredCamera)?
"facingMode":"deviceId",b=[{width:{min:1024}},{width:{min:768}},{}],c=b.map(d=>Object.assign({},d,{[a]:{exact:this._preferredCamera}}));for(let d of[...c,...b])try{let f=await navigator.mediaDevices.getUserMedia({video:d,audio:!1}),h=this._getFacingMode(f)||(d.facingMode?this._preferredCamera:"environment"===this._preferredCamera?"user":"environment");return{stream:f,facingMode:h}}catch(f){}throw"Camera not found.";}async _restartVideoStream(){let a=this._paused;await this.pause(!0)&&!a&&this._active&&
await this.start()}static _stopVideoStream(a){for(let b of a.getTracks())b.stop(),a.removeTrack(b)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _drawToCanvas(a,b,c,d=!1){c=c||document.createElement("canvas");let f=b&&b.x?b.x:0,h=b&&b.y?b.y:0,m=b&&b.width?b.width:a.videoWidth||a.width,k=b&&b.height?b.height:
a.videoHeight||a.height;d||(d=b&&b.downScaledWidth?b.downScaledWidth:m,b=b&&b.downScaledHeight?b.downScaledHeight:k,c.width!==d&&(c.width=d),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,f,h,m,k,0,0,c.width,c.height);return[c,b]}static async _loadImage(a){if(a instanceof Image)return await e._awaitImageLoad(a),a;if(a instanceof HTMLVideoElement||a instanceof HTMLCanvasElement||a instanceof SVGImageElement||"OffscreenCanvas"in window&&a instanceof
OffscreenCanvas||"ImageBitmap"in window&&a instanceof ImageBitmap)return a;if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a.toString();try{return await e._awaitImageLoad(b),b}finally{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src)}}else throw"Unsupported image type.";}static async _awaitImageLoad(a){a.complete&&0!==a.naturalWidth||await new Promise((b,c)=>{let d=f=>
{a.removeEventListener("load",d);a.removeEventListener("error",d);f instanceof ErrorEvent?c("Image load error"):b()};a.addEventListener("load",d);a.addEventListener("error",d)})}static async _postWorkerMessage(a,b,c){a=await a;a instanceof Worker&&a.postMessage({type:b,data:c})}}e.DEFAULT_CANVAS_SIZE=400;e.NO_QR_CODE_FOUND="No QR code found";e.WORKER_PATH="qr-scanner-worker.min.js";return e})
window.removeEventListener("resize",this._updateHighlightOverlay);this._destroyed=!0;this._flashOn=!1;this.stop();e._postWorkerMessage(this._qrEnginePromise,"close")}async start(){if(!(this._active&&!this._paused||this._destroyed||("https:"!==window.location.protocol&&console.warn("The camera stream is only accessible if the page is transferred via https."),this._active=!0,document.hidden)))if(this._paused=!1,this.$video.srcObject)await this.$video.play();else try{let {stream:a,facingMode:b}=await this._getCameraStream();
!this._active||this._paused?e._stopVideoStream(a):(this._setVideoMirror(b),this.$video.srcObject=a,await this.$video.play(),this._flashOn&&(this._flashOn=!1,this.turnFlashOn().catch(()=>{})))}catch(a){if(!this._paused)throw this._active=!1,a;}}stop(){this.pause();this._active=!1}async pause(a=!1){this._paused=!0;if(!this._active)return!0;this.$video.pause();this.$highlightOverlay&&(this.$highlightOverlay.style.display="none");let b=()=>{this.$video.srcObject instanceof MediaStream&&(e._stopVideoStream(this.$video.srcObject),
this.$video.srcObject=null)};if(a)return b(),!0;await new Promise(c=>setTimeout(c,300));if(!this._paused)return!1;b();return!0}async setCamera(a){a!==this._preferredCamera&&(this._preferredCamera=a,await this._restartVideoStream())}static async scanImage(a,b,c,d,f=!1,h=!1){let n,k=!1;b&&("scanRegion"in b||"qrEngine"in b||"canvas"in b||"disallowCanvasResizing"in b||"alsoTryWithoutScanRegion"in b||"returnDetailedScanResult"in b)?(n=b.scanRegion,c=b.qrEngine,d=b.canvas,f=b.disallowCanvasResizing||!1,
h=b.alsoTryWithoutScanRegion||!1,k=!0):b||c||d||f||h?console.warn("You're using a deprecated api for scanImage which will be removed in the future."):console.warn("Note that the return type of scanImage will change in the future. To already switch to the new api today, you can pass returnDetailedScanResult: true.");b=!!c;try{let q,l;[c,q]=await Promise.all([c||e.createQrEngine(),e._loadImage(a)]);[d,l]=e._drawToCanvas(q,n,d,f);if(c instanceof Worker){let g=c;b||g.postMessage({type:"inversionMode",
data:"both"});return await new Promise((m,p)=>{let u,v,t;v=r=>{"qrResult"===r.data.type&&(g.removeEventListener("message",v),g.removeEventListener("error",t),clearTimeout(u),null!==r.data.data?m(k?{data:r.data.data,cornerPoints:e._convertPoints(r.data.cornerPoints,n)}:r.data.data):p(e.NO_QR_CODE_FOUND))};t=r=>{g.removeEventListener("message",v);g.removeEventListener("error",t);clearTimeout(u);p("Scanner error: "+(r?r.message||r:"Unknown Error"))};g.addEventListener("message",v);g.addEventListener("error",
t);u=setTimeout(()=>t("timeout"),1E4);let w=l.getImageData(0,0,d.width,d.height);g.postMessage({type:"decode",data:w},[w.data.buffer])})}return await Promise.race([new Promise((g,m)=>window.setTimeout(()=>m("Scanner error: timeout"),1E4)),(async()=>{try{let [g]=await c.detect(d);if(!g)throw e.NO_QR_CODE_FOUND;return k?{data:g.rawValue,cornerPoints:e._convertPoints(g.cornerPoints,n)}:g.rawValue}catch(g){throw`Scanner error: ${g.message||g}`;}})()])}catch(q){if(!n||!h)throw q;let l=await e.scanImage(a,
{qrEngine:c,canvas:d,disallowCanvasResizing:f});return k?l:l.data}finally{b||e._postWorkerMessage(c,"close")}}setGrayscaleWeights(a,b,c,d=!0){e._postWorkerMessage(this._qrEnginePromise,"grayscaleWeights",{red:a,green:b,blue:c,useIntegerApproximation:d})}setInversionMode(a){e._postWorkerMessage(this._qrEnginePromise,"inversionMode",a)}static async createQrEngine(a=e.WORKER_PATH){return"BarcodeDetector"in window&&BarcodeDetector.getSupportedFormats&&(await BarcodeDetector.getSupportedFormats()).includes("qr_code")?
new BarcodeDetector({formats:["qr_code"]}):new Worker(a)}_onPlay(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateHighlightOverlay();this.$highlightOverlay&&(this.$highlightOverlay.style.display="");this._scanFrame()}_onLoadedMetaData(){this._scanRegion=this._calculateScanRegion(this.$video);this._updateHighlightOverlay()}_onVisibilityChange(){document.hidden?this.pause():this._active&&this.start()}_calculateScanRegion(a){let b=Math.round(2/3*Math.min(a.videoWidth,a.videoHeight));
return{x:Math.round((a.videoWidth-b)/2),y:Math.round((a.videoHeight-b)/2),width:b,height:b,downScaledWidth:this._legacyCanvasSize,downScaledHeight:this._legacyCanvasSize}}_updateHighlightOverlay(){requestAnimationFrame(()=>{if(this.$highlightOverlay){var a=this.$video,b=a.videoWidth,c=a.videoHeight,d=a.offsetWidth,f=a.offsetHeight,h=a.offsetLeft,n=a.offsetTop,k=window.getComputedStyle(a),q=k.objectFit,l=b/c,g=d/f;switch(q){case "none":var m=b;var p=c;break;case "fill":m=d;p=f;break;default:("cover"===
q?l>g:l<g)?(p=f,m=p*l):(m=d,p=m/l),"scale-down"===q&&(m=Math.min(m,b),p=Math.min(p,c))}var [u,v]=k.objectPosition.split(" ").map((w,r)=>{const x=parseFloat(w);return w.endsWith("%")?(r?f-p:d-m)*x/100:x});k=this._scanRegion.width||b;g=this._scanRegion.height||c;q=this._scanRegion.x||0;var t=this._scanRegion.y||0;l=this.$highlightOverlay.style;l.width=`${k/b*m}px`;l.height=`${g/c*p}px`;l.top=`${n+v+t/c*p}px`;c=/scaleX\(-1\)/.test(a.style.transform);l.left=`${h+(c?d-u-m:u)+(c?b-q-k:q)/b*m}px`;l.transform=
a.style.transform}})}static _convertPoints(a,b){if(!b)return a;let c=b.x||0,d=b.y||0,f=b.width&&b.downScaledWidth?b.width/b.downScaledWidth:1;b=b.height&&b.downScaledHeight?b.height/b.downScaledHeight:1;for(let h of a)h.x=h.x*f+c,h.y=h.y*b+d;return a}_scanFrame(){!this._active||this.$video.paused||this.$video.ended||requestAnimationFrame(async()=>{if(!(1>=this.$video.readyState)){try{var a=await e.scanImage(this.$video,{scanRegion:this._scanRegion,qrEngine:this._qrEnginePromise,canvas:this.$canvas})}catch(b){if(!this._active)return;
(b.message||b).includes("service unavailable")&&(this._qrEnginePromise=e.createQrEngine());this._onDecodeError(b)}a?(this._onDecode?this._onDecode(a):this._legacyOnDecode&&this._legacyOnDecode(a.data),this.$codeOutlineHighlight&&(clearTimeout(this._codeOutlineHighlightRemovalTimeout),this._codeOutlineHighlightRemovalTimeout=void 0,this.$codeOutlineHighlight.setAttribute("viewBox",`${this._scanRegion.x||0} `+`${this._scanRegion.y||0} `+`${this._scanRegion.width||this.$video.videoWidth} `+`${this._scanRegion.height||
this.$video.videoHeight}`),this.$codeOutlineHighlight.firstElementChild.setAttribute("points",a.cornerPoints.map(({x:b,y:c})=>`${b},${c}`).join(" ")),this.$codeOutlineHighlight.style.display="")):this.$codeOutlineHighlight&&!this._codeOutlineHighlightRemovalTimeout&&(this._codeOutlineHighlightRemovalTimeout=setTimeout(()=>this.$codeOutlineHighlight.style.display="none",100))}this._scanFrame()})}_onDecodeError(a){a!==e.NO_QR_CODE_FOUND&&console.log(a)}async _getCameraStream(){if(!navigator.mediaDevices)throw"Camera not found.";
let a=/^(environment|user)$/.test(this._preferredCamera)?"facingMode":"deviceId",b=[{width:{min:1024}},{width:{min:768}},{}],c=b.map(d=>Object.assign({},d,{[a]:{exact:this._preferredCamera}}));for(let d of[...c,...b])try{let f=await navigator.mediaDevices.getUserMedia({video:d,audio:!1}),h=this._getFacingMode(f)||(d.facingMode?this._preferredCamera:"environment"===this._preferredCamera?"user":"environment");return{stream:f,facingMode:h}}catch(f){}throw"Camera not found.";}async _restartVideoStream(){let a=
this._paused;await this.pause(!0)&&!a&&this._active&&await this.start()}static _stopVideoStream(a){for(let b of a.getTracks())b.stop(),a.removeTrack(b)}_setVideoMirror(a){this.$video.style.transform="scaleX("+("user"===a?-1:1)+")"}_getFacingMode(a){return(a=a.getVideoTracks()[0])?/rear|back|environment/i.test(a.label)?"environment":/front|user|face/i.test(a.label)?"user":null:null}static _drawToCanvas(a,b,c,d=!1){c=c||document.createElement("canvas");let f=b&&b.x?b.x:0,h=b&&b.y?b.y:0,n=b&&b.width?
b.width:a.videoWidth||a.width,k=b&&b.height?b.height:a.videoHeight||a.height;d||(d=b&&b.downScaledWidth?b.downScaledWidth:n,b=b&&b.downScaledHeight?b.downScaledHeight:k,c.width!==d&&(c.width=d),c.height!==b&&(c.height=b));b=c.getContext("2d",{alpha:!1});b.imageSmoothingEnabled=!1;b.drawImage(a,f,h,n,k,0,0,c.width,c.height);return[c,b]}static async _loadImage(a){if(a instanceof Image)return await e._awaitImageLoad(a),a;if(a instanceof HTMLVideoElement||a instanceof HTMLCanvasElement||a instanceof SVGImageElement||
"OffscreenCanvas"in window&&a instanceof OffscreenCanvas||"ImageBitmap"in window&&a instanceof ImageBitmap)return a;if(a instanceof File||a instanceof Blob||a instanceof URL||"string"===typeof a){let b=new Image;b.src=a instanceof File||a instanceof Blob?URL.createObjectURL(a):a.toString();try{return await e._awaitImageLoad(b),b}finally{(a instanceof File||a instanceof Blob)&&URL.revokeObjectURL(b.src)}}else throw"Unsupported image type.";}static async _awaitImageLoad(a){a.complete&&0!==a.naturalWidth||
await new Promise((b,c)=>{let d=f=>{a.removeEventListener("load",d);a.removeEventListener("error",d);f instanceof ErrorEvent?c("Image load error"):b()};a.addEventListener("load",d);a.addEventListener("error",d)})}static async _postWorkerMessage(a,b,c){a=await a;a instanceof Worker&&a.postMessage({type:b,data:c})}}e.DEFAULT_CANVAS_SIZE=400;e.NO_QR_CODE_FOUND="No QR code found";e.WORKER_PATH="qr-scanner-worker.min.js";return e})
//# sourceMappingURL=qr-scanner.umd.min.js.map
File diff suppressed because one or more lines are too long
+84 -32
View File
@@ -48,12 +48,14 @@ export default class QrScanner {
readonly $video: HTMLVideoElement;
readonly $canvas: HTMLCanvasElement;
readonly $scanRegionHighlight?: HTMLDivElement;
readonly $highlightOverlay?: HTMLDivElement;
private readonly $codeOutlineHighlight?: SVGSVGElement;
private readonly _onDecode?: (result: QrScanner.ScanResult) => void;
private readonly _legacyOnDecode?: (result: string) => void;
private readonly _legacyCanvasSize: number = QrScanner.DEFAULT_CANVAS_SIZE;
private _preferredCamera: QrScanner.FacingMode | QrScanner.DeviceId = 'environment';
private _scanRegion: QrScanner.ScanRegion;
private _codeOutlineHighlightRemovalTimeout?: number;
private _qrEnginePromise: Promise<Worker | BarcodeDetector>
private _active: boolean = false;
private _paused: boolean = false;
@@ -68,6 +70,7 @@ export default class QrScanner {
calculateScanRegion?: (video: HTMLVideoElement) => QrScanner.ScanRegion,
preferredCamera?: QrScanner.FacingMode | QrScanner.DeviceId,
highlightScanRegion?: boolean,
highlightCodeOutline?: boolean,
/** just a temporary flag until we switch entirely to the new api */
returnDetailedScanResult?: true,
},
@@ -98,6 +101,7 @@ export default class QrScanner {
calculateScanRegion?: (video: HTMLVideoElement) => QrScanner.ScanRegion,
preferredCamera?: QrScanner.FacingMode | QrScanner.DeviceId,
highlightScanRegion?: boolean,
highlightCodeOutline?: boolean,
/** just a temporary flag until we switch entirely to the new api */
returnDetailedScanResult?: true,
},
@@ -142,21 +146,41 @@ export default class QrScanner {
? canvasSizeOrCalculateScanRegion
: this._legacyCanvasSize;
if (options.highlightScanRegion) {
this.$scanRegionHighlight = document.createElement('div');
this.$scanRegionHighlight.classList.add('scan-region-highlight');
this.$scanRegionHighlight.style.position = 'absolute';
this.$scanRegionHighlight.style.display = 'none';
// default style; can be overwritten via css
this.$scanRegionHighlight.style.outline = 'rgba(255, 255, 255, .3) solid 7px';
this.$scanRegionHighlight.style.pointerEvents = 'none';
if (options.highlightScanRegion || options.highlightCodeOutline) {
this.$highlightOverlay = document.createElement('div');
const highlightOverlayStyle = this.$highlightOverlay.style;
highlightOverlayStyle.position = 'absolute';
highlightOverlayStyle.display = 'none';
highlightOverlayStyle.pointerEvents = 'none';
if (options.highlightScanRegion) {
// default style; can be overwritten via css
this.$highlightOverlay.classList.add('scan-region-highlight');
highlightOverlayStyle.outline = '#e9b213 solid 5px';
}
if (options.highlightCodeOutline) {
this.$codeOutlineHighlight = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const polygon = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
this.$codeOutlineHighlight.appendChild(polygon);
this.$highlightOverlay.appendChild(this.$codeOutlineHighlight);
const codeOutlineHighlightStyle = this.$codeOutlineHighlight.style;
codeOutlineHighlightStyle.width = '100%';
codeOutlineHighlightStyle.height = '100%';
// to support distorted videos, e.g. via object-fit: fill
this.$codeOutlineHighlight.setAttribute('preserveAspectRatio', 'none');
codeOutlineHighlightStyle.display = 'none';
// default style; can be overwritten via css
this.$codeOutlineHighlight.classList.add('code-outline-highlight');
codeOutlineHighlightStyle.fill = 'none';
codeOutlineHighlightStyle.stroke = '#e9b213';
codeOutlineHighlightStyle.strokeWidth = '4';
}
}
this._scanRegion = this._calculateScanRegion(video);
this._onPlay = this._onPlay.bind(this);
this._onLoadedMetaData = this._onLoadedMetaData.bind(this);
this._onVisibilityChange = this._onVisibilityChange.bind(this);
this._updateScanRegionHighlight = this._updateScanRegionHighlight.bind(this);
this._updateHighlightOverlay = this._updateHighlightOverlay.bind(this);
// @ts-ignore
video.disablePictureInPicture = true;
@@ -198,19 +222,21 @@ export default class QrScanner {
video.style.width = '0';
video.style.height = '0';
// @ts-ignore
delete this.$scanRegionHighlight!;
delete this.$highlightOverlay!;
// @ts-ignore
delete this.$codeOutlineHighlight!;
}
if (this.$scanRegionHighlight) {
this._updateScanRegionHighlight();
videoContainer.appendChild(this.$scanRegionHighlight);
if (this.$highlightOverlay) {
this._updateHighlightOverlay();
videoContainer.appendChild(this.$highlightOverlay);
}
});
video.addEventListener('play', this._onPlay);
video.addEventListener('loadedmetadata', this._onLoadedMetaData);
document.addEventListener('visibilitychange', this._onVisibilityChange);
window.addEventListener('resize', this._updateScanRegionHighlight);
window.addEventListener('resize', this._updateHighlightOverlay);
this._qrEnginePromise = QrScanner.createQrEngine();
}
@@ -279,7 +305,7 @@ export default class QrScanner {
this.$video.removeEventListener('loadedmetadata', this._onLoadedMetaData);
this.$video.removeEventListener('play', this._onPlay);
document.removeEventListener('visibilitychange', this._onVisibilityChange);
window.removeEventListener('resize', this._updateScanRegionHighlight);
window.removeEventListener('resize', this._updateHighlightOverlay);
this._destroyed = true;
this._flashOn = false;
@@ -337,8 +363,8 @@ export default class QrScanner {
if (!this._active) return true;
this.$video.pause();
if (this.$scanRegionHighlight) {
this.$scanRegionHighlight.style.display = 'none';
if (this.$highlightOverlay) {
this.$highlightOverlay.style.display = 'none';
}
const stopStream = () => {
@@ -549,16 +575,16 @@ export default class QrScanner {
private _onPlay(): void {
this._scanRegion = this._calculateScanRegion(this.$video);
this._updateScanRegionHighlight();
if (this.$scanRegionHighlight) {
this.$scanRegionHighlight.style.display = '';
this._updateHighlightOverlay();
if (this.$highlightOverlay) {
this.$highlightOverlay.style.display = '';
}
this._scanFrame();
}
private _onLoadedMetaData(): void {
this._scanRegion = this._calculateScanRegion(this.$video);
this._updateScanRegionHighlight();
this._updateHighlightOverlay();
}
private _onVisibilityChange(): void {
@@ -583,11 +609,11 @@ export default class QrScanner {
};
}
private _updateScanRegionHighlight(): void {
private _updateHighlightOverlay(): void {
requestAnimationFrame(() => {
// Running in requestAnimationFrame which should avoid a potential additional re-flow for getComputedStyle
// and offsetWidth, offsetHeight, offsetLeft, offsetTop.
if (!this.$scanRegionHighlight) return;
if (!this.$highlightOverlay) return;
const video = this.$video;
const videoWidth = video.videoWidth;
const videoHeight = video.videoHeight;
@@ -648,13 +674,16 @@ export default class QrScanner {
const regionX = this._scanRegion.x || 0;
const regionY = this._scanRegion.y || 0;
this.$scanRegionHighlight.style.width = `${regionWidth / videoWidth * videoScaledWidth}px`;
this.$scanRegionHighlight.style.height = `${regionHeight / videoHeight * videoScaledHeight}px`;
this.$scanRegionHighlight.style.top = `${elementY + videoY + regionY / videoHeight * videoScaledHeight}px`;
const highlightOverlayStyle = this.$highlightOverlay.style;
highlightOverlayStyle.width = `${regionWidth / videoWidth * videoScaledWidth}px`;
highlightOverlayStyle.height = `${regionHeight / videoHeight * videoScaledHeight}px`;
highlightOverlayStyle.top = `${elementY + videoY + regionY / videoHeight * videoScaledHeight}px`;
const isVideoMirrored = /scaleX\(-1\)/.test(video.style.transform!);
this.$scanRegionHighlight.style.left = `${elementX
highlightOverlayStyle.left = `${elementX
+ (isVideoMirrored ? elementWidth - videoX - videoScaledWidth : videoX)
+ (isVideoMirrored ? videoWidth - regionX - regionWidth : regionX) / videoWidth * videoScaledWidth}px`;
// apply same mirror as on video
highlightOverlayStyle.transform = video.style.transform;
});
}
@@ -708,10 +737,33 @@ export default class QrScanner {
this._onDecodeError(error as Error | string);
}
if (result && this._onDecode) {
this._onDecode(result);
} else if (result && this._legacyOnDecode) {
this._legacyOnDecode(result.data);
if (result) {
if (this._onDecode) {
this._onDecode(result);
} else if (this._legacyOnDecode) {
this._legacyOnDecode(result.data);
}
if (this.$codeOutlineHighlight) {
clearTimeout(this._codeOutlineHighlightRemovalTimeout);
this._codeOutlineHighlightRemovalTimeout = undefined;
this.$codeOutlineHighlight.setAttribute(
'viewBox',
`${this._scanRegion.x || 0} `
+ `${this._scanRegion.y || 0} `
+ `${this._scanRegion.width || this.$video.videoWidth} `
+ `${this._scanRegion.height || this.$video.videoHeight}`,
);
const polygon = this.$codeOutlineHighlight.firstElementChild!;
polygon.setAttribute('points', result.cornerPoints.map(({x, y}) => `${x},${y}`).join(' '));
this.$codeOutlineHighlight.style.display = '';
}
} else if (this.$codeOutlineHighlight && !this._codeOutlineHighlightRemovalTimeout) {
// hide after timeout to make it flash less when on some frames the QR code is detected and on some not
this._codeOutlineHighlightRemovalTimeout = setTimeout(
() => this.$codeOutlineHighlight!.style.display = 'none',
100,
);
}
this._scanFrame();
+5 -2
View File
@@ -7,12 +7,14 @@ export default class QrScanner {
static listCameras(requestLabels?: boolean): Promise<Array<QrScanner.Camera>>;
readonly $video: HTMLVideoElement;
readonly $canvas: HTMLCanvasElement;
readonly $scanRegionHighlight?: HTMLDivElement;
readonly $highlightOverlay?: HTMLDivElement;
private readonly $codeOutlineHighlight?;
private readonly _onDecode?;
private readonly _legacyOnDecode?;
private readonly _legacyCanvasSize;
private _preferredCamera;
private _scanRegion;
private _codeOutlineHighlightRemovalTimeout?;
private _qrEnginePromise;
private _active;
private _paused;
@@ -23,6 +25,7 @@ export default class QrScanner {
calculateScanRegion?: (video: HTMLVideoElement) => QrScanner.ScanRegion;
preferredCamera?: QrScanner.FacingMode | QrScanner.DeviceId;
highlightScanRegion?: boolean;
highlightCodeOutline?: boolean;
/** just a temporary flag until we switch entirely to the new api */
returnDetailedScanResult?: true;
});
@@ -60,7 +63,7 @@ export default class QrScanner {
private _onLoadedMetaData;
private _onVisibilityChange;
private _calculateScanRegion;
private _updateScanRegionHighlight;
private _updateHighlightOverlay;
private static _convertPoints;
private _scanFrame;
private _onDecodeError;