gulp-image-diff
Version:
Image diff'ing tool that compares pixel by pixel.
173 lines (140 loc) • 4.93 kB
JavaScript
var Promise = require('bluebird');
var streamToBuffer = require('stream-to-buffer');
// Use to create/write a png (encode). Yes, I know this also does have a decoder
var png = require('pngjs').PNG;
// Compares two images and generates a difference map image
function compareImages(referenceImage, compareImage, pixelColorTolerance, differenceMapColor) {
// Default it to no tolerance if it isn't specified
pixelColorTolerance = pixelColorTolerance || 0;
// Default the difference color to red
differenceMapColor = differenceMapColor || {
r: 255,
g: 0,
b: 0,
a: 255
};
// Create a buffer that we will use to generate a difference map png image
var numColorChannels = 4;
// http://stackoverflow.com/a/13735425/796832
var differenceMapImageBuffer = new Array(referenceImage.width*referenceImage.height*numColorChannels+1).join('0').split('').map(parseFloat);
var currentDifferenceMapIndex = 0;
var addColorToDifferenceMap = function(color) {
differenceMapImageBuffer[currentDifferenceMapIndex] = color.r || 0;
differenceMapImageBuffer[currentDifferenceMapIndex+1] = color.g || 0;
differenceMapImageBuffer[currentDifferenceMapIndex+2] = color.b || 0;
differenceMapImageBuffer[currentDifferenceMapIndex+3] = color.a || 255;
currentDifferenceMapIndex += numColorChannels;
};
var numPixelDifferences = 0;
for(var y = 0; y < referenceImage.height; y++) {
for(var x = 0; x < referenceImage.width; x++) {
var referencePixel = getPixel(referenceImage, x, y);
var comparePixel = getPixel(compareImage, x, y);
if(referencePixel && comparePixel) {
var isPixelWithinTolerance = compareColorEquality(referencePixel, comparePixel, pixelColorTolerance);
if(!isPixelWithinTolerance) {
numPixelDifferences++;
// Mark on the difference map where the disparity was
addColorToDifferenceMap(alphaBlendColors(differenceMapColor, referencePixel));
}
else {
// Add the reference as back onto the map so we can show what parts are similar
addColorToDifferenceMap(referencePixel);
}
}
/* * /
else {
// We consider a pixel that doesn't exist in either image as a difference
numPixelDifferences++;
}
/* */
}
}
var whenPngReadyPromise = new Promise(function(resolve, reject) {
var differenceMapPng = new png({
width: referenceImage.width,
height: referenceImage.height
});
differenceMapPng.data = new Buffer(differenceMapImageBuffer);
var pngPackStream = differenceMapPng.pack();
streamToBuffer(pngPackStream, function(err, buffer) {
if(err) {
reject(err);
}
resolve(buffer);
});
});
return {
numDifferences: numPixelDifferences,
differenceMapImagePromise: whenPngReadyPromise
};
}
function compareColorEquality(referenceColor, compareColor, tolerance) {
var isColorWithinTolerance = true;
// Loop through each channel and check if it is within tolerance
Object.keys({r: true, g: true, b: true, a: true}).forEach(function(colorChannel) {
var isChannelWithinTolerance = Math.abs(compareColor[colorChannel] - referenceColor[colorChannel]) <= (tolerance*255);
isColorWithinTolerance = isColorWithinTolerance && isChannelWithinTolerance;
});
return isColorWithinTolerance;
}
// Color A is blended on top of color B
function alphaBlendColors(colorA, colorB) {
aAlpha = colorA.a || 255;
bAlpha = colorB.a || 255;
var resultantColor = {
a: bAlpha + ((1-bAlpha)*aAlpha)
};
var alphaPercentage = (aAlpha || 255) / 255;
Object.keys({r: true, g: true, b: true}).forEach(function(colorChannel) {
resultantColor[colorChannel] = (colorA[colorChannel] || 0)*alphaPercentage + (colorB[colorChannel] || 0)*(1-alphaPercentage);
});
return resultantColor;
}
function getPixel(imageData, x, y) {
var pixelColor = null;
if(imageData && x >= 0 && y >= 0) {
// https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Pixel_manipulation_with_canvas
var numChannels = imageData.channels;
var arrayIndex = ((y*(imageData.width*numChannels)) + (x*numChannels));
var rawData = Array.prototype.slice.call(imageData.data, arrayIndex, arrayIndex+numChannels);
// grayscale
if(numChannels === 1) {
pixelColor = {
r: rawData[0],
g: rawData[0],
b: rawData[0],
a: 255
};
}
// grayscale + alpha
else if(numChannels === 2) {
pixelColor = {
r: rawData[0],
g: rawData[0],
b: rawData[0],
a: rawData[1]
};
}
// RGB
else if(numChannels === 3) {
pixelColor = {
r: rawData[0],
g: rawData[1],
b: rawData[2],
a: 255
};
}
// RGBA
else if(numChannels === 4) {
pixelColor = {
r: rawData[0],
g: rawData[1],
b: rawData[2],
a: rawData[3],
};
}
}
return pixelColor;
}
module.exports = compareImages;