Skip to content

Commit

Permalink
Handle “undefined” as passed option values (#146)
Browse files Browse the repository at this point in the history
Allow the consumers to pass “undefined” as options values instead of misbehaving.

Today if the consumer passes “undefined”, we ignore the default options and allow the “undefined” value to fall through to the rest of the program, resulting in undefined behavior. For an example, the maxDelta becomes NaN if threshold: undefined is provided.

After this change we ensure that “undefined” as options values is converted into a default value.

It is customary in JavaScript, especially in options objects, to treat a missing key and a key with value of undefined the same way. In particular, it is a distinction that is easily introduced when the customer receives a value from somewhere else and wants to continue to support default values.
  • Loading branch information
denis-sokolov authored Feb 10, 2025
1 parent cce03e1 commit bf7001f
Show file tree
Hide file tree
Showing 3 changed files with 19 additions and 21 deletions.
39 changes: 18 additions & 21 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@

const defaultOptions = {
threshold: 0.1, // matching threshold (0 to 1); smaller is more sensitive
includeAA: false, // whether to skip anti-aliasing detection
alpha: 0.1, // opacity of original image in diff output
aaColor: [255, 255, 0], // color of anti-aliased pixels in diff output
diffColor: [255, 0, 0], // color of different pixels in diff output
diffColorAlt: null, // whether to detect dark on light differences between img1 and img2 and set an alternative color to differentiate between the two
diffMask: false // draw the diff over a transparent background (a mask)
};

export default function pixelmatch(img1, img2, output, width, height, options) {
const {
threshold = 0.1, // matching threshold (0 to 1); smaller is more sensitive
includeAA = false, // whether to skip anti-aliasing detection
alpha = 0.1, // opacity of original image in diff output
aaColor = [255, 255, 0], // color of anti-aliased pixels in diff output
diffColor = [255, 0, 0], // color of different pixels in diff output
diffColorAlt = null, // whether to detect dark on light differences between img1 and img2 and set an alternative color to differentiate between the two
diffMask = false // draw the diff over a transparent background (a mask)
} = options;

if (!isPixelData(img1) || !isPixelData(img2) || (output && !isPixelData(output)))
throw new Error('Image data: Uint8Array, Uint8ClampedArray or Buffer expected.');
Expand All @@ -19,8 +18,6 @@ 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.');

options = Object.assign({}, defaultOptions, options);

// check if images are identical
const len = width * height;
const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len);
Expand All @@ -31,18 +28,18 @@ export default function pixelmatch(img1, img2, output, width, height, options) {
if (a32[i] !== b32[i]) { identical = false; break; }
}
if (identical) { // fast path if identical
if (output && !options.diffMask) {
for (let i = 0; i < len; i++) drawGrayPixel(img1, 4 * i, options.alpha, output);
if (output && !diffMask) {
for (let i = 0; i < len; i++) drawGrayPixel(img1, 4 * i, alpha, output);
}
return 0;
}

// maximum acceptable square distance between two colors;
// 35215 is the maximum possible value for the YIQ difference metric
const maxDelta = 35215 * options.threshold * options.threshold;
const [aaR, aaG, aaB] = options.aaColor;
const [diffR, diffG, diffB] = options.diffColor;
const [altR, altG, altB] = options.diffColorAlt || options.diffColor;
const maxDelta = 35215 * threshold * threshold;
const [aaR, aaG, aaB] = aaColor;
const [diffR, diffG, diffB] = diffColor;
const [altR, altG, altB] = diffColorAlt || diffColor;
let diff = 0;

// compare each pixel of one image against the other one
Expand All @@ -57,11 +54,11 @@ export default function pixelmatch(img1, img2, output, width, height, options) {
// the color difference is above the threshold
if (Math.abs(delta) > maxDelta) {
// check it's a real rendering difference or just anti-aliasing
if (!options.includeAA && (antialiased(img1, x, y, width, height, img2) ||
if (!includeAA && (antialiased(img1, x, y, width, height, img2) ||
antialiased(img2, x, y, width, height, img1))) {
// 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
if (output && !options.diffMask) drawPixel(output, pos, aaR, aaG, aaB);
if (output && !diffMask) drawPixel(output, pos, aaR, aaG, aaB);

} else {
// found substantial difference not caused by anti-aliasing; draw it as such
Expand All @@ -77,7 +74,7 @@ export default function pixelmatch(img1, img2, output, width, height, options) {

} else if (output) {
// pixels are similar; draw background as grayscale image blended with white
if (!options.diffMask) drawGrayPixel(img1, pos, options.alpha, output);
if (!diffMask) drawGrayPixel(img1, pos, alpha, output);
}
}
}
Expand Down
Binary file added test/fixtures/1diffdefaultthreshold.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import match from '../index.js';
const options = {threshold: 0.05};

diffTest('1a', '1b', '1diff', options, 143);
diffTest('1a', '1b', '1diffdefaultthreshold', {threshold: undefined}, 106);
diffTest('1a', '1b', '1diffmask', {threshold: 0.05, includeAA: false, diffMask: true}, 143);
diffTest('1a', '1a', '1emptydiffmask', {threshold: 0, diffMask: true}, 0);
diffTest('2a', '2b', '2diff', {
Expand Down

0 comments on commit bf7001f

Please sign in to comment.