comparators.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.getComparator = getComparator;
  6. var _utilsBundle = require("../utilsBundle");
  7. var _compare = require("../image_tools/compare");
  8. /**
  9. * Copyright 2017 Google Inc. All rights reserved.
  10. * Modifications copyright (c) Microsoft Corporation.
  11. *
  12. * Licensed under the Apache License, Version 2.0 (the "License");
  13. * you may not use this file except in compliance with the License.
  14. * You may obtain a copy of the License at
  15. *
  16. * http://www.apache.org/licenses/LICENSE-2.0
  17. *
  18. * Unless required by applicable law or agreed to in writing, software
  19. * distributed under the License is distributed on an "AS IS" BASIS,
  20. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  21. * See the License for the specific language governing permissions and
  22. * limitations under the License.
  23. */
  24. const pixelmatch = require('../third_party/pixelmatch');
  25. const {
  26. diff_match_patch,
  27. DIFF_INSERT,
  28. DIFF_DELETE,
  29. DIFF_EQUAL
  30. } = require('../third_party/diff_match_patch');
  31. function getComparator(mimeType) {
  32. if (mimeType === 'image/png') return compareImages.bind(null, 'image/png');
  33. if (mimeType === 'image/jpeg') return compareImages.bind(null, 'image/jpeg');
  34. if (mimeType === 'text/plain') return compareText;
  35. return compareBuffersOrStrings;
  36. }
  37. const JPEG_JS_MAX_BUFFER_SIZE_IN_MB = 5 * 1024; // ~5 GB
  38. function compareBuffersOrStrings(actualBuffer, expectedBuffer) {
  39. if (typeof actualBuffer === 'string') return compareText(actualBuffer, expectedBuffer);
  40. if (!actualBuffer || !(actualBuffer instanceof Buffer)) return {
  41. errorMessage: 'Actual result should be a Buffer or a string.'
  42. };
  43. if (Buffer.compare(actualBuffer, expectedBuffer)) return {
  44. errorMessage: 'Buffers differ'
  45. };
  46. return null;
  47. }
  48. function compareImages(mimeType, actualBuffer, expectedBuffer, options = {}) {
  49. var _options$_comparator, _ref;
  50. if (!actualBuffer || !(actualBuffer instanceof Buffer)) return {
  51. errorMessage: 'Actual result should be a Buffer.'
  52. };
  53. validateBuffer(expectedBuffer, mimeType);
  54. let actual = mimeType === 'image/png' ? _utilsBundle.PNG.sync.read(actualBuffer) : _utilsBundle.jpegjs.decode(actualBuffer, {
  55. maxMemoryUsageInMB: JPEG_JS_MAX_BUFFER_SIZE_IN_MB
  56. });
  57. let expected = mimeType === 'image/png' ? _utilsBundle.PNG.sync.read(expectedBuffer) : _utilsBundle.jpegjs.decode(expectedBuffer, {
  58. maxMemoryUsageInMB: JPEG_JS_MAX_BUFFER_SIZE_IN_MB
  59. });
  60. const size = {
  61. width: Math.max(expected.width, actual.width),
  62. height: Math.max(expected.height, actual.height)
  63. };
  64. let sizesMismatchError = '';
  65. if (expected.width !== actual.width || expected.height !== actual.height) {
  66. sizesMismatchError = `Expected an image ${expected.width}px by ${expected.height}px, received ${actual.width}px by ${actual.height}px. `;
  67. actual = resizeImage(actual, size);
  68. expected = resizeImage(expected, size);
  69. }
  70. const diff = new _utilsBundle.PNG({
  71. width: size.width,
  72. height: size.height
  73. });
  74. let count;
  75. if (options._comparator === 'ssim-cie94') {
  76. count = (0, _compare.compare)(expected.data, actual.data, diff.data, size.width, size.height, {
  77. // All ΔE* formulae are originally designed to have the difference of 1.0 stand for a "just noticeable difference" (JND).
  78. // See https://en.wikipedia.org/wiki/Color_difference#CIELAB_%CE%94E*
  79. maxColorDeltaE94: 1.0
  80. });
  81. } else if (((_options$_comparator = options._comparator) !== null && _options$_comparator !== void 0 ? _options$_comparator : 'pixelmatch') === 'pixelmatch') {
  82. var _options$threshold;
  83. count = pixelmatch(expected.data, actual.data, diff.data, size.width, size.height, {
  84. threshold: (_options$threshold = options.threshold) !== null && _options$threshold !== void 0 ? _options$threshold : 0.2
  85. });
  86. } else {
  87. throw new Error(`Configuration specifies unknown comparator "${options._comparator}"`);
  88. }
  89. const maxDiffPixels1 = options.maxDiffPixels;
  90. const maxDiffPixels2 = options.maxDiffPixelRatio !== undefined ? expected.width * expected.height * options.maxDiffPixelRatio : undefined;
  91. let maxDiffPixels;
  92. if (maxDiffPixels1 !== undefined && maxDiffPixels2 !== undefined) maxDiffPixels = Math.min(maxDiffPixels1, maxDiffPixels2);else maxDiffPixels = (_ref = maxDiffPixels1 !== null && maxDiffPixels1 !== void 0 ? maxDiffPixels1 : maxDiffPixels2) !== null && _ref !== void 0 ? _ref : 0;
  93. const ratio = Math.ceil(count / (expected.width * expected.height) * 100) / 100;
  94. const pixelsMismatchError = count > maxDiffPixels ? `${count} pixels (ratio ${ratio.toFixed(2)} of all image pixels) are different.` : '';
  95. if (pixelsMismatchError || sizesMismatchError) return {
  96. errorMessage: sizesMismatchError + pixelsMismatchError,
  97. diff: _utilsBundle.PNG.sync.write(diff)
  98. };
  99. return null;
  100. }
  101. function validateBuffer(buffer, mimeType) {
  102. if (mimeType === 'image/png') {
  103. const pngMagicNumber = [137, 80, 78, 71, 13, 10, 26, 10];
  104. if (buffer.length < pngMagicNumber.length || !pngMagicNumber.every((byte, index) => buffer[index] === byte)) throw new Error('could not decode image as PNG.');
  105. } else if (mimeType === 'image/jpeg') {
  106. const jpegMagicNumber = [255, 216];
  107. if (buffer.length < jpegMagicNumber.length || !jpegMagicNumber.every((byte, index) => buffer[index] === byte)) throw new Error('could not decode image as JPEG.');
  108. }
  109. }
  110. function compareText(actual, expectedBuffer) {
  111. if (typeof actual !== 'string') return {
  112. errorMessage: 'Actual result should be a string'
  113. };
  114. const expected = expectedBuffer.toString('utf-8');
  115. if (expected === actual) return null;
  116. const dmp = new diff_match_patch();
  117. const d = dmp.diff_main(expected, actual);
  118. dmp.diff_cleanupSemantic(d);
  119. return {
  120. errorMessage: diff_prettyTerminal(d)
  121. };
  122. }
  123. function diff_prettyTerminal(diffs) {
  124. const html = [];
  125. for (let x = 0; x < diffs.length; x++) {
  126. const op = diffs[x][0]; // Operation (insert, delete, equal)
  127. const data = diffs[x][1]; // Text of change.
  128. const text = data;
  129. switch (op) {
  130. case DIFF_INSERT:
  131. html[x] = _utilsBundle.colors.green(text);
  132. break;
  133. case DIFF_DELETE:
  134. html[x] = _utilsBundle.colors.reset(_utilsBundle.colors.strikethrough(_utilsBundle.colors.red(text)));
  135. break;
  136. case DIFF_EQUAL:
  137. html[x] = text;
  138. break;
  139. }
  140. }
  141. return html.join('');
  142. }
  143. function resizeImage(image, size) {
  144. if (image.width === size.width && image.height === size.height) return image;
  145. const buffer = new Uint8Array(size.width * size.height * 4);
  146. for (let y = 0; y < size.height; y++) {
  147. for (let x = 0; x < size.width; x++) {
  148. const to = (y * size.width + x) * 4;
  149. if (y < image.height && x < image.width) {
  150. const from = (y * image.width + x) * 4;
  151. buffer[to] = image.data[from];
  152. buffer[to + 1] = image.data[from + 1];
  153. buffer[to + 2] = image.data[from + 2];
  154. buffer[to + 3] = image.data[from + 3];
  155. } else {
  156. buffer[to] = 0;
  157. buffer[to + 1] = 0;
  158. buffer[to + 2] = 0;
  159. buffer[to + 3] = 0;
  160. }
  161. }
  162. }
  163. return {
  164. data: Buffer.from(buffer),
  165. width: size.width,
  166. height: size.height
  167. };
  168. }