You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
320 lines
6.7 KiB
320 lines
6.7 KiB
// 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;
|