blob: 3dd46a19610d5e13bc52464b4f4349a6de43e70f [file] [log] [blame]
epoger@google.comec3ed6a2011-07-28 14:26:00 +00001
2/*
3 * Copyright 2011 Google Inc.
4 *
5 * Use of this source code is governed by a BSD-style license that can be
6 * found in the LICENSE file.
7 */
tomhudson@google.com4b33d282011-04-27 15:39:30 +00008#include "SkColorPriv.h"
epoger@google.com46256ea2012-05-22 13:45:35 +00009#include "SkData.h"
tomhudson@google.com4b33d282011-04-27 15:39:30 +000010#include "SkImageDecoder.h"
11#include "SkImageEncoder.h"
12#include "SkOSFile.h"
13#include "SkStream.h"
14#include "SkTDArray.h"
15#include "SkTemplates.h"
16#include "SkTime.h"
17#include "SkTSearch.h"
18#include "SkTypes.h"
19
20/**
21 * skdiff
22 *
23 * Given three directory names, expects to find identically-named files in
24 * each of the first two; the first are treated as a set of baseline,
25 * the second a set of variant images, and a diff image is written into the
26 * third directory for each pair.
tomhudson@google.com7d042802011-07-14 13:15:55 +000027 * Creates an index.html in the current third directory to compare each
tomhudson@google.com4b33d282011-04-27 15:39:30 +000028 * pair that does not match exactly.
29 * Does *not* recursively descend directories.
30 */
31
bsalomon@google.com1a315fe2011-09-23 14:56:37 +000032#if SK_BUILD_FOR_WIN32
33 #define PATH_DIV_STR "\\"
34 #define PATH_DIV_CHAR '\\'
35#else
36 #define PATH_DIV_STR "/"
37 #define PATH_DIV_CHAR '/'
38#endif
39
epoger@google.com292aff62012-05-16 14:57:28 +000040// Result of comparison for each pair of files.
epoger@google.com292aff62012-05-16 14:57:28 +000041enum Result {
42 kEqualBits, // both files in the pair contain exactly the same bits
43 kEqualPixels, // not bitwise equal, but their pixels are exactly the same
44 kDifferentSizes, // both are images we can parse, but of different sizes
45 kDifferentPixels,// both are images we can parse, but with different pixels
46 kDifferentOther, // files have different bits but are not parsable images
47 kBaseMissing, // missing from baseDir
48 kComparisonMissing,// missing from comparisonDir
epoger@google.com76222c02012-05-31 15:12:09 +000049 kUnknown, // not compared yet
50 //
51 kNumResultTypes // NOT A VALID VALUE--used to set up arrays. Must be last.
epoger@google.com292aff62012-05-16 14:57:28 +000052};
53
tomhudson@google.com4b33d282011-04-27 15:39:30 +000054struct DiffRecord {
tomhudson@google.com4e305982011-07-13 17:42:46 +000055 DiffRecord (const SkString filename,
56 const SkString basePath,
epoger@google.com5fd53852012-03-22 18:20:06 +000057 const SkString comparisonPath,
epoger@google.com292aff62012-05-16 14:57:28 +000058 const Result result = kUnknown)
tomhudson@google.com4b33d282011-04-27 15:39:30 +000059 : fFilename (filename)
tomhudson@google.com4e305982011-07-13 17:42:46 +000060 , fBasePath (basePath)
61 , fComparisonPath (comparisonPath)
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000062 , fBaseBitmap (new SkBitmap ())
63 , fComparisonBitmap (new SkBitmap ())
64 , fDifferenceBitmap (new SkBitmap ())
epoger@google.com25d961c2012-02-02 20:50:36 +000065 , fWhiteBitmap (new SkBitmap ())
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000066 , fBaseHeight (0)
67 , fBaseWidth (0)
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000068 , fFractionDifference (0)
69 , fWeightedFraction (0)
tomhudson@google.com4b33d282011-04-27 15:39:30 +000070 , fAverageMismatchR (0)
71 , fAverageMismatchG (0)
72 , fAverageMismatchB (0)
73 , fMaxMismatchR (0)
74 , fMaxMismatchG (0)
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +000075 , fMaxMismatchB (0)
epoger@google.com292aff62012-05-16 14:57:28 +000076 , fResult(result) {
tomhudson@google.com4e305982011-07-13 17:42:46 +000077 };
tomhudson@google.com4b33d282011-04-27 15:39:30 +000078
79 SkString fFilename;
tomhudson@google.com4e305982011-07-13 17:42:46 +000080 SkString fBasePath;
81 SkString fComparisonPath;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000082
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000083 SkBitmap* fBaseBitmap;
84 SkBitmap* fComparisonBitmap;
85 SkBitmap* fDifferenceBitmap;
epoger@google.com25d961c2012-02-02 20:50:36 +000086 SkBitmap* fWhiteBitmap;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000087
88 int fBaseHeight;
89 int fBaseWidth;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000090
91 /// Arbitrary floating-point metric to be used to sort images from most
92 /// to least different from baseline; values of 0 will be omitted from the
93 /// summary webpage.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000094 float fFractionDifference;
95 float fWeightedFraction;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000096
97 float fAverageMismatchR;
98 float fAverageMismatchG;
99 float fAverageMismatchB;
100
101 uint32_t fMaxMismatchR;
102 uint32_t fMaxMismatchG;
103 uint32_t fMaxMismatchB;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000104
epoger@google.com292aff62012-05-16 14:57:28 +0000105 /// Which category of diff result.
106 Result fResult;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000107};
108
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000109#define MAX2(a,b) (((b) < (a)) ? (a) : (b))
110#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
111
epoger@google.com25d961c2012-02-02 20:50:36 +0000112const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE);
113const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK);
114
epoger@google.coma5f406e2012-05-01 13:26:16 +0000115typedef SkTDArray<SkString*> StringArray;
116typedef StringArray FileArray;
epoger@google.com5fd53852012-03-22 18:20:06 +0000117
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000118struct DiffSummary {
119 DiffSummary ()
120 : fNumMatches (0)
121 , fNumMismatches (0)
122 , fMaxMismatchV (0)
123 , fMaxMismatchPercent (0) { };
124
epoger@google.com5fd53852012-03-22 18:20:06 +0000125 ~DiffSummary() {
epoger@google.com76222c02012-05-31 15:12:09 +0000126 for (int i = 0; i < kNumResultTypes; i++) {
127 fResultsOfType[i].deleteAll();
128 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000129 }
130
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000131 uint32_t fNumMatches;
132 uint32_t fNumMismatches;
133 uint32_t fMaxMismatchV;
134 float fMaxMismatchPercent;
135
epoger@google.com76222c02012-05-31 15:12:09 +0000136 FileArray fResultsOfType[kNumResultTypes];
137
138 // Print the contents of this FileArray, if any, to stdout.
139 // (If the FileArray is empty, print nothing.)
140 void printContents(const FileArray& fileArray, const char* headerText) {
141 int n = fileArray.count();
142 if (n > 0) {
143 printf("%s:\n", headerText);
144 for (int i = 0; i < n; ++i) {
145 printf("\t%s\n", fileArray[i]->c_str());
146 }
147 }
148 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000149
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000150 void print () {
epoger@google.com76222c02012-05-31 15:12:09 +0000151 printContents(fResultsOfType[kBaseMissing],
152 "Missing in baseDir");
153 printContents(fResultsOfType[kComparisonMissing],
154 "Missing in comparisonDir");
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000155 printf("%d of %d images matched.\n", fNumMatches,
156 fNumMatches + fNumMismatches);
157 if (fNumMismatches > 0) {
158 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
159 printf("Largest area mismatch was %.2f%% of pixels\n",
160 fMaxMismatchPercent);
161 }
162
163 }
164
165 void add (DiffRecord* drp) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000166 uint32_t mismatchValue;
epoger@google.com292aff62012-05-16 14:57:28 +0000167
epoger@google.com76222c02012-05-31 15:12:09 +0000168 fResultsOfType[drp->fResult].push(new SkString(drp->fFilename));
epoger@google.com292aff62012-05-16 14:57:28 +0000169 switch (drp->fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000170 case kEqualBits:
171 fNumMatches++;
172 break;
epoger@google.com292aff62012-05-16 14:57:28 +0000173 case kEqualPixels:
174 fNumMatches++;
175 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000176 case kDifferentSizes:
epoger@google.com5fd53852012-03-22 18:20:06 +0000177 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000178 drp->fFractionDifference = 2.0;// sort as if 200% of pixels differed
epoger@google.com292aff62012-05-16 14:57:28 +0000179 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000180 case kDifferentPixels:
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000181 fNumMismatches++;
182 if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
183 fMaxMismatchPercent = drp->fFractionDifference * 100;
184 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000185 mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
186 drp->fMaxMismatchB);
187 if (mismatchValue > fMaxMismatchV) {
188 fMaxMismatchV = mismatchValue;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000189 }
epoger@google.com292aff62012-05-16 14:57:28 +0000190 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000191 case kDifferentOther:
192 fNumMismatches++;
193 drp->fFractionDifference = 3.0;// sort as if 300% of pixels differed
194 break;
195 case kBaseMissing:
196 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000197 break;
198 case kComparisonMissing:
199 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000200 break;
201 case kUnknown:
202 SkDEBUGFAIL("adding uncategorized DiffRecord");
203 break;
204 default:
205 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
206 break;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000207 }
208 }
209};
210
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000211typedef SkTDArray<DiffRecord*> RecordArray;
212
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000213/// Comparison routine for qsort; sorts by fFractionDifference
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000214/// from largest to smallest.
215static int compare_diff_metrics (DiffRecord** lhs, DiffRecord** rhs) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000216 if ((*lhs)->fFractionDifference < (*rhs)->fFractionDifference) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000217 return 1;
218 }
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000219 if ((*rhs)->fFractionDifference < (*lhs)->fFractionDifference) {
tomhudson@google.com5b325292011-05-24 19:41:13 +0000220 return -1;
221 }
222 return 0;
223}
224
225static int compare_diff_weighted (DiffRecord** lhs, DiffRecord** rhs) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000226 if ((*lhs)->fWeightedFraction < (*rhs)->fWeightedFraction) {
tomhudson@google.com5b325292011-05-24 19:41:13 +0000227 return 1;
228 }
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000229 if ((*lhs)->fWeightedFraction > (*rhs)->fWeightedFraction) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000230 return -1;
231 }
232 return 0;
233}
234
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000235/// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
236/// from largest to smallest.
237static int compare_diff_mean_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
238 float leftValue = MAX3((*lhs)->fAverageMismatchR,
239 (*lhs)->fAverageMismatchG,
240 (*lhs)->fAverageMismatchB);
241 float rightValue = MAX3((*rhs)->fAverageMismatchR,
242 (*rhs)->fAverageMismatchG,
243 (*rhs)->fAverageMismatchB);
244 if (leftValue < rightValue) {
245 return 1;
246 }
247 if (rightValue < leftValue) {
248 return -1;
249 }
250 return 0;
251}
252
253/// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
254/// from largest to smallest.
255static int compare_diff_max_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000256 uint32_t leftValue = MAX3((*lhs)->fMaxMismatchR,
257 (*lhs)->fMaxMismatchG,
258 (*lhs)->fMaxMismatchB);
259 uint32_t rightValue = MAX3((*rhs)->fMaxMismatchR,
260 (*rhs)->fMaxMismatchG,
261 (*rhs)->fMaxMismatchB);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000262 if (leftValue < rightValue) {
263 return 1;
264 }
265 if (rightValue < leftValue) {
266 return -1;
267 }
268 return compare_diff_mean_mismatches(lhs, rhs);
269}
270
271
272
273/// Parameterized routine to compute the color of a pixel in a difference image.
274typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
275
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000276static void expand_and_copy (int width, int height, SkBitmap** dest) {
277 SkBitmap* temp = new SkBitmap ();
278 temp->reset();
279 temp->setConfig((*dest)->config(), width, height);
280 temp->allocPixels();
281 (*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(),
282 temp->rowBytes());
283 *dest = temp;
284}
285
epoger@google.com46256ea2012-05-22 13:45:35 +0000286/// Returns true if the two buffers passed in are both non-NULL, and include
287/// exactly the same byte values (and identical lengths).
288static bool are_buffers_equal(SkData* skdata1, SkData* skdata2) {
289 if ((NULL == skdata1) || (NULL == skdata2)) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000290 return false;
291 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000292 if (skdata1->size() != skdata2->size()) {
293 return false;
294 }
295 return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size()));
296}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000297
epoger@google.com46256ea2012-05-22 13:45:35 +0000298/// Reads the file at the given path and returns its complete contents as an
299/// SkData object (or returns NULL on error).
300static SkData* read_file(const char* file_path) {
301 SkFILEStream fileStream(file_path);
302 if (!fileStream.isValid()) {
303 SkDebugf("WARNING: could not open file <%s> for reading\n", file_path);
304 return NULL;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000305 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000306 size_t bytesInFile = fileStream.getLength();
307 size_t bytesLeftToRead = bytesInFile;
308
309 void* bufferStart = sk_malloc_throw(bytesInFile);
310 char* bufferPointer = (char*)bufferStart;
311 while (bytesLeftToRead > 0) {
312 size_t bytesReadThisTime = fileStream.read(
313 bufferPointer, bytesLeftToRead);
314 if (0 == bytesReadThisTime) {
315 SkDebugf("WARNING: error reading from <%s>\n", file_path);
316 sk_free(bufferStart);
317 return NULL;
318 }
319 bytesLeftToRead -= bytesReadThisTime;
320 bufferPointer += bytesReadThisTime;
321 }
322 return SkData::NewFromMalloc(bufferStart, bytesInFile);
323}
324
325/// Decodes binary contents of baseFile and comparisonFile into
326/// diffRecord->fBaseBitmap and diffRecord->fComparisonBitmap.
327/// Returns true if that succeeds.
328static bool get_bitmaps (SkData* baseFileContents,
329 SkData* comparisonFileContents,
330 DiffRecord* diffRecord) {
331 SkMemoryStream compareStream(comparisonFileContents->data(),
332 comparisonFileContents->size());
333 SkMemoryStream baseStream(baseFileContents->data(),
334 baseFileContents->size());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000335
336 SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
337 if (NULL == codec) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000338 SkDebugf("ERROR: no codec found for basePath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000339 diffRecord->fBasePath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000340 return false;
341 }
342
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000343 // In debug, the DLL will automatically be unloaded when this is deleted,
344 // but that shouldn't be a problem in release mode.
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000345 SkAutoTDelete<SkImageDecoder> ad(codec);
346
347 baseStream.rewind();
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000348 if (!codec->decode(&baseStream, diffRecord->fBaseBitmap,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000349 SkBitmap::kARGB_8888_Config,
350 SkImageDecoder::kDecodePixels_Mode)) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000351 SkDebugf("ERROR: codec failed for basePath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000352 diffRecord->fBasePath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000353 return false;
354 }
355
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000356 diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width();
357 diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height();
358
359 if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000360 SkBitmap::kARGB_8888_Config,
361 SkImageDecoder::kDecodePixels_Mode)) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000362 SkDebugf("ERROR: codec failed for comparisonPath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000363 diffRecord->fComparisonPath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000364 return false;
365 }
366
367 return true;
368}
369
epoger@google.com5fd53852012-03-22 18:20:06 +0000370static bool get_bitmap_height_width(const SkString& path,
371 int *height, int *width) {
372 SkFILEStream stream(path.c_str());
373 if (!stream.isValid()) {
374 SkDebugf("ERROR: couldn't open file <%s>\n",
375 path.c_str());
376 return false;
377 }
378
379 SkImageDecoder* codec = SkImageDecoder::Factory(&stream);
380 if (NULL == codec) {
381 SkDebugf("ERROR: no codec found for <%s>\n",
382 path.c_str());
383 return false;
384 }
385
386 SkAutoTDelete<SkImageDecoder> ad(codec);
387 SkBitmap bm;
388
389 stream.rewind();
390 if (!codec->decode(&stream, &bm,
391 SkBitmap::kARGB_8888_Config,
392 SkImageDecoder::kDecodePixels_Mode)) {
393 SkDebugf("ERROR: codec failed for <%s>\n",
394 path.c_str());
395 return false;
396 }
397
398 *height = bm.height();
399 *width = bm.width();
400
401 return true;
402}
403
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000404// from gm - thanks to PNG, we need to force all pixels 100% opaque
405static void force_all_opaque(const SkBitmap& bitmap) {
406 SkAutoLockPixels lock(bitmap);
407 for (int y = 0; y < bitmap.height(); y++) {
408 for (int x = 0; x < bitmap.width(); x++) {
409 *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
410 }
411 }
412}
413
414// from gm
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000415static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000416 SkBitmap copy;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000417 bitmap->copyTo(&copy, SkBitmap::kARGB_8888_Config);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000418 force_all_opaque(copy);
419 return SkImageEncoder::EncodeFile(path.c_str(), copy,
420 SkImageEncoder::kPNG_Type, 100);
421}
422
423// from gm
424static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
425 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
426 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
427 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
428
429 return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
430}
431
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000432static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
433 const int threshold) {
434 int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
435 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
436 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
437 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
438
439 return ((SkAbs32(da) <= threshold) &&
440 (SkAbs32(dr) <= threshold) &&
441 (SkAbs32(dg) <= threshold) &&
442 (SkAbs32(db) <= threshold));
443}
444
445// based on gm
epoger@google.com66008522012-05-16 17:40:57 +0000446// Postcondition: when we exit this method, dr->fResult should have some value
447// other than kUnknown.
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000448static void compute_diff(DiffRecord* dr,
449 DiffMetricProc diffFunction,
450 const int colorThreshold) {
epoger@google.com25d961c2012-02-02 20:50:36 +0000451 SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap);
452 SkAutoLockPixels alpWhite(*dr->fWhiteBitmap);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000453
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000454 const int w = dr->fComparisonBitmap->width();
455 const int h = dr->fComparisonBitmap->height();
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000456 int mismatchedPixels = 0;
457 int totalMismatchR = 0;
458 int totalMismatchG = 0;
459 int totalMismatchB = 0;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000460
461 if (w != dr->fBaseWidth || h != dr->fBaseHeight) {
epoger@google.com292aff62012-05-16 14:57:28 +0000462 dr->fResult = kDifferentSizes;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000463 return;
464 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000465 // Accumulate fractionally different pixels, then divide out
466 // # of pixels at the end.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000467 dr->fWeightedFraction = 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000468 for (int y = 0; y < h; y++) {
469 for (int x = 0; x < w; x++) {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000470 SkPMColor c0 = *dr->fBaseBitmap->getAddr32(x, y);
471 SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000472 SkPMColor trueDifference = compute_diff_pmcolor(c0, c1);
473 SkPMColor outputDifference = diffFunction(c0, c1);
474 uint32_t thisR = SkGetPackedR32(trueDifference);
475 uint32_t thisG = SkGetPackedG32(trueDifference);
476 uint32_t thisB = SkGetPackedB32(trueDifference);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000477 totalMismatchR += thisR;
478 totalMismatchG += thisG;
479 totalMismatchB += thisB;
480 // In HSV, value is defined as max RGB component.
481 int value = MAX3(thisR, thisG, thisB);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000482 dr->fWeightedFraction += ((float) value) / 255;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000483 if (thisR > dr->fMaxMismatchR) {
484 dr->fMaxMismatchR = thisR;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000485 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000486 if (thisG > dr->fMaxMismatchG) {
487 dr->fMaxMismatchG = thisG;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000488 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000489 if (thisB > dr->fMaxMismatchB) {
490 dr->fMaxMismatchB = thisB;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000491 }
492 if (!colors_match_thresholded(c0, c1, colorThreshold)) {
493 mismatchedPixels++;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000494 *dr->fDifferenceBitmap->getAddr32(x, y) = outputDifference;
epoger@google.com25d961c2012-02-02 20:50:36 +0000495 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000496 } else {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000497 *dr->fDifferenceBitmap->getAddr32(x, y) = 0;
epoger@google.com25d961c2012-02-02 20:50:36 +0000498 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000499 }
500 }
501 }
epoger@google.com292aff62012-05-16 14:57:28 +0000502 if (0 == mismatchedPixels) {
503 dr->fResult = kEqualPixels;
504 return;
505 }
506 dr->fResult = kDifferentPixels;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000507 int pixelCount = w * h;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000508 dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
509 dr->fWeightedFraction /= pixelCount;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000510 dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
511 dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
512 dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000513}
514
epoger@google.com25d961c2012-02-02 20:50:36 +0000515static SkString filename_to_derived_filename (const SkString& filename,
516 const char *suffix) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000517 SkString diffName (filename);
518 const char* cstring = diffName.c_str();
519 int dotOffset = strrchr(cstring, '.') - cstring;
520 diffName.remove(dotOffset, diffName.size() - dotOffset);
epoger@google.com25d961c2012-02-02 20:50:36 +0000521 diffName.append(suffix);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000522 return diffName;
523}
524
epoger@google.com25d961c2012-02-02 20:50:36 +0000525/// Given a image filename, returns the name of the file containing the
526/// associated difference image.
527static SkString filename_to_diff_filename (const SkString& filename) {
528 return filename_to_derived_filename(filename, "-diff.png");
529}
530
531/// Given a image filename, returns the name of the file containing the
532/// "white" difference image.
533static SkString filename_to_white_filename (const SkString& filename) {
534 return filename_to_derived_filename(filename, "-white.png");
535}
536
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000537static void release_bitmaps(DiffRecord* drp) {
538 delete drp->fBaseBitmap;
539 drp->fBaseBitmap = NULL;
540 delete drp->fComparisonBitmap;
541 drp->fComparisonBitmap = NULL;
542 delete drp->fDifferenceBitmap;
543 drp->fDifferenceBitmap = NULL;
epoger@google.com25d961c2012-02-02 20:50:36 +0000544 delete drp->fWhiteBitmap;
545 drp->fWhiteBitmap = NULL;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000546}
547
tomhudson@google.com7d042802011-07-14 13:15:55 +0000548
epoger@google.coma5f406e2012-05-01 13:26:16 +0000549/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com7d042802011-07-14 13:15:55 +0000550static void create_and_write_diff_image(DiffRecord* drp,
551 DiffMetricProc dmp,
552 const int colorThreshold,
553 const SkString& outputDir,
554 const SkString& filename) {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000555 const int w = drp->fBaseWidth;
556 const int h = drp->fBaseHeight;
557 drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
558 drp->fDifferenceBitmap->allocPixels();
epoger@google.com25d961c2012-02-02 20:50:36 +0000559 drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
560 drp->fWhiteBitmap->allocPixels();
tomhudson@google.com7d042802011-07-14 13:15:55 +0000561
epoger@google.com66008522012-05-16 17:40:57 +0000562 SkASSERT(kUnknown == drp->fResult);
563 compute_diff(drp, dmp, colorThreshold);
564 SkASSERT(kUnknown != drp->fResult);
565
566 if ((kDifferentPixels == drp->fResult) && !outputDir.isEmpty()) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000567 SkString differencePath (outputDir);
568 differencePath.append(filename_to_diff_filename(filename));
569 write_bitmap(differencePath, drp->fDifferenceBitmap);
570 SkString whitePath (outputDir);
571 whitePath.append(filename_to_white_filename(filename));
572 write_bitmap(whitePath, drp->fWhiteBitmap);
573 }
epoger@google.com66008522012-05-16 17:40:57 +0000574
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000575 release_bitmaps(drp);
tomhudson@google.com4e305982011-07-13 17:42:46 +0000576}
577
epoger@google.coma5f406e2012-05-01 13:26:16 +0000578/// Returns true if string contains any of these substrings.
579static bool string_contains_any_of(const SkString& string,
580 const StringArray& substrings) {
581 for (int i = 0; i < substrings.count(); i++) {
582 if (string.contains(substrings[i]->c_str())) {
583 return true;
584 }
585 }
586 return false;
587}
588
589/// Iterate over dir and get all files that:
590/// - match any of the substrings in matchSubstrings, but...
591/// - DO NOT match any of the substrings in nomatchSubstrings
592/// Returns the list of files in *files.
593static void get_file_list(const SkString& dir,
594 const StringArray& matchSubstrings,
595 const StringArray& nomatchSubstrings,
596 FileArray *files) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000597 SkOSFile::Iter it(dir.c_str());
598 SkString filename;
599 while (it.next(&filename)) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000600 if (string_contains_any_of(filename, matchSubstrings) &&
601 !string_contains_any_of(filename, nomatchSubstrings)) {
602 files->push(new SkString(filename));
epoger@google.com5fd53852012-03-22 18:20:06 +0000603 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000604 }
605}
606
607static void release_file_list(FileArray *files) {
608 files->deleteAll();
609}
610
611/// Comparison routines for qsort, sort by file names.
612static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
613 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
614}
615
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000616/// Creates difference images, returns the number that have a 0 metric.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000617/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000618static void create_diff_images (DiffMetricProc dmp,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000619 const int colorThreshold,
620 RecordArray* differences,
621 const SkString& baseDir,
622 const SkString& comparisonDir,
623 const SkString& outputDir,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000624 const StringArray& matchSubstrings,
625 const StringArray& nomatchSubstrings,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000626 DiffSummary* summary) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000627 SkASSERT(!baseDir.isEmpty());
628 SkASSERT(!comparisonDir.isEmpty());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000629
epoger@google.com5fd53852012-03-22 18:20:06 +0000630 FileArray baseFiles;
631 FileArray comparisonFiles;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000632
epoger@google.coma5f406e2012-05-01 13:26:16 +0000633 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, &baseFiles);
634 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings,
635 &comparisonFiles);
epoger@google.com5fd53852012-03-22 18:20:06 +0000636
epoger@google.coma5f406e2012-05-01 13:26:16 +0000637 if (!baseFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000638 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
639 SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000640 }
641 if (!comparisonFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000642 qsort(comparisonFiles.begin(), comparisonFiles.count(),
643 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000644 }
epoger@google.com66008522012-05-16 17:40:57 +0000645
epoger@google.com5fd53852012-03-22 18:20:06 +0000646 int i = 0;
647 int j = 0;
648
649 while (i < baseFiles.count() &&
650 j < comparisonFiles.count()) {
651
tomhudson@google.com4e305982011-07-13 17:42:46 +0000652 SkString basePath (baseDir);
epoger@google.com5fd53852012-03-22 18:20:06 +0000653 basePath.append(*baseFiles[i]);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000654 SkString comparisonPath (comparisonDir);
epoger@google.com5fd53852012-03-22 18:20:06 +0000655 comparisonPath.append(*comparisonFiles[j]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000656
epoger@google.com5fd53852012-03-22 18:20:06 +0000657 DiffRecord *drp = NULL;
658 int v = strcmp(baseFiles[i]->c_str(),
659 comparisonFiles[j]->c_str());
660
661 if (v < 0) {
662 // in baseDir, but not in comparisonDir
epoger@google.com292aff62012-05-16 14:57:28 +0000663 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath,
664 kComparisonMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000665 ++i;
666 } else if (v > 0) {
667 // in comparisonDir, but not in baseDir
epoger@google.com292aff62012-05-16 14:57:28 +0000668 drp = new DiffRecord(*comparisonFiles[j], basePath, comparisonPath,
669 kBaseMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000670 ++j;
671 } else {
epoger@google.com46256ea2012-05-22 13:45:35 +0000672 // Found the same filename in both baseDir and comparisonDir.
epoger@google.com5fd53852012-03-22 18:20:06 +0000673 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath);
epoger@google.com46256ea2012-05-22 13:45:35 +0000674 SkASSERT(kUnknown == drp->fResult);
epoger@google.com5fd53852012-03-22 18:20:06 +0000675
epoger@google.com46256ea2012-05-22 13:45:35 +0000676 SkData* baseFileBits;
677 SkData* comparisonFileBits;
678 if (NULL == (baseFileBits = read_file(basePath.c_str()))) {
679 SkDebugf("WARNING: couldn't read base file <%s>\n",
680 basePath.c_str());
681 drp->fResult = kBaseMissing;
682 } else if (NULL == (comparisonFileBits = read_file(
683 comparisonPath.c_str()))) {
684 SkDebugf("WARNING: couldn't read comparison file <%s>\n",
685 comparisonPath.c_str());
686 drp->fResult = kComparisonMissing;
687 } else {
688 if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
689 drp->fResult = kEqualBits;
690 } else if (get_bitmaps(baseFileBits, comparisonFileBits, drp)) {
691 create_and_write_diff_image(drp, dmp, colorThreshold,
692 outputDir, *baseFiles[i]);
693 } else {
694 drp->fResult = kDifferentOther;
695 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000696 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000697 if (baseFileBits) {
698 baseFileBits->unref();
699 }
700 if (comparisonFileBits) {
701 comparisonFileBits->unref();
702 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000703 ++i;
704 ++j;
705 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000706 SkASSERT(kUnknown != drp->fResult);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000707 differences->push(drp);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000708 summary->add(drp);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000709 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000710
711 for (; i < baseFiles.count(); ++i) {
712 // files only in baseDir
713 SkString basePath (baseDir);
714 basePath.append(*baseFiles[i]);
715 SkString comparisonPath;
716 DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath,
epoger@google.com292aff62012-05-16 14:57:28 +0000717 comparisonPath, kComparisonMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000718 differences->push(drp);
719 summary->add(drp);
720 }
721
722 for (; j < comparisonFiles.count(); ++j) {
723 // files only in comparisonDir
724 SkString basePath;
725 SkString comparisonPath(comparisonDir);
726 comparisonPath.append(*comparisonFiles[j]);
727 DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath,
epoger@google.com292aff62012-05-16 14:57:28 +0000728 comparisonPath, kBaseMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000729 differences->push(drp);
730 summary->add(drp);
731 }
732
733 release_file_list(&baseFiles);
734 release_file_list(&comparisonFiles);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000735}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000736
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000737/// Make layout more consistent by scaling image to 240 height, 360 width,
738/// or natural size, whichever is smallest.
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000739static int compute_image_height (int height, int width) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000740 int retval = 240;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000741 if (height < retval) {
742 retval = height;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000743 }
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000744 float scale = (float) retval / height;
745 if (width * scale > 360) {
746 scale = (float) 360 / width;
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000747 retval = static_cast<int>(height * scale);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000748 }
749 return retval;
750}
751
epoger@google.com25d961c2012-02-02 20:50:36 +0000752static void print_table_header (SkFILEWStream* stream,
753 const int matchCount,
754 const int colorThreshold,
755 const RecordArray& differences,
756 const SkString &baseDir,
epoger@google.coma2b793c2012-05-15 14:58:53 +0000757 const SkString &comparisonDir,
758 bool doOutputDate=false) {
epoger@google.com25d961c2012-02-02 20:50:36 +0000759 stream->writeText("<table>\n");
760 stream->writeText("<tr><th>");
epoger@google.coma2b793c2012-05-15 14:58:53 +0000761 if (doOutputDate) {
762 SkTime::DateTime dt;
763 SkTime::GetDateTime(&dt);
764 stream->writeText("SkDiff run at ");
765 stream->writeDecAsText(dt.fHour);
766 stream->writeText(":");
767 if (dt.fMinute < 10) {
768 stream->writeText("0");
769 }
770 stream->writeDecAsText(dt.fMinute);
771 stream->writeText(":");
772 if (dt.fSecond < 10) {
773 stream->writeText("0");
774 }
775 stream->writeDecAsText(dt.fSecond);
776 stream->writeText("<br>");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000777 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000778 stream->writeDecAsText(matchCount);
779 stream->writeText(" of ");
780 stream->writeDecAsText(differences.count());
781 stream->writeText(" images matched ");
782 if (colorThreshold == 0) {
783 stream->writeText("exactly");
784 } else {
785 stream->writeText("within ");
786 stream->writeDecAsText(colorThreshold);
787 stream->writeText(" color units per component");
788 }
789 stream->writeText(".<br>");
epoger@google.com25d961c2012-02-02 20:50:36 +0000790 stream->writeText("</th>\n<th>");
791 stream->writeText("every different pixel shown in white");
792 stream->writeText("</th>\n<th>");
793 stream->writeText("color difference at each pixel");
794 stream->writeText("</th>\n<th>");
795 stream->writeText(baseDir.c_str());
796 stream->writeText("</th>\n<th>");
797 stream->writeText(comparisonDir.c_str());
798 stream->writeText("</th>\n");
799 stream->writeText("</tr>\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000800}
801
802static void print_pixel_count (SkFILEWStream* stream,
803 const DiffRecord& diff) {
804 stream->writeText("<br>(");
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000805 stream->writeDecAsText(static_cast<int>(diff.fFractionDifference *
806 diff.fBaseWidth *
807 diff.fBaseHeight));
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000808 stream->writeText(" pixels)");
tomhudson@google.com5b325292011-05-24 19:41:13 +0000809/*
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000810 stream->writeDecAsText(diff.fWeightedFraction *
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000811 diff.fBaseWidth *
812 diff.fBaseHeight);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000813 stream->writeText(" weighted pixels)");
814*/
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000815}
816
817static void print_label_cell (SkFILEWStream* stream,
818 const DiffRecord& diff) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000819 char metricBuf [20];
820
821 stream->writeText("<td><b>");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000822 stream->writeText(diff.fFilename.c_str());
epoger@google.com46256ea2012-05-22 13:45:35 +0000823 stream->writeText("</b><br>");
epoger@google.com292aff62012-05-16 14:57:28 +0000824 switch (diff.fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000825 case kEqualBits:
826 SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here");
827 return;
828 case kEqualPixels:
829 SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here");
epoger@google.com5fd53852012-03-22 18:20:06 +0000830 return;
epoger@google.com292aff62012-05-16 14:57:28 +0000831 case kDifferentSizes:
epoger@google.com46256ea2012-05-22 13:45:35 +0000832 stream->writeText("Image sizes differ</td>");
833 return;
834 case kDifferentPixels:
835 sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference);
836 stream->writeText(metricBuf);
837 stream->writeText(" of pixels differ");
838 stream->writeText("\n (");
839 sprintf(metricBuf, "%12.4f%%", 100 * diff.fWeightedFraction);
840 stream->writeText(metricBuf);
841 stream->writeText(" weighted)");
842 // Write the actual number of pixels that differ if it's < 1%
843 if (diff.fFractionDifference < 0.01) {
844 print_pixel_count(stream, diff);
845 }
846 stream->writeText("<br>Average color mismatch ");
847 stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR,
848 diff.fAverageMismatchG,
849 diff.fAverageMismatchB)));
850 stream->writeText("<br>Max color mismatch ");
851 stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
852 diff.fMaxMismatchG,
853 diff.fMaxMismatchB));
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000854 stream->writeText("</td>");
epoger@google.com46256ea2012-05-22 13:45:35 +0000855 break;
856 case kDifferentOther:
857 stream->writeText("Files differ; unable to parse one or both files</td>");
858 return;
859 case kBaseMissing:
860 stream->writeText("Missing from baseDir</td>");
861 return;
862 case kComparisonMissing:
863 stream->writeText("Missing from comparisonDir</td>");
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000864 return;
epoger@google.com292aff62012-05-16 14:57:28 +0000865 default:
epoger@google.com46256ea2012-05-22 13:45:35 +0000866 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
867 return;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000868 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000869}
870
871static void print_image_cell (SkFILEWStream* stream,
tomhudson@google.com4e305982011-07-13 17:42:46 +0000872 const SkString& path,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000873 int height) {
874 stream->writeText("<td><a href=\"");
tomhudson@google.com4e305982011-07-13 17:42:46 +0000875 stream->writeText(path.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000876 stream->writeText("\"><img src=\"");
tomhudson@google.com4e305982011-07-13 17:42:46 +0000877 stream->writeText(path.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000878 stream->writeText("\" height=\"");
879 stream->writeDecAsText(height);
880 stream->writeText("px\"></a></td>");
881}
882
epoger@google.com01f78702012-04-12 16:32:04 +0000883static void print_text_cell (SkFILEWStream* stream, const char* text) {
884 stream->writeText("<td align=center>");
885 if (NULL != text) {
886 stream->writeText(text);
887 }
888 stream->writeText("</td>");
889}
890
epoger@google.com5fd53852012-03-22 18:20:06 +0000891static void print_diff_with_missing_file(SkFILEWStream* stream,
892 DiffRecord& diff,
893 const SkString& relativePath) {
894 stream->writeText("<tr>\n");
895 print_label_cell(stream, diff);
896 stream->writeText("<td>N/A</td>");
897 stream->writeText("<td>N/A</td>");
epoger@google.com292aff62012-05-16 14:57:28 +0000898 if (kBaseMissing != diff.fResult) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000899 int h, w;
900 if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) {
901 stream->writeText("<td>N/A</td>");
902 } else {
903 int height = compute_image_height(h, w);
904 if (!diff.fBasePath.startsWith(PATH_DIV_STR)) {
905 diff.fBasePath.prepend(relativePath);
906 }
907 print_image_cell(stream, diff.fBasePath, height);
908 }
909 } else {
910 stream->writeText("<td>N/A</td>");
911 }
epoger@google.com292aff62012-05-16 14:57:28 +0000912 if (kComparisonMissing != diff.fResult) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000913 int h, w;
914 if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) {
915 stream->writeText("<td>N/A</td>");
916 } else {
917 int height = compute_image_height(h, w);
918 if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) {
919 diff.fComparisonPath.prepend(relativePath);
920 }
921 print_image_cell(stream, diff.fComparisonPath, height);
922 }
923 } else {
924 stream->writeText("<td>N/A</td>");
925 }
926 stream->writeText("</tr>\n");
927 stream->flush();
928}
929
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000930static void print_diff_page (const int matchCount,
931 const int colorThreshold,
932 const RecordArray& differences,
933 const SkString& baseDir,
934 const SkString& comparisonDir,
935 const SkString& outputDir) {
936
epoger@google.coma5f406e2012-05-01 13:26:16 +0000937 SkASSERT(!baseDir.isEmpty());
938 SkASSERT(!comparisonDir.isEmpty());
939 SkASSERT(!outputDir.isEmpty());
940
tomhudson@google.com5b325292011-05-24 19:41:13 +0000941 SkString outputPath (outputDir);
942 outputPath.append("index.html");
943 //SkFILEWStream outputStream ("index.html");
944 SkFILEWStream outputStream (outputPath.c_str());
945
tomhudson@google.com4e305982011-07-13 17:42:46 +0000946 // Need to convert paths from relative-to-cwd to relative-to-outputDir
tomhudson@google.com5b325292011-05-24 19:41:13 +0000947 // FIXME this doesn't work if there are '..' inside the outputDir
948 unsigned int ui;
949 SkString relativePath;
950 for (ui = 0; ui < outputDir.size(); ui++) {
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000951 if (outputDir[ui] == PATH_DIV_CHAR) {
952 relativePath.append(".." PATH_DIV_STR);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000953 }
954 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000955
956 outputStream.writeText("<html>\n<body>\n");
epoger@google.com25d961c2012-02-02 20:50:36 +0000957 print_table_header(&outputStream, matchCount, colorThreshold, differences,
958 baseDir, comparisonDir);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000959 int i;
960 for (i = 0; i < differences.count(); i++) {
961 DiffRecord* diff = differences[i];
epoger@google.com5fd53852012-03-22 18:20:06 +0000962
epoger@google.com292aff62012-05-16 14:57:28 +0000963 switch (diff->fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000964 // Cases in which there is no diff to report.
965 case kEqualBits:
epoger@google.com292aff62012-05-16 14:57:28 +0000966 case kEqualPixels:
967 continue;
epoger@google.com46256ea2012-05-22 13:45:35 +0000968 // Cases in which we want a detailed pixel diff.
969 case kDifferentPixels:
970 break;
971 // Cases in which the files differed, but we can't display the diff.
972 case kDifferentSizes:
973 case kDifferentOther:
epoger@google.com292aff62012-05-16 14:57:28 +0000974 case kBaseMissing:
epoger@google.com292aff62012-05-16 14:57:28 +0000975 case kComparisonMissing:
epoger@google.com5fd53852012-03-22 18:20:06 +0000976 print_diff_with_missing_file(&outputStream, *diff, relativePath);
977 continue;
epoger@google.com292aff62012-05-16 14:57:28 +0000978 default:
epoger@google.com46256ea2012-05-22 13:45:35 +0000979 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
980 continue;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000981 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000982
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000983 if (!diff->fBasePath.startsWith(PATH_DIV_STR)) {
tomhudson@google.com4e305982011-07-13 17:42:46 +0000984 diff->fBasePath.prepend(relativePath);
985 }
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000986 if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) {
tomhudson@google.com4e305982011-07-13 17:42:46 +0000987 diff->fComparisonPath.prepend(relativePath);
988 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000989
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000990 int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000991 outputStream.writeText("<tr>\n");
992 print_label_cell(&outputStream, *diff);
epoger@google.com46256ea2012-05-22 13:45:35 +0000993 print_image_cell(&outputStream,
994 filename_to_white_filename(diff->fFilename), height);
995 print_image_cell(&outputStream,
996 filename_to_diff_filename(diff->fFilename), height);
epoger@google.com25d961c2012-02-02 20:50:36 +0000997 print_image_cell(&outputStream, diff->fBasePath, height);
tomhudson@google.com4e305982011-07-13 17:42:46 +0000998 print_image_cell(&outputStream, diff->fComparisonPath, height);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000999 outputStream.writeText("</tr>\n");
1000 outputStream.flush();
1001 }
1002 outputStream.writeText("</table>\n");
1003 outputStream.writeText("</body>\n</html>\n");
1004 outputStream.flush();
1005}
1006
1007static void usage (char * argv0) {
1008 SkDebugf("Skia baseline image diff tool\n");
epoger@google.coma5f406e2012-05-01 13:26:16 +00001009 SkDebugf("\n"
1010"Usage: \n"
1011" %s <baseDir> <comparisonDir> [outputDir] \n"
epoger@google.coma611c3e2012-05-18 20:10:06 +00001012, argv0, argv0);
epoger@google.coma5f406e2012-05-01 13:26:16 +00001013 SkDebugf("\n"
1014"Arguments: \n"
1015" --nodiffs: don't write out image diffs or index.html, just generate \n"
1016" report on stdout \n"
1017" --threshold <n>: only report differences > n (per color channel) [default 0]\n"
1018" --match: compare files whose filenames contain this substring; if \n"
1019" unspecified, compare ALL files. \n"
1020" this flag may be repeated to add more matching substrings. \n"
1021" --nomatch: regardless of --match, DO NOT compare files whose filenames \n"
1022" contain this substring. \n"
1023" this flag may be repeated to add more forbidden substrings. \n"
tomhudson@google.com7d042802011-07-14 13:15:55 +00001024" --sortbymismatch: sort by average color channel mismatch\n");
1025 SkDebugf(
epoger@google.coma5f406e2012-05-01 13:26:16 +00001026" --sortbymaxmismatch: sort by worst color channel mismatch;\n"
1027" break ties with -sortbymismatch\n"
1028" [default sort is by fraction of pixels mismatching]\n");
tomhudson@google.com5b325292011-05-24 19:41:13 +00001029 SkDebugf(
tomhudson@google.com7d042802011-07-14 13:15:55 +00001030" --weighted: sort by # pixels different weighted by color difference\n");
1031 SkDebugf(
epoger@google.coma611c3e2012-05-18 20:10:06 +00001032" baseDir: directory to read baseline images from.\n");
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001033 SkDebugf(
epoger@google.coma5f406e2012-05-01 13:26:16 +00001034" comparisonDir: directory to read comparison images from\n");
1035 SkDebugf(
1036" outputDir: directory to write difference images and index.html to; \n"
epoger@google.coma611c3e2012-05-18 20:10:06 +00001037" defaults to comparisonDir \n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001038}
1039
1040int main (int argc, char ** argv) {
1041 DiffMetricProc diffProc = compute_diff_pmcolor;
reed@google.comc7a67cb2012-05-07 14:52:12 +00001042 int (*sortProc)(const void*, const void*) = SkCastForQSort(compare_diff_metrics);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001043
1044 // Maximum error tolerated in any one color channel in any one pixel before
1045 // a difference is reported.
1046 int colorThreshold = 0;
1047 SkString baseDir;
1048 SkString comparisonDir;
1049 SkString outputDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001050 StringArray matchSubstrings;
1051 StringArray nomatchSubstrings;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001052
epoger@google.coma5f406e2012-05-01 13:26:16 +00001053 bool generateDiffs = true;
tomhudson@google.com7d042802011-07-14 13:15:55 +00001054
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001055 RecordArray differences;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001056 DiffSummary summary;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001057
epoger@google.coma5f406e2012-05-01 13:26:16 +00001058 int i;
1059 int numUnflaggedArguments = 0;
1060 for (i = 1; i < argc; i++) {
tomhudson@google.com7d042802011-07-14 13:15:55 +00001061 if (!strcmp(argv[i], "--help")) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001062 usage(argv[0]);
1063 return 0;
1064 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001065 if (!strcmp(argv[i], "--nodiffs")) {
1066 generateDiffs = false;
1067 continue;
1068 }
1069 if (!strcmp(argv[i], "--threshold")) {
1070 colorThreshold = atoi(argv[++i]);
1071 continue;
1072 }
1073 if (!strcmp(argv[i], "--match")) {
1074 matchSubstrings.push(new SkString(argv[++i]));
1075 continue;
1076 }
1077 if (!strcmp(argv[i], "--nomatch")) {
1078 nomatchSubstrings.push(new SkString(argv[++i]));
1079 continue;
1080 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001081 if (!strcmp(argv[i], "--sortbymismatch")) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001082 sortProc = SkCastForQSort(compare_diff_mean_mismatches);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001083 continue;
1084 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001085 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001086 sortProc = SkCastForQSort(compare_diff_max_mismatches);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001087 continue;
1088 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001089 if (!strcmp(argv[i], "--weighted")) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001090 sortProc = SkCastForQSort(compare_diff_weighted);
tomhudson@google.com5b325292011-05-24 19:41:13 +00001091 continue;
1092 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001093 if (argv[i][0] != '-') {
epoger@google.coma5f406e2012-05-01 13:26:16 +00001094 switch (numUnflaggedArguments++) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001095 case 0:
1096 baseDir.set(argv[i]);
1097 continue;
1098 case 1:
1099 comparisonDir.set(argv[i]);
1100 continue;
1101 case 2:
1102 outputDir.set(argv[i]);
1103 continue;
1104 default:
epoger@google.coma5f406e2012-05-01 13:26:16 +00001105 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001106 usage(argv[0]);
1107 return 0;
1108 }
1109 }
1110
1111 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
1112 usage(argv[0]);
1113 return 0;
1114 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001115
epoger@google.coma5f406e2012-05-01 13:26:16 +00001116 if (numUnflaggedArguments == 2) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001117 outputDir = comparisonDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001118 } else if (numUnflaggedArguments != 3) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001119 usage(argv[0]);
1120 return 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001121 }
1122
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001123 if (!baseDir.endsWith(PATH_DIV_STR)) {
1124 baseDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001125 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001126 printf("baseDir is [%s]\n", baseDir.c_str());
1127
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001128 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
1129 comparisonDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001130 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001131 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
1132
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001133 if (!outputDir.endsWith(PATH_DIV_STR)) {
1134 outputDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001135 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001136 if (generateDiffs) {
1137 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
1138 } else {
1139 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
1140 outputDir.set("");
1141 }
1142
1143 // Default substring matching:
1144 // - No matter what, don't match any PDF files.
1145 // We may want to change this later, but for now this maintains the filter
1146 // that get_file_list() used to always apply.
1147 // - If no matchSubstrings were specified, match ALL strings.
1148 nomatchSubstrings.push(new SkString(".pdf"));
1149 if (matchSubstrings.isEmpty()) {
1150 matchSubstrings.push(new SkString(""));
1151 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001152
epoger@google.coma611c3e2012-05-18 20:10:06 +00001153 create_diff_images(diffProc, colorThreshold, &differences,
1154 baseDir, comparisonDir, outputDir,
1155 matchSubstrings, nomatchSubstrings, &summary);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001156 summary.print();
tomhudson@google.com7d042802011-07-14 13:15:55 +00001157
1158 if (differences.count()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001159 qsort(differences.begin(), differences.count(),
1160 sizeof(DiffRecord*), sortProc);
tomhudson@google.com7d042802011-07-14 13:15:55 +00001161 }
epoger@google.com66008522012-05-16 17:40:57 +00001162
epoger@google.coma5f406e2012-05-01 13:26:16 +00001163 if (generateDiffs) {
1164 print_diff_page(summary.fNumMatches, colorThreshold, differences,
1165 baseDir, comparisonDir, outputDir);
1166 }
epoger@google.com76222c02012-05-31 15:12:09 +00001167
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001168 for (i = 0; i < differences.count(); i++) {
1169 delete differences[i];
1170 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001171 matchSubstrings.deleteAll();
1172 nomatchSubstrings.deleteAll();
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001173}