pixelmatch.js 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. /**
  2. *
  3. * ISC License
  4. *
  5. * Copyright (c) 2019, Mapbox
  6. * Permission to use, copy, modify, and/or distribute this software for any purpose
  7. * with or without fee is hereby granted, provided that the above copyright notice
  8. * and this permission notice appear in all copies.
  9. *
  10. * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
  11. * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
  12. * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
  13. * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
  14. * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
  15. * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
  16. * THIS SOFTWARE.
  17. */
  18. 'use strict';
  19. module.exports = pixelmatch;
  20. const defaultOptions = {
  21. threshold: 0.1, // matching threshold (0 to 1); smaller is more sensitive
  22. includeAA: false, // whether to skip anti-aliasing detection
  23. alpha: 0.1, // opacity of original image in diff output
  24. aaColor: [255, 255, 0], // color of anti-aliased pixels in diff output
  25. diffColor: [255, 0, 0], // color of different pixels in diff output
  26. diffColorAlt: null, // whether to detect dark on light differences between img1 and img2 and set an alternative color to differentiate between the two
  27. diffMask: false // draw the diff over a transparent background (a mask)
  28. };
  29. function pixelmatch(img1, img2, output, width, height, options) {
  30. if (!isPixelData(img1) || !isPixelData(img2) || (output && !isPixelData(output)))
  31. throw new Error('Image data: Uint8Array, Uint8ClampedArray or Buffer expected.');
  32. if (img1.length !== img2.length || (output && output.length !== img1.length))
  33. throw new Error('Image sizes do not match.');
  34. if (img1.length !== width * height * 4) throw new Error('Image data size does not match width/height.');
  35. options = Object.assign({}, defaultOptions, options);
  36. // check if images are identical
  37. const len = width * height;
  38. const a32 = new Uint32Array(img1.buffer, img1.byteOffset, len);
  39. const b32 = new Uint32Array(img2.buffer, img2.byteOffset, len);
  40. let identical = true;
  41. for (let i = 0; i < len; i++) {
  42. if (a32[i] !== b32[i]) { identical = false; break; }
  43. }
  44. if (identical) { // fast path if identical
  45. if (output && !options.diffMask) {
  46. for (let i = 0; i < len; i++) drawGrayPixel(img1, 4 * i, options.alpha, output);
  47. }
  48. return 0;
  49. }
  50. // maximum acceptable square distance between two colors;
  51. // 35215 is the maximum possible value for the YIQ difference metric
  52. const maxDelta = 35215 * options.threshold * options.threshold;
  53. let diff = 0;
  54. // compare each pixel of one image against the other one
  55. for (let y = 0; y < height; y++) {
  56. for (let x = 0; x < width; x++) {
  57. const pos = (y * width + x) * 4;
  58. // squared YUV distance between colors at this pixel position, negative if the img2 pixel is darker
  59. const delta = colorDelta(img1, img2, pos, pos);
  60. // the color difference is above the threshold
  61. if (Math.abs(delta) > maxDelta) {
  62. // check it's a real rendering difference or just anti-aliasing
  63. if (!options.includeAA && (antialiased(img1, x, y, width, height, img2) ||
  64. antialiased(img2, x, y, width, height, img1))) {
  65. // one of the pixels is anti-aliasing; draw as yellow and do not count as difference
  66. // note that we do not include such pixels in a mask
  67. if (output && !options.diffMask) drawPixel(output, pos, ...options.aaColor);
  68. } else {
  69. // found substantial difference not caused by anti-aliasing; draw it as such
  70. if (output) {
  71. drawPixel(output, pos, ...(delta < 0 && options.diffColorAlt || options.diffColor));
  72. }
  73. diff++;
  74. }
  75. } else if (output) {
  76. // pixels are similar; draw background as grayscale image blended with white
  77. if (!options.diffMask) drawGrayPixel(img1, pos, options.alpha, output);
  78. }
  79. }
  80. }
  81. // return the number of different pixels
  82. return diff;
  83. }
  84. function isPixelData(arr) {
  85. // work around instanceof Uint8Array not working properly in some Jest environments
  86. return ArrayBuffer.isView(arr) && arr.constructor.BYTES_PER_ELEMENT === 1;
  87. }
  88. // check if a pixel is likely a part of anti-aliasing;
  89. // based on "Anti-aliased Pixel and Intensity Slope Detector" paper by V. Vysniauskas, 2009
  90. function antialiased(img, x1, y1, width, height, img2) {
  91. const x0 = Math.max(x1 - 1, 0);
  92. const y0 = Math.max(y1 - 1, 0);
  93. const x2 = Math.min(x1 + 1, width - 1);
  94. const y2 = Math.min(y1 + 1, height - 1);
  95. const pos = (y1 * width + x1) * 4;
  96. let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
  97. let min = 0;
  98. let max = 0;
  99. let minX, minY, maxX, maxY;
  100. // go through 8 adjacent pixels
  101. for (let x = x0; x <= x2; x++) {
  102. for (let y = y0; y <= y2; y++) {
  103. if (x === x1 && y === y1) continue;
  104. // brightness delta between the center pixel and adjacent one
  105. const delta = colorDelta(img, img, pos, (y * width + x) * 4, true);
  106. // count the number of equal, darker and brighter adjacent pixels
  107. if (delta === 0) {
  108. zeroes++;
  109. // if found more than 2 equal siblings, it's definitely not anti-aliasing
  110. if (zeroes > 2) return false;
  111. // remember the darkest pixel
  112. } else if (delta < min) {
  113. min = delta;
  114. minX = x;
  115. minY = y;
  116. // remember the brightest pixel
  117. } else if (delta > max) {
  118. max = delta;
  119. maxX = x;
  120. maxY = y;
  121. }
  122. }
  123. }
  124. // if there are no both darker and brighter pixels among siblings, it's not anti-aliasing
  125. if (min === 0 || max === 0) return false;
  126. // if either the darkest or the brightest pixel has 3+ equal siblings in both images
  127. // (definitely not anti-aliased), this pixel is anti-aliased
  128. return (hasManySiblings(img, minX, minY, width, height) && hasManySiblings(img2, minX, minY, width, height)) ||
  129. (hasManySiblings(img, maxX, maxY, width, height) && hasManySiblings(img2, maxX, maxY, width, height));
  130. }
  131. // check if a pixel has 3+ adjacent pixels of the same color.
  132. function hasManySiblings(img, x1, y1, width, height) {
  133. const x0 = Math.max(x1 - 1, 0);
  134. const y0 = Math.max(y1 - 1, 0);
  135. const x2 = Math.min(x1 + 1, width - 1);
  136. const y2 = Math.min(y1 + 1, height - 1);
  137. const pos = (y1 * width + x1) * 4;
  138. let zeroes = x1 === x0 || x1 === x2 || y1 === y0 || y1 === y2 ? 1 : 0;
  139. // go through 8 adjacent pixels
  140. for (let x = x0; x <= x2; x++) {
  141. for (let y = y0; y <= y2; y++) {
  142. if (x === x1 && y === y1) continue;
  143. const pos2 = (y * width + x) * 4;
  144. if (img[pos] === img[pos2] &&
  145. img[pos + 1] === img[pos2 + 1] &&
  146. img[pos + 2] === img[pos2 + 2] &&
  147. img[pos + 3] === img[pos2 + 3]) zeroes++;
  148. if (zeroes > 2) return true;
  149. }
  150. }
  151. return false;
  152. }
  153. // calculate color difference according to the paper "Measuring perceived color difference
  154. // using YIQ NTSC transmission color space in mobile applications" by Y. Kotsarenko and F. Ramos
  155. function colorDelta(img1, img2, k, m, yOnly) {
  156. let r1 = img1[k + 0];
  157. let g1 = img1[k + 1];
  158. let b1 = img1[k + 2];
  159. let a1 = img1[k + 3];
  160. let r2 = img2[m + 0];
  161. let g2 = img2[m + 1];
  162. let b2 = img2[m + 2];
  163. let a2 = img2[m + 3];
  164. if (a1 === a2 && r1 === r2 && g1 === g2 && b1 === b2) return 0;
  165. if (a1 < 255) {
  166. a1 /= 255;
  167. r1 = blend(r1, a1);
  168. g1 = blend(g1, a1);
  169. b1 = blend(b1, a1);
  170. }
  171. if (a2 < 255) {
  172. a2 /= 255;
  173. r2 = blend(r2, a2);
  174. g2 = blend(g2, a2);
  175. b2 = blend(b2, a2);
  176. }
  177. const y1 = rgb2y(r1, g1, b1);
  178. const y2 = rgb2y(r2, g2, b2);
  179. const y = y1 - y2;
  180. if (yOnly) return y; // brightness difference only
  181. const i = rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2);
  182. const q = rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2);
  183. const delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
  184. // encode whether the pixel lightens or darkens in the sign
  185. return y1 > y2 ? -delta : delta;
  186. }
  187. function rgb2y(r, g, b) { return r * 0.29889531 + g * 0.58662247 + b * 0.11448223; }
  188. function rgb2i(r, g, b) { return r * 0.59597799 - g * 0.27417610 - b * 0.32180189; }
  189. function rgb2q(r, g, b) { return r * 0.21147017 - g * 0.52261711 + b * 0.31114694; }
  190. // blend semi-transparent color with white
  191. function blend(c, a) {
  192. return 255 + (c - 255) * a;
  193. }
  194. function drawPixel(output, pos, r, g, b) {
  195. output[pos + 0] = r;
  196. output[pos + 1] = g;
  197. output[pos + 2] = b;
  198. output[pos + 3] = 255;
  199. }
  200. function drawGrayPixel(img, i, alpha, output) {
  201. const r = img[i + 0];
  202. const g = img[i + 1];
  203. const b = img[i + 2];
  204. const val = blend(rgb2y(r, g, b), alpha * img[i + 3] / 255);
  205. drawPixel(output, i, val, val, val);
  206. }