UNPKG

blink-diff

Version:

A lightweight image comparison tool

320 lines (264 loc) 6.81 kB
// Copyright 2015 Yahoo! Inc. // Copyrights licensed under the Mit License. See the accompanying LICENSE file for terms. var Base = require('preceptor-core').Base; var PNGImage = require('pngjs-image'); /** * @class Image * @extends Base * @module Compare * * @property {PNGImage} _image * @property {number[]} _refWhite * @property {boolean} _gammaCorrection * @property {boolean} _perceptual * @property {boolean} _filters */ var Image = Base.extend( /** * Image constructor * * @param {object} options * @param {PNGImage} options.image Image * @constructor */ function (options) { this.__super(); this._image = options.image; this._refWhite = []; this._convertRgbToXyz([1, 1, 1, 1], 0, this._refWhite); this._gammaCorrection = false; this._perceptual = false; this._filters = false; }, { /** * Gets the image * * @method getImage * @return {PNGImage} */ getImage: function () { return this._image; }, /** * Gets the image data * * @method getData * @return {Buffer} */ getData: function () { return this.getImage()._data; }, /** * Gets the width of the image * * @method getWidth * @return {int} */ getWidth: function () { return this.getImage().getWidth(); }, /** * Gets the height of the image * * @method getHeight * @return {int} */ getHeight: function () { return this.getImage().getHeight(); }, /** * Gets the length of bytes of the image * * @method getLength * @return {int} */ getLength: function () { return this.getWidth() * this.getHeight() * 4; }, /** * Determines if gamma-correction has been applied * * @method hasGammaCorrection * @return {boolean} */ hasGammaCorrection: function () { return this._gammaCorrection; }, /** * Is image converted to a perceptual image? * * @method isPerceptual * @return {boolean} */ isPerceptual: function () { return this._perceptual; }, /** * Has applied filters * * @method hasFilters * @return {boolean} */ hasFilters: function () { return this._filters; }, /** * Applies gamma correction on the image * * @method applyGamma * @param {Color} gamma */ applyGamma: function (gamma) { var i, len, image, localGamma; if (!this._perceptual) { len = this.getLength(); image = this.getData(); localGamma = gamma.getColor(); for (i = 0; i < len; i += 4) { image[i] = Math.pow(image[i], 1 / localGamma.red); image[i + 1] = Math.pow(image[i + 1], 1 / localGamma.green); image[i + 2] = Math.pow(image[i + 2], 1 / localGamma.blue); } this._gammaCorrection = true; } }, /** * Converts the image to a perceptual color-space * * @method convertToPerceptual */ convertToPerceptual: function () { var i, len, data, pixelList, bounds; if (!this._perceptual) { len = this.getLength(); data = this.getData(); pixelList = []; bounds = [ {min: 20000000, max: -20000000}, {min: 20000000, max: -20000000}, {min: 20000000, max: -20000000} ]; for (i = 0; i < len; i += 4) { this._convertRgbToXyz(data, i, pixelList); this._convertXyzToCieLab(data, i, pixelList); bounds[0].min = Math.min(bounds[0].min, pixelList[i]); bounds[1].min = Math.min(bounds[1].min, pixelList[i + 1]); bounds[2].min = Math.min(bounds[2].min, pixelList[i + 2]); bounds[0].max = Math.max(bounds[0].max, pixelList[i]); bounds[1].max = Math.max(bounds[1].max, pixelList[i + 1]); bounds[2].max = Math.max(bounds[2].max, pixelList[i + 2]); } for (i = 0; i < len; i += 4) { data[i] = 255 * ((pixelList[i] - bounds[0].min) / bounds[0].max); data[i + 1] = 255 * ((pixelList[i + 1] - bounds[1].min) / bounds[1].max); data[i + 2] = 255 * ((pixelList[i + 2] - bounds[2].min) / bounds[2].max); } this._perceptual = true; } }, /** * Converts the color from RGB to XYZ * * @method _convertRgbToXyz * @param {Buffer} buffer * @param {int} offset * @param {int[]} output * @private */ _convertRgbToXyz: function (buffer, offset, output) { var result = [ buffer[offset] * 0.4887180 + buffer[offset + 1] * 0.3106803 + buffer[offset + 2] * 0.2006017, buffer[offset] * 0.1762044 + buffer[offset + 1] * 0.8129847 + buffer[offset + 2] * 0.0108109, buffer[offset + 1] * 0.0102048 + buffer[offset + 2] * 0.9897952, buffer[offset + 3] ]; output[offset] = result[0]; output[offset + 1] = result[1]; output[offset + 2] = result[2]; output[offset + 3] = result[3]; }, /** * Converts the color from Xyz to CieLab * * @method _convertXyzToCieLab * @param {Buffer} buffer * @param {int} offset * @param {int[]} output * @private */ _convertXyzToCieLab: function (buffer, offset, output) { var c1, c2, c3; function f (t) { return (t > 0.00885645167904) ? Math.pow(t, 1 / 3) : 70.08333333333263 * t + 0.13793103448276; } c1 = f(buffer[offset] / this._refWhite[0]); c2 = f(buffer[offset + 1] / this._refWhite[1]); c3 = f(buffer[offset + 2] / this._refWhite[2]); output[offset] = (116 * c2) - 16; output[offset + 1] = 500 * (c1 - c2); output[offset + 2] = 200 * (c2 - c3); output[offset + 3] = buffer[offset + 3]; }, /** * Applies a list of filters * * @method applyFilters * @param {string[]} filters */ applyFilters: function (filters) { if (!this._filters) { this.getImage().applyFilters(filters); this._filters = true; } }, /** * Processes image for the comparison configuration * * @method processImage * @param {PixelComparison|StructureComparison} comparison */ processImage: function (comparison) { if (comparison.hasGamma && comparison.hasGamma()) { this.applyGamma(comparison.getGamma()) } if (comparison.isPerceptual && comparison.isPerceptual()) { this.convertToPerceptual(); } if (comparison.getFilters) { this.applyFilters(comparison.getFilters()); } if (comparison.getBlockOuts) { // Important - do this after filtering comparison.getBlockOuts().forEach(function (blockOut) { this._image = blockOut.processImage(this.getImage()); }.bind(this)); } return this.getImage(); } }, /** * @lends Image */ { /** * Processes an image with comparison configuration * * @param {PNGImage} image * @param {PixelComparison|StructureComparison} comparison * @return {PNGImage} */ processImage: function (image, comparison) { var obj = new Image({ image: image }); obj.processImage(comparison); return obj.getImage(); } } ); module.exports = Image;