import { createUUID } from "../../utils/uuid"; import { getConfig } from "../config"; import scanBeepAudio from "../../../res/scan_beep.ogg"; const scanWeb = { uuid: null, finish: true } const DEFAULT_SCAN_BEEP_AUDIO = scanBeepAudio; function removeEl(id) { try { let el = document.getElementById(id); document.body.removeChild(el); } catch (error) { } } function createEl(tagName, id, style, appendChild) { let el = document.getElementById(id); if (!el) { el = document.createElement(tagName); el.id = id; el.style = style; appendChild && document.body.appendChild(el); } return el; } function canvasDrawLine(context, width, begin, end, color) { context.beginPath(); context.moveTo(width - begin.x, begin.y); context.lineTo(width - end.x, end.y); context.lineWidth = 4; context.strokeStyle = color; context.stroke(); } function getBarcodeFormats(scanType) { let formats = []; if (!scanType) { scanType = ["qrCode", "barCode"]; } if (scanType.includes('qrCode')) { formats.push('qr_code'); } if (scanType.includes('barCode')) { formats.push( 'ean_13', 'ean_8', 'code_128', 'code_39', 'codabar', 'upc_a', 'upc_e', 'itf', 'aztec', 'data_matrix', 'pdf417' ); } return formats; } function createBarcodeDetector(scanType) { return Promise.resolve().then(() => { if (typeof BarcodeDetector === 'undefined') { throw new Error("BarcodeDetector is not supported"); } const formats = getBarcodeFormats(scanType); if (BarcodeDetector.getSupportedFormats) { return BarcodeDetector.getSupportedFormats().then(supportedFormats => { const supported = formats.filter(format => supportedFormats.indexOf(format) !== -1); if (!supported.length) { throw new Error("No supported barcode formats"); } return new BarcodeDetector({ formats: supported }); }); } return new BarcodeDetector({ formats }); }); } function drawBarcode(context, width, barcode) { const cornerPoints = barcode.cornerPoints; if (cornerPoints && cornerPoints.length) { for (let i = 0; i < cornerPoints.length; i++) { canvasDrawLine(context, width, cornerPoints[i], cornerPoints[(i + 1) % cornerPoints.length], "#FF3B58"); } return; } if (barcode.boundingBox) { const rect = barcode.boundingBox; const points = [ { x: rect.x, y: rect.y }, { x: rect.x + rect.width, y: rect.y }, { x: rect.x + rect.width, y: rect.y + rect.height }, { x: rect.x, y: rect.y + rect.height } ]; for (let i = 0; i < points.length; i++) { canvasDrawLine(context, width, points[i], points[(i + 1) % points.length], "#FF3B58"); } } } function playScanBeep() { if (getConfig("webScanBeepEnabled") === false) { return; } const audioSrc = getConfig("webScanBeepAudio") || DEFAULT_SCAN_BEEP_AUDIO; if (!audioSrc || typeof Audio === 'undefined') { return; } try { const audio = new Audio(audioSrc); const playPromise = audio.play(); playPromise && playPromise.catch && playPromise.catch(() => { // no thing }); } catch (e) { } } export function isSupportWebScan() { return typeof navigator !== 'undefined' && navigator.mediaDevices && navigator.mediaDevices.getUserMedia && typeof BarcodeDetector !== 'undefined'; } export function isSupportImageScan() { return typeof document !== 'undefined' && typeof BarcodeDetector !== 'undefined' && typeof URL !== 'undefined' && URL.createObjectURL; } export function stopScanForWeb() { return Promise.resolve().then(() => { scanWeb.uuid = null; }) } function chooseImageFile() { return new Promise(resolve => { const input = document.createElement("input"); input.type = "file"; input.accept = "image/*"; input.style.display = "none"; let finished = false; let finish = file => { if (finished) { return; } finished = true; removeEl("__webscan_image_input__"); resolve(file); }; input.id = "__webscan_image_input__"; input.onchange = () => { finish(input.files && input.files[0]); }; input.oncancel = () => { finish(null); }; document.body.appendChild(input); input.click(); }); } function detectImageFile(detector, file) { if (!file) { return Promise.resolve(null); } if (typeof createImageBitmap !== 'undefined') { return createImageBitmap(file).then(image => { return detector.detect(image).then(barcodes => { image.close && image.close(); return barcodes && barcodes[0]; }).catch(err => { image.close && image.close(); throw err; }); }); } return new Promise((resolve, reject) => { const image = new Image(); const url = URL.createObjectURL(file); image.onload = () => { detector.detect(image).then(barcodes => { URL.revokeObjectURL(url); resolve(barcodes && barcodes[0]); }).catch(err => { URL.revokeObjectURL(url); reject(err); }); }; image.onerror = err => { URL.revokeObjectURL(url); reject(err); }; image.src = url; }); } export function startScanForImage() { return createBarcodeDetector(getConfig("webScanType")).then(detector => { return chooseImageFile().then(file => detectImageFile(detector, file)); }).then(code => { if (code && code.rawValue) { return { result: code.rawValue }; } return { success: false, error: "未识别到二维码或条形码" }; }); } export function startScanForWeb(canvasStyle, onResult) { return new Promise((resolve, reject) => { try { scanWeb.uuid = createUUID(); scanWeb.finish = false; let videoEl = createEl("video", "__webscan_video__", "display: none", false); let canvasEnabled = getConfig("webCanvasEnabled") !== false; let canvasDisplay = ""; let canvasBaseStyle = canvasStyle || "position: fixed; width: 300px; height: 240px; top: 0; left: 0; z-index: 9999;"; let canvasEl = createEl("canvas", "__webscan_canvas__", canvasBaseStyle + " display: none;", true); canvasDisplay = canvasEl.style.display; canvasEl.style.cssText = canvasBaseStyle; canvasDisplay = canvasEl.style.display; canvasEl.style.display = "none"; let context = canvasEl.getContext("2d"); let currentUuid = scanWeb.uuid; videoEl.width = 300; videoEl.height = 300; videoEl.uuid = scanWeb.uuid; createBarcodeDetector(getConfig("webScanType")).then(detector => { return navigator.mediaDevices.getUserMedia({ video: { facingMode: "environment" } }).then(stream => { return { detector, stream }; }); }).then(function (options) { const detector = options.detector; const stream = options.stream; videoEl.srcObject = stream; videoEl.setAttribute("playsinline", true); // iOS使用 videoEl.play(); canvasEl.style.display = "none"; let detecting = false; let displayed = false; let closed = false; let close = () => { if (closed) { return; } closed = true; try { stream.getTracks()[0].stop(); } catch (_e) { } }; let tick = () => { try { if (videoEl.readyState === videoEl.HAVE_ENOUGH_DATA && !detecting) { canvasEl.height = videoEl.videoHeight; canvasEl.width = videoEl.videoWidth; context.setTransform(-1, 0, 0, 1, canvasEl.width, 0); context.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height); context.setTransform(1, 0, 0, 1, 0, 0); if (canvasEnabled && !displayed) { displayed = true; canvasEl.style.display = canvasDisplay || ""; } detecting = true; detector.detect(videoEl).then(barcodes => { const code = barcodes && barcodes[0]; if (code && code.rawValue && scanWeb.uuid == currentUuid) { if (!onResult || !onResult(code.rawValue)) { return; } drawBarcode(context, canvasEl.width, code); playScanBeep(); scanWeb.uuid = null; scanWeb.finish = true; close(); resolve({ result: code.rawValue }) } }).catch(() => { }).finally(() => { detecting = false; }); } } catch (e) { detecting = false; } if (scanWeb.uuid == currentUuid) { requestAnimationFrame(() => { tick() }); } else { if (!scanWeb.finish) { reject({ cancel: 1 }); scanWeb.finish = true; } close(); } if (scanWeb.finish) { close(); } }; requestAnimationFrame(() => { tick(); }); }).catch(err => { reject({ error: err }); }); } catch (e) { reject({ error: e }); } }).finally(() => { removeEl("__webscan_video__"); removeEl("__webscan_canvas__"); }) }