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