diff --git a/index.js b/index.js index 2cc83d9..1502e94 100644 --- a/index.js +++ b/index.js @@ -17,6 +17,7 @@ * @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. * @param {number} [options.windowSize=Infinity] If finite, return the maximum number of diff pixels found in any N×N sliding window instead of the total diff count. + * @param {Uint8Array | Uint8ClampedArray | void} [options.ignoreMask] Image data mask where any non-zero color value skips the corresponding img1 and img2 pixels being checked. * * @return {number} The number of mismatched pixels (or the maximum per-window count if windowSize is finite). */ @@ -28,7 +29,8 @@ export default function pixelmatch(img1, img2, output, width, height, options = diffColor = [255, 0, 0], checkerboard = true, windowSize = Infinity, - includeAA, diffColorAlt, diffMask + includeAA, diffColorAlt, diffMask, + ignoreMask } = options; if (!isPixelData(img1) || !isPixelData(img2) || (output && !isPixelData(output))) @@ -39,14 +41,19 @@ export default function pixelmatch(img1, img2, output, width, height, options = if (img1.length !== width * height * 4) throw new Error(`Image data size does not match width/height. Expecting ${width * height * 4}. Got ${img1.length}`); + // check if ignored mask is identical + if (ignoreMask && ignoreMask.length !== width * height) + throw new Error(`Ignore mask size does not match image width/height. Expecting ${width * height}. Got ${ignoreMask.length}`); + // check if images are identical const len = width * height; const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len); const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len); + let identical = true; for (let i = 0; i < len; i++) { - if (a32[i] !== b32[i]) { identical = false; break; } + if (a32[i] !== b32[i] && (!ignoreMask || ignoreMask[i] <= 0)) { identical = false; break; } } if (identical) { // fast path if identical if (output && !diffMask) { @@ -75,7 +82,7 @@ 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) { // whether the HyAB OKLab distance exceeds the threshold: 0 if not, ±1 if yes (negative if img2 pixel is darker) - const delta = a32[i] === b32[i] ? 0 : colorDelta(img1, img2, pos, pos, checkerboard, maxDelta); + const delta = (a32[i] === b32[i] || (ignoreMask && ignoreMask[i] > 0)) ? 0 : colorDelta(img1, img2, pos, pos, checkerboard, maxDelta); // the color difference is above the threshold if (delta) { diff --git a/test/fixtures/10a.png b/test/fixtures/10a.png new file mode 100644 index 0000000..7ed33d1 Binary files /dev/null and b/test/fixtures/10a.png differ diff --git a/test/fixtures/10b.png b/test/fixtures/10b.png new file mode 100644 index 0000000..6d2bd46 Binary files /dev/null and b/test/fixtures/10b.png differ diff --git a/test/fixtures/10diff.png b/test/fixtures/10diff.png new file mode 100644 index 0000000..eb2f6d3 Binary files /dev/null and b/test/fixtures/10diff.png differ diff --git a/test/fixtures/9a.png b/test/fixtures/9a.png new file mode 100644 index 0000000..7ed33d1 Binary files /dev/null and b/test/fixtures/9a.png differ diff --git a/test/fixtures/9b.png b/test/fixtures/9b.png new file mode 100644 index 0000000..357790a Binary files /dev/null and b/test/fixtures/9b.png differ diff --git a/test/fixtures/9diff.png b/test/fixtures/9diff.png new file mode 100644 index 0000000..b004c2f Binary files /dev/null and b/test/fixtures/9diff.png differ diff --git a/test/test.js b/test/test.js index efb3f27..1d095c2 100644 --- a/test/test.js +++ b/test/test.js @@ -25,6 +25,16 @@ diffTest('6a', '6b', '6diff', options, 51); diffTest('6a', '6a', '6empty', {threshold: 0}, 0); diffTest('7a', '7b', '7diff', {diffColorAlt: [0, 255, 0]}, 2440); diffTest('8a', '5b', '8diff', options, 32896); +// Test diff for parts not within an ignore mask +diffTest('9a', '9b', '9diff', { + threshold: 0.05, + ignoreMask: true +}, 12422); +// Test fast path with a diff within an ignore mask +diffTest('10a', '10b', '10diff', { + threshold: 0.05, + ignoreMask: true +}, 0); test('OKLab metric uses Lr toe correction for near-black differences', () => { const pixel = gray => new Uint8Array([gray, gray, gray, 255]); @@ -164,6 +174,18 @@ function diffTest(imgPath1, imgPath2, diffPath, options, expectedMismatch) { const {width, height} = img1; const diff = new PNG({width, height}); + if (options?.ignoreMask) { + const ignoreMask = new Uint8Array(width * height); + for (let index = 0; index < ignoreMask.length; index++) { + if (index > ignoreMask.length / 2) { + ignoreMask[index] = 0; + } else { + ignoreMask[index] = 1; + } + } + options.ignoreMask = ignoreMask; + } + const mismatch = match(img1.data, img2.data, diff.data, width, height, options); const mismatch2 = match(img1.data, img2.data, null, width, height, options);