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));