cefense/scripts/visual-compare.mjs
2026-08-18 19:38:47 +05:30

83 lines
2.8 KiB
JavaScript

import fs from "node:fs/promises";
import path from "node:path";
import jpeg from "jpeg-js";
import pixelmatch from "pixelmatch";
import { PNG } from "pngjs";
const root = path.resolve(process.argv[2] ?? "docs/visual-parity");
const maxDiffRatio = Number(process.env.VISUAL_MAX_DIFF_RATIO ?? "0.003");
const threshold = Number(process.env.VISUAL_PIXEL_THRESHOLD ?? "0.1");
const writeDiffs = process.env.VISUAL_WRITE_DIFFS === "1";
async function walk(directory) {
const entries = await fs.readdir(directory, { withFileTypes: true });
const files = await Promise.all(
entries.map((entry) => {
const target = path.join(directory, entry.name);
return entry.isDirectory() ? walk(target) : [target];
}),
);
return files.flat();
}
const files = await walk(root);
const sources = files.filter(
(file) => path.basename(file).startsWith("source-") && /\.(?:jpe?g|png)$/i.test(file),
);
if (sources.length === 0) {
throw new Error(`No source baseline images found under ${root}`);
}
let failed = false;
const results = [];
for (const sourcePath of sources.sort()) {
const portPath = path.join(path.dirname(sourcePath), path.basename(sourcePath).replace(/^source-/, "port-"));
const [sourceBytes, portBytes] = await Promise.all([fs.readFile(sourcePath), fs.readFile(portPath)]);
const decode = (bytes) =>
bytes[0] === 0xff && bytes[1] === 0xd8
? jpeg.decode(bytes, { useTArray: true })
: PNG.sync.read(bytes);
const source = decode(sourceBytes);
const port = decode(portBytes);
if (source.width !== port.width || source.height !== port.height) {
failed = true;
results.push({
pair: path.relative(root, sourcePath).replace(/^source-/, ""),
source: `${source.width}x${source.height}`,
port: `${port.width}x${port.height}`,
error: "dimension mismatch",
});
continue;
}
const pixelCount = source.width * source.height;
const diff = writeDiffs ? new PNG({ width: source.width, height: source.height }) : null;
const differentPixels = pixelmatch(source.data, port.data, diff?.data ?? null, source.width, source.height, {
threshold,
includeAA: false,
});
const diffRatio = differentPixels / pixelCount;
if (writeDiffs && differentPixels > 0 && diff) {
const diffPath = path.join(
path.dirname(sourcePath),
path.basename(sourcePath).replace(/^source-/, "diff-").replace(/\.jpe?g$/i, ".png"),
);
await fs.writeFile(diffPath, PNG.sync.write(diff));
}
if (diffRatio > maxDiffRatio) failed = true;
results.push({
pair: path.relative(root, sourcePath).replace(/^source-/, ""),
differentPixels,
pixelCount,
diffRatio: Number(diffRatio.toFixed(8)),
pass: diffRatio <= maxDiffRatio,
});
}
console.table(results);
console.log(`Compared ${results.length} source/port screenshot pairs (max diff ratio ${maxDiffRatio}).`);
if (failed) process.exitCode = 1;