UNPKG

7 kBJavaScriptView Raw
1'use strict';
2
3const _ = require('lodash');
4const parseColor = require('parse-color');
5const colorDiff = require('color-diff');
6const png = require('./lib/png');
7const areColorsSame = require('./lib/same-colors');
8const AntialiasingComparator = require('./lib/antialiasing-comparator');
9const IgnoreCaretComparator = require('./lib/ignore-caret-comparator');
10const utils = require('./lib/utils');
11const readPair = utils.readPair;
12const getDiffPixelsCoords = utils.getDiffPixelsCoords;
13
14const JND = 2.3; // Just noticeable difference if ciede2000 >= JND then colors difference is noticeable by human eye
15
16const 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
34const makeAntialiasingComparator = (comparator, png1, png2, opts) => {
35 const antialiasingComparator = new AntialiasingComparator(comparator, png1, png2, opts);
36 return (data) => antialiasingComparator.compare(data);
37};
38
39const makeNoCaretColorComparator = (comparator, pixelRatio) => {
40 const caretComparator = new IgnoreCaretComparator(comparator, pixelRatio);
41 return (data) => caretComparator.compare(data);
42};
43
44function makeCIEDE2000Comparator(tolerance) {
45 return function doColorsLookSame(data) {
46 if (areColorsSame(data)) {
47 return true;
48 }
49 /*jshint camelcase:false*/
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
57const 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
71const 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
91const 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
116const 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
126const 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
138const prepareOpts = (opts) => {
139 opts.tolerance = getToleranceFromOpts(opts);
140
141 _.defaults(opts, {
142 ignoreAntialiasing: true,
143 antialiasingTolerance: 0
144 });
145};
146
147const 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
154module.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
185exports.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
217exports.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
240exports.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};