351 lines
7.8 KiB
JavaScript
351 lines
7.8 KiB
JavaScript
import {
|
|
prepareZXingModule,
|
|
readBarcodes,
|
|
writeBarcode,
|
|
} from 'zxing-wasm/full';
|
|
|
|
/** 默认背景颜色 */
|
|
const DEFAULT_BGC = 'transparent';
|
|
|
|
/** 默认前景颜色 */
|
|
const DEFAULT_FGC = '#000000';
|
|
|
|
/** 模块名称 */
|
|
const PREFIX = '[qr-code]';
|
|
|
|
/**
|
|
* @desc 二维码读取配置选项
|
|
* @type { import('zxing-wasm').ReaderOptions }
|
|
*/
|
|
const readerOptions = {
|
|
formats: ['QRCode'],
|
|
maxNumberOfSymbols: 8,
|
|
textMode: 'Plain',
|
|
tryDownscale: true,
|
|
tryHarder: true,
|
|
tryInvert: true,
|
|
tryRotate: true,
|
|
};
|
|
|
|
/**
|
|
* @desc 二维码生成配置选项
|
|
* @type { import('zxing-wasm').WriterOptions }
|
|
*/
|
|
const writerOptions = {
|
|
ecLevel: '',
|
|
format: 'QRCode',
|
|
scale: 0,
|
|
};
|
|
|
|
// 配置 wasm 文件路径
|
|
prepareZXingModule({
|
|
overrides: {
|
|
locateFile: (path, prefix) => {
|
|
if (path.endsWith('.wasm')) {
|
|
return `./wasm/${path}`;
|
|
} else {
|
|
return (prefix + path);
|
|
}
|
|
},
|
|
},
|
|
});
|
|
|
|
/**
|
|
* @description 转换 Blob 为 DataURL
|
|
* @param {Blob} blob
|
|
* @param {Callback} callback
|
|
*/
|
|
function blobToDataURL(blob, callback) {
|
|
|
|
/** @typedef {(data: { error: boolean, result: string }) => void} Callback */
|
|
|
|
let reader = new FileReader();
|
|
|
|
reader.onerror = function () {
|
|
callback({
|
|
error: true,
|
|
result: reader.result,
|
|
});
|
|
};
|
|
|
|
reader.onload = function () {
|
|
callback({
|
|
error: false,
|
|
result: reader.result,
|
|
});
|
|
};
|
|
|
|
reader.readAsDataURL(blob);
|
|
|
|
}
|
|
|
|
/**
|
|
* @description 修改 SVG 内容,获取信息
|
|
* @param {object} options
|
|
* @param {string} options.background
|
|
* @param {string} options.foreground
|
|
* @param {string} options.svgString
|
|
*/
|
|
function modifySvgContent(options) {
|
|
|
|
let {
|
|
background = DEFAULT_BGC,
|
|
foreground = DEFAULT_FGC,
|
|
svgString,
|
|
} = options;
|
|
|
|
let divElement = document.createElement('div');
|
|
let gElement = null;
|
|
let pathElement = null;
|
|
let rectElement = null;
|
|
let svgElement = null;
|
|
|
|
// 添加 DOM 元素,用于获取位置大小信息
|
|
document.body.appendChild(divElement);
|
|
|
|
divElement.innerHTML = svgString;
|
|
gElement = divElement.getElementsByTagName('g')[0] || null;
|
|
pathElement = divElement.getElementsByTagName('path')[0] || null;
|
|
rectElement = divElement.getElementsByTagName('rect')[0] || null;
|
|
svgElement = divElement.getElementsByTagName('svg')[0] || null;
|
|
|
|
if (!(gElement && pathElement && rectElement && svgElement)) {
|
|
return null;
|
|
}
|
|
|
|
// 修改填充颜色
|
|
gElement.setAttribute('fill', foreground);
|
|
rectElement.setAttribute('fill', background);
|
|
|
|
// 获取位置大小信息
|
|
let rectOfG = gElement.getBoundingClientRect();
|
|
let rectOfPath = pathElement.getBoundingClientRect();
|
|
|
|
let offsetX = Math.round(rectOfPath.left - rectOfG.left);
|
|
let offsetY = Math.round(rectOfPath.top - rectOfG.top);
|
|
let sizeW = Math.round(rectOfPath.width);
|
|
let sizeH = Math.round(rectOfPath.height);
|
|
let result = {
|
|
offsetX: offsetX,
|
|
offsetY: offsetY,
|
|
sizeW: sizeW,
|
|
sizeH: sizeH,
|
|
svgString: divElement.innerHTML,
|
|
};
|
|
|
|
// 输出处理结果
|
|
console.debug(PREFIX, '处理 SVG', result);
|
|
|
|
// 处理完成,移除 DOM 元素
|
|
document.body.removeChild(divElement);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
/**
|
|
* @description 将 SVG 字符串渲染到 Canvas
|
|
* @param {object} options
|
|
* @param {HTMLCanvasElement} options.canvas
|
|
* @param {string} options.svgString
|
|
* @param {string} options.background
|
|
* @param {string} options.foreground
|
|
* @param {number} options.drawLeft
|
|
* @param {number} options.drawTop
|
|
* @param {number} options.drawWidth
|
|
* @param {number} options.drawHeight
|
|
* @returns {Promise<boolean>}
|
|
*/
|
|
function renderSvgToCanvas(options) {
|
|
|
|
let {
|
|
canvas, svgString,
|
|
background = DEFAULT_BGC, foreground = DEFAULT_FGC,
|
|
drawLeft = 0, drawTop = 0,
|
|
drawWidth = 0, drawHeight = 0,
|
|
} = options;
|
|
|
|
let svgInfo = modifySvgContent({
|
|
background: background,
|
|
foreground: foreground,
|
|
svgString: svgString,
|
|
});
|
|
|
|
if (!svgInfo) {
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
let svgBlob = new Blob([svgInfo.svgString], {
|
|
type: 'image/svg+xml;charset=utf-8',
|
|
});
|
|
let svgUrl = URL.createObjectURL(svgBlob);
|
|
let image = new Image();
|
|
|
|
image.onerror = () => {
|
|
console.error(PREFIX, '加载 SVG 失败');
|
|
URL.revokeObjectURL(svgUrl);
|
|
resolve(false);
|
|
};
|
|
|
|
image.onload = () => {
|
|
let ctx = canvas.getContext('2d');
|
|
ctx.drawImage(
|
|
image,
|
|
svgInfo.offsetX, svgInfo.offsetY, svgInfo.sizeW, svgInfo.sizeH,
|
|
drawLeft, drawTop, drawWidth, drawHeight,
|
|
);
|
|
URL.revokeObjectURL(svgUrl);
|
|
resolve(true);
|
|
};
|
|
|
|
image.src = svgUrl;
|
|
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @description 解析二维码图片
|
|
* @param {Blob} image 图片二进制
|
|
*/
|
|
export function readQrCodeImage(image) {
|
|
|
|
/**
|
|
* @desc 返回结果
|
|
* @type {{ error: string; image: string; textList: string[]; }}
|
|
*/
|
|
let returns = {
|
|
error: '',
|
|
image: '',
|
|
textList: [],
|
|
};
|
|
|
|
/** 读取图片,转换为 DataURL */
|
|
let fileReader = new FileReader();
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
// 处理读取异常
|
|
fileReader.onerror = function () {
|
|
console.error(PREFIX, '解析二维码失败:读取图片失败');
|
|
returns.error = '读取图片失败';
|
|
resolve('');
|
|
};
|
|
|
|
// 处理读取完成
|
|
fileReader.onload = function () {
|
|
resolve(fileReader.result);
|
|
};
|
|
|
|
// 开始读取
|
|
fileReader.readAsDataURL(image);
|
|
|
|
}).then((dataURL) => {
|
|
if (dataURL) {
|
|
returns.image = dataURL;
|
|
return readBarcodes(image, readerOptions);
|
|
} else {
|
|
return null;
|
|
}
|
|
}).then((resultList) => {
|
|
|
|
let textList = returns.textList;
|
|
|
|
if (resultList.length === 0) {
|
|
console.warn(PREFIX, '解析二维码失败:未识别到内容');
|
|
return returns;
|
|
} else {
|
|
console.debug(PREFIX, '解析二维码成功:', resultList);
|
|
}
|
|
|
|
for (let i = 0; i < resultList.length; i++) {
|
|
|
|
let item = resultList[i];
|
|
|
|
textList.push(item.text);
|
|
|
|
}
|
|
|
|
return returns;
|
|
|
|
}).catch((error) => {
|
|
console.error(PREFIX, '解析二维码失败:');
|
|
console.error(error);
|
|
returns.error = String(error);
|
|
return returns;
|
|
});;
|
|
|
|
}
|
|
|
|
/**
|
|
* @description 生成二维码图片
|
|
* @param {object} options
|
|
* @param {string} options.content
|
|
* @param {string} options.background
|
|
* @param {string} options.foreground
|
|
* @param {number} options.width
|
|
* @param {number} options.height
|
|
* @returns 二维码图片 DataURL
|
|
*/
|
|
export function writeQrCodeImage(options = {}) {
|
|
|
|
let {
|
|
content = '',
|
|
background = DEFAULT_BGC, foreground = DEFAULT_FGC,
|
|
width = 256, height = 256,
|
|
} = options;
|
|
|
|
let canvas = document.createElement('canvas');
|
|
let ctx = canvas.getContext('2d');
|
|
|
|
// 更新画布大小
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
|
|
// 设置背景颜色
|
|
ctx.fillStyle = background;
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
return writeBarcode(content, writerOptions).then((result) => {
|
|
|
|
console.debug(PREFIX, '生成二维码', result);
|
|
|
|
if (result.error) {
|
|
console.error(PREFIX, `生成二维码失败:${result.error}`);
|
|
return '';
|
|
} else {
|
|
return result.svg;
|
|
}
|
|
|
|
}).then((svgString) => {
|
|
if (svgString) {
|
|
return renderSvgToCanvas({
|
|
canvas: canvas,
|
|
svgString: svgString,
|
|
background: background,
|
|
foreground: foreground,
|
|
drawLeft: 0,
|
|
drawTop: 0,
|
|
drawWidth: width,
|
|
drawHeight: height,
|
|
});
|
|
} else {
|
|
return false;
|
|
}
|
|
}).then((success) => {
|
|
|
|
if (success) {
|
|
return canvas.toDataURL('image/png');
|
|
} else {
|
|
return '';
|
|
}
|
|
|
|
}).catch((error) => {
|
|
console.error(PREFIX, '生成二维码失败:');
|
|
console.error(error);
|
|
return '';
|
|
});
|
|
|
|
}
|