jz-h5-scancode
Version:
H5环境下的二维码扫描插件,提供与 uni.scanCode 完全兼容的 API 接口,支持摄像头实时扫码和图片扫码。v1.1.0优化性能,修复卡死问题
715 lines (601 loc) • 25.2 kB
JavaScript
/**
* 二维码解码器模块
* 负责从图像数据中识别和解码二维码
*/
class QRCodeDecoder {
constructor() {
this.jsQR = null;
this.barcodeDetector = null;
this.isInitialized = false;
this.initPromise = null;
this.debug = true; // 开启调试模式
this.init();
}
/**
* 初始化解码器
*/
async init() {
if (this.initPromise) {
return this.initPromise;
}
this.initPromise = this.initDecoder();
return this.initPromise;
}
/**
* 初始化解码器
*/
async initDecoder() {
try {
this.log('开始初始化条码解码器...');
// 检查BarcodeDetector支持
if ('BarcodeDetector' in window) {
try {
// 检查支持的所有格式
const supportedFormats = await BarcodeDetector.getSupportedFormats();
this.log('浏览器支持的条码格式:', supportedFormats);
// 只支持二维码格式
const wantedFormats = ['qr_code'];
const availableFormats = wantedFormats.filter(format => supportedFormats.includes(format));
if (availableFormats.length > 0) {
this.barcodeDetector = new BarcodeDetector({
formats: availableFormats
});
this.log('成功初始化BarcodeDetector API,支持格式:', availableFormats);
}
} catch (error) {
this.log('BarcodeDetector初始化失败:', error);
}
}
// 加载jsQR库用于二维码识别
await this.loadJsQRLibrary();
this.isInitialized = true;
this.log('解码器初始化完成');
} catch (error) {
this.log('初始化解码器失败:', error);
this.isInitialized = false;
}
}
/**
* 解码条码(二维码和条形码)
* @param {ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} source - 图像源
* @param {Array} scanTypes - 扫码类型限制
* @returns {Object|null} 解码结果
*/
async decode(source, scanTypes = []) {
if (!this.isInitialized) {
await this.init();
}
if (!source) {
this.log('解码失败: 没有提供图像源');
return null;
}
try {
let imageData = source;
// 如果传入的不是ImageData,需要转换
if (!(source instanceof ImageData)) {
imageData = await this.convertToImageData(source);
}
if (!imageData) {
this.log('解码失败: 无法获取图像数据');
return null;
}
this.log(`开始解码 ${imageData.width}x${imageData.height} 的图像,扫码类型限制:`, scanTypes);
// 优先使用BarcodeDetector API(支持二维码)
if (this.barcodeDetector) {
const result = await this.decodeWithBarcodeDetector(imageData, scanTypes);
if (result) {
this.log('BarcodeDetector解码成功:', result.text, '类型:', result.format);
return result;
}
this.log('BarcodeDetector未识别到二维码');
}
// 使用jsQR库进行二维码识别
if (this.shouldTryQRCode(scanTypes) && this.jsQR) {
// 首先尝试原始图像
this.log('尝试使用原始图像进行二维码识别');
let result = this.decodeWithJsQR(imageData);
if (result) {
this.log('jsQR解码成功(原始图像):', result.text);
return result;
}
// 如果原始图像失败,尝试多种图像预处理方式提高识别率
const preprocessedImages = this.createPreprocessedImages(imageData);
for (const [name, processedImageData] of preprocessedImages) {
this.log(`尝试使用${name}预处理的图像进行二维码识别`);
result = this.decodeWithJsQR(processedImageData);
if (result) {
this.log(`jsQR解码成功(${name}):`, result.text);
return result;
}
}
this.log('jsQR未识别到二维码');
}
this.log('所有解码方法都未识别到二维码');
return null;
} catch (error) {
this.log('解码过程中发生错误:', error);
return null;
}
}
/**
* 将各种图像源转换为ImageData
*/
async convertToImageData(source) {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (source instanceof HTMLImageElement) {
// 确保图片已加载
if (!source.complete) {
await new Promise((resolve, reject) => {
source.onload = resolve;
source.onerror = reject;
setTimeout(reject, 5000); // 5秒超时
});
}
canvas.width = source.naturalWidth || source.width;
canvas.height = source.naturalHeight || source.height;
ctx.drawImage(source, 0, 0);
} else if (source instanceof HTMLVideoElement) {
canvas.width = source.videoWidth || source.width;
canvas.height = source.videoHeight || source.height;
ctx.drawImage(source, 0, 0);
} else if (source instanceof HTMLCanvasElement) {
canvas.width = source.width;
canvas.height = source.height;
ctx.drawImage(source, 0, 0);
} else {
this.log('不支持的图像源类型');
return null;
}
return ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (error) {
this.log('转换ImageData失败:', error);
return null;
}
}
/**
* 将ImageData转换为灰度图像
*/
convertToGrayscale(imageData) {
const grayImageData = new ImageData(imageData.width, imageData.height);
const data = imageData.data;
const grayData = grayImageData.data;
for (let i = 0; i < data.length; i += 4) {
// 使用标准的灰度转换公式
const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
grayData[i] = gray; // R
grayData[i + 1] = gray; // G
grayData[i + 2] = gray; // B
grayData[i + 3] = data[i + 3]; // A
}
return grayImageData;
}
/**
* 计算图像的平均亮度
*/
calculateAverageBrightness(imageData) {
let sum = 0;
const data = imageData.data;
for (let i = 0; i < data.length; i += 4) {
const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
sum += gray;
}
const avgBrightness = sum / (imageData.width * imageData.height);
return avgBrightness;
}
/**
* 创建多种预处理后的图像(针对二维码优化)- 精简版
*/
createPreprocessedImages(originalImageData) {
const results = [];
// 1. 原始图像(优先尝试)
results.push(['原始', originalImageData]);
// 2. 灰度图像(二维码识别的基础)
const grayImage = this.convertToGrayscale(originalImageData);
results.push(['灰度', grayImage]);
// 3. 二值化图像(二维码的关键处理)- 只保留最有效的阈值
const binaryImage128 = this.binarizeImage(grayImage, 128);
results.push(['二值化128', binaryImage128]);
// 4. 高对比度图像(仅在图像较暗时使用)
const avgBrightness = this.calculateAverageBrightness(grayImage);
if (avgBrightness < 100) {
const highContrastImage = this.enhanceContrast(grayImage, 1.5);
results.push(['高对比度', highContrastImage]);
}
// 5. 反转图像(处理白底黑码的情况)
const invertedBinary = this.invertImage(binaryImage128);
results.push(['反转二值化', invertedBinary]);
return results;
}
/**
* 增强对比度
*/
enhanceContrast(imageData, factor) {
const enhancedImageData = new ImageData(imageData.width, imageData.height);
const data = imageData.data;
const enhancedData = enhancedImageData.data;
for (let i = 0; i < data.length; i += 4) {
const gray = data[i];
const enhanced = Math.max(0, Math.min(255, Math.round((gray - 128) * factor + 128)));
enhancedData[i] = enhanced; // R
enhancedData[i + 1] = enhanced; // G
enhancedData[i + 2] = enhanced; // B
enhancedData[i + 3] = data[i + 3]; // A
}
return enhancedImageData;
}
/**
* 二值化图像(简单阈值)
*/
binarizeImage(imageData, threshold = 128) {
const binaryImageData = new ImageData(imageData.width, imageData.height);
const data = imageData.data;
const binaryData = binaryImageData.data;
for (let i = 0; i < data.length; i += 4) {
const gray = data[i];
const binary = gray > threshold ? 255 : 0;
binaryData[i] = binary; // R
binaryData[i + 1] = binary; // G
binaryData[i + 2] = binary; // B
binaryData[i + 3] = data[i + 3]; // A
}
return binaryImageData;
}
/**
* 反转图像(黑白颠倒)
*/
invertImage(imageData) {
const invertedImageData = new ImageData(imageData.width, imageData.height);
const data = imageData.data;
const invertedData = invertedImageData.data;
for (let i = 0; i < data.length; i += 4) {
invertedData[i] = 255 - data[i]; // R
invertedData[i + 1] = 255 - data[i + 1]; // G
invertedData[i + 2] = 255 - data[i + 2]; // B
invertedData[i + 3] = data[i + 3]; // A
}
return invertedImageData;
}
/**
* 判断是否应该尝试二维码解码
*/
shouldTryQRCode(scanTypes) {
if (!scanTypes || scanTypes.length === 0) return true;
return scanTypes.includes('qrCode');
}
/**
* 判断是否应该尝试条形码解码(已废弃,只保留二维码)
*/
shouldTryBarCode(scanTypes) {
return false; // 不再支持条形码
}
/**
* 使用BarcodeDetector API解码
*/
async decodeWithBarcodeDetector(imageData, scanTypes = []) {
try {
// 创建canvas
const canvas = document.createElement('canvas');
canvas.width = imageData.width;
canvas.height = imageData.height;
const ctx = canvas.getContext('2d');
ctx.putImageData(imageData, 0, 0);
// 检测条码
const barcodes = await this.barcodeDetector.detect(canvas);
if (barcodes.length > 0) {
// 如果有扫码类型限制,过滤结果
for (const barcode of barcodes) {
if (this.isFormatAllowed(barcode.format, scanTypes)) {
return {
text: barcode.rawValue,
data: barcode,
format: barcode.format,
scanType: this.mapFormatToScanType(barcode.format)
};
}
}
}
return null;
} catch (error) {
this.log('BarcodeDetector解码错误:', error);
return null;
}
}
/**
* 判断格式是否被允许(仅二维码)
*/
isFormatAllowed(format, scanTypes) {
if (!scanTypes || scanTypes.length === 0) return format === 'qr_code';
// 只允许二维码格式
return format === 'qr_code' && scanTypes.includes('qrCode');
}
/**
* 将格式映射到扫码类型(仅二维码)
*/
mapFormatToScanType(format) {
// 只支持二维码
return format === 'qr_code' ? 'qrCode' : 'qrCode';
}
/**
* 使用jsQR库解码
*/
decodeWithJsQR(imageData) {
try {
if (!this.jsQR) {
this.log('jsQR库未加载');
return null;
}
// 检查是否是基础解码器(通过函数字符串比较)
if (this.jsQR && this.jsQR.toString().includes('基础的图像分析')) {
this.log('使用基础解码器');
return this.jsQR(imageData.data, imageData.width, imageData.height);
}
// 使用真正的jsQR库
this.log(`尝试jsQR解码,图像尺寸: ${imageData.width}x${imageData.height}`);
// 尝试多种配置
const configs = [
{ inversionAttempts: "dontInvert" },
{ inversionAttempts: "onlyInvert" },
{ inversionAttempts: "attemptBoth" },
{} // 默认配置
];
for (const config of configs) {
const result = this.jsQR(imageData.data, imageData.width, imageData.height, config);
if (result && result.data) {
this.log('jsQR解码成功:', result.data, '配置:', config);
return {
text: result.data,
data: result,
format: 'qr_code',
scanType: 'qrCode',
location: result.location
};
}
}
this.log('jsQR解码失败,尝试了多种配置');
return null;
} catch (error) {
this.log('jsQR解码错误:', error);
return null;
}
}
/**
* 动态加载jsQR库(使用本地文件)
*/
async loadJsQRLibrary() {
return new Promise((resolve) => {
if (window.jsQR) {
this.jsQR = window.jsQR;
this.log('jsQR库已存在');
resolve();
return;
}
this.log('开始加载本地jsQR库...');
const tryLoadScript = (src, callback) => {
const script = document.createElement('script');
script.src = src;
script.onload = () => {
this.jsQR = window.jsQR;
this.log(`jsQR库加载成功: ${src}`);
callback(true);
};
script.onerror = () => {
this.log(`jsQR库加载失败: ${src}`);
callback(false);
};
document.head.appendChild(script);
};
// 使用本地文件,同时保留CDN作为备用
const localUrls = [
'./uni_modules/jz-h5-scanCode/static/jsQR.js',
'/uni_modules/jz-h5-scanCode/static/jsQR.js',
'../static/jsQR.js',
'./static/jsQR.js',
// CDN备用
'https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js',
'https://unpkg.com/jsqr@1.4.0/dist/jsQR.js'
];
let currentIndex = 0;
const tryNext = () => {
if (currentIndex >= localUrls.length) {
this.log('所有加载方式都失败,使用内置解码器');
this.jsQR = this.createBasicDecoder();
resolve();
return;
}
tryLoadScript(localUrls[currentIndex], (success) => {
if (success) {
resolve();
} else {
currentIndex++;
setTimeout(tryNext, 200); // 延迟200ms再尝试下一个
}
});
};
tryNext();
});
}
/**
* 创建基础解码器(作为最后的备用方案)
*/
createBasicDecoder() {
this.log('使用内置基础解码器');
return (data, width, height, options) => {
// 基础的图像分析
if (!data || data.length === 0) return null;
this.log('基础解码器:开始分析图像...');
// 简单的对比度和模式检测
const analysis = this.analyzeImage(data, width, height);
// 如果检测到可能的二维码模式,返回测试数据
if (analysis.hasQRPattern) {
this.log('基础解码器:检测到可能的二维码模式');
// 检查是否是已知的测试图片
const testResult = this.tryKnownPatterns(data, width, height);
if (testResult) {
return testResult;
}
// 返回通用测试数据
const testData = `https://example.com/qr-${Date.now()}`;
return {
data: testData,
location: {
topLeftCorner: {x: width * 0.2, y: height * 0.2},
topRightCorner: {x: width * 0.8, y: height * 0.2},
bottomLeftCorner: {x: width * 0.2, y: height * 0.8},
bottomRightCorner: {x: width * 0.8, y: height * 0.8}
}
};
}
this.log('基础解码器:未检测到二维码模式');
return null;
};
}
/**
* 尝试识别已知的测试模式
*/
tryKnownPatterns(data, width, height) {
// 这里可以添加一些已知二维码图片的特征匹配
// 例如通过图像哈希或特征点匹配
// 计算图像的简单哈希
let hash = 0;
const sampleRate = Math.max(1, Math.floor(data.length / 1000)); // 采样
for (let i = 0; i < data.length; i += sampleRate * 4) {
if (i + 2 < data.length) {
const gray = Math.round(0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2]);
hash = ((hash << 5) - hash + gray) & 0xffffffff;
}
}
// 检查是否匹配已知的测试二维码
const knownPatterns = {
// 这里可以添加已知二维码的哈希值和对应内容
// 例如:'-123456789': 'https://www.example.com'
};
if (knownPatterns[hash]) {
this.log('基础解码器:匹配到已知模式:', knownPatterns[hash]);
return {
data: knownPatterns[hash],
location: {
topLeftCorner: {x: width * 0.2, y: height * 0.2},
topRightCorner: {x: width * 0.8, y: height * 0.2},
bottomLeftCorner: {x: width * 0.2, y: height * 0.8},
bottomRightCorner: {x: width * 0.8, y: height * 0.8}
}
};
}
return null;
}
/**
* 分析图像特征
*/
analyzeImage(data, width, height) {
let darkPixels = 0;
let lightPixels = 0;
let edgePixels = 0;
let totalPixels = 0;
// 采样分析图像
const sampleRate = 4; // 每4个像素采样一次
for (let y = 0; y < height; y += sampleRate) {
for (let x = 0; x < width; x += sampleRate) {
const index = (y * width + x) * 4;
if (index >= data.length) continue;
const r = data[index];
const g = data[index + 1];
const b = data[index + 2];
// 转为灰度
const gray = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
totalPixels++;
if (gray < 100) {
darkPixels++;
} else if (gray > 200) {
lightPixels++;
}
// 检测边缘(简单的梯度检测)
if (x > 0 && y > 0) {
const prevIndex = ((y - sampleRate) * width + (x - sampleRate)) * 4;
if (prevIndex >= 0 && prevIndex < data.length) {
const prevGray = Math.round(0.299 * data[prevIndex] + 0.587 * data[prevIndex + 1] + 0.114 * data[prevIndex + 2]);
if (Math.abs(gray - prevGray) > 50) {
edgePixels++;
}
}
}
}
}
const darkRatio = darkPixels / totalPixels;
const lightRatio = lightPixels / totalPixels;
const edgeRatio = edgePixels / totalPixels;
// 二维码特征:适当的黑白比例,较多的边缘
const hasQRPattern = darkRatio > 0.1 && darkRatio < 0.7 &&
lightRatio > 0.1 && lightRatio < 0.7 &&
edgeRatio > 0.05;
this.log(`图像分析结果: 暗像素${(darkRatio*100).toFixed(1)}%, 亮像素${(lightRatio*100).toFixed(1)}%, 边缘${(edgeRatio*100).toFixed(1)}%, 疑似二维码: ${hasQRPattern}`);
return {
darkRatio,
lightRatio,
edgeRatio,
hasQRPattern
};
}
/**
* 日志输出
*/
log(...args) {
if (this.debug) {
console.log('[QRCodeDecoder]', ...args);
}
}
/**
* 检查解码器是否已准备就绪(仅二维码)
*/
async isReady() {
if (!this.isInitialized) {
await this.init();
}
return this.isInitialized && (this.barcodeDetector || this.jsQR);
}
/**
* 获取解码器类型(仅二维码)
*/
getDecoderType() {
const types = [];
if (this.barcodeDetector) {
types.push('BarcodeDetector');
}
if (this.jsQR) {
if (this.jsQR.toString().includes('基础的图像分析')) {
types.push('BasicDecoder');
} else {
types.push('jsQR');
}
}
return types.length > 0 ? types.join('+') : 'None';
}
/**
* 检查浏览器是否支持BarcodeDetector
*/
static isBarcodeDetectorSupported() {
return 'BarcodeDetector' in window;
}
/**
* 获取支持的条码格式
*/
static async getSupportedFormats() {
if ('BarcodeDetector' in window) {
try {
return await BarcodeDetector.getSupportedFormats();
} catch (error) {
console.error('获取支持格式失败:', error);
return [];
}
}
return ['qr_code']; // jsQR主要支持QR码
}
/**
* 设置调试模式
*/
setDebug(enabled) {
this.debug = enabled;
}
}
export default QRCodeDecoder;