UNPKG

@syngrisi/node-resemble.js

Version:
657 lines (532 loc) 19.3 kB
/* James Cryer / Huddle 2014 URL: https://github.com/Huddle/Resemble.js */ 'use strict'; var PNG = require('pngjs').PNG; var fs = require('fs'); var jpeg = require('jpeg-js'); var sharp = require('sharp'); //keeping wrong indentation and '_this' for better diff with origin resemble.js var _this = {}; var pixelTransparency = 1; var errorPixelColor = { // Color for Error Pixels. Between 0 and 255. red: 255, green: 0, blue: 255, alpha: 255 }; var errorPixelTransform = { flat: function (d1, d2) { return { r: errorPixelColor.red, g: errorPixelColor.green, b: errorPixelColor.blue, a: errorPixelColor.alpha }; }, movement: function (d1, d2) { return { r: ((d2.r * (errorPixelColor.red / 255)) + errorPixelColor.red) / 2, g: ((d2.g * (errorPixelColor.green / 255)) + errorPixelColor.green) / 2, b: ((d2.b * (errorPixelColor.blue / 255)) + errorPixelColor.blue) / 2, a: d2.a }; } }; var errorPixelTransformer = errorPixelTransform.flat; var largeImageThreshold = 1200; _this['resemble'] = function (fileData) { var data = {}; var images = []; var updateCallbackArray = []; var tolerance = { // between 0 and 255 red: 16, green: 16, blue: 16, alpha: 16, minBrightness: 16, maxBrightness: 240 }; var ignoreAntialiasing = false; var ignoreColors = false; var ignoreRectangles = null; var includeRectangles = null; function triggerDataUpdate() { var len = updateCallbackArray.length; var i; for (i = 0; i < len; i++) { if (typeof updateCallbackArray[i] === 'function') { updateCallbackArray[i](data); } } } function loop(x, y, callback) { var i, j; for (i = 0; i < x; i++) { for (j = 0; j < y; j++) { callback(i, j); } } } function parseImage(sourceImageData, width, height) { var pixleCount = 0; var redTotal = 0; var greenTotal = 0; var blueTotal = 0; var brightnessTotal = 0; loop(height, width, function (verticalPos, horizontalPos) { var offset = (verticalPos * width + horizontalPos) * 4; var red = sourceImageData[offset]; var green = sourceImageData[offset + 1]; var blue = sourceImageData[offset + 2]; var brightness = getBrightness(red, green, blue); pixleCount++; redTotal += red / 255 * 100; greenTotal += green / 255 * 100; blueTotal += blue / 255 * 100; brightnessTotal += brightness / 255 * 100; }); data.red = Math.floor(redTotal / pixleCount); data.green = Math.floor(greenTotal / pixleCount); data.blue = Math.floor(blueTotal / pixleCount); data.brightness = Math.floor(brightnessTotal / pixleCount); triggerDataUpdate(); } function loadImageData(fileData, callback) { sharp(fileData) .png() .toBuffer() .then(pngData => { var png = new PNG(); png.parse(pngData, function (err, data) { callback(data, data.width, data.height); }); }); } function isColorSimilar(a, b, color) { var absDiff = Math.abs(a - b); if (typeof a === 'undefined') { return false; } if (typeof b === 'undefined') { return false; } if (a === b) { return true; } else if (absDiff < tolerance[color]) { return true; } else { return false; } } function isWithinRectangle(x, y, rectangle) { return (x >= rectangle[1]) && (x < rectangle[1] + rectangle[3]) && (y >= rectangle[0]) && (y < rectangle[0] + rectangle[2]); } function isNumber(n) { return !isNaN(parseFloat(n)); } function isPixelBrightnessSimilar(d1, d2) { var alpha = isColorSimilar(d1.a, d2.a, 'alpha'); var brightness = isColorSimilar(d1.brightness, d2.brightness, 'minBrightness'); return brightness && alpha; } function getBrightness(r, g, b) { return 0.3 * r + 0.59 * g + 0.11 * b; } function isRGBSame(d1, d2) { var red = d1.r === d2.r; var green = d1.g === d2.g; var blue = d1.b === d2.b; return red && green && blue; } function isRGBSimilar(d1, d2) { var red = isColorSimilar(d1.r, d2.r, 'red'); var green = isColorSimilar(d1.g, d2.g, 'green'); var blue = isColorSimilar(d1.b, d2.b, 'blue'); var alpha = isColorSimilar(d1.a, d2.a, 'alpha'); return red && green && blue && alpha; } function isContrasting(d1, d2) { return Math.abs(d1.brightness - d2.brightness) > tolerance.maxBrightness; } function getHue(r, g, b) { r = r / 255; g = g / 255; b = b / 255; var max = Math.max(r, g, b), min = Math.min(r, g, b); var h; var d; if (max == min) { h = 0; // achromatic } else { d = max - min; switch (max) { case r: h = (g - b) / d + (g < b ? 6 : 0); break; case g: h = (b - r) / d + 2; break; case b: h = (r - g) / d + 4; break; } h /= 6; } return h; } function isAntialiased(sourcePix, data, cacheSet, verticalPos, horizontalPos, width) { var offset; var targetPix; var distance = 1; var i; var j; var hasHighContrastSibling = 0; var hasSiblingWithDifferentHue = 0; var hasEquivilantSibling = 0; addHueInfo(sourcePix); for (i = distance * -1; i <= distance; i++) { for (j = distance * -1; j <= distance; j++) { if (i === 0 && j === 0) { // ignore source pixel } else { offset = ((verticalPos + j) * width + (horizontalPos + i)) * 4; targetPix = getPixelInfo(data, offset, cacheSet); if (targetPix === null) { continue; } addBrightnessInfo(targetPix); addHueInfo(targetPix); if (isContrasting(sourcePix, targetPix)) { hasHighContrastSibling++; } if (isRGBSame(sourcePix, targetPix)) { hasEquivilantSibling++; } if (Math.abs(targetPix.h - sourcePix.h) > 0.3) { hasSiblingWithDifferentHue++; } if (hasSiblingWithDifferentHue > 1 || hasHighContrastSibling > 1) { return true; } } } } if (hasEquivilantSibling < 2) { return true; } return false; } function errorPixel(px, offset, data1, data2) { var data = errorPixelTransformer(data1, data2); px[offset] = data.r; px[offset + 1] = data.g; px[offset + 2] = data.b; px[offset + 3] = data.a; } function copyPixel(px, offset, data) { px[offset] = data.r; //r px[offset + 1] = data.g; //g px[offset + 2] = data.b; //b px[offset + 3] = data.a * pixelTransparency; //a } function copyGrayScalePixel(px, offset, data) { px[offset] = data.brightness; //r px[offset + 1] = data.brightness; //g px[offset + 2] = data.brightness; //b px[offset + 3] = data.a * pixelTransparency; //a } function getPixelInfo(data, offset, cacheSet) { var r; var g; var b; var d; var a; r = data[offset]; if (typeof r !== 'undefined') { g = data[offset + 1]; b = data[offset + 2]; a = data[offset + 3]; d = { r: r, g: g, b: b, a: a }; return d; } else { return null; } } function addBrightnessInfo(data) { data.brightness = getBrightness(data.r, data.g, data.b); // 'corrected' lightness } function addHueInfo(data) { data.h = getHue(data.r, data.g, data.b); } function analyseImages(img1, img2, width, height) { var data1 = img1.data; var data2 = img2.data; //TODO var imgd = new PNG({ width: img1.width, height: img1.height, deflateChunkSize: img1.deflateChunkSize, deflateLevel: img1.deflateLevel, deflateStrategy: img1.deflateStrategy, }); var targetPix = imgd.data; var mismatchCount = 0; var time = Date.now(); var skip; var currentRectangle = null; var rectagnlesIdx = 0; if (!!largeImageThreshold && ignoreAntialiasing && (width > largeImageThreshold || height > largeImageThreshold)) { skip = 6; } loop(height, width, function (verticalPos, horizontalPos) { var offset = (verticalPos * width + horizontalPos) * 4; if (skip) { // only skip if the image isn't small if (verticalPos % skip === 0 || horizontalPos % skip === 0) { copyPixel(targetPix, offset, { r: 0, b: 0, g: 0, a: 0 }); return; } } var pixel1 = getPixelInfo(data1, offset, 1); var pixel2 = getPixelInfo(data2, offset, 2); if (pixel1 === null || pixel2 === null) { return; } if (ignoreRectangles) { for (rectagnlesIdx = 0; rectagnlesIdx < ignoreRectangles.length; rectagnlesIdx++) { currentRectangle = ignoreRectangles[rectagnlesIdx]; //console.log(currentRectangle, verticalPos, horizontalPos); if (isWithinRectangle(verticalPos, horizontalPos, currentRectangle)) { copyGrayScalePixel(targetPix, offset, pixel2); //copyPixel(targetPix, offset, pixel1, pixel2); return; } } } if (includeRectangles) { var isWithinAnyRectangle = false; for (rectagnlesIdx = 0; rectagnlesIdx < includeRectangles.length; rectagnlesIdx++) { currentRectangle = includeRectangles[rectagnlesIdx]; if (isWithinRectangle(verticalPos, horizontalPos, currentRectangle)) { isWithinAnyRectangle = true; } } // Better solution but i'm not sure if you want to use reducers in your repo? // var isWithinAnyRectangle = includeRectangles.reduce(function (acc, currentRectangle) { // if(isWithinRectangle) { // return true; // } // return acc; // }, false); if (!isWithinAnyRectangle) { copyGrayScalePixel(targetPix, offset, pixel2); return; } } if (ignoreColors) { addBrightnessInfo(pixel1); addBrightnessInfo(pixel2); if (isPixelBrightnessSimilar(pixel1, pixel2)) { copyGrayScalePixel(targetPix, offset, pixel2); } else { errorPixel(targetPix, offset, pixel1, pixel2); mismatchCount++; } return; } if (isRGBSimilar(pixel1, pixel2)) { copyPixel(targetPix, offset, pixel1); } else if (ignoreAntialiasing && ( addBrightnessInfo(pixel1), // jit pixel info augmentation looks a little weird, sorry. addBrightnessInfo(pixel2), isAntialiased(pixel1, data1, 1, verticalPos, horizontalPos, width) || isAntialiased(pixel2, data2, 2, verticalPos, horizontalPos, width) )) { if (isPixelBrightnessSimilar(pixel1, pixel2)) { copyGrayScalePixel(targetPix, offset, pixel2); } else { errorPixel(targetPix, offset, pixel1, pixel2); mismatchCount++; } } else { errorPixel(targetPix, offset, pixel1, pixel2); mismatchCount++; } }); data.rawMisMatchPercentage = (mismatchCount / (height * width) * 100); data.misMatchPercentage = data.rawMisMatchPercentage.toFixed(2); data.analysisTime = Date.now() - time; data.getDiffImage = function (text) { return imgd; }; data.getDiffImageAsJPEG = function (quality) { return jpeg.encode({ data: targetPix, width: img1.width, height: img1.height }, quality !== undefined ? quality : 50).data; }; } function compare(one, two) { function onceWeHaveBoth(img) { var width; var height; images.push(img); if (images.length === 2) { width = images[0].width > images[1].width ? images[0].width : images[1].width; height = images[0].height > images[1].height ? images[0].height : images[1].height; if ((images[0].width === images[1].width) && (images[0].height === images[1].height)) { data.isSameDimensions = true; } else { data.isSameDimensions = false; } data.dimensionDifference = { width: images[0].width - images[1].width, height: images[0].height - images[1].height }; //lksv: normalization removed analyseImages(images[0], images[1], width, height); triggerDataUpdate(); } } images = []; loadImageData(one, onceWeHaveBoth); loadImageData(two, onceWeHaveBoth); } function getCompareApi(param) { var secondFileData, hasMethod = typeof param === 'function'; if (!hasMethod) { // assume it's file data secondFileData = param; } var self = { ignoreNothing: function () { tolerance.red = 16; tolerance.green = 16; tolerance.blue = 16; tolerance.alpha = 16; tolerance.minBrightness = 16; tolerance.maxBrightness = 240; ignoreAntialiasing = false; ignoreColors = false; if (hasMethod) { param(); } return self; }, exact: function () { // True pixel-to-pixel comparison: any per-channel difference counts as a mismatch. tolerance.red = 0; tolerance.green = 0; tolerance.blue = 0; tolerance.alpha = 0; tolerance.minBrightness = 0; tolerance.maxBrightness = 255; ignoreAntialiasing = false; ignoreColors = false; if (hasMethod) { param(); } return self; }, ignoreAntialiasing: function () { tolerance.red = 32; tolerance.green = 32; tolerance.blue = 32; tolerance.alpha = 32; tolerance.minBrightness = 64; tolerance.maxBrightness = 96; ignoreAntialiasing = true; ignoreColors = false; if (hasMethod) { param(); } return self; }, ignoreColors: function () { tolerance.alpha = 16; tolerance.minBrightness = 16; tolerance.maxBrightness = 240; ignoreAntialiasing = false; ignoreColors = true; if (hasMethod) { param(); } return self; }, //array of rectangles, each rectangle is defined as (x, y, width. height) //e.g. [[325, 170, 100, 40]] ignoreRectangles: function (rectangles) { ignoreRectangles = rectangles; return self; }, //array of rectangles, each rectangle is defined as (x, y, width. height) //e.g. [[325, 170, 100, 40]] includeRectangles: function (rectangles) { includeRectangles = rectangles; return self; }, repaint: function () { if (hasMethod) { param(); } return self; }, onComplete: function (callback) { updateCallbackArray.push(callback); var wrapper = function () { compare(fileData, secondFileData); }; wrapper(); return getCompareApi(wrapper); } }; return self; } return { onComplete: function (callback) { updateCallbackArray.push(callback); loadImageData(fileData, function (imageData, width, height) { parseImage(imageData.data, width, height); }); }, compareTo: function (secondFileData) { return getCompareApi(secondFileData); } }; }; _this['resemble'].outputSettings = function (options) { var key; var undefined; if (options.errorColor) { for (key in options.errorColor) { errorPixelColor[key] = options.errorColor[key] === undefined ? errorPixelColor[key] : options.errorColor[key]; } } if (options.errorType && errorPixelTransform[options.errorType]) { errorPixelTransformer = errorPixelTransform[options.errorType]; } pixelTransparency = options.transparency || pixelTransparency; if (options.largeImageThreshold !== undefined) { largeImageThreshold = options.largeImageThreshold; } return this; }; module.exports = _this['resemble'];