Files
scan-code-jssdk/src/services/web/index.js
2026-04-30 11:17:05 +08:00

340 lines
9.4 KiB
JavaScript

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__");
})
}