gulp-image-diff
Version:
Image diff'ing tool that compares pixel by pixel.
197 lines (147 loc) • 6.54 kB
JavaScript
var fs = require('fs-extra');
var path = require('path');
var Promise = require('bluebird');
var outputFile = Promise.promisify(fs.outputFile);
var through = require('through2');
var extend = require('extend');
var gutil = require('gulp-util');
var jsonReporter = require('./lib/json-reporter.js');
var compareImages = require('./lib/compare-images.js');
var convertToPng = require('./lib/convert-to-png.js');
// consts
const PLUGIN_NAME = 'gulp-image-diff';
var differ = function(options) {
var defaults = {
// String path+filename, buffer, or vinyl file of where the reference image
referenceImage: null,
// 0-1 representing the allowed color difference between pixels
// 0 means no tolerance. Pixels need to be exactly the same
// This allows for slight differences in aliasing
pixelColorTolerance: 0.01,
// Pass a string path+filename or a function that returns string path+filename of where to save the difference image
// function(referencePath, compareImagePath): Return string path+filename
differenceMapImage: null,
// The color for each pixel that is different
differenceMapColor: {
r: 255,
g: 0,
b: 0,
a: 200
},
// Log to the console
// You can also hook onto `.on('log', ...)` events which are emitted no matter what
logProgress: false
};
var settings = extend({}, defaults, options);
var whenReferenceImageReadyPromise = convertToPng(settings.referenceImage);
var stream = through.obj(function(chunk, enc, cb) {
// http://nodejs.org/docs/latest/api/stream.html#stream_transform_transform_chunk_encoding_callback
//console.log('transform');
// Each `chunk` is a vinyl file: https://www.npmjs.com/package/vinyl
// chunk.cwd
// chunk.base
// chunk.path
// chunk.contents
var self = this;
if (chunk.isStream()) {
self.emit('error', new gutil.PluginError(PLUGIN_NAME, 'Cannot operate on stream'));
}
else if (chunk.isBuffer()) {
whenReferenceImageReadyPromise.then(function(referenceImage) {
convertToPng(chunk).then(function(compareImage) {
var totalPixels = compareImage.width*compareImage.height;
var compareResult = compareImages(referenceImage, compareImage, settings.pixelColorTolerance, settings.differenceMapColor);
var analysis = {
differences: compareResult.numDifferences,
total: totalPixels,
disparity: compareResult.numDifferences/totalPixels,
referenceImage: path.normalize(settings.referenceImage),
compareImage: path.relative(chunk.cwd, chunk.path)
};
compareResult.differenceMapImagePromise.then(function(differenceMapImageBuffer) {
var whenImageDealtWithPromise = new Promise(function(resolve, reject) {
if(settings.differenceMapImage) {
// You can pass a string or function to generate the diff save path
// We give you the path of the reference and compare image to construct a path
var differenceMapSavePath = settings.differenceMapImage;
if(typeof(settings.differenceMapImage) === "function") {
differenceMapSavePath = settings.differenceMapImage(analysis.referenceImage, analysis.compareImage);
}
// Save out the difference map
if(differenceMapSavePath) {
outputFile(differenceMapSavePath, differenceMapImageBuffer).then(function() {
// Add to the analysis if we saved the image
analysis.differenceMap = differenceMapSavePath;
resolve();
}).catch(function(err) {
err.message = 'Error saving difference image:\n' + err.message;
self.emit('error', new gutil.PluginError(PLUGIN_NAME, err));
//reject(err);
// The error was handled by emitting
resolve();
});
}
else {
var err = new Error('`options.differenceMapImage` defined but no string path was passed in or returned');
self.emit('error', new gutil.PluginError(PLUGIN_NAME, err));
//reject(err);
// The error was handled by emitting
resolve();
}
}
else {
resolve();
}
});
whenImageDealtWithPromise.finally(function() {
// Attach some extra data to what we emit in case something else wants to consume down the line
// Since we can chain the diffs, we need to maintain all of the analysis's
// If array, add to the array
if(chunk.analysis instanceof Array) {
chunk.analysis.push(analysis);
}
// If it already exists, make it into an array
else if(chunk.analysis) {
chunk.analysis = [chunk.analysis, analysis];
}
// Else, just set it to itself
else {
chunk.analysis = analysis;
}
// We don't maintain multiple difference images through chains
// We could, but this is just a design decision
chunk.differenceMap = differenceMapImageBuffer;
var logMessage = 'Diff complete: ' + analysis.differences + '/' + analysis.total + ' = ' + gutil.colors.cyan(analysis.differences / analysis.total) + ' -- ' + gutil.colors.magenta(analysis.compareImage) + ' compared to ' + gutil.colors.magenta(analysis.referenceImage);
// Emit some log events for anyone to catch
self.emit('log', logMessage);
// We also have a setting to log to the console
if(settings.logProgress) {
gutil.log(logMessage);
}
// Push out the original image
// So you can pipe it multiple times into the diff plugin against different references
self.push(chunk);
// "call callback when the transform operation is complete."
return cb();
});
}, function(err) {
err.message = 'Error making difference image:\n' + err.message;
self.emit('error', new gutil.PluginError(PLUGIN_NAME, err));
});
});
}, function(err) {
err.message = 'Error getting reference image\n:' + err.message;
self.emit('error', new gutil.PluginError(PLUGIN_NAME, err));
});
}
}, function(cb) {
// http://nodejs.org/docs/latest/api/stream.html#stream_transform_flush_callback
//console.log('flush');
// "call callback when the flush operation is complete."
cb();
});
// returning the file stream
return stream;
};
module.exports = differ;
module.exports.jsonReporter = jsonReporter;