New 'skdiff' tool to compare two directories full of files, write diff images
[platform/upstream/libSkiaSharp.git] / tools / skdiff_main.cpp
1 #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
24 struct 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
55 typedef SkTDArray<DiffRecord*> RecordArray;
56
57 /// Comparison routine for qsort;  sorts by fMetricValue
58 /// from largest to smallest.
59 static 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.
73 static 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.
91 static 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.
110 typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
111
112 static 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
163 static 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
173 static 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
182 static 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.
193 static inline SkPMColor compute_diff_white(SkPMColor c0, SkPMColor c1) {
194     return SkPackARGB32(0xFF, 0xFF, 0xFF, 0xFF);
195 }
196
197 static 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
211 static 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.
255 static 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.
302 static 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
315 static 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
349 static 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
358 static 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
382 static 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
397 static 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
431 static 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
448 int 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 }