Skip to content

Commit b16a2be

Browse files
authored
compare image data (#1807)
* re-encode image data * adopt canvas.toDataURL * compare image data * parallelize getImageData * statistical image comparison
1 parent c3f29b9 commit b16a2be

File tree

3 files changed

+39
-9
lines changed

3 files changed

+39
-9
lines changed

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@rollup/plugin-node-resolve": "^15.0.1",
5353
"@rollup/plugin-terser": "^0.4.0",
5454
"@types/d3": "^7.4.0",
55+
"@types/node": "^20.5.0",
5556
"@typescript-eslint/eslint-plugin": "^6.0.0",
5657
"@typescript-eslint/parser": "^6.0.0",
5758
"canvas": "^2.0.0",

test/plot.js

+33-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {promises as fs} from "fs";
2+
import {createCanvas, loadImage} from "canvas";
3+
import {max, mean, quantile} from "d3";
24
import * as path from "path";
35
import beautify from "js-beautify";
46
import assert from "./assert.js";
@@ -38,10 +40,8 @@ for (const [name, plot] of Object.entries(plots)) {
3840
}
3941

4042
// node-canvas won’t produce the same output on different architectures, so
41-
// until we have a way to normalize the output, we need to ignore the
42-
// generated image data during comparison. But you can still review the
43-
// generated output visually and hopefully it’ll be correct.
44-
const equal = process.env.CI === "true" ? stripImageData(actual) === stripImageData(expected) : actual === expected;
43+
// we parse and compare pixel values instead of the encoded output.
44+
const equal = stripImages(actual) === stripImages(expected) && (await compareImages(actual, expected));
4545

4646
if (equal) {
4747
if (process.env.CI !== "true") {
@@ -108,9 +108,33 @@ function reindexClip(root) {
108108
}
109109
}
110110

111-
function stripImageData(string) {
112-
return string.replace(
113-
/data:image\/png;base64,[^"]+/g,
114-
"data:image/svg+xml,%3Csvg width='15' height='15' viewBox='0 0 20 20' style='background-color: white' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h10v10H0zm10 10h10v10H10z' fill='%23f4f4f4' fill-rule='evenodd'/%3E%3C/svg%3E"
115-
);
111+
const imageRe = /data:image\/png;base64,[^"]+/g;
112+
113+
function stripImages(string) {
114+
return string.replace(imageRe, "<replaced>");
115+
}
116+
117+
async function compareImages(a, b) {
118+
const reA = new RegExp(imageRe, "g");
119+
const reB = new RegExp(imageRe, "g");
120+
let matchA;
121+
let matchB;
122+
while (((matchA = reA.exec(a)), (matchB = reB.exec(b)))) {
123+
const [imageA, imageB] = await Promise.all([getImageData(matchA[0]), getImageData(matchB[0])]);
124+
const {width, height} = imageA;
125+
if (width !== imageB.width || height !== imageB.height) return false;
126+
const E = imageA.data.map((a, i) => Math.abs(a - imageB.data[i]));
127+
if (!(quantile(E, 0.95) <= 1)) return false; // at least 95% with almost no error
128+
if (!(mean(E) < 0.1)) return false; // no more than 0.1 average error
129+
if (!(max(E) < 10)) return false; // no more than 10 maximum error
130+
}
131+
return true;
132+
}
133+
134+
async function getImageData(url) {
135+
const image = await loadImage(url);
136+
const canvas = createCanvas(image.width, image.height);
137+
const context = canvas.getContext("2d");
138+
context.drawImage(image, 0, 0);
139+
return context.getImageData(0, 0, image.width, image.height);
116140
}

yarn.lock

+5
Original file line numberDiff line numberDiff line change
@@ -820,6 +820,11 @@
820820
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
821821
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
822822

823+
"@types/node@^20.5.0":
824+
version "20.5.0"
825+
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.0.tgz#7fc8636d5f1aaa3b21e6245e97d56b7f56702313"
826+
integrity sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==
827+
823828
824829
version "1.20.2"
825830
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"

0 commit comments

Comments
 (0)