245 lines
9.4 KiB
HTML
245 lines
9.4 KiB
HTML
<!doctype html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>QR 诊断测试</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/jsqr@1.4.0/dist/jsQR.js"></script>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { font-family: system-ui, sans-serif; margin: 0; padding: 16px; background: #f5f5f5; }
|
|
h2 { margin: 0 0 8px; }
|
|
.card { background: #fff; border-radius: 8px; padding: 16px; margin-bottom: 12px; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
|
|
input[type=file] { margin: 4px 0 12px; }
|
|
img, canvas { max-width: 100%; border: 1px solid #ccc; border-radius: 6px; margin: 6px 0; }
|
|
button { padding: 8px 14px; border: none; border-radius: 6px; background: #1976d2; color: #fff; font-size: 14px; cursor: pointer; margin-right: 8px; }
|
|
.result { margin-top: 10px; padding: 10px; border-radius: 6px; font-size: 13px; white-space: pre-wrap; word-break: break-all; }
|
|
.ok { background: #e8f5e9; color: #2e7d32; }
|
|
.fail { background: #ffebee; color: #c62828; }
|
|
.info { background: #e3f2fd; color: #1565c0; }
|
|
.row { display: flex; flex-wrap: wrap; gap: 12px; }
|
|
.col { flex: 1; min-width: 280px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h2>jsQR 二维码诊断工具</h2>
|
|
|
|
<div class="card">
|
|
<input type="file" id="file" accept="image/*" />
|
|
<button onclick="testAll()">全面诊断</button>
|
|
<button onclick="testFull()">原始尺寸识别</button>
|
|
<button onclick="testHalf()">50%缩放识别</button>
|
|
<button onclick="testQuarter()">25%缩放识别</button>
|
|
<button onclick="testCenter()">中心裁剪识别</button>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col">
|
|
<h3>原图</h3>
|
|
<img id="preview" style="display:none" />
|
|
</div>
|
|
<div class="col">
|
|
<h3>当前处理结果</h3>
|
|
<canvas id="resultCanvas" style="display:none"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="diag" style="display:none">
|
|
<h3>诊断日志</h3>
|
|
<div id="log"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const fileInput = document.getElementById('file');
|
|
const preview = document.getElementById('preview');
|
|
const resultCanvas = document.getElementById('resultCanvas');
|
|
const logDiv = document.getElementById('log');
|
|
const diagDiv = document.getElementById('diag');
|
|
|
|
let cachedImageData = null;
|
|
|
|
/** 获取图片的 ImageData 缓存。@returns {Promise<ImageData>} */
|
|
async function getImageData() {
|
|
if (cachedImageData) return cachedImageData;
|
|
const file = fileInput.files[0];
|
|
if (!file) throw new Error('请先选择图片');
|
|
const url = URL.createObjectURL(file);
|
|
preview.src = url;
|
|
preview.style.display = 'block';
|
|
const img = await loadImage(url);
|
|
const c = document.createElement('canvas');
|
|
c.width = img.width; c.height = img.height;
|
|
const ctx = c.getContext('2d');
|
|
ctx.drawImage(img, 0, 0);
|
|
cachedImageData = ctx.getImageData(0, 0, img.width, img.height);
|
|
URL.revokeObjectURL(url);
|
|
return cachedImageData;
|
|
}
|
|
|
|
/** 加载图片。@param {string} url @returns {Promise<HTMLImageElement>} */
|
|
function loadImage(url) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => resolve(img);
|
|
img.onerror = () => reject(new Error('图片加载失败'));
|
|
img.src = url;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 尝试 JSQR 识别并返回结果。
|
|
* @param {ImageData} imageData 图片数据。
|
|
* @param {string} label 测试标签。
|
|
* @returns {{ok:boolean, data?:string, msg:string}}
|
|
*/
|
|
function tryDecode(imageData, label) {
|
|
const start = performance.now();
|
|
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
|
inversionAttempts: 'attemptBoth',
|
|
});
|
|
const ms = (performance.now() - start).toFixed(0);
|
|
if (code) {
|
|
return { ok: true, data: code.data, msg: `${label}: 成功 (${ms}ms) → ${code.data}` };
|
|
}
|
|
return { ok: false, msg: `${label}: 失败 (${ms}ms) — ${imageData.width}x${imageData.height}` };
|
|
}
|
|
|
|
/**
|
|
* 缩放 ImageData 到指定倍数。
|
|
* @param {ImageData} src 源数据。
|
|
* @param {number} scale 缩放倍率 (0.5=一半, 0.25=四分之一)。
|
|
* @returns {ImageData}
|
|
*/
|
|
function scaleImageData(src, scale) {
|
|
const w = Math.floor(src.width * scale);
|
|
const h = Math.floor(src.height * scale);
|
|
const c = document.createElement('canvas');
|
|
c.width = w; c.height = h;
|
|
const ctx = c.getContext('2d');
|
|
const temp = document.createElement('canvas');
|
|
temp.width = src.width; temp.height = src.height;
|
|
temp.getContext('2d').putImageData(src, 0, 0);
|
|
ctx.drawImage(temp, 0, 0, w, h);
|
|
return ctx.getImageData(0, 0, w, h);
|
|
}
|
|
|
|
/**
|
|
* 裁剪图片中心区域(可能包含二维码的核心部分)。
|
|
* @param {ImageData} src 源数据。
|
|
* @returns {ImageData}
|
|
*/
|
|
function cropCenter(src) {
|
|
const cw = Math.floor(src.width * 0.6);
|
|
const ch = Math.floor(src.height * 0.6);
|
|
const ox = Math.floor((src.width - cw) / 2);
|
|
const oy = Math.floor((src.height - ch) / 2);
|
|
const c = document.createElement('canvas');
|
|
c.width = cw; c.height = ch;
|
|
const ctx = c.getContext('2d');
|
|
const temp = document.createElement('canvas');
|
|
temp.width = src.width; temp.height = src.height;
|
|
temp.getContext('2d').putImageData(src, 0, 0);
|
|
ctx.drawImage(temp, ox, oy, cw, ch, 0, 0, cw, ch);
|
|
return ctx.getImageData(0, 0, cw, ch);
|
|
}
|
|
|
|
/** 显示处理后的图片到 canvas。@param {ImageData} data */
|
|
function showResult(data) {
|
|
resultCanvas.width = data.width;
|
|
resultCanvas.height = data.height;
|
|
resultCanvas.getContext('2d').putImageData(data, 0, 0);
|
|
resultCanvas.style.display = 'block';
|
|
}
|
|
|
|
/** 添加日志。@param {string} msg @param {string} cls */
|
|
function addLog(msg, cls) {
|
|
const div = document.createElement('div');
|
|
div.className = 'result ' + cls;
|
|
div.textContent = msg;
|
|
logDiv.appendChild(div);
|
|
}
|
|
|
|
/** 原始尺寸识别。@returns {Promise<void>} */
|
|
async function testFull() {
|
|
cachedImageData = null;
|
|
diagDiv.style.display = 'block';
|
|
logDiv.innerHTML = '';
|
|
try {
|
|
const data = await getImageData();
|
|
addLog(`图片原始尺寸: ${data.width}x${data.height}`, 'info');
|
|
showResult(data);
|
|
const r = tryDecode(data, '原始尺寸');
|
|
addLog(r.msg, r.ok ? 'ok' : 'fail');
|
|
} catch (e) { addLog('错误: ' + e.message, 'fail'); }
|
|
}
|
|
|
|
/** 50% 缩放识别。@returns {Promise<void>} */
|
|
async function testHalf() {
|
|
try {
|
|
const data = await getImageData();
|
|
const half = scaleImageData(data, 0.5);
|
|
showResult(half);
|
|
addLog(`缩放至 50%: ${half.width}x${half.height}`, 'info');
|
|
const r = tryDecode(half, '50%缩放');
|
|
addLog(r.msg, r.ok ? 'ok' : 'fail');
|
|
} catch (e) { addLog('错误: ' + e.message, 'fail'); }
|
|
}
|
|
|
|
/** 25% 缩放识别。@returns {Promise<void>} */
|
|
async function testQuarter() {
|
|
try {
|
|
const data = await getImageData();
|
|
const quarter = scaleImageData(data, 0.25);
|
|
showResult(quarter);
|
|
addLog(`缩放至 25%: ${quarter.width}x${quarter.height}`, 'info');
|
|
const r = tryDecode(quarter, '25%缩放');
|
|
addLog(r.msg, r.ok ? 'ok' : 'fail');
|
|
} catch (e) { addLog('错误: ' + e.message, 'fail'); }
|
|
}
|
|
|
|
/** 中心裁剪识别。@returns {Promise<void>} */
|
|
async function testCenter() {
|
|
try {
|
|
const data = await getImageData();
|
|
const crop = cropCenter(data);
|
|
showResult(crop);
|
|
addLog(`中心裁剪: ${crop.width}x${crop.height}`, 'info');
|
|
const r = tryDecode(crop, '中心裁剪');
|
|
addLog(r.msg, r.ok ? 'ok' : 'fail');
|
|
} catch (e) { addLog('错误: ' + e.message, 'fail'); }
|
|
}
|
|
|
|
/** 全面诊断:依次尝试多种策略。@returns {Promise<void>} */
|
|
async function testAll() {
|
|
diagDiv.style.display = 'block';
|
|
logDiv.innerHTML = '';
|
|
try {
|
|
const data = await getImageData();
|
|
addLog(`图片原始尺寸: ${data.width}x${data.height} (${(data.width * data.height / 1000000).toFixed(1)}MP)`, 'info');
|
|
|
|
const tests = [
|
|
{ fn: () => tryDecode(data, '原始尺寸'), show: () => showResult(data) },
|
|
{ fn: () => tryDecode(scaleImageData(data, 0.5), '50%缩放'), show: () => showResult(scaleImageData(data, 0.5)) },
|
|
{ fn: () => tryDecode(scaleImageData(data, 0.25), '25%缩放'), show: () => showResult(scaleImageData(data, 0.25)) },
|
|
{ fn: () => tryDecode(scaleImageData(data, 0.125), '12.5%缩放'), show: () => showResult(scaleImageData(data, 0.125)) },
|
|
{ fn: () => tryDecode(cropCenter(data), '中心60%裁剪'), show: () => showResult(cropCenter(data)) },
|
|
{ fn: () => tryDecode(scaleImageData(cropCenter(data), 0.5), '中心裁剪+50%缩放'), show: () => showResult(scaleImageData(cropCenter(data), 0.5)) },
|
|
];
|
|
|
|
for (const t of tests) {
|
|
t.show();
|
|
const r = t.fn();
|
|
addLog(r.msg, r.ok ? 'ok' : 'fail');
|
|
if (r.ok) {
|
|
addLog('🎉 识别成功!内容: ' + r.data, 'ok');
|
|
break;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
addLog('错误: ' + e.message, 'fail');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|