/** * 无依赖 PNG 像素差异对比(8 位、非隔行、RGB/RGBA)。 * * 用途:验证 CSS 重构(如 token 化)是否改变了实际渲染。 * * 用法:node scripts/pngdiff.mjs [channelThreshold] */ import { readFileSync } from 'node:fs' import { inflateSync } from 'node:zlib' function decodePng(buf) { if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('不是 PNG') let pos = 8 let width = 0 let height = 0 let bitDepth = 0 let colorType = 0 let interlace = 0 const idat = [] while (pos < buf.length) { const len = buf.readUInt32BE(pos) const type = buf.toString('ascii', pos + 4, pos + 8) const data = buf.subarray(pos + 8, pos + 8 + len) if (type === 'IHDR') { width = data.readUInt32BE(0) height = data.readUInt32BE(4) bitDepth = data[8] colorType = data[9] interlace = data[12] } else if (type === 'IDAT') { idat.push(data) } else if (type === 'IEND') { break } pos += 12 + len } if (bitDepth !== 8) throw new Error(`仅支持 8 位深度,实际 ${bitDepth}`) if (interlace !== 0) throw new Error('不支持隔行 PNG') const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : 0 if (!channels) throw new Error(`不支持的颜色类型 ${colorType}`) const raw = inflateSync(Buffer.concat(idat)) const stride = width * channels const pixels = Buffer.alloc(height * stride) for (let y = 0; y < height; y++) { const filter = raw[y * (stride + 1)] const line = raw.subarray(y * (stride + 1) + 1, y * (stride + 1) + 1 + stride) const prev = y > 0 ? pixels.subarray((y - 1) * stride, y * stride) : null const out = pixels.subarray(y * stride, (y + 1) * stride) for (let i = 0; i < stride; i++) { const a = i >= channels ? out[i - channels] : 0 const b = prev ? prev[i] : 0 const c = prev && i >= channels ? prev[i - channels] : 0 let v = line[i] switch (filter) { case 0: break case 1: v = (v + a) & 0xff break case 2: v = (v + b) & 0xff break case 3: v = (v + ((a + b) >> 1)) & 0xff break case 4: { const p = a + b - c const pa = Math.abs(p - a) const pb = Math.abs(p - b) const pc = Math.abs(p - c) const pr = pa <= pb && pa <= pc ? a : pb <= pc ? b : c v = (v + pr) & 0xff break } default: throw new Error(`未知过滤器 ${filter}`) } out[i] = v } } return { width, height, channels, pixels } } const [, , beforePath, afterPath, thresholdArg] = process.argv const threshold = Number(thresholdArg ?? 0) const a = decodePng(readFileSync(beforePath)) const b = decodePng(readFileSync(afterPath)) if (a.width !== b.width || a.height !== b.height) { console.log(`尺寸不同:${a.width}x${a.height} vs ${b.width}x${b.height}`) const minW = Math.min(a.width, b.width) const minH = Math.min(a.height, b.height) console.log(`仅比较重叠区域 ${minW}x${minH}`) } const w = Math.min(a.width, b.width) const h = Math.min(a.height, b.height) let changed = 0 let maxDelta = 0 let sumDelta = 0 const perChannel = [0, 0, 0, 0] for (let y = 0; y < h; y++) { for (let x = 0; x < w; x++) { let diff = false for (let c = 0; c < 3; c++) { const ia = y * a.width * a.channels + x * a.channels + c const ib = y * b.width * b.channels + x * b.channels + c const d = Math.abs(a.pixels[ia] - b.pixels[ib]) if (d > threshold) diff = true if (d > 0) perChannel[c] += 1 if (d > maxDelta) maxDelta = d sumDelta += d } if (diff) changed++ } } const total = w * h console.log(`总像素 ${total}`) console.log(`变化像素 ${changed} (${((changed / total) * 100).toFixed(3)}%)`) console.log(`最大通道差 ${maxDelta}`) console.log(`平均通道差 ${(sumDelta / (total * 3)).toFixed(4)}`) console.log(`通道变化数 R=${perChannel[0]} G=${perChannel[1]} B=${perChannel[2]}`)