looks-same
Version:
Pure node.js library for comparing PNG-images, taking into account human color perception.
203 lines (161 loc) • 6.28 kB
JavaScript
;
const parseColor = require('parse-color');
const img = require('./image');
const buffer = require('./img-buffer');
const DiffArea = require('./diff-area');
const DiffClusters = require('./diff-clusters');
const validators = require('./validators');
const areColorsSame = require('./same-colors');
const {PNG: {RGBA_CHANNELS}} = require('./constants');
exports.readImgCb = async ({source, ...opts}) => {
const readFunc = Buffer.isBuffer(source) ? img.fromBuffer : img.fromFile;
const image = await readFunc(source, opts);
await image.init();
return image;
};
exports.readBufferCb = ({source, ...opts}) => {
const readFunc = Buffer.isBuffer(source) ? buffer.create : buffer.fromFile;
return readFunc(source, opts);
};
exports.readPair = async (first, second, readCb = exports.readImgCb) => {
const [firstImg, secondImg] = await Promise.all([first, second].map(readCb));
return {first: firstImg, second: secondImg};
};
const getDiffClusters = (diffClusters, diffArea, {shouldCluster}) => {
return shouldCluster ? diffClusters.clusters : [diffArea.area];
};
exports.getDiffPixelsCoords = async (img1, img2, predicate, opts = {}) => {
const stopOnFirstFail = Object.hasOwn(opts, 'stopOnFirstFail') ? opts.stopOnFirstFail : false;
const width = Math.min(img1.width, img2.width);
const height = Math.min(img1.height, img2.height);
const diffArea = new DiffArea();
const diffClusters = new DiffClusters(opts.clustersSize);
return new Promise((resolve) => {
const processRow = (y) => {
setImmediate(() => {
for (let x = 0; x < width; x++) {
const color1 = img1.getPixel(x, y);
const color2 = img2.getPixel(x, y);
const result = predicate({
color1, color2,
img1, img2,
x, y,
width, height
});
if (!result) {
const {x: actX, y: actY} = img1.getActualCoord(x, y);
diffArea.update(actX, actY);
if (opts.shouldCluster) {
diffClusters.update(actX, actY);
}
if (stopOnFirstFail) {
return resolve({diffArea, diffClusters: getDiffClusters(diffClusters, diffArea, opts)});
}
}
}
y++;
if (y < height) {
processRow(y);
} else {
resolve({diffArea, diffClusters: getDiffClusters(diffClusters, diffArea, opts)});
}
});
};
processRow(0);
});
};
exports.formatImages = (img1, img2) => {
validators.validateImages(img1, img2);
return [img1, img2].map((i) => {
return i !== null && typeof i === 'object' && !Buffer.isBuffer(i) ? i : {source: i, boundingBox: null};
});
};
exports.areBuffersEqual = (img1, img2) => {
if (img1.boundingBox || img2.boundingBox) {
return false;
}
return img1.buffer.equals(img2.buffer);
};
exports.parseColorString = (str) => {
const parsed = parseColor(str || '#ff00ff');
return {
R: parsed.rgb[0],
G: parsed.rgb[1],
B: parsed.rgb[2]
};
};
exports.calcDiffImage = async (img1, img2, comparator, {highlightColor, shouldCluster, clustersSize}) => {
const diffColor = exports.parseColorString(highlightColor);
const minHeight = Math.min(img1.height, img2.height);
const minWidth = Math.min(img1.width, img2.width);
const maxHeight = Math.max(img1.height, img2.height);
const maxWidth = Math.max(img1.width, img2.width);
const totalPixels = maxHeight * maxWidth;
const metaInfo = {refImg: {size: {width: img1.width, height: img1.height}}};
const diffBuffer = Buffer.allocUnsafe(maxHeight * maxWidth * RGBA_CHANNELS);
const diffArea = new DiffArea();
const diffClusters = new DiffClusters(clustersSize);
let differentPixels = 0;
let diffBufferPos = 0;
const markDiff = (x, y) => {
diffBuffer[diffBufferPos++] = diffColor.R;
diffBuffer[diffBufferPos++] = diffColor.G;
diffBuffer[diffBufferPos++] = diffColor.B;
diffBuffer[diffBufferPos++] = 0xff;
differentPixels++;
diffArea.update(x, y);
if (shouldCluster) {
diffClusters.update(x, y);
}
};
for (let y = 0; y < maxHeight; y++) {
for (let x = 0; x < maxWidth; x++) {
if (y >= minHeight || x >= minWidth) {
markDiff(x, y); // Out of bounds pixels considered as diff
continue;
}
const color1 = img1.getPixel(x, y);
const color2 = img2.getPixel(x, y);
const areSame = areColorsSame({color1, color2}) || comparator({
img1,
img2,
x,
y,
color1,
color2,
width: maxWidth,
height: maxHeight,
minWidth,
minHeight
});
if (areSame) {
diffBuffer[diffBufferPos++] = color2.R;
diffBuffer[diffBufferPos++] = color2.G;
diffBuffer[diffBufferPos++] = color2.B;
diffBuffer[diffBufferPos++] = 0xff;
} else {
markDiff(x, y);
}
}
// eslint-disable-next-line no-bitwise
if (!(y & 0xff)) { // Release event queue every 256 rows
await new Promise(setImmediate);
}
}
if (diffBufferPos !== diffBuffer.byteLength) {
throw new Error("Couldn't build diff image");
}
let diffImage = null;
if (differentPixels) {
diffImage = await img.fromBuffer(diffBuffer, {rgb: {width: maxWidth, height: maxHeight}});
}
return {
equal: !differentPixels,
metaInfo,
diffImage,
differentPixels,
totalPixels,
diffBounds: diffArea.area,
diffClusters: getDiffClusters(diffClusters, diffArea, {shouldCluster})
};
};