1 (function (global) { 2 'use strict'; 3 /** 4 * bDebug is a private flag to know if we want to know what's happening. 5 * @private 6 * @type Boolean 7 */ 8 var bDebug = false, 9 /** 10 * oContainerDiff is a container where create the canvas element with the result. 11 * @private 12 * @type Object 13 */ 14 oContainerDiff = null, 15 /** 16 * nMinPercentage is the tolerance to set a difference between images as ok. 17 * @private 18 * @type Number 19 */ 20 nMinPercentage = 100, 21 /** 22 * bAsynchronous is a private flag to know if we want to execute the comparison using asynchronous mode or not. 23 * @private 24 * @type Boolean 25 */ 26 bAsynchronous = false, 27 /** 28 * Number of image that are loaded not important to know if are loaded or with error. 29 * @private 30 * @type Number 31 */ 32 nImagesLoaded = 0, 33 /** 34 * Array of canvas that are created dynamically. 35 * @private 36 * @type Array 37 */ 38 aCanvas = [], 39 /** 40 * fpLoop is a method that will save the asynchronous or not loop type 41 * @private 42 * @type Function 43 */ 44 fpLoop = loop, 45 /** 46 * proxyFloat is a proxy where save the parseFloat to use in parseFloat in local 47 */ 48 proxyFloat = window.parseFloat; 49 50 /** 51 * parseFloat to return 52 * @param number 53 */ 54 function parseFloat(number) { 55 var nDecimals = 4, 56 stringNumber = number.toString(), 57 decimalTypes = [".",","], 58 lastDec, posFinal, numberMore, result, decimalType, decimals, pos; 59 60 for (var i = 0; i < decimalTypes.length; i++) { 61 pos = stringNumber.indexOf(decimalTypes[i]); 62 if (pos != -1) { 63 decimalType = decimalTypes[i]; 64 break; 65 } 66 } 67 68 decimals = (stringNumber.length - 1) - pos; 69 if (typeof nDecimals != "undefined") { 70 decimals = nDecimals; 71 } 72 posFinal = pos + (decimals + 1); 73 if (pos != -1) { 74 lastDec = stringNumber.substr(posFinal, 1); 75 stringNumber = stringNumber.substr(0, posFinal); 76 if (lastDec >= 5) { 77 numberMore = stringNumber.substr(stringNumber.length - 1, 1); 78 if (numberMore == decimalType) { 79 stringNumber = (stringNumber.substr(0, stringNumber.length - 1) * 1) + 1; 80 } else { 81 stringNumber = stringNumber.substr(0, stringNumber.length - 1) + ((numberMore * 1) + 1); 82 } 83 84 } 85 } 86 result = proxyFloat(stringNumber); 87 return result; 88 } 89 90 /** 91 * loopWithoutBlocking is a function to process items in asynchronous mode to avoid the environment to be freeze. 92 * @private 93 * @param aItems {Array} Items array to traverse. 94 * @param fpProcess {Function} Callback to execute on each iteration. 95 * @param fpFinish {Function} Callback to execute when all the items are traversed. 96 */ 97 function loopWithoutBlocking(aItems, fpProcess, fpFinish) { 98 var aCopy = aItems.concat(); 99 var nIndex = aItems.length - 1; 100 var nStart = +new Date(); 101 setTimeout(function recursive() { 102 do { 103 nIndex--; 104 if (fpProcess(aCopy.shift(), nIndex) === false) { 105 return; 106 } 107 } while (aCopy.length > 0 && (+new Date() - nStart < 50)); 108 109 if (aCopy.length > 0) { 110 setTimeout(recursive, 25); 111 } else { 112 fpFinish(aItems); 113 } 114 }, 25); 115 } 116 117 /** 118 * loop is a function to process items. 119 * @private 120 * @param aItems {Array} Items array to traverse. 121 * @param fpProcess {Function} Callback to execute on each iteration. 122 * @param fpFinish {Function} Callback to execute when all the items are traversed. 123 */ 124 function loop(aItems, fpProcess, fpFinish) { 125 var aCopy = aItems.concat(); 126 var nIndex = aItems.length ; 127 var oItem = null; 128 while (Boolean(oItem = aCopy.shift())) { 129 nIndex--; 130 131 if (fpProcess(oItem, nIndex) === false) { 132 133 return; 134 } 135 } 136 fpFinish(aItems); 137 } 138 139 /** 140 * compare is the function that starts the comparing of image data. 141 * @param aCanvas {Array} Canvas items to execute the compare of image data. 142 * @param fpSuccess {Function} Callback to execute if all the images are equals in pixel level. 143 * @param fpFail {Function} Callback to execute if any of the images is different in pixel level. 144 */ 145 function compareWithoutCreate(aCanvas, fpSuccess, fpFail, nStart) { 146 var sLastData = null, 147 oLastImageData = null, 148 nElapsedTime = undefined, 149 nPercentageDiff = undefined, 150 oDiffObject = null, 151 oDiffCanvas = null; 152 if (bDebug && typeof nStart === "undefined") { 153 nStart = +new Date(); 154 } 155 fpLoop(aCanvas, function (oCanvas, nIndex) { 156 var oContext = oCanvas.getContext("2d"), 157 aCanvasData = oContext.getImageData(0, 0, oCanvas.width, oCanvas.height), 158 sData = JSON.stringify([].slice.call(aCanvasData.data)); 159 if (sLastData !== null) { 160 if (sLastData.localeCompare(sData) !== 0) { 161 oDiffObject = diff(oCanvas.width, oCanvas.height, aCanvasData, oLastImageData); 162 nPercentageDiff = oDiffObject.percentage; 163 oDiffCanvas = oDiffObject.canvas; 164 if (nPercentageDiff >= nMinPercentage) 165 { 166 return true; 167 } 168 if (oContainerDiff) { 169 oContainerDiff.appendChild(oDiffCanvas); 170 } 171 oCanvas.className = "fail"; 172 if (bDebug) { 173 nElapsedTime = (+new Date() - nStart); 174 } 175 fpFail(oCanvas, nElapsedTime, nPercentageDiff); 176 return false; 177 } 178 } 179 oLastImageData = aCanvasData; 180 sLastData = sData; 181 }, function (aCanvas) { 182 if (bDebug) { 183 nElapsedTime = (+new Date() - nStart); 184 } 185 fpSuccess(aCanvas, nElapsedTime, nPercentageDiff); 186 }); 187 } 188 189 function diff(nWidth, nHeight, aDataImage, aLastDataImage) { 190 var aData = aDataImage.data, 191 aLastData = aLastDataImage.data, 192 nLenPixels = 0, 193 nDiffPixels = 0, 194 nDiffPercentage = 0, 195 oCanvas = document.createElement("canvas"), 196 oContext = oCanvas.getContext("2d"), 197 oDataImage = oContext.createImageData(nWidth, nHeight), 198 aCreatedDataImage = oDataImage.data, 199 nData = 0, 200 nRow = 0, 201 nColumn = 0, 202 nX = 0, 203 nY = 0, 204 nLenData = aCreatedDataImage.length, 205 nRed, nGreen, nBlue, nAlpha, nLastRed, nLastGreen, nLastBlue, nLastAlpha; 206 oCanvas.width = nWidth; 207 oCanvas.height = nHeight; 208 oCanvas.style.border = "#000 1px solid"; 209 210 for (nData = nLenData - 1; nData > 0; nData = nData - 4) { 211 aCreatedDataImage[nData] = 255; 212 } 213 nLenPixels = aDataImage.height * aDataImage.width; 214 for (nRow = aDataImage.height; nRow--;) { 215 for (nColumn = aDataImage.width; nColumn--;) { 216 nX = 4 * (nRow * nWidth + nColumn); 217 nY = 4 * (nRow * aDataImage.width + nColumn); 218 nRed = aData[nY + 0]; 219 nGreen = aData[nY + 1]; 220 nBlue = aData[nY + 2]; 221 nAlpha = aData[nY + 3]; 222 nLastRed = aLastData[nY + 0]; 223 nLastGreen = aLastData[nY + 1]; 224 nLastBlue = aLastData[nY + 2]; 225 nLastAlpha = aLastData[nY + 3]; 226 227 if (nRed === nLastRed && nGreen === nLastGreen && nBlue === nLastBlue && nAlpha === nLastAlpha) { 228 aCreatedDataImage[nX + 0] = Math.abs(nRed - nLastRed); // r 229 aCreatedDataImage[nX + 1] = Math.abs(nGreen - nLastGreen); // g 230 aCreatedDataImage[nX + 2] = Math.abs(nBlue - nLastBlue); // b 231 aCreatedDataImage[nX + 3] = Math.abs(nAlpha - nLastAlpha); // a 232 } else { 233 nDiffPixels++; 234 aCreatedDataImage[nX + 0] = aData[nY + 0]; // r 235 aCreatedDataImage[nX + 1] = aData[nY + 1]; // g 236 aCreatedDataImage[nX + 2] = aData[nY + 2]; // b 237 aCreatedDataImage[nX + 3] = aData[nY + 3]; // a 238 } 239 240 } 241 } 242 243 oContext.putImageData(oDataImage, 0, 0); 244 nDiffPercentage = Math.abs((((nDiffPixels - nLenPixels) / nLenPixels) * 100)); 245 return { 246 percentage: parseFloat(nDiffPercentage), 247 canvas: oCanvas 248 }; 249 } 250 251 /** 252 * createAndCompare creates canvas in oContainer and adding images to these canvas, then compare it 253 * @private 254 * @param oContainer {Object} Dom element that will contain all the canvas 255 * @param aImages {Array} Array of objects that will represent images ( 256 * @param fpSuccess 257 * @param fpFail 258 */ 259 function createAndCompare(oContainer, aImages, fpSuccess, fpFail) { 260 aCanvas = []; 261 if (bDebug) { 262 var nStart = +new Date(); 263 } 264 fpLoop(aImages, function (oImageConfig, nIndex) { 265 var oCanvas, oContext, oImage; 266 oCanvas = document.createElement("canvas"); 267 oCanvas.id = "canvasCompare_" + nIndex; 268 aCanvas.push(oCanvas); 269 oCanvas.width = oImageConfig.width; 270 oCanvas.height = oImageConfig.height; 271 oContainer.appendChild(oCanvas); 272 oContext = oCanvas.getContext("2d"); 273 oImage = new Image(); 274 oImage.onload = function() { 275 nImagesLoaded++; 276 oContext.drawImage(oImage, 0, 0); 277 }; 278 oImage.onerror = function() { 279 nImagesLoaded++; 280 }; 281 oImage.src = oImageConfig.src; 282 }, function finishCallback(aImages) { 283 if (nImagesLoaded < aImages.length) { 284 setTimeout(function() { 285 finishCallback(aImages); 286 }, 25); 287 } else { 288 compareWithoutCreate(aCanvas, fpSuccess, fpFail, nStart); 289 } 290 }); 291 } 292 293 /** 294 * ImageToCompare is a JSON helper to create new images objects to be compared. 295 * @param sUrl {String} represents the src of the image to be loaded. 296 * @param nWidth 297 * @param nHeight 298 */ 299 var ImageToCompare = function(sUrl, nWidth, nHeight) { 300 this.src = sUrl + (sUrl.indexOf("?") === -1 ? "?" : "&") + (+new Date()); 301 this.width = nWidth; 302 this.height = nHeight; 303 }; 304 305 /** 306 * IM (Image Match) is a class to compare images using canvas at pixel level 307 * @class Represents an Image Match 308 * @constructor 309 * @name IM 310 * @author Tomas Corral Casas 311 * @version 1.0 312 */ 313 function IM() { 314 } 315 316 /** 317 * setDebug is the method to set the debug to allow check the incorrect canvas and log how many time it tooks. 318 * @member IM.prototype 319 * @param bLocalDebug 320 * @returns {Boolean} bDebug 321 */ 322 IM.prototype.setDebug = function setDebug(bLocalDebug) { 323 bDebug = bLocalDebug; 324 return bDebug; 325 }; 326 /** 327 * Change the loop type to and from asynchronous. 328 * @member IM.prototype 329 * @param bLocalAsynchronous 330 * @returns {Boolean} bLocalAsynchronous 331 */ 332 IM.prototype.setAsynchronous = function setAsynchronous(bLocalAsynchronous) { 333 bAsynchronous = bLocalAsynchronous; 334 fpLoop = bAsynchronous ? loopWithoutBlocking : loop; 335 return bLocalAsynchronous; 336 }; 337 /** 338 * showDiffInCanvas is the method that sets the diff mode to create a canvas with the difference 339 * @member IM.prototype 340 * @param {Object} oLocalContainerDiff 341 * @returns {Object} Element where put the result canvas 342 */ 343 IM.prototype.showDiffInCanvas = function showDiffInCanvas(oLocalContainerDiff) { 344 oContainerDiff = oLocalContainerDiff; 345 return oContainerDiff; 346 }; 347 /** 348 * setTolerance must be used if you want to check if the match you want is correct. 349 * It is important to assign a tolerance of difference between images. 350 * If the image has a difference lower than nMinPercentage the image will be treated as ok. 351 * @member IM.prototype 352 * @param {Number} nMinPercentage 353 */ 354 IM.prototype.setTolerance = function percentageDiff(nMinPercent) { 355 nMinPercentage = nMinPercent; 356 return nMinPercentage; 357 }; 358 /** 359 * Compare is the method that change the behaviour if it's needed to create canvas or not. 360 * @member IM.prototype 361 * @param oContainer/aCanvas 362 * @param aElements/fpSuccess 363 * @param fpSuccess/fpFail 364 * @param fpFail 365 */ 366 IM.prototype.compare = function(oContainer, aElements, fpSuccess, fpFail) { 367 if (!oContainer.nodeType) { 368 compareWithoutCreate.apply(this, arguments); 369 } else { 370 createAndCompare.apply(this, arguments); 371 } 372 }; 373 /** 374 * Image is a reference to ImageToCompare. 375 * @member IM.prototype 376 */ 377 IM.prototype.image = ImageToCompare; 378 global.IM = new IM(); 379 }(window));