blob: 915e8bd1a8d2905d282c22d598fdcd9533550071 [file] [log] [blame]
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001#include "SkColorPriv.h"
2#include "SkImageDecoder.h"
3#include "SkImageEncoder.h"
4#include "SkOSFile.h"
5#include "SkStream.h"
6#include "SkTDArray.h"
7#include "SkTemplates.h"
8#include "SkTime.h"
9#include "SkTSearch.h"
10#include "SkTypes.h"
11
12/**
13 * skdiff
14 *
15 * Given three directory names, expects to find identically-named files in
16 * each of the first two; the first are treated as a set of baseline,
17 * the second a set of variant images, and a diff image is written into the
18 * third directory for each pair.
19 * Creates an index.html in the current working directory to compare each
20 * pair that does not match exactly.
21 * Does *not* recursively descend directories.
22 */
23
24struct DiffRecord {
25 DiffRecord (const SkString filename)
26 : fFilename (filename)
27 , fMetricValue (0)
28 , fAverageMismatchR (0)
29 , fAverageMismatchG (0)
30 , fAverageMismatchB (0)
31 , fMaxMismatchR (0)
32 , fMaxMismatchG (0)
33 , fMaxMismatchB (0) { };
34
35 SkString fFilename;
36
37 SkBitmap fBaseBitmap;
38 SkBitmap fComparisonBitmap;
39 SkBitmap fDifferenceBitmap;
40
41 /// Arbitrary floating-point metric to be used to sort images from most
42 /// to least different from baseline; values of 0 will be omitted from the
43 /// summary webpage.
44 float fMetricValue;
45
46 float fAverageMismatchR;
47 float fAverageMismatchG;
48 float fAverageMismatchB;
49
50 uint32_t fMaxMismatchR;
51 uint32_t fMaxMismatchG;
52 uint32_t fMaxMismatchB;
53};
54
55typedef SkTDArray<DiffRecord*> RecordArray;
56
57/// Comparison routine for qsort; sorts by fMetricValue
58/// from largest to smallest.
59static int compare_diff_metrics (DiffRecord** lhs, DiffRecord** rhs) {
60 if ((*lhs)->fMetricValue < (*rhs)->fMetricValue) {
61 return 1;
62 }
63 if ((*rhs)->fMetricValue < (*lhs)->fMetricValue) {
64 return -1;
65 }
66 return 0;
67}
68
69#define MAX2(a,b) (((b) < (a)) ? (a) : (b))
70#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
71/// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
72/// from largest to smallest.
73static int compare_diff_mean_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
74 float leftValue = MAX3((*lhs)->fAverageMismatchR,
75 (*lhs)->fAverageMismatchG,
76 (*lhs)->fAverageMismatchB);
77 float rightValue = MAX3((*rhs)->fAverageMismatchR,
78 (*rhs)->fAverageMismatchG,
79 (*rhs)->fAverageMismatchB);
80 if (leftValue < rightValue) {
81 return 1;
82 }
83 if (rightValue < leftValue) {
84 return -1;
85 }
86 return 0;
87}
88
89/// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
90/// from largest to smallest.
91static int compare_diff_max_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
92 float leftValue = MAX3((*lhs)->fMaxMismatchR,
93 (*lhs)->fMaxMismatchG,
94 (*lhs)->fMaxMismatchB);
95 float rightValue = MAX3((*rhs)->fMaxMismatchR,
96 (*rhs)->fMaxMismatchG,
97 (*rhs)->fMaxMismatchB);
98 if (leftValue < rightValue) {
99 return 1;
100 }
101 if (rightValue < leftValue) {
102 return -1;
103 }
104 return compare_diff_mean_mismatches(lhs, rhs);
105}
106
107
108
109/// Parameterized routine to compute the color of a pixel in a difference image.
110typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
111
112static bool get_bitmaps (DiffRecord* diffRecord,
113 const SkString& baseDir,
114 const SkString& comparisonDir) {
115 SkString comparePath (comparisonDir);
116 comparePath.append(diffRecord->fFilename);
117 SkFILEStream compareStream(comparePath.c_str());
118 if (!compareStream.isValid()) {
119 SkDebugf("WARNING: couldn't open comparison file <%s>\n",
120 comparePath.c_str());
121 return false;
122 }
123
124 SkString basePath (baseDir);
125 basePath.append(diffRecord->fFilename);
126 SkFILEStream baseStream(basePath.c_str());
127 if (!baseStream.isValid()) {
128 SkDebugf("ERROR: couldn't open base file <%s>\n",
129 basePath.c_str());
130 return false;
131 }
132
133 SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
134 if (NULL == codec) {
135 SkDebugf("ERROR: no codec found for <%s>\n",
136 basePath.c_str());
137 return false;
138 }
139
140 SkAutoTDelete<SkImageDecoder> ad(codec);
141
142 baseStream.rewind();
143 if (!codec->decode(&baseStream, &diffRecord->fBaseBitmap,
144 SkBitmap::kARGB_8888_Config,
145 SkImageDecoder::kDecodePixels_Mode)) {
146 SkDebugf("ERROR: codec failed for <%s>\n",
147 basePath.c_str());
148 return false;
149 }
150
151 if (!codec->decode(&compareStream, &diffRecord->fComparisonBitmap,
152 SkBitmap::kARGB_8888_Config,
153 SkImageDecoder::kDecodePixels_Mode)) {
154 SkDebugf("ERROR: codec failed for <%s>\n",
155 comparePath.c_str());
156 return false;
157 }
158
159 return true;
160}
161
162// from gm - thanks to PNG, we need to force all pixels 100% opaque
163static void force_all_opaque(const SkBitmap& bitmap) {
164 SkAutoLockPixels lock(bitmap);
165 for (int y = 0; y < bitmap.height(); y++) {
166 for (int x = 0; x < bitmap.width(); x++) {
167 *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
168 }
169 }
170}
171
172// from gm
173static bool write_bitmap(const SkString& path, const SkBitmap& bitmap) {
174 SkBitmap copy;
175 bitmap.copyTo(&copy, SkBitmap::kARGB_8888_Config);
176 force_all_opaque(copy);
177 return SkImageEncoder::EncodeFile(path.c_str(), copy,
178 SkImageEncoder::kPNG_Type, 100);
179}
180
181// from gm
182static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
183 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
184 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
185 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
186
187 return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
188}
189
190/// Returns white on every pixel so that differences jump out at you;
191/// makes it easy to spot areas of difference that are in the least-significant
192/// bits.
193static inline SkPMColor compute_diff_white(SkPMColor c0, SkPMColor c1) {
194 return SkPackARGB32(0xFF, 0xFF, 0xFF, 0xFF);
195}
196
197static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
198 const int threshold) {
199 int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
200 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
201 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
202 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
203
204 return ((SkAbs32(da) <= threshold) &&
205 (SkAbs32(dr) <= threshold) &&
206 (SkAbs32(dg) <= threshold) &&
207 (SkAbs32(db) <= threshold));
208}
209
210// based on gm
211static void compute_diff(DiffRecord* dr,
212 DiffMetricProc diffFunction,
213 const int colorThreshold) {
214 SkAutoLockPixels alp(dr->fDifferenceBitmap);
215
216 const int w = dr->fComparisonBitmap.width();
217 const int h = dr->fComparisonBitmap.height();
218 int mismatchedPixels = 0;
219 int totalMismatchR = 0;
220 int totalMismatchG = 0;
221 int totalMismatchB = 0;
222 for (int y = 0; y < h; y++) {
223 for (int x = 0; x < w; x++) {
224 SkPMColor c0 = *dr->fBaseBitmap.getAddr32(x, y);
225 SkPMColor c1 = *dr->fComparisonBitmap.getAddr32(x, y);
226 SkPMColor d = 0;
227 d = diffFunction(c0, c1);
228 totalMismatchR += SkGetPackedR32(d);
229 totalMismatchG += SkGetPackedG32(d);
230 totalMismatchB += SkGetPackedB32(d);
231 if (SkGetPackedR32(d) > dr->fMaxMismatchR) {
232 dr->fMaxMismatchR = SkGetPackedR32(d);
233 }
234 if (SkGetPackedG32(d) > dr->fMaxMismatchG) {
235 dr->fMaxMismatchG = SkGetPackedG32(d);
236 }
237 if (SkGetPackedB32(d) > dr->fMaxMismatchB) {
238 dr->fMaxMismatchB = SkGetPackedB32(d);
239 }
240 if (!colors_match_thresholded(c0, c1, colorThreshold)) {
241 mismatchedPixels++;
242 *dr->fDifferenceBitmap.getAddr32(x, y) = d;
243 } else {
244 *dr->fDifferenceBitmap.getAddr32(x, y) = 0;
245 }
246 }
247 }
248 dr->fMetricValue = ((float) mismatchedPixels) / (w * h);
249 dr->fAverageMismatchR = ((float) totalMismatchR) / (w * h);
250 dr->fAverageMismatchG = ((float) totalMismatchG) / (w * h);
251 dr->fAverageMismatchB = ((float) totalMismatchB) / (w * h);
252}
253
254/// Creates difference images, returns the number that have a 0 metric.
255static int create_diff_images (DiffMetricProc dmp,
256 SkQSortCompareProc scp,
257 const int colorThreshold,
258 RecordArray* differences,
259 const SkString& baseDir,
260 const SkString& comparisonDir,
261 const SkString& outputDir) {
262
263 //@todo thudson 19 Apr 2011
264 // this lets us know about files in baseDir not in compareDir, but it
265 // doesn't detect files in compareDir not in baseDir. Doing that
266 // efficiently seems to imply iterating through both directories to
267 // create a merged list, and then attempting to process every entry
268 // in that list?
269
270 SkOSFile::Iter baseIterator (baseDir.c_str());
271 SkString filename;
272 int matchCount = 0;
273 while (baseIterator.next(&filename)) {
274 DiffRecord * drp = new DiffRecord (filename);
275 if (!get_bitmaps(drp, baseDir, comparisonDir)) {
276 continue;
277 }
278
279 const int w = drp->fBaseBitmap.width();
280 const int h = drp->fBaseBitmap.height();
281 drp->fDifferenceBitmap.setConfig(SkBitmap::kARGB_8888_Config, w, h);
282 drp->fDifferenceBitmap.allocPixels();
283 compute_diff(drp, dmp, colorThreshold);
284
285 SkString outPath (outputDir);
286 outPath.append(filename);
287 write_bitmap(outPath, drp->fDifferenceBitmap);
288
289 differences->push(drp);
290 if (0 == drp->fMetricValue) {
291 matchCount++;
292 }
293 }
294
295 SkQSort(differences->begin(), differences->count(), sizeof(DiffRecord*),
296 scp);
297 return matchCount;
298}
299
300/// Make layout more consistent by scaling image to 240 height, 360 width,
301/// or natural size, whichever is smallest.
302static int compute_image_height (const SkBitmap& bmp) {
303 int retval = 240;
304 if (bmp.height() < retval) {
305 retval = bmp.height();
306 }
307 float scale = (float) retval / bmp.height();
308 if (bmp.width() * scale > 360) {
309 scale = (float) 360 / bmp.width();
310 retval = bmp.height() * scale;
311 }
312 return retval;
313}
314
315static void print_page_header (SkFILEWStream* stream,
316 const int matchCount,
317 const int colorThreshold,
318 const RecordArray& differences) {
319 SkTime::DateTime dt;
320 SkTime::GetDateTime(&dt);
321 stream->writeText("SkDiff run at ");
322 stream->writeDecAsText(dt.fHour);
323 stream->writeText(":");
324 if (dt.fMinute < 10) {
325 stream->writeText("0");
326 }
327 stream->writeDecAsText(dt.fMinute);
328 stream->writeText(":");
329 if (dt.fSecond < 10) {
330 stream->writeText("0");
331 }
332 stream->writeDecAsText(dt.fSecond);
333 stream->writeText("<br>");
334 stream->writeDecAsText(matchCount);
335 stream->writeText(" of ");
336 stream->writeDecAsText(differences.count());
337 stream->writeText(" images matched ");
338 if (colorThreshold == 0) {
339 stream->writeText("exactly");
340 } else {
341 stream->writeText("within ");
342 stream->writeDecAsText(colorThreshold);
343 stream->writeText(" color units per component");
344 }
345 stream->writeText(".<br>");
346
347}
348
349static void print_pixel_count (SkFILEWStream* stream,
350 const DiffRecord& diff) {
351 stream->writeText("<br>(");
352 stream->writeDecAsText(diff.fMetricValue *
353 diff.fBaseBitmap.width() *
354 diff.fBaseBitmap.height());
355 stream->writeText(" pixels)");
356}
357
358static void print_label_cell (SkFILEWStream* stream,
359 const DiffRecord& diff) {
360 stream->writeText("<td>");
361 stream->writeText(diff.fFilename.c_str());
362 stream->writeText("<br>");
363 char metricBuf [20];
364 sprintf(metricBuf, "%12.4f%%", 100 * diff.fMetricValue);
365 stream->writeText(metricBuf);
366 stream->writeText(" of pixels differ");
367 // Write the actual number of pixels that differ if it's < 1%
368 if (diff.fMetricValue < 0.01) {
369 print_pixel_count(stream, diff);
370 }
371 stream->writeText("<br>Average color mismatch ");
372 stream->writeDecAsText(MAX3(diff.fAverageMismatchR,
373 diff.fAverageMismatchG,
374 diff.fAverageMismatchB));
375 stream->writeText("<br>Max color mismatch ");
376 stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
377 diff.fMaxMismatchG,
378 diff.fMaxMismatchB));
379 stream->writeText("</td>");
380}
381
382static void print_image_cell (SkFILEWStream* stream,
383 const SkString& directory,
384 const SkString& filename,
385 int height) {
386 stream->writeText("<td><a href=\"");
387 stream->writeText(directory.c_str());
388 stream->writeText(filename.c_str());
389 stream->writeText("\"><img src=\"");
390 stream->writeText(directory.c_str());
391 stream->writeText(filename.c_str());
392 stream->writeText("\" height=\"");
393 stream->writeDecAsText(height);
394 stream->writeText("px\"></a></td>");
395}
396
397static void print_diff_page (const int matchCount,
398 const int colorThreshold,
399 const RecordArray& differences,
400 const SkString& baseDir,
401 const SkString& comparisonDir,
402 const SkString& outputDir) {
403
404 SkFILEWStream outputStream ("index.html");
405
406 outputStream.writeText("<html>\n<body>\n");
407 print_page_header(&outputStream, matchCount, colorThreshold, differences);
408
409 outputStream.writeText("<table>\n");
410 int i;
411 for (i = 0; i < differences.count(); i++) {
412 DiffRecord* diff = differences[i];
413 if (0 == diff->fMetricValue) {
414 continue;
415 }
416 int height = compute_image_height(diff->fBaseBitmap);
417 outputStream.writeText("<tr>\n");
418 print_label_cell(&outputStream, *diff);
419 print_image_cell(&outputStream, baseDir, diff->fFilename, height);
420 print_image_cell(&outputStream, outputDir, diff->fFilename, height);
421 print_image_cell(&outputStream, comparisonDir, diff->fFilename,
422 height);
423 outputStream.writeText("</tr>\n");
424 outputStream.flush();
425 }
426 outputStream.writeText("</table>\n");
427 outputStream.writeText("</body>\n</html>\n");
428 outputStream.flush();
429}
430
431static void usage (char * argv0) {
432 SkDebugf("Skia baseline image diff tool\n");
433 SkDebugf("Usage: %s baseDir comparisonDir outputDir\n", argv0);
434 SkDebugf(
435" -white: force all difference pixels to white\n"
436" -threshold n: only report differences > n (in one channel) [default 0]\n"
437" -sortbymismatch: sort by average color channel mismatch\n");
438 SkDebugf(
439" -sortbymaxmismatch: sort by worst color channel mismatch,\n"
440" break ties with -sortbymismatch,\n"
441" [default by fraction of pixels mismatching]\n");
442 SkDebugf(" baseDir: directory to read baseline images from\n");
443 SkDebugf(" comparisonDir: directory to read comparison images from\n");
444 SkDebugf(" outputDir: directory to write difference images to\n");
445 SkDebugf("Also creates an \"index.html\" file in the current directory.\n");
446}
447
448int main (int argc, char ** argv) {
449 DiffMetricProc diffProc = compute_diff_pmcolor;
450 SkQSortCompareProc sortProc = (SkQSortCompareProc) compare_diff_metrics;
451
452 // Maximum error tolerated in any one color channel in any one pixel before
453 // a difference is reported.
454 int colorThreshold = 0;
455 SkString baseDir;
456 SkString comparisonDir;
457 SkString outputDir;
458
459 RecordArray differences;
460
461 int i, j;
462 for (i = 1, j = 0; i < argc; i++) {
463 if (!strcmp(argv[i], "-help")) {
464 usage(argv[0]);
465 return 0;
466 }
467 if (!strcmp(argv[i], "-white")) {
468 diffProc = compute_diff_white;
469 continue;
470 }
471 if (!strcmp(argv[i], "-sortbymismatch")) {
472 sortProc = (SkQSortCompareProc) compare_diff_mean_mismatches;
473 continue;
474 }
475 if (!strcmp(argv[i], "-sortbymaxmismatch")) {
476 sortProc = (SkQSortCompareProc) compare_diff_max_mismatches;
477 continue;
478 }
479 if (!strcmp(argv[i], "-threshold")) {
480 colorThreshold = atoi(argv[++i]);
481 continue;
482 }
483 if (argv[i][0] != '-') {
484 switch (j++) {
485 case 0:
486 baseDir.set(argv[i]);
487 continue;
488 case 1:
489 comparisonDir.set(argv[i]);
490 continue;
491 case 2:
492 outputDir.set(argv[i]);
493 continue;
494 default:
495 usage(argv[0]);
496 return 0;
497 }
498 }
499
500 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
501 usage(argv[0]);
502 return 0;
503 }
504 if (j != 3) {
505 usage(argv[0]);
506 return 0;
507 }
508
509 if (!baseDir.endsWith("/")) {
510 baseDir.append("/");
511 }
512 if (!comparisonDir.endsWith("/")) {
513 comparisonDir.append("/");
514 }
515 if (!outputDir.endsWith("/")) {
516 outputDir.append("/");
517 }
518
519 int matchCount = create_diff_images(diffProc, sortProc,
520 colorThreshold, &differences,
521 baseDir, comparisonDir,
522 outputDir);
523 print_diff_page(matchCount, colorThreshold, differences,
524 baseDir, comparisonDir, outputDir);
525
526 for (i = 0; i < differences.count(); i++) {
527 delete differences[i];
528 }
529}