UNPKG

ngx-scanner-qrcode

Version:

This library is built to provide a solution scanner QR code. This library takes in raw images and will locate, extract and parse any QR code found within.

1,247 lines (1,241 loc) 60 kB
import * as i0 from '@angular/core'; import { Injectable, EventEmitter, Input, Output, ViewChild, ViewEncapsulation, Component } from '@angular/core'; import { AsyncSubject, BehaviorSubject } from 'rxjs'; var ScannerQRCodeSymbolType; (function (ScannerQRCodeSymbolType) { ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_NONE"] = 0] = "ScannerQRCode_NONE"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_PARTIAL"] = 1] = "ScannerQRCode_PARTIAL"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_EAN2"] = 2] = "ScannerQRCode_EAN2"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_EAN5"] = 5] = "ScannerQRCode_EAN5"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_EAN8"] = 8] = "ScannerQRCode_EAN8"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_UPCE"] = 9] = "ScannerQRCode_UPCE"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_ISBN10"] = 10] = "ScannerQRCode_ISBN10"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_UPCA"] = 12] = "ScannerQRCode_UPCA"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_EAN13"] = 13] = "ScannerQRCode_EAN13"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_ISBN13"] = 14] = "ScannerQRCode_ISBN13"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_COMPOSITE"] = 15] = "ScannerQRCode_COMPOSITE"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_I25"] = 25] = "ScannerQRCode_I25"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_DATABAR"] = 34] = "ScannerQRCode_DATABAR"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_DATABAR_EXP"] = 35] = "ScannerQRCode_DATABAR_EXP"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_CODABAR"] = 38] = "ScannerQRCode_CODABAR"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_CODE39"] = 39] = "ScannerQRCode_CODE39"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_PDF417"] = 57] = "ScannerQRCode_PDF417"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_QRCODE"] = 64] = "ScannerQRCode_QRCODE"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_SQCODE"] = 80] = "ScannerQRCode_SQCODE"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_CODE93"] = 93] = "ScannerQRCode_CODE93"; ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_CODE128"] = 128] = "ScannerQRCode_CODE128"; /* * Please see _ScannerQRCode_get_symbol_hash() if adding * anything after 128 */ /** mask for base symbol type. * @deprecated in 0.11, remove this from existing code */ ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_SYMBOL"] = 255] = "ScannerQRCode_SYMBOL"; /** 2-digit add-on flag. * @deprecated in 0.11, a ::ScannerQRCode_EAN2 component is used for * 2-digit GS1 add-ons */ ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_ADDON2"] = 512] = "ScannerQRCode_ADDON2"; /** 5-digit add-on flag. * @deprecated in 0.11, a ::ScannerQRCode_EAN5 component is used for * 5-digit GS1 add-ons */ ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_ADDON5"] = 1280] = "ScannerQRCode_ADDON5"; /** add-on flag mask. * @deprecated in 0.11, GS1 add-ons are represented using composite * symbols of type ::ScannerQRCode_COMPOSITE; add-on components use ::ScannerQRCode_EAN2 * or ::ScannerQRCode_EAN5 */ ScannerQRCodeSymbolType[ScannerQRCodeSymbolType["ScannerQRCode_ADDON"] = 1792] = "ScannerQRCode_ADDON"; })(ScannerQRCodeSymbolType || (ScannerQRCodeSymbolType = {})); var ScannerQRCodeConfigType; (function (ScannerQRCodeConfigType) { ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_ENABLE"] = 0] = "ScannerQRCode_CFG_ENABLE"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_ADD_CHECK"] = 1] = "ScannerQRCode_CFG_ADD_CHECK"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_EMIT_CHECK"] = 2] = "ScannerQRCode_CFG_EMIT_CHECK"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_ASCII"] = 3] = "ScannerQRCode_CFG_ASCII"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_BINARY"] = 4] = "ScannerQRCode_CFG_BINARY"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_NUM"] = 5] = "ScannerQRCode_CFG_NUM"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_MIN_LEN"] = 32] = "ScannerQRCode_CFG_MIN_LEN"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_MAX_LEN"] = 33] = "ScannerQRCode_CFG_MAX_LEN"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_UNCERTAINTY"] = 64] = "ScannerQRCode_CFG_UNCERTAINTY"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_POSITION"] = 128] = "ScannerQRCode_CFG_POSITION"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_TEST_INVERTED"] = 129] = "ScannerQRCode_CFG_TEST_INVERTED"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_X_DENSITY"] = 256] = "ScannerQRCode_CFG_X_DENSITY"; ScannerQRCodeConfigType[ScannerQRCodeConfigType["ScannerQRCode_CFG_Y_DENSITY"] = 257] = "ScannerQRCode_CFG_Y_DENSITY"; })(ScannerQRCodeConfigType || (ScannerQRCodeConfigType = {})); var ScannerQRCodeOrientation; (function (ScannerQRCodeOrientation) { ScannerQRCodeOrientation[ScannerQRCodeOrientation["ScannerQRCode_ORIENT_UNKNOWN"] = -1] = "ScannerQRCode_ORIENT_UNKNOWN"; ScannerQRCodeOrientation[ScannerQRCodeOrientation["ScannerQRCode_ORIENT_UP"] = 0] = "ScannerQRCode_ORIENT_UP"; ScannerQRCodeOrientation[ScannerQRCodeOrientation["ScannerQRCode_ORIENT_RIGHT"] = 1] = "ScannerQRCode_ORIENT_RIGHT"; ScannerQRCodeOrientation[ScannerQRCodeOrientation["ScannerQRCode_ORIENT_DOWN"] = 2] = "ScannerQRCode_ORIENT_DOWN"; ScannerQRCodeOrientation[ScannerQRCodeOrientation["ScannerQRCode_ORIENT_LEFT"] = 3] = "ScannerQRCode_ORIENT_LEFT"; })(ScannerQRCodeOrientation || (ScannerQRCodeOrientation = {})); class ScannerQRCodeTypePointer { ptr; ptr32; buf; HEAP8; HEAP32; HEAPU32; constructor(ptr, buf) { this.ptr = ptr; this.ptr32 = ptr >> 2; this.buf = buf; this.HEAP8 = new Int8Array(buf); this.HEAPU32 = new Uint32Array(buf); this.HEAP32 = new Int32Array(buf); } } class ScannerQRCodeSymbolPtr extends ScannerQRCodeTypePointer { get type() { return this.HEAPU32[this.ptr32]; } get data() { const len = this.HEAPU32[this.ptr32 + 4]; const ptr = this.HEAPU32[this.ptr32 + 5]; return Int8Array.from(this.HEAP8.subarray(ptr, ptr + len)); } get points() { const len = this.HEAPU32[this.ptr32 + 7]; const ptr = this.HEAPU32[this.ptr32 + 8]; const ptr32 = ptr >> 2; const res = []; for (let i = 0; i < len; ++i) { const x = this.HEAP32[ptr32 + i * 2]; const y = this.HEAP32[ptr32 + i * 2 + 1]; res.push({ x, y }); } return res; } get orientation() { return this.HEAP32[this.ptr32 + 9]; } get next() { const ptr = this.HEAPU32[this.ptr32 + 11]; if (!ptr) return null; return new ScannerQRCodeSymbolPtr(ptr, this.buf); } get time() { return this.HEAPU32[this.ptr32 + 13]; } get cacheCount() { return this.HEAP32[this.ptr32 + 14]; } get quality() { return this.HEAP32[this.ptr32 + 15]; } } class SymbolSetPtr extends ScannerQRCodeTypePointer { get head() { const ptr = this.HEAPU32[this.ptr32 + 2]; if (!ptr) return null; return new ScannerQRCodeSymbolPtr(ptr, this.buf); } } class ScannerQRCodeResult { type; typeName; data; points; orientation; time; cacheCount; quality; value = ''; constructor(ptr) { this.type = ptr.type; this.typeName = ScannerQRCodeSymbolType[this.type]; this.data = ptr.data; this.points = ptr.points; this.orientation = ptr.orientation; this.time = ptr.time; this.cacheCount = ptr.cacheCount; this.quality = ptr.quality; } static createSymbolsFromPtr(ptr, buf) { if (ptr == 0) return []; const set = new SymbolSetPtr(ptr, buf); let symbol = set.head; const res = []; while (symbol !== null) { res.push(new ScannerQRCodeResult(symbol)); symbol = symbol.next; } return res; } decode(encoding) { const decoder = new TextDecoder(encoding); return decoder.decode(this.data); } } const WASMPROJECT = "assets/wasm/index.js"; const WASMREMOTE = "https://cdn.jsdelivr.net/npm/ngx-scanner-qrcode@1.7.6/wasm/index.js"; const WASMREMOTELATEST = "https://cdn.jsdelivr.net/npm/ngx-scanner-qrcode@latest/wasm/index.js"; const BEEP = `data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjI5LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAABQAAAkAAgICAgICAgICAgICAgICAgICAgKCgoKCgoKCgoKCgoKCgoKCgoKCgwMDAwMDAwMDAwMDAwMDAwMDAwMDg4ODg4ODg4ODg4ODg4ODg4ODg4P//////////////////////////AAAAAExhdmM1OC41NAAAAAAAAAAAAAAAACQEUQAAAAAAAAJAk0uXRQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAANQAbGeUEQAAHZYZ3fASqD4P5TKBgocg+Bw/8+CAYBA4XB9/4EBAEP4nB9+UOf/6gfUCAIKyjgQ/Kf//wfswAAAwQA/+MYxAYOqrbdkZGQAMA7DJLCsQxNOij///////////+tv///3RWiZGBEhsf/FO/+LoCSFs1dFVS/g8f/4Mhv0nhqAieHleLy/+MYxAYOOrbMAY2gABf/////////////////usPJ66R0wI4boY9/8jQYg//g2SPx1M0N3Z0kVJLIs///Uw4aMyvHJJYmPBYG/+MYxAgPMALBucAQAoGgaBoFQVBUFQWDv6gZBUFQVBUGgaBr5YSgqCoKhIGg7+IQVBUFQVBoGga//SsFSoKnf/iVTEFNRTMu/+MYxAYAAANIAAAAADEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV`; const MEDIA_STREAM_DEFAULT = { audio: false, video: true }; const CANVAS_STYLES_LAYER = { lineWidth: 1, strokeStyle: 'green', fillStyle: '#55f02880' }; const CANVAS_STYLES_TEXT = { font: '15px serif', strokeStyle: '#fff0', fillStyle: '#ff0000' }; const CONFIG_DEFAULT = { src: '', fps: 30, vibrate: 300, decode: 'utf-8', unScan: false, isBeep: true, isMasked: true, loadWasmUrl: '', symbolType: [ScannerQRCodeSymbolType.ScannerQRCode_NONE], constraints: MEDIA_STREAM_DEFAULT, canvasStyles: [CANVAS_STYLES_LAYER, CANVAS_STYLES_TEXT] }; /** * WASM_READY * @returns */ var WASM_READY = () => ('zbarWasm' in window); /** * OVERRIDES * @param variableKey * @param config * @param defaultConfig * @returns */ const OVERRIDES = (variableKey, config, defaultConfig) => { if (config && Object.keys(config[variableKey]).length) { for (const key in defaultConfig) { const cloneDeep = JSON.parse(JSON.stringify({ ...config[variableKey], ...{ [key]: defaultConfig[key] } })); config[variableKey] = config[variableKey].hasOwnProperty(key) ? config[variableKey] : cloneDeep; } return config[variableKey]; } else { return defaultConfig; } }; /** * AS_COMPLETE * @param as * @param data * @param error */ const AS_COMPLETE = (as, data, error) => { error ? as.error(error) : as.next(data); as.complete(); }; /** * PLAY_AUDIO * @param isPlay * @returns */ const PLAY_AUDIO = (isPlay = false) => { if (isPlay === false) return; const audio = new Audio(BEEP); // when the sound has been loaded, execute your code audio.oncanplaythrough = () => { const promise = audio.play(); if (promise) { promise.catch((e) => { if (e.name === "NotAllowedError" || e.name === "NotSupportedError") { // console.log(e.name); } }); } }; }; /** * DRAW_RESULT_APPEND_CHILD * @param code * @param oriCanvas * @param elTarget * @param canvasStyles */ const DRAW_RESULT_APPEND_CHILD = (code, oriCanvas, elTarget, canvasStyles) => { let widthZoom; let heightZoom; let oriWidth = oriCanvas.width; let oriHeight = oriCanvas.height; let oriWHRatio = oriWidth / oriHeight; let imgWidth = parseInt(getComputedStyle(oriCanvas).width); let imgHeight = parseInt(getComputedStyle(oriCanvas).height); let imgWHRatio = imgWidth / imgHeight; elTarget.innerHTML = ''; if (oriWHRatio > imgWHRatio) { widthZoom = imgWidth / oriWidth; heightZoom = imgWidth / oriWHRatio / oriHeight; } else { heightZoom = imgHeight / oriHeight; widthZoom = (imgHeight * oriWHRatio) / oriWidth; } for (let i = 0; i < code.length; i++) { const _code = code[i]; // New canvas let cvs = document.createElement("canvas"); let ctx = cvs.getContext('2d', { willReadFrequently: true }); let loc = {}; let X = []; let Y = []; let fontSize = 0; let svgSize = 0; let num = canvasStyles.length === 2 && canvasStyles[1]?.font?.replace(/[^0-9]/g, ''); if (num && /[0-9]/g.test(num)) { fontSize = parseFloat(num); svgSize = (widthZoom || 1) * fontSize; if (Number.isNaN(svgSize)) { svgSize = fontSize; } } // Point x,y const points = _code.points; for (let j = 0; j < points.length; j++) { const xj = points?.[j]?.x ?? 0; const yj = points?.[j]?.y ?? 0; loc[`x${j + 1}`] = xj; loc[`y${j + 1}`] = yj; X.push(xj); Y.push(yj); } // Min max let maxX = Math.max(...X); let minX = Math.min(...X); let maxY = Math.max(...Y); let minY = Math.min(...Y); // Add class cvs.setAttribute('class', 'qrcode-polygon'); // Size with screen zoom if (oriWHRatio > imgWHRatio) { cvs.style.top = minY * heightZoom + (imgHeight - imgWidth / oriWHRatio) * 0.5 + "px"; cvs.style.left = minX * widthZoom + "px"; cvs.width = (maxX - minX) * widthZoom; cvs.height = (maxY - minY) * widthZoom; } else { cvs.style.top = minY * heightZoom + "px"; cvs.style.left = minX * widthZoom + (imgWidth - imgHeight * oriWHRatio) * 0.5 + "px"; cvs.width = (maxX - minX) * heightZoom; cvs.height = (maxY - minY) * heightZoom; } // Style for canvas for (const key in canvasStyles[0]) { ctx[key] = canvasStyles[0][key]; } // polygon [x,y, x,y, x,y.....]; const polygon = []; for (let k = 0; k < X.length; k++) { polygon.push((loc[`x${k + 1}`] - minX) * heightZoom); polygon.push((loc[`y${k + 1}`] - minY) * widthZoom); } // Copy array const shape = polygon.slice(0); // Draw polygon ctx.beginPath(); ctx.moveTo(shape.shift(), shape.shift()); while (shape.length) { ctx.lineTo(shape.shift(), shape.shift()); //x,y } ctx.closePath(); ctx.fill(); ctx.stroke(); if (fontSize) { // Tooltip result const qrcodeTooltipTemp = document.createElement('div'); qrcodeTooltipTemp.setAttribute('class', 'qrcode-tooltip-temp'); qrcodeTooltipTemp.innerText = _code.value; qrcodeTooltipTemp.style.maxWidth = ((oriWidth > window.innerWidth) ? window.innerWidth * 0.9 : oriWidth) + "px"; qrcodeTooltipTemp.style.borderRadius = `clamp(1px, ${(widthZoom * fontSize) - 10}px, 3px)`; qrcodeTooltipTemp.style.paddingBlock = `clamp(1px, ${(widthZoom * fontSize) - 10}px, 3px)`; // top - bottom qrcodeTooltipTemp.style.paddingInline = `clamp(2.5px, ${(widthZoom * fontSize) - 6}px, 10px)`; // left - right const xmlString = `<svg xmlns="http://www.w3.org/2000/svg" width="${svgSize}" height="${svgSize}" viewBox="0 0 512 512"><rect x="128" y="128" width="336" height="336" rx="57" ry="57"></rect><path d="M383.5,128l.5-24a56.16,56.16,0,0,0-56-56H112a64.19,64.19,0,0,0-64,64V328a56.16,56.16,0,0,0,56,56h24"></path></svg>`; const xmlDom = new DOMParser().parseFromString(xmlString, 'application/xml'); const svgDom = qrcodeTooltipTemp.ownerDocument.importNode(xmlDom.documentElement, true); svgDom.style.marginLeft = `clamp(1px, ${(widthZoom * fontSize) - 10}px, 3px)`; // left - right qrcodeTooltipTemp.appendChild(svgDom); svgDom.addEventListener("click", () => window.navigator['clipboard'].writeText(_code.value)); qrcodeTooltipTemp.addEventListener("click", () => window.navigator['clipboard'].writeText(_code.value)); // Tooltip box const qrcodeTooltip = document.createElement('div'); qrcodeTooltip.setAttribute('class', 'qrcode-tooltip'); qrcodeTooltip.appendChild(qrcodeTooltipTemp); heightZoom = imgHeight / oriHeight; widthZoom = (imgHeight * oriWHRatio) / oriWidth; qrcodeTooltip.style.fontSize = widthZoom * fontSize + 'px'; qrcodeTooltip.style.top = minY * heightZoom + "px"; qrcodeTooltip.style.left = minX * widthZoom + (imgWidth - imgHeight * oriWHRatio) * 0.5 + "px"; qrcodeTooltip.style.width = (maxX - minX) * heightZoom + "px"; qrcodeTooltip.style.height = (maxY - minY) * heightZoom + "px"; // Result text const resultText = document.createElement('span'); resultText.innerText = _code.value; // Set position result text resultText.style.top = minY * heightZoom + (-20 * heightZoom) + "px"; resultText.style.left = minX * widthZoom + (imgWidth - imgHeight * oriWHRatio) * 0.5 + "px"; // Style text const ff = canvasStyles[1]?.font?.split(' ')?.[1]; resultText.style.fontFamily = ff; resultText.style.fontSize = widthZoom * fontSize + 'px'; resultText.style.color = canvasStyles?.[1]?.fillStyle; elTarget?.appendChild(qrcodeTooltip); elTarget?.appendChild(resultText); } elTarget?.appendChild(cvs); } ; }; /** * DRAW_RESULT_ON_CANVAS * @param code * @param cvs * @param canvasStyles */ const DRAW_RESULT_ON_CANVAS = (code, cvs, canvasStyles) => { let ctx = cvs.getContext('2d', { willReadFrequently: true }); for (let i = 0; i < code.length; i++) { const _code = code[i]; let loc = {}; let X = []; let Y = []; let fontSize = 0; const fs = canvasStyles[1]?.font?.split(' ')?.[0]; let num = fs?.replace(/[^0-9]/g, ''); if (num && /[0-9]/g.test(num)) { fontSize = parseFloat(num); } // Point x,y const points = _code.points; for (let j = 0; j < points.length; j++) { const xj = points?.[j]?.x ?? 0; const yj = points?.[j]?.y ?? 0; loc[`x${j + 1}`] = xj; loc[`y${j + 1}`] = yj; X.push(xj); Y.push(yj); } // Min max let minX = Math.min(...X); let minY = Math.min(...Y); const styleLayer = () => { for (const key in canvasStyles[0]) { ctx[key] = canvasStyles[0][key]; } // polygon [x,y, x,y, x,y.....]; const polygon = []; for (let k = 0; k < X.length; k++) { polygon.push(loc[`x${k + 1}`]); polygon.push(loc[`y${k + 1}`]); } // Copy array const shape = polygon.slice(0); // Draw polygon ctx.beginPath(); ctx.moveTo(shape.shift(), shape.shift()); while (shape.length) { ctx.lineTo(shape.shift(), shape.shift()); //x,y } ctx.closePath(); ctx.fill(); ctx.stroke(); }; let cvs2 = document.createElement('canvas'); const styleText = () => { const ff = canvasStyles[1]?.font?.split(' ')?.[1]; cvs2.height = cvs.height; cvs2.width = cvs.width; let ctx2 = cvs2.getContext('2d', { willReadFrequently: true }); ctx2.font = fontSize + `px ` + ff; for (const key in canvasStyles[1]) { ctx2[key] = canvasStyles[1][key]; } FILL_TEXT_MULTI_LINE(ctx2, _code.value, minX, minY - 5); }; styleLayer(); styleText(); // Merge cvs2 into cvs ctx.drawImage(cvs2, 0, 0); } ; }; /** * READ_AS_DATA_URL * @param file * @param configs * @returns */ const READ_AS_DATA_URL = (file, configs) => { /** overrides **/ let decode = configs?.decode ?? CONFIG_DEFAULT.decode; let canvasStyles = configs?.canvasStyles?.length === 2 ? configs?.canvasStyles : [CANVAS_STYLES_LAYER, CANVAS_STYLES_TEXT]; let isBeep = configs?.isBeep ?? CONFIG_DEFAULT.isBeep; let isMasked = configs?.isMasked ?? CONFIG_DEFAULT.isMasked; let unScan = configs?.unScan ?? CONFIG_DEFAULT.unScan; let symbolType = configs?.symbolType ?? CONFIG_DEFAULT.symbolType; /** drawImage **/ return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = () => { const objectFile = { name: file.name, file: file, url: URL.createObjectURL(file) }; // Set the src of this Image object. const image = new Image(); // Setting cross origin value to anonymous image.setAttribute('crossOrigin', 'anonymous'); // When our image has loaded. image.onload = async () => { // Get the canvas element by using the getElementById method. const canvas = document.createElement('canvas'); // HTMLImageElement size canvas.width = image.naturalWidth || image.width; canvas.height = image.naturalHeight || image.height; // Get a 2D drawing context for the canvas. const ctx = canvas.getContext('2d'); // Draw image ctx.drawImage(image, 0, 0, canvas.width, canvas.height); // Data image const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); // Unscan if (unScan) { return resolve(Object.assign({}, objectFile, { data: [], canvas: canvas })); } // Scanner if (WASM_READY()) { let code = await zbarWasm.scanImageData(imageData); // SymbolType const hasSymbolType = symbolType?.some(s => s.toString() !== ScannerQRCodeSymbolType.ScannerQRCode_NONE.toString()); if (hasSymbolType) { code = code.filter((s) => { const type = s.typeName.replace('ZBAR', 'ScannerQRCode'); const valid = symbolType?.some(s => s.toString() === ScannerQRCodeSymbolType[type].toString()); return valid; }); } if (code?.length) { // Decode code.forEach((s) => s.value = s.decode(decode?.toLocaleLowerCase())); // Overlay if (isMasked) { DRAW_RESULT_ON_CANVAS(code, canvas, canvasStyles); } // Emit object const blob = await CANVAS_TO_BLOB(canvas); const url = URL.createObjectURL(blob); const blobToFile = (theBlob, fileName) => new File([theBlob], fileName, { lastModified: new Date().getTime(), type: theBlob.type }); resolve(Object.assign({}, objectFile, { data: code, url: url, canvas: canvas, file: blobToFile(blob, objectFile.name) })); PLAY_AUDIO(isBeep); } else { resolve(Object.assign({}, objectFile, { data: code, canvas: canvas })); } } }; // Set src image.src = objectFile.url; }; fileReader.onerror = (error) => reject(error); fileReader.readAsDataURL(file); }); }; /** * Convert canvas to blob * canvas.toBlob((blob) => { .. }, 'image/jpeg', 0.95); // JPEG at 95% quality * @param canvas * @param type * @returns */ const CANVAS_TO_BLOB = (canvas, type) => { return new Promise((resolve, reject) => canvas.toBlob(blob => resolve(blob), type)); }; /** * Convert blob to file * @param theBlob * @param fileName * @returns */ const BLOB_TO_FILE = (theBlob, fileName) => { return new File([theBlob], fileName, { lastModified: new Date().getTime(), type: theBlob.type }); }; /** * FILES_TO_SCAN * @param files * @param configs * @param percentage * @param quality * @param as * @returns */ const FILES_TO_SCAN = (files = [], configs, percentage, quality, as = new AsyncSubject()) => { COMPRESS_IMAGE_FILE(files, percentage, quality).then((_files) => { Promise.all(Object.assign([], _files).map((m) => READ_AS_DATA_URL(m, configs))).then((img) => { AS_COMPLETE(as, img); }).catch((error) => AS_COMPLETE(as, null, error)); }); return as; }; /** * FILL_TEXT_MULTI_LINE * @param ctx * @param text * @param x * @param y */ const FILL_TEXT_MULTI_LINE = (ctx, text, x, y) => { let lineHeight = ctx.measureText("M").width * 1.2; let lines = text.split("\n"); for (var i = 0; i < lines.length; ++i) { ctx.fillText(lines[i], x, y); ctx.strokeText(lines[i], x, y); y += lineHeight; } }; /** * COMPRESS_IMAGE_FILE * @param files * @param percentage * @param quality * @returns */ const COMPRESS_IMAGE_FILE = (files = [], percentage = 100, quality = 100) => { if (files.length && (percentage < 100 || quality < 100)) { // Have files const resizedFiles = []; return new Promise((resolve, reject) => { for (const file of files) { const image = new Image(); const reader = new FileReader(); reader.onload = function (event) { image.onload = function () { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const newWidth = Math.round(image.width * (percentage / 100)); const newHeight = Math.round(image.height * (percentage / 100)); canvas.width = newWidth; canvas.height = newHeight; ctx.drawImage(image, 0, 0, newWidth, newHeight); canvas.toBlob((blob) => { const resizedFile = new File([blob], file.name, { type: file.type }); resizedFiles.push(resizedFile); if (files.length === resizedFiles.length) { resolve(resizedFiles); } }, file.type, quality / 100); }; image.src = event.target.result; }; reader.onerror = (error) => reject(error); reader.readAsDataURL(file); } }); } else { // No files selected return Promise.resolve(files); } }; /** * REMOVE_RESULT_PANEL * @param element */ const REMOVE_RESULT_PANEL = (element) => { // clear text result and tooltip Object.assign([], element.childNodes).forEach(el => element.removeChild(el)); }; /** * RESET_CANVAS * @param canvas */ const RESET_CANVAS = (canvas) => { // reset canvas const context = canvas.getContext('2d', { willReadFrequently: true }); // clear frame when reloop context.clearRect(0, 0, canvas.width, canvas.height); }; /** * UPDATE_WIDTH_HEIGHT_VIDEO * @param video * @param canvas */ const UPDATE_WIDTH_HEIGHT_VIDEO = (video, canvas) => { video.style.width = canvas.offsetWidth + 'px'; video.style.height = canvas.offsetHeight + 'px'; }; /** * VIBRATE * @param time */ const VIBRATE = (time = 300) => { time && IS_MOBILE() && window?.navigator?.vibrate(time); }; /** * IS_MOBILE * @returns */ const IS_MOBILE = () => { const vendor = navigator.userAgent || navigator['vendor'] || window['opera']; const phone = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i; const version = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i; const isSafari = /^((?!chrome|android).)*safari/i; return !!(phone.test(vendor) || version.test(vendor.substr(0, 4))) && !isSafari.test(vendor); }; class NgxScannerQrcodeService { /** * loadFiles * @param files * @param percentage * @param quality * @returns */ loadFiles(files = [], percentage, quality) { const as = new AsyncSubject(); COMPRESS_IMAGE_FILE(files, percentage, quality).then((_files) => { Promise.all(Object.assign([], _files).map((m) => this.readAsDataURL(m))).then((img) => AS_COMPLETE(as, img)).catch((error) => AS_COMPLETE(as, null, error)); }); return as; } /** * loadFilesToScan * @param files * @param config * @param percentage * @param quality * @returns */ loadFilesToScan(files = [], config, percentage, quality) { return FILES_TO_SCAN(files, config, percentage, quality); } /** * readAsDataURL * @param file * @returns */ readAsDataURL(file) { /** drawImage **/ return new Promise((resolve, reject) => { const fileReader = new FileReader(); fileReader.onload = () => { const objectFile = { name: file.name, file: file, url: URL.createObjectURL(file) }; resolve(objectFile); }; fileReader.onerror = (error) => reject(error); fileReader.readAsDataURL(file); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxScannerQrcodeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxScannerQrcodeService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: NgxScannerQrcodeService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }] }); /** * LOAD_WASM * @param loadWasmUrl * @param as * @param renderer * @returns */ const LOAD_WASM = (loadWasmUrl = '', as = new AsyncSubject(), renderer) => { let retry = 0; const LOAD_WASM_RETRY = (isLoadWasmRemote = false) => { const DONE = (isFirst) => { let timeoutId; try { const END = () => { if (isFirst) { zbarWasm.setModuleArgs({ locateFile: (filename, directory) => loadWasmUrl ? loadWasmUrl : directory + filename }); } setTimeout(() => AS_COMPLETE(as, true)); }; const L = () => { clearTimeout(timeoutId); WASM_READY() ? END() : timeoutId = setTimeout(() => L()); }; setTimeout(() => L()); setTimeout(() => clearTimeout(timeoutId), 3000); } catch (error) { clearTimeout(timeoutId); } }; const scriptRemote = document.querySelectorAll(`script[src="${WASMREMOTE}"]`); const scriptRemoteLatest = document.querySelectorAll(`script[src="${WASMREMOTELATEST}"]`); if (scriptRemote.length || scriptRemoteLatest.length) { DONE(false); } else { const scriptProject = document.querySelectorAll(`script[src="${WASMPROJECT}"]`); if (scriptProject.length === 1) { DONE(false); } else { scriptProject.forEach(f => f.remove()); if (renderer) { const script = renderer.createElement("script"); renderer.setAttribute(script, "src", isLoadWasmRemote ? WASMREMOTE : WASMPROJECT); renderer.setAttribute(script, "type", "text/javascript"); renderer.setAttribute(script, "async", ""); renderer.appendChild(document.head, script); script.onload = () => DONE(true); script.onerror = () => { if (retry < 2) { document.head.removeChild(script); LOAD_WASM_RETRY(true); } else { AS_COMPLETE(as, false, 'Could not load script ' + isLoadWasmRemote ? WASMREMOTE : WASMPROJECT); } }; retry += 1; } else { const mod = document.createElement('script'); mod.setAttribute("src", isLoadWasmRemote ? WASMREMOTE : WASMPROJECT); mod.setAttribute("type", "text/javascript"); mod.setAttribute("async", ""); document.head.appendChild(mod); mod.onload = () => DONE(true); mod.onerror = () => { if (retry < 2) { document.head.removeChild(mod); LOAD_WASM_RETRY(true); } else { AS_COMPLETE(as, false, 'Could not load script ' + isLoadWasmRemote ? WASMREMOTE : WASMPROJECT); } }; retry += 1; } } } }; LOAD_WASM_RETRY(); return as; }; class NgxScannerQrcodeComponent { renderer; elementRef; /** * Element * playsinline required to tell iOS safari we don't want fullscreen */ video; canvas; resultsPanel; /** * EventEmitter */ event = new EventEmitter(); /** * Input */ config = CONFIG_DEFAULT; src = CONFIG_DEFAULT.src; fps = CONFIG_DEFAULT.fps; vibrate = CONFIG_DEFAULT.vibrate; decode = CONFIG_DEFAULT.decode; isBeep = CONFIG_DEFAULT.isBeep; isMasked = CONFIG_DEFAULT.isMasked; unScan = CONFIG_DEFAULT.unScan; loadWasmUrl = CONFIG_DEFAULT.loadWasmUrl; symbolType = CONFIG_DEFAULT.symbolType; constraints = CONFIG_DEFAULT.constraints; canvasStyles = [CANVAS_STYLES_LAYER, CANVAS_STYLES_TEXT]; /** * Export */ isStart = false; isPause = false; isLoading = false; isTorch = false; data = new BehaviorSubject([]); devices = new BehaviorSubject([]); deviceIndexActive = 0; /** * Private */ rAF_ID; dataForResize = []; ready = new AsyncSubject(); STATUS = { startON: () => this.isStart = true, pauseON: () => this.isPause = true, loadingON: () => this.isLoading = true, startOFF: () => this.isStart = false, pauseOFF: () => this.isPause = false, loadingOFF: () => this.isLoading = false, torchOFF: () => this.isTorch = false, }; constructor(renderer, elementRef) { this.renderer = renderer; this.elementRef = elementRef; } ngOnInit() { this.overrideConfig(); LOAD_WASM(this.loadWasmUrl, this.ready, this.renderer).subscribe(() => { if (this.src) { this.loadImage(this.src); } this.resize(); }); } /** * start * @param playDeviceCustom * @returns */ start(playDeviceCustom) { const as = new AsyncSubject(); if (this.isStart) { // Reject AS_COMPLETE(as, false); } else { // fix safari this.safariWebRTC(as, playDeviceCustom); } return as; } /** * stop * @returns */ stop() { this.STATUS.pauseOFF(); this.STATUS.startOFF(); this.STATUS.torchOFF(); this.STATUS.loadingOFF(); const as = new AsyncSubject(); try { clearTimeout(this.rAF_ID); this.video.nativeElement.srcObject.getTracks().forEach((track) => { track.stop(); AS_COMPLETE(as, true); }); this.dataForResize = []; RESET_CANVAS(this.canvas.nativeElement); REMOVE_RESULT_PANEL(this.resultsPanel.nativeElement); } catch (error) { AS_COMPLETE(as, false, error); } return as; } /** * play * @returns */ play() { const as = new AsyncSubject(); if (this.isPause) { this.video.nativeElement.play(); this.STATUS.pauseOFF(); this.requestAnimationFrame(); AS_COMPLETE(as, true); } else { AS_COMPLETE(as, false); } return as; } /** * pause * @returns */ pause() { const as = new AsyncSubject(); if (this.isStart) { clearTimeout(this.rAF_ID); this.video.nativeElement.pause(); this.STATUS.pauseON(); AS_COMPLETE(as, true); } else { AS_COMPLETE(as, false); } return as; } /** * playDevice * @param deviceId * @param as * @returns */ playDevice(deviceId, as = new AsyncSubject()) { const constraints = this.getConstraints(); const existDeviceId = (this.isStart && constraints) ? constraints.deviceId !== deviceId : true; switch (true) { case deviceId === 'null' || deviceId === 'undefined' || !deviceId: stop(); this.stop(); AS_COMPLETE(as, false); break; case deviceId && existDeviceId: stop(); this.stop(); // Loading on this.STATUS.loadingON(); this.deviceIndexActive = this.devices.value.findIndex((f) => f.deviceId === deviceId); const constraints = { ...this.constraints, audio: false, video: { deviceId: deviceId, ...this.constraints.video } }; // MediaStream navigator.mediaDevices.getUserMedia(constraints).then((stream) => { this.video.nativeElement.srcObject = stream; this.video.nativeElement.onloadedmetadata = () => { this.video.nativeElement.play(); this.requestAnimationFrame(); AS_COMPLETE(as, true); this.STATUS.startON(); this.STATUS.loadingOFF(); }; }).catch((error) => { this.eventEmit(false); AS_COMPLETE(as, false, error); this.STATUS.startOFF(); this.STATUS.loadingOFF(); }); break; default: AS_COMPLETE(as, false); this.STATUS.loadingOFF(); break; } return as; } /** * loadImage * @param src * @returns */ loadImage(src) { const as = new AsyncSubject(); // Loading on this.STATUS.startOFF(); this.STATUS.loadingON(); // Set the src of this Image object. const image = new Image(); // Setting cross origin value to anonymous image.setAttribute('crossOrigin', 'anonymous'); // When our image has loaded. image.onload = () => { WASM_READY() && this.drawImage(image, (flag) => { AS_COMPLETE(as, flag); this.STATUS.startOFF(); this.STATUS.loadingOFF(); }); }; // Set src image.src = src; return as; } /** * torcher * @returns */ torcher() { const as = this.applyConstraints({ advanced: [{ torch: this.isTorch }] }); as.subscribe(() => false, () => this.isTorch = !this.isTorch); return as; } /** * applyConstraints * @param constraints * @param deviceIndex * @returns */ applyConstraints(constraints, deviceIndex = 0) { const as = new AsyncSubject(); if (this.isStart) { const stream = this.video.nativeElement.srcObject; if (deviceIndex !== null || deviceIndex !== undefined || !Number.isNaN(deviceIndex)) { const videoTrack = stream.getVideoTracks()[deviceIndex]; const imageCapture = new window.ImageCapture(videoTrack); imageCapture.getPhotoCapabilities().then(async () => { await videoTrack.applyConstraints(constraints); UPDATE_WIDTH_HEIGHT_VIDEO(this.video.nativeElement, this.canvas.nativeElement); AS_COMPLETE(as, true); }).catch((error) => { switch (error?.name) { case 'NotFoundError': case 'DevicesNotFoundError': AS_COMPLETE(as, false, 'Required track is missing'); break; case 'NotReadableError': case 'TrackStartError': AS_COMPLETE(as, false, 'Webcam or mic are already in use'); break; case 'OverconstrainedError': case 'ConstraintNotSatisfiedError': AS_COMPLETE(as, false, 'Constraints can not be satisfied by avb. devices'); break; case 'NotAllowedError': case 'PermissionDeniedError': AS_COMPLETE(as, false, 'Permission denied in browser'); break; case 'TypeError': AS_COMPLETE(as, false, 'Empty constraints object'); break; default: AS_COMPLETE(as, false, error); break; } }); } else { AS_COMPLETE(as, false, 'Please check again deviceIndex'); } } else { AS_COMPLETE(as, false, 'Please start the scanner'); } return as; } ; /** * getConstraints * @param deviceIndex * @returns */ getConstraints(deviceIndex = 0) { const stream = this.video.nativeElement.srcObject; const videoTrack = stream?.getVideoTracks()[deviceIndex]; return videoTrack?.getConstraints(); } /** * download * @param fileName * @param percentage * @param quality * @returns */ download(fileName = `ngx_scanner_qrcode_${Date.now()}.png`, percentage, quality) { const as = new AsyncSubject(); (async () => { const blob = await CANVAS_TO_BLOB(this.canvas.nativeElement); const file = BLOB_TO_FILE(blob, fileName); FILES_TO_SCAN([file], this.config, percentage, quality, as).subscribe((res) => { res.forEach((item) => { if (item?.data?.length) { const link = document.createElement('a'); link.href = item.url; link.download = item.name; link.click(); link.remove(); } }); }); })(); return as; } /** * resize */ resize() { window.addEventListener("resize", () => { DRAW_RESULT_APPEND_CHILD(this.dataForResize, this.canvas.nativeElement, this.resultsPanel.nativeElement, this.canvasStyles); UPDATE_WIDTH_HEIGHT_VIDEO(this.video.nativeElement, this.canvas.nativeElement); }); } /** * overrideConfig */ overrideConfig() { if ('src' in this.config) this.src = this.config.src; if ('fps' in this.config) this.fps = this.config.fps; if ('vibrate' in this.config) this.vibrate = this.config.vibrate; if ('decode' in this.config) this.decode = this.config.decode; if ('isBeep' in this.config) this.isBeep = this.config.isBeep; if ('isMasked' in this.config) this.isMasked = this.config.isMasked; if ('unScan' in this.config) this.unScan = this.config.unScan; if ('loadWasmUrl' in this.config) this.loadWasmUrl = this.config.loadWasmUrl; if ('symbolType' in this.config) this.symbolType = this.config.symbolType; if ('constraints' in this.config) this.constraints = OVERRIDES('constraints', this.config, MEDIA_STREAM_DEFAULT); if ('canvasStyles' in this.config && this.config?.canvasStyles?.length === 2) this.canvasStyles = this.config.canvasStyles; } /** * safariWebRTC * Fix issue on safari * https://webrtchacks.com/guide-to-safari-webrtc * @param as * @param playDeviceCustom */ safariWebRTC(as, playDeviceCustom) { // Loading on this.STATUS.startOFF(); this.STATUS.loadingON(); navigator.mediaDevices.getUserMedia(this.constraints).then((stream) => { stream.getTracks().forEach(track => track.stop()); this.loadAllDevices(as, playDeviceCustom); }).catch((error) => { AS_COMPLETE(as, false, error); this.STATUS.startOFF(); this.STATUS.loadingOFF(); }); } /** * loadAllDevices * @param as * @param playDeviceCustom */ loadAllDevices(as, playDeviceCustom) { navigator.mediaDevices.enumerateDevices().then(devices => { let cameraDevices = devices.filter(f => f.kind == 'videoinput'); this.devices.next(cameraDevices); if (cameraDevices?.length > 0) { AS_COMPLETE(as, cameraDevices); playDeviceCustom ? playDeviceCustom(cameraDevices) : this.playDevice(cameraDevices[0].deviceId); } else { AS_COMPLETE(as, false, 'No camera detected.'); this.STATUS.startOFF(); this.STATUS.loadingOFF(); } }).catch((error) => { AS_COMPLETE(as, false, error); this.STATUS.startOFF(); this.STATUS.loadingOFF(); }); } /** * drawImage * @param element * @param callback */ async drawImage(element, callback = () => { }) { // Get the canvas element by using the getElementById method. const canvas = this.canvas.nativeElement; // Get a 2D drawing context for the canvas. const