Files
drawlogo/audit-posters.html

188 lines
8.3 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>海报素材像素审计(canvas 非透明像素 + 原始/下载逐像素对比)</title>
<style>
body{font-family:monospace;font-size:12px;margin:8px;background:#111;color:#eee}
.src{margin:6px 0;padding:6px;border:1px solid #333;border-radius:4px}
.src b{color:#7ee787}.src .bad{color:#ff7b72}.src .ok{color:#7ee787}
h1{font-size:16px}h2{font-size:14px}
</style>
</head>
<body>
<h1>海报素材像素审计</h1>
<p>支持参数:<code>?only=源1,源2</code> 定向审计;<code>?max=N</code> 每源最多素材数;<code>?size=N</code> 渲染尺寸;<code>?compare=1</code> 开启「原始 vs 下载」逐像素对比。</p>
<div id="out"><p>加载中…</p></div>
<script src="js/data/poster-sources.js?v=20260823e"></script>
<script src="js/data/poster-builtin.js?v=20260823e"></script>
<script src="js/core/posterlib.js?v=20260823e"></script>
<script>
/** 渲染 svg 字符串 → 像素统计 {blank, alphaSum, data, width, height} */
function svgToCanvas(svgStr, viewBox, size) {
return new Promise(resolve => {
const img = new Image();
img.onload = () => {
const c = document.createElement('canvas');
c.width = size; c.height = size;
const ctx = c.getContext('2d');
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.fillRect(0,0,size,size);
ctx.drawImage(img, 0, 0, size, size);
let data;
try { data = ctx.getImageData(0,0,size,size).data; }
catch(e){ resolve({blank:false, err:'getImageData受限'}); return; }
let alphaSum = 0;
for (let i = 3; i < data.length; i += 4) alphaSum += data[i];
resolve({ blank: alphaSum === 0, alphaSum, data });
};
img.onerror = () => resolve({ blank: false, err: 'SVG加载失败' });
img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr);
});
}
/** 位图(PNG/JPG)→ 像素统计 */
function imgToCanvas(src, size) {
return new Promise(resolve => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
const c = document.createElement('canvas');
c.width = size; c.height = size;
const ctx = c.getContext('2d');
ctx.fillStyle = 'rgba(0,0,0,0)';
ctx.fillRect(0,0,size,size);
ctx.drawImage(img, 0, 0, size, size);
let data;
try { data = ctx.getImageData(0,0,size,size).data; }
catch(e){ resolve({blank:false, err:'getImageData受限', width:img.naturalWidth, height:img.naturalHeight}); return; }
let alphaSum = 0;
for (let i = 3; i < data.length; i += 4) alphaSum += data[i];
resolve({ blank: alphaSum === 0, alphaSum, data, width: img.naturalWidth, height: img.naturalHeight });
};
img.onerror = () => resolve({ blank: false, err: '位图加载失败' });
img.src = src;
});
}
/** 两张像素数据逐像素对比,返回差异率(0~1) */
function pixelDiff(a, b) {
if (!a || !b || a.length !== b.length) return 1;
let diff = 0, total = a.length / 4;
for (let i = 0; i < a.length; i += 4) {
if (Math.abs(a[i]-b[i]) + Math.abs(a[i+1]-b[i+1]) + Math.abs(a[i+2]-b[i+2]) + Math.abs(a[i+3]-b[i+3]) > 8) diff++;
}
return total ? diff / total : 0;
}
/** 由源配置重建原始 CDN URL(用于「原始 vs 下载」对比) */
function originalUrl(src, item) {
const name = item.name || String(item.id).split(':')[1];
const ext = (item.mime === 'image/jpeg') ? '.jpg' : (item.mime === 'image/png' ? '.png' : '.svg');
if (src.gh) {
const repo = src.gh + '@' + (src.ghBranch || 'master');
const dir = (src.dirs || [])[0] || '';
return `https://cdn.jsdelivr.net/gh/${repo}/${dir}/${name}${ext}`;
}
if (src.npm) {
const dir = (src.dirs || [])[0] || '';
return `https://unpkg.com/${src.npm}/${dir}/${name}${ext}`;
}
return null;
}
function wrapSvg(body, viewBox) {
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox || '0 0 24 24'}">${body}</svg>`;
}
async function run() {
const out = document.getElementById('out');
const qs = new URLSearchParams(location.search);
const only = qs.get('only') ? qs.get('only').split(',').map(s => s.trim()) : null;
const max = parseInt(qs.get('max') || '300', 10);
const size = parseInt(qs.get('size') || '64', 10);
const compare = qs.get('compare') === '1';
const srcMap = {};
for (const s of (window.HGB_PosterSources || [])) srcMap[s.id] = s;
const builtin = window.HGB_PosterBuiltin || { sources: {} };
const ids = Object.keys(builtin.sources || {});
if (!ids.length) { out.innerHTML = '<p>⚠️ 暂无海报素材源(poster-builtin.js 为空),请先运行 node scripts/build-posters.js。</p>'; return; }
let totalItems = 0, totalBlank = 0, totalErr = 0, totalDiff = 0;
const rows = [];
for (const sid of ids) {
if (only && !only.includes(sid)) continue;
let data = null;
try {
data = await HGB_PosterLib.loadSource(sid);
} catch (e) { totalErr++; rows.push({ sid, error: '加载失败 ' + e.message }); continue; }
if (!data || !Array.isArray(data.items)) { totalErr++; rows.push({ sid, error: '无 items 数组' }); continue; }
const items = data.items.slice(0, max);
const src = srcMap[sid] || {};
let blank = 0, diffBad = 0;
const samples = [];
for (const it of items) {
totalItems++;
// 字体无像素可审(文件完整性已由构建端 sha256/bytes 校验),跳过渲染
if (src.type === 'font') continue;
const viewBox = it.viewBox || (it.width && it.height ? `0 0 ${it.width} ${it.height}` : '0 0 24 24');
let r;
if (it.svg) {
r = await svgToCanvas(wrapSvg(it.svg, viewBox), viewBox, size);
} else if (it.file) {
r = await imgToCanvas('js/data/' + it.file, size);
// 分辨率参数校验:渲染出的原始尺寸需与构建记录一致(防止下载损坏/错位)
if (!r.err && it.width && (r.width !== it.width || r.height !== it.height)) {
r.resMismatch = true;
}
} else {
// 配色 JSON / 字体:无像素,仅结构完整性校验
continue;
}
if (r.blank) { blank++; totalBlank++; if (samples.length < 12) samples.push(it.id + (r.err ? '[' + r.err + ']' : '')); }
if (r.resMismatch) { if (samples.length < 12) samples.push(it.id + '[分辨率不符 ' + r.width + 'x' + r.height + '≠' + it.width + 'x' + it.height + ']'); }
// 「原始 vs 下载」逐像素对比:重新拉取 CDN 原始文件渲染,与下载数据比差异率
// 注:transform 源(如 heropatterns JS 模板 → 静态 SVG)为有意格式转换,无 1:1 原始文件可对比,跳过
if (compare && it.svg && !src.transform) {
const orig = originalUrl(src, it);
if (orig) {
try {
const raw = await (await fetch(orig)).text();
if (!/<svg[\s>]/i.test(raw)) continue; // 404/错误页等非 SVG 响应,跳过
const body = raw.replace(/[\s\S]*?<svg[^>]*>/, '').replace(/<\/svg>[\s\S]*/, '');
const ro = await svgToCanvas(wrapSvg(body, viewBox), viewBox, size);
if (ro.data && r.data) {
const d = pixelDiff(r.data, ro.data);
if (d > 0.01) { diffBad++; totalDiff++; if (samples.length < 12) samples.push(it.id + '[差异率 ' + (d * 100).toFixed(2) + '%]'); }
}
} catch (e) { /* 原始拉取失败忽略(网络/CDN) */ }
}
}
}
rows.push({ sid, total: items.length, blank, diffBad, samples });
out.innerHTML = '<p>进度 ' + ids.indexOf(sid) + 1 + '/' + ids.length + ' …</p>';
}
let html = '<h2>扫描 ' + totalItems.toLocaleString() + ' 个,空白 ' + totalBlank.toLocaleString() + ',失败 ' + totalErr + (compare ? ',像素差异异常 ' + totalDiff : '') + '</h2>';
rows.sort((a, b) => b.blank - a.blank).forEach(r => {
if (r.error) html += '<div class="src">✗ ' + r.sid + ':' + r.error + '</div>';
else html += '<div class="src"><b>' + r.sid + '</b>:空白 <span class="' + (r.blank ? 'bad' : 'ok') + '">' + r.blank + '/' + r.total + '</span>' +
(r.diffBad ? ' · 像素差异异常 <span class="bad">' + r.diffBad + '</span>' : '') +
(r.samples.length ? '<br>' + r.samples.join('<br>') : '') + '</div>';
});
out.innerHTML = html;
document.title = 'POSTER-AUDIT-DONE ' + totalItems + '/' + totalBlank + '/' + totalErr;
}
run();
</script>
</body>
</html>