83 lines
2.8 KiB
JavaScript
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;
|