Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
* @param {[number, number, number]} [options.diffColor=[255, 0, 0]] Color of different pixels in diff output.
* @param {[number, number, number]} [options.diffColorAlt=options.diffColor] Whether to detect dark on light differences between img1 and img2 and set an alternative color to differentiate between the two.
* @param {boolean} [options.diffMask=false] Draw the diff over a transparent background (a mask).
* @param {boolean} [options.checkerboard=true] Whether to blend semi-transparent pixels against a checkerboard pattern (true) or plain white (false) when comparing.
*
* @return {number} The number of mismatched pixels.
*/
Expand All @@ -24,6 +25,7 @@ export default function pixelmatch(img1, img2, output, width, height, options =
alpha = 0.1,
aaColor = [255, 255, 0],
diffColor = [255, 0, 0],
checkerboard = true,
includeAA, diffColorAlt, diffMask
} = options;

Expand Down Expand Up @@ -62,14 +64,14 @@ export default function pixelmatch(img1, img2, output, width, height, options =
// compare each pixel of one image against the other one
for (let i = 0, pos = 0; i < len; i++, pos += 4) {
// squared YUV distance between colors at this pixel position, negative if the img2 pixel is darker
const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos);
const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos, checkerboard);

// the color difference is above the threshold
if (Math.abs(delta) > maxDelta) {
const x = i % width;
const y = (i / width) | 0;
// check it's a real rendering difference or just anti-aliasing
const isExcludedAA = !includeAA && (antialiased(img1, x, y, width, height, a32, b32) || antialiased(img2, x, y, width, height, b32, a32));
const isExcludedAA = !includeAA && (antialiased(img1, x, y, width, height, a32, b32, checkerboard) || antialiased(img2, x, y, width, height, b32, a32, checkerboard));
if (isExcludedAA) {
// one of the pixels is anti-aliasing; draw as yellow and do not count as difference
// note that we do not include such pixels in a mask
Expand Down Expand Up @@ -113,8 +115,9 @@ function isPixelData(arr) {
* @param {number} height
* @param {Uint32Array} a32
* @param {Uint32Array} b32
* @param {boolean} checkerboard
*/
function antialiased(img, x1, y1, width, height, a32, b32) {
function antialiased(img, x1, y1, width, height, a32, b32, checkerboard) {
const x0 = Math.max(x1 - 1, 0);
const y0 = Math.max(y1 - 1, 0);
const x2 = Math.min(x1 + 1, width - 1);
Expand All @@ -139,7 +142,7 @@ function antialiased(img, x1, y1, width, height, a32, b32) {
if (x === x1 && y === y1) continue;

// brightness delta between the center pixel and adjacent one
const delta = brightnessDelta(img, pos4, (y * width + x) * 4, cr, cg, cb, ca);
const delta = brightnessDelta(img, pos4, (y * width + x) * 4, cr, cg, cb, ca, checkerboard);

// count the number of equal, darker and brighter adjacent pixels
if (delta === 0) {
Expand Down Expand Up @@ -206,8 +209,9 @@ function hasManySiblings(img, x1, y1, width, height) {
* @param {Uint8Array | Uint8ClampedArray} img2
* @param {number} k
* @param {number} m
* @param {boolean} checkerboard
*/
function colorDelta(img1, img2, k, m) {
function colorDelta(img1, img2, k, m, checkerboard) {
const r1 = img1[k];
const g1 = img1[k + 1];
const b1 = img1[k + 2];
Expand All @@ -223,9 +227,12 @@ function colorDelta(img1, img2, k, m) {
const da = a1 - a2;

if (a1 < 255 || a2 < 255) { // blend pixels with background
const rb = 48 + 159 * (k % 2);
const gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
const bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
let rb = 255, gb = 255, bb = 255;
if (checkerboard) {
rb = 48 + 159 * (k % 2);
gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
}
dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
db = (b1 * a1 - b2 * a2 - bb * da) / 255;
Expand All @@ -251,8 +258,9 @@ function colorDelta(img1, img2, k, m) {
* @param {number} g1
* @param {number} b1
* @param {number} a1
* @param {boolean} checkerboard
*/
function brightnessDelta(img, k, m, r1, g1, b1, a1) {
function brightnessDelta(img, k, m, r1, g1, b1, a1, checkerboard) {
const r2 = img[m];
const g2 = img[m + 1];
const b2 = img[m + 2];
Expand All @@ -266,9 +274,12 @@ function brightnessDelta(img, k, m, r1, g1, b1, a1) {
if (!dr && !dg && !db && !da) return 0;

if (a1 < 255 || a2 < 255) {
const rb = 48 + 159 * (k % 2);
const gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
const bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
let rb = 255, gb = 255, bb = 255;
if (checkerboard) {
rb = 48 + 159 * (k % 2);
gb = 48 + 159 * ((k / 1.618033988749895 | 0) % 2);
bb = 48 + 159 * ((k / 2.618033988749895 | 0) % 2);
}
dr = (r1 * a1 - r2 * a2 - rb * da) / 255;
dg = (g1 * a1 - g2 * a2 - gb * da) / 255;
db = (b1 * a1 - b2 * a2 - bb * da) / 255;
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pixelmatch",
"version": "7.1.1",
"version": "7.2.0",
"type": "module",
"description": "The smallest and fastest pixel-level image comparison library.",
"main": "index.js",
Expand Down
8 changes: 8 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ diffTest('6a', '6a', '6empty', {threshold: 0}, 0);
diffTest('7a', '7b', '7diff', {diffColorAlt: [0, 255, 0]}, 2448);
diffTest('8a', '5b', '8diff', options, 32896);

test('checkerboard: false blends semi-transparent pixels against white', () => {
// These two pixels are visually identical composited on white but differ on a dark checkerboard
const img1 = new Uint8Array([0, 0, 0, 128]); // 50% transparent black
const img2 = new Uint8Array([127, 127, 127, 255]); // opaque gray
assert.equal(match(img1, img2, null, 1, 1, {checkerboard: false}), 0);
assert.equal(match(img1, img2, null, 1, 1), 1);
});

test('throws error if image sizes do not match', () => {
assert.throws(() => match(new Uint8Array(8), new Uint8Array(9), null, 2, 1), 'Image sizes do not match');
});
Expand Down
Loading