1 | 'use strict';
|
2 |
|
3 | const _ = require('lodash');
|
4 | const parseColor = require('parse-color');
|
5 | const colorDiff = require('color-diff');
|
6 | const png = require('./lib/png');
|
7 | const areColorsSame = require('./lib/same-colors');
|
8 | const AntialiasingComparator = require('./lib/antialiasing-comparator');
|
9 | const IgnoreCaretComparator = require('./lib/ignore-caret-comparator');
|
10 | const utils = require('./lib/utils');
|
11 | const readPair = utils.readPair;
|
12 | const getDiffPixelsCoords = utils.getDiffPixelsCoords;
|
13 |
|
14 | const JND = 2.3;
|
15 |
|
16 | const getDiffArea = (diffPixelsCoords) => {
|
17 | const xs = [];
|
18 | const ys = [];
|
19 |
|
20 | diffPixelsCoords.forEach((coords) => {
|
21 | xs.push(coords[0]);
|
22 | ys.push(coords[1]);
|
23 | });
|
24 |
|
25 | const top = Math.min.apply(Math, ys);
|
26 | const bottom = Math.max.apply(Math, ys);
|
27 |
|
28 | const left = Math.min.apply(Math, xs);
|
29 | const right = Math.max.apply(Math, xs);
|
30 |
|
31 | return {left, top, right, bottom};
|
32 | };
|
33 |
|
34 | const makeAntialiasingComparator = (comparator, png1, png2, opts) => {
|
35 | const antialiasingComparator = new AntialiasingComparator(comparator, png1, png2, opts);
|
36 | return (data) => antialiasingComparator.compare(data);
|
37 | };
|
38 |
|
39 | const makeNoCaretColorComparator = (comparator, pixelRatio) => {
|
40 | const caretComparator = new IgnoreCaretComparator(comparator, pixelRatio);
|
41 | return (data) => caretComparator.compare(data);
|
42 | };
|
43 |
|
44 | function makeCIEDE2000Comparator(tolerance) {
|
45 | return function doColorsLookSame(data) {
|
46 | if (areColorsSame(data)) {
|
47 | return true;
|
48 | }
|
49 |
|
50 | const lab1 = colorDiff.rgb_to_lab(data.color1);
|
51 | const lab2 = colorDiff.rgb_to_lab(data.color2);
|
52 |
|
53 | return colorDiff.diff(lab1, lab2) < tolerance;
|
54 | };
|
55 | }
|
56 |
|
57 | const createComparator = (png1, png2, opts) => {
|
58 | let comparator = opts.strict ? areColorsSame : makeCIEDE2000Comparator(opts.tolerance);
|
59 |
|
60 | if (opts.ignoreAntialiasing) {
|
61 | comparator = makeAntialiasingComparator(comparator, png1, png2, opts);
|
62 | }
|
63 |
|
64 | if (opts.ignoreCaret) {
|
65 | comparator = makeNoCaretColorComparator(comparator, opts.pixelRatio);
|
66 | }
|
67 |
|
68 | return comparator;
|
69 | };
|
70 |
|
71 | const iterateRect = (width, height, callback, endCallback) => {
|
72 | const processRow = (y) => {
|
73 | setImmediate(() => {
|
74 | for (let x = 0; x < width; x++) {
|
75 | callback(x, y);
|
76 | }
|
77 |
|
78 | y++;
|
79 |
|
80 | if (y < height) {
|
81 | processRow(y);
|
82 | } else {
|
83 | endCallback();
|
84 | }
|
85 | });
|
86 | };
|
87 |
|
88 | processRow(0);
|
89 | };
|
90 |
|
91 | const buildDiffImage = (png1, png2, options, callback) => {
|
92 | const width = Math.max(png1.width, png2.width);
|
93 | const height = Math.max(png1.height, png2.height);
|
94 | const minWidth = Math.min(png1.width, png2.width);
|
95 | const minHeight = Math.min(png1.height, png2.height);
|
96 | const highlightColor = options.highlightColor;
|
97 | const result = png.empty(width, height);
|
98 |
|
99 | iterateRect(width, height, (x, y) => {
|
100 | if (x >= minWidth || y >= minHeight) {
|
101 | result.setPixel(x, y, highlightColor);
|
102 | return;
|
103 | }
|
104 |
|
105 | const color1 = png1.getPixel(x, y);
|
106 | const color2 = png2.getPixel(x, y);
|
107 |
|
108 | if (!options.comparator({color1, color2, png1, png2, x, y, width, height})) {
|
109 | result.setPixel(x, y, highlightColor);
|
110 | } else {
|
111 | result.setPixel(x, y, color1);
|
112 | }
|
113 | }, () => callback(result));
|
114 | };
|
115 |
|
116 | const parseColorString = (str) => {
|
117 | const parsed = parseColor(str);
|
118 |
|
119 | return {
|
120 | R: parsed.rgb[0],
|
121 | G: parsed.rgb[1],
|
122 | B: parsed.rgb[2]
|
123 | };
|
124 | };
|
125 |
|
126 | const getToleranceFromOpts = (opts) => {
|
127 | if (!_.hasIn(opts, 'tolerance')) {
|
128 | return JND;
|
129 | }
|
130 |
|
131 | if (opts.strict) {
|
132 | throw new TypeError('Unable to use "strict" and "tolerance" options together');
|
133 | }
|
134 |
|
135 | return opts.tolerance;
|
136 | };
|
137 |
|
138 | const prepareOpts = (opts) => {
|
139 | opts.tolerance = getToleranceFromOpts(opts);
|
140 |
|
141 | _.defaults(opts, {
|
142 | ignoreAntialiasing: true,
|
143 | antialiasingTolerance: 0
|
144 | });
|
145 | };
|
146 |
|
147 | const getMaxDiffBounds = (first, second) => ({
|
148 | left: 0,
|
149 | top: 0,
|
150 | right: Math.max(first.width, second.width) - 1,
|
151 | bottom: Math.max(first.height, second.height) - 1
|
152 | });
|
153 |
|
154 | module.exports = exports = function looksSame(reference, image, opts, callback) {
|
155 | if (!callback) {
|
156 | callback = opts;
|
157 | opts = {};
|
158 | }
|
159 |
|
160 | prepareOpts(opts);
|
161 |
|
162 | readPair(reference, image, (error, pair) => {
|
163 | if (error) {
|
164 | return callback(error);
|
165 | }
|
166 |
|
167 | const first = pair.first;
|
168 | const second = pair.second;
|
169 |
|
170 | if (first.width !== second.width || first.height !== second.height) {
|
171 | return process.nextTick(() => callback(null, {equal: false, diffBounds: getMaxDiffBounds(first, second)}));
|
172 | }
|
173 |
|
174 | const comparator = createComparator(first, second, opts);
|
175 | const {stopOnFirstFail} = opts;
|
176 |
|
177 | getDiffPixelsCoords(first, second, comparator, {stopOnFirstFail}, (result) => {
|
178 | const diffBounds = getDiffArea(result);
|
179 |
|
180 | callback(null, {equal: result.length === 0, diffBounds});
|
181 | });
|
182 | });
|
183 | };
|
184 |
|
185 | exports.getDiffArea = function(reference, image, opts, callback) {
|
186 | if (!callback) {
|
187 | callback = opts;
|
188 | opts = {};
|
189 | }
|
190 |
|
191 | prepareOpts(opts);
|
192 |
|
193 | readPair(reference, image, (error, pair) => {
|
194 | if (error) {
|
195 | return callback(error);
|
196 | }
|
197 |
|
198 | const first = pair.first;
|
199 | const second = pair.second;
|
200 |
|
201 | if (first.width !== second.width || first.height !== second.height) {
|
202 | return process.nextTick(() => callback(null, getMaxDiffBounds(first, second)));
|
203 | }
|
204 |
|
205 | const comparator = createComparator(first, second, opts);
|
206 |
|
207 | getDiffPixelsCoords(first, second, comparator, opts, (result) => {
|
208 | if (!result.length) {
|
209 | return callback(null, null);
|
210 | }
|
211 |
|
212 | callback(null, getDiffArea(result));
|
213 | });
|
214 | });
|
215 | };
|
216 |
|
217 | exports.createDiff = function saveDiff(opts, callback) {
|
218 | opts.tolerance = getToleranceFromOpts(opts);
|
219 |
|
220 | readPair(opts.reference, opts.current, (error, {first, second}) => {
|
221 | if (error) {
|
222 | return callback(error);
|
223 | }
|
224 |
|
225 | const diffOptions = {
|
226 | highlightColor: parseColorString(opts.highlightColor),
|
227 | comparator: createComparator(first, second, opts)
|
228 | };
|
229 |
|
230 | buildDiffImage(first, second, diffOptions, (result) => {
|
231 | if (opts.diff === undefined) {
|
232 | result.createBuffer(callback);
|
233 | } else {
|
234 | result.save(opts.diff, callback);
|
235 | }
|
236 | });
|
237 | });
|
238 | };
|
239 |
|
240 | exports.colors = (color1, color2, opts) => {
|
241 | opts = opts || {};
|
242 |
|
243 | if (opts.tolerance === undefined) {
|
244 | opts.tolerance = JND;
|
245 | }
|
246 |
|
247 | const comparator = makeCIEDE2000Comparator(opts.tolerance);
|
248 |
|
249 | return comparator({color1, color2});
|
250 | };
|