blob: 3bd14c6cc084043a8b6f1d70729666f5befa8c4b [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.
epoger@google.com71329d82012-08-16 13:42:13 +000029 * Recursively descends directories, unless run with --norecurse.
epoger@google.combe6188d2012-05-31 15:13:45 +000030 *
31 * Returns zero exit code if all images match across baseDir and comparisonDir.
tomhudson@google.com4b33d282011-04-27 15:39:30 +000032 */
33
bsalomon@google.com1a315fe2011-09-23 14:56:37 +000034#if SK_BUILD_FOR_WIN32
35 #define PATH_DIV_STR "\\"
36 #define PATH_DIV_CHAR '\\'
37#else
38 #define PATH_DIV_STR "/"
39 #define PATH_DIV_CHAR '/'
40#endif
41
epoger@google.com292aff62012-05-16 14:57:28 +000042// Result of comparison for each pair of files.
epoger@google.com28060e72012-06-28 16:47:34 +000043// Listed from "better" to "worse", for sorting of results.
epoger@google.com292aff62012-05-16 14:57:28 +000044enum Result {
epoger@google.com46a45962012-07-12 18:16:02 +000045 kEqualBits,
46 kEqualPixels,
47 kDifferentPixels,
48 kDifferentSizes,
49 kDifferentOther,
50 kComparisonMissing,
51 kBaseMissing,
52 kUnknown,
epoger@google.com76222c02012-05-31 15:12:09 +000053 //
54 kNumResultTypes // NOT A VALID VALUE--used to set up arrays. Must be last.
epoger@google.com292aff62012-05-16 14:57:28 +000055};
56
epoger@google.comdfbf24e2012-07-13 21:22:02 +000057// Returns the Result with this name.
58// If there is no Result with this name, returns kNumResultTypes.
59// TODO: Is there a better return value for the fall-through case?
bsalomon@google.com100abf42012-09-05 17:40:04 +000060static Result getResultByName(const char *name) {
epoger@google.comdfbf24e2012-07-13 21:22:02 +000061 if (0 == strcmp("EqualBits", name)) {
62 return kEqualBits;
63 }
64 if (0 == strcmp("EqualPixels", name)) {
65 return kEqualPixels;
66 }
67 if (0 == strcmp("DifferentPixels", name)) {
68 return kDifferentPixels;
69 }
70 if (0 == strcmp("DifferentSizes", name)) {
71 return kDifferentSizes;
72 }
73 if (0 == strcmp("DifferentOther", name)) {
74 return kDifferentOther;
75 }
76 if (0 == strcmp("ComparisonMissing", name)) {
77 return kComparisonMissing;
78 }
79 if (0 == strcmp("BaseMissing", name)) {
80 return kBaseMissing;
81 }
82 if (0 == strcmp("Unknown", name)) {
83 return kUnknown;
84 }
85 return kNumResultTypes;
86}
87
epoger@google.com46a45962012-07-12 18:16:02 +000088// Returns a text description of the given Result type.
bsalomon@google.com100abf42012-09-05 17:40:04 +000089static const char *getResultDescription(Result result) {
epoger@google.com46a45962012-07-12 18:16:02 +000090 switch (result) {
91 case kEqualBits:
92 return "contain exactly the same bits";
93 case kEqualPixels:
94 return "contain the same pixel values, but not the same bits";
95 case kDifferentPixels:
96 return "have identical dimensions but some differing pixels";
97 case kDifferentSizes:
98 return "have differing dimensions";
99 case kDifferentOther:
100 return "contain different bits and are not parsable images";
101 case kBaseMissing:
102 return "missing from baseDir";
103 case kComparisonMissing:
104 return "missing from comparisonDir";
105 case kUnknown:
106 return "not compared yet";
107 default:
108 return NULL;
109 }
110}
111
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000112struct DiffRecord {
tomhudson@google.com4e305982011-07-13 17:42:46 +0000113 DiffRecord (const SkString filename,
114 const SkString basePath,
epoger@google.com5fd53852012-03-22 18:20:06 +0000115 const SkString comparisonPath,
epoger@google.com292aff62012-05-16 14:57:28 +0000116 const Result result = kUnknown)
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000117 : fFilename (filename)
tomhudson@google.com4e305982011-07-13 17:42:46 +0000118 , fBasePath (basePath)
119 , fComparisonPath (comparisonPath)
scroggo@google.com4f65a772012-11-19 16:30:08 +0000120 , fBaseBitmap (NULL)
121 , fComparisonBitmap (NULL)
122 , fDifferenceBitmap (NULL)
123 , fWhiteBitmap (NULL)
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000124 , fBaseHeight (0)
125 , fBaseWidth (0)
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000126 , fFractionDifference (0)
127 , fWeightedFraction (0)
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000128 , fAverageMismatchR (0)
129 , fAverageMismatchG (0)
130 , fAverageMismatchB (0)
131 , fMaxMismatchR (0)
132 , fMaxMismatchG (0)
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000133 , fMaxMismatchB (0)
epoger@google.com292aff62012-05-16 14:57:28 +0000134 , fResult(result) {
tomhudson@google.com4e305982011-07-13 17:42:46 +0000135 };
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000136
137 SkString fFilename;
tomhudson@google.com4e305982011-07-13 17:42:46 +0000138 SkString fBasePath;
139 SkString fComparisonPath;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000140
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000141 SkBitmap* fBaseBitmap;
142 SkBitmap* fComparisonBitmap;
143 SkBitmap* fDifferenceBitmap;
epoger@google.com25d961c2012-02-02 20:50:36 +0000144 SkBitmap* fWhiteBitmap;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000145
146 int fBaseHeight;
147 int fBaseWidth;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000148
149 /// Arbitrary floating-point metric to be used to sort images from most
150 /// to least different from baseline; values of 0 will be omitted from the
151 /// summary webpage.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000152 float fFractionDifference;
153 float fWeightedFraction;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000154
155 float fAverageMismatchR;
156 float fAverageMismatchG;
157 float fAverageMismatchB;
158
159 uint32_t fMaxMismatchR;
160 uint32_t fMaxMismatchG;
161 uint32_t fMaxMismatchB;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000162
epoger@google.com292aff62012-05-16 14:57:28 +0000163 /// Which category of diff result.
164 Result fResult;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000165};
166
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000167#define MAX2(a,b) (((b) < (a)) ? (a) : (b))
168#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
169
epoger@google.com25d961c2012-02-02 20:50:36 +0000170const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE);
171const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK);
172
epoger@google.coma5f406e2012-05-01 13:26:16 +0000173typedef SkTDArray<SkString*> StringArray;
174typedef StringArray FileArray;
epoger@google.com5fd53852012-03-22 18:20:06 +0000175
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000176struct DiffSummary {
177 DiffSummary ()
178 : fNumMatches (0)
179 , fNumMismatches (0)
180 , fMaxMismatchV (0)
181 , fMaxMismatchPercent (0) { };
182
epoger@google.com5fd53852012-03-22 18:20:06 +0000183 ~DiffSummary() {
epoger@google.com76222c02012-05-31 15:12:09 +0000184 for (int i = 0; i < kNumResultTypes; i++) {
185 fResultsOfType[i].deleteAll();
186 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000187 }
188
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000189 uint32_t fNumMatches;
190 uint32_t fNumMismatches;
191 uint32_t fMaxMismatchV;
192 float fMaxMismatchPercent;
193
epoger@google.com76222c02012-05-31 15:12:09 +0000194 FileArray fResultsOfType[kNumResultTypes];
195
epoger@google.com3af4ff42012-07-19 17:35:04 +0000196 // Print a line about the contents of this FileArray to stdout.
epoger@google.com46a45962012-07-12 18:16:02 +0000197 void printContents(const FileArray& fileArray, const char* headerText, bool listFilenames) {
epoger@google.com76222c02012-05-31 15:12:09 +0000198 int n = fileArray.count();
epoger@google.com3af4ff42012-07-19 17:35:04 +0000199 printf("%d file pairs %s", n, headerText);
200 if (listFilenames) {
201 printf(": ");
202 for (int i = 0; i < n; ++i) {
203 printf("%s ", fileArray[i]->c_str());
epoger@google.com76222c02012-05-31 15:12:09 +0000204 }
205 }
epoger@google.com3af4ff42012-07-19 17:35:04 +0000206 printf("\n");
epoger@google.com76222c02012-05-31 15:12:09 +0000207 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000208
epoger@google.com3af4ff42012-07-19 17:35:04 +0000209 void print(bool listFilenames, bool failOnResultType[kNumResultTypes]) {
210 printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches);
epoger@google.com46a45962012-07-12 18:16:02 +0000211 for (int resultInt = 0; resultInt < kNumResultTypes; resultInt++) {
212 Result result = static_cast<Result>(resultInt);
epoger@google.com3af4ff42012-07-19 17:35:04 +0000213 if (failOnResultType[result]) {
214 printf("[*] ");
215 } else {
216 printf("[_] ");
217 }
epoger@google.com46a45962012-07-12 18:16:02 +0000218 printContents(fResultsOfType[result], getResultDescription(result), listFilenames);
219 }
epoger@google.com3af4ff42012-07-19 17:35:04 +0000220 printf("(results marked with [*] will cause nonzero return value)\n");
221 printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000222 if (fNumMismatches > 0) {
223 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
epoger@google.com46a45962012-07-12 18:16:02 +0000224 printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000225 }
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000226 }
227
228 void add (DiffRecord* drp) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000229 uint32_t mismatchValue;
epoger@google.com292aff62012-05-16 14:57:28 +0000230
epoger@google.com76222c02012-05-31 15:12:09 +0000231 fResultsOfType[drp->fResult].push(new SkString(drp->fFilename));
epoger@google.com292aff62012-05-16 14:57:28 +0000232 switch (drp->fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000233 case kEqualBits:
234 fNumMatches++;
235 break;
epoger@google.com292aff62012-05-16 14:57:28 +0000236 case kEqualPixels:
237 fNumMatches++;
238 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000239 case kDifferentSizes:
epoger@google.com5fd53852012-03-22 18:20:06 +0000240 fNumMismatches++;
epoger@google.com292aff62012-05-16 14:57:28 +0000241 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000242 case kDifferentPixels:
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000243 fNumMismatches++;
244 if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
245 fMaxMismatchPercent = drp->fFractionDifference * 100;
246 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000247 mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
248 drp->fMaxMismatchB);
249 if (mismatchValue > fMaxMismatchV) {
250 fMaxMismatchV = mismatchValue;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000251 }
epoger@google.com292aff62012-05-16 14:57:28 +0000252 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000253 case kDifferentOther:
254 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000255 break;
256 case kBaseMissing:
257 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000258 break;
259 case kComparisonMissing:
260 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000261 break;
262 case kUnknown:
263 SkDEBUGFAIL("adding uncategorized DiffRecord");
264 break;
265 default:
266 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
267 break;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000268 }
269 }
270};
271
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000272typedef SkTDArray<DiffRecord*> RecordArray;
273
epoger@google.com28060e72012-06-28 16:47:34 +0000274/// A wrapper for any sortProc (comparison routine) which applies a first-order
275/// sort beforehand, and a tiebreaker if the sortProc returns 0.
276template<typename T>
277static int compare(const void* untyped_lhs, const void* untyped_rhs) {
278 const DiffRecord* lhs = *reinterpret_cast<DiffRecord* const*>(untyped_lhs);
279 const DiffRecord* rhs = *reinterpret_cast<DiffRecord* const*>(untyped_rhs);
280
281 // First-order sort... these comparisons should be applied before comparing
282 // pixel values, no matter what.
283 if (lhs->fResult != rhs->fResult) {
284 return (lhs->fResult < rhs->fResult) ? 1 : -1;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000285 }
epoger@google.com28060e72012-06-28 16:47:34 +0000286
287 // Passed first-order sort, so call the pixel comparison routine.
288 int result = T::comparePixels(lhs, rhs);
289 if (result != 0) {
290 return result;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000291 }
epoger@google.com28060e72012-06-28 16:47:34 +0000292
293 // Tiebreaker... if we got to this point, we don't really care
294 // which order they are sorted in, but let's at least be consistent.
295 return strcmp(lhs->fFilename.c_str(), rhs->fFilename.c_str());
tomhudson@google.com5b325292011-05-24 19:41:13 +0000296}
297
epoger@google.com28060e72012-06-28 16:47:34 +0000298/// Comparison routine for qsort; sorts by fFractionDifference
299/// from largest to smallest.
300class CompareDiffMetrics {
301public:
302 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
303 if (lhs->fFractionDifference < rhs->fFractionDifference) {
304 return 1;
305 }
306 if (rhs->fFractionDifference < lhs->fFractionDifference) {
307 return -1;
308 }
309 return 0;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000310 }
epoger@google.com28060e72012-06-28 16:47:34 +0000311};
312
313class CompareDiffWeighted {
314public:
315 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
316 if (lhs->fWeightedFraction < rhs->fWeightedFraction) {
317 return 1;
318 }
319 if (lhs->fWeightedFraction > rhs->fWeightedFraction) {
320 return -1;
321 }
322 return 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000323 }
epoger@google.com28060e72012-06-28 16:47:34 +0000324};
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000325
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000326/// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
327/// from largest to smallest.
epoger@google.com28060e72012-06-28 16:47:34 +0000328class CompareDiffMeanMismatches {
329public:
330 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
331 float leftValue = MAX3(lhs->fAverageMismatchR,
332 lhs->fAverageMismatchG,
333 lhs->fAverageMismatchB);
334 float rightValue = MAX3(rhs->fAverageMismatchR,
335 rhs->fAverageMismatchG,
336 rhs->fAverageMismatchB);
337 if (leftValue < rightValue) {
338 return 1;
339 }
340 if (rightValue < leftValue) {
341 return -1;
342 }
343 return 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000344 }
epoger@google.com28060e72012-06-28 16:47:34 +0000345};
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000346
347/// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
348/// from largest to smallest.
epoger@google.com28060e72012-06-28 16:47:34 +0000349class CompareDiffMaxMismatches {
350public:
351 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
352 uint32_t leftValue = MAX3(lhs->fMaxMismatchR,
353 lhs->fMaxMismatchG,
354 lhs->fMaxMismatchB);
355 uint32_t rightValue = MAX3(rhs->fMaxMismatchR,
356 rhs->fMaxMismatchG,
357 rhs->fMaxMismatchB);
358 if (leftValue < rightValue) {
359 return 1;
360 }
361 if (rightValue < leftValue) {
362 return -1;
363 }
364
365 return CompareDiffMeanMismatches::comparePixels(lhs, rhs);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000366 }
epoger@google.com28060e72012-06-28 16:47:34 +0000367};
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000368
369
370
371/// Parameterized routine to compute the color of a pixel in a difference image.
372typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
373
caryclark@google.com3dd45912012-06-06 12:11:10 +0000374#if 0 // UNUSED
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000375static void expand_and_copy (int width, int height, SkBitmap** dest) {
376 SkBitmap* temp = new SkBitmap ();
377 temp->reset();
378 temp->setConfig((*dest)->config(), width, height);
379 temp->allocPixels();
380 (*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(),
381 temp->rowBytes());
382 *dest = temp;
383}
caryclark@google.com3dd45912012-06-06 12:11:10 +0000384#endif
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000385
epoger@google.com46256ea2012-05-22 13:45:35 +0000386/// Returns true if the two buffers passed in are both non-NULL, and include
387/// exactly the same byte values (and identical lengths).
388static bool are_buffers_equal(SkData* skdata1, SkData* skdata2) {
389 if ((NULL == skdata1) || (NULL == skdata2)) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000390 return false;
391 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000392 if (skdata1->size() != skdata2->size()) {
393 return false;
394 }
395 return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size()));
396}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000397
epoger@google.com46256ea2012-05-22 13:45:35 +0000398/// Reads the file at the given path and returns its complete contents as an
399/// SkData object (or returns NULL on error).
400static SkData* read_file(const char* file_path) {
401 SkFILEStream fileStream(file_path);
402 if (!fileStream.isValid()) {
403 SkDebugf("WARNING: could not open file <%s> for reading\n", file_path);
404 return NULL;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000405 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000406 size_t bytesInFile = fileStream.getLength();
407 size_t bytesLeftToRead = bytesInFile;
408
409 void* bufferStart = sk_malloc_throw(bytesInFile);
410 char* bufferPointer = (char*)bufferStart;
411 while (bytesLeftToRead > 0) {
412 size_t bytesReadThisTime = fileStream.read(
413 bufferPointer, bytesLeftToRead);
414 if (0 == bytesReadThisTime) {
415 SkDebugf("WARNING: error reading from <%s>\n", file_path);
416 sk_free(bufferStart);
417 return NULL;
418 }
419 bytesLeftToRead -= bytesReadThisTime;
420 bufferPointer += bytesReadThisTime;
421 }
422 return SkData::NewFromMalloc(bufferStart, bytesInFile);
423}
424
425/// Decodes binary contents of baseFile and comparisonFile into
426/// diffRecord->fBaseBitmap and diffRecord->fComparisonBitmap.
427/// Returns true if that succeeds.
428static bool get_bitmaps (SkData* baseFileContents,
429 SkData* comparisonFileContents,
430 DiffRecord* diffRecord) {
431 SkMemoryStream compareStream(comparisonFileContents->data(),
432 comparisonFileContents->size());
433 SkMemoryStream baseStream(baseFileContents->data(),
434 baseFileContents->size());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000435
436 SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
437 if (NULL == codec) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000438 SkDebugf("ERROR: no codec found for basePath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000439 diffRecord->fBasePath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000440 return false;
441 }
442
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000443 // In debug, the DLL will automatically be unloaded when this is deleted,
444 // but that shouldn't be a problem in release mode.
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000445 SkAutoTDelete<SkImageDecoder> ad(codec);
446
447 baseStream.rewind();
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000448 if (!codec->decode(&baseStream, diffRecord->fBaseBitmap,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000449 SkBitmap::kARGB_8888_Config,
450 SkImageDecoder::kDecodePixels_Mode)) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000451 SkDebugf("ERROR: codec failed for basePath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000452 diffRecord->fBasePath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000453 return false;
454 }
455
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000456 diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width();
457 diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height();
458
459 if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000460 SkBitmap::kARGB_8888_Config,
461 SkImageDecoder::kDecodePixels_Mode)) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000462 SkDebugf("ERROR: codec failed for comparisonPath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000463 diffRecord->fComparisonPath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000464 return false;
465 }
466
467 return true;
468}
469
epoger@google.com5fd53852012-03-22 18:20:06 +0000470static bool get_bitmap_height_width(const SkString& path,
471 int *height, int *width) {
472 SkFILEStream stream(path.c_str());
473 if (!stream.isValid()) {
474 SkDebugf("ERROR: couldn't open file <%s>\n",
475 path.c_str());
476 return false;
477 }
478
479 SkImageDecoder* codec = SkImageDecoder::Factory(&stream);
480 if (NULL == codec) {
481 SkDebugf("ERROR: no codec found for <%s>\n",
482 path.c_str());
483 return false;
484 }
485
486 SkAutoTDelete<SkImageDecoder> ad(codec);
487 SkBitmap bm;
488
489 stream.rewind();
490 if (!codec->decode(&stream, &bm,
491 SkBitmap::kARGB_8888_Config,
492 SkImageDecoder::kDecodePixels_Mode)) {
493 SkDebugf("ERROR: codec failed for <%s>\n",
494 path.c_str());
495 return false;
496 }
497
498 *height = bm.height();
499 *width = bm.width();
500
501 return true;
502}
503
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000504// from gm - thanks to PNG, we need to force all pixels 100% opaque
505static void force_all_opaque(const SkBitmap& bitmap) {
506 SkAutoLockPixels lock(bitmap);
507 for (int y = 0; y < bitmap.height(); y++) {
508 for (int x = 0; x < bitmap.width(); x++) {
509 *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
510 }
511 }
512}
513
514// from gm
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000515static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000516 SkBitmap copy;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000517 bitmap->copyTo(&copy, SkBitmap::kARGB_8888_Config);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000518 force_all_opaque(copy);
519 return SkImageEncoder::EncodeFile(path.c_str(), copy,
520 SkImageEncoder::kPNG_Type, 100);
521}
522
523// from gm
524static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
525 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
526 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
527 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
528
529 return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
530}
531
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000532static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
533 const int threshold) {
534 int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
535 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
536 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
537 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
538
539 return ((SkAbs32(da) <= threshold) &&
540 (SkAbs32(dr) <= threshold) &&
541 (SkAbs32(dg) <= threshold) &&
542 (SkAbs32(db) <= threshold));
543}
544
545// based on gm
epoger@google.com66008522012-05-16 17:40:57 +0000546// Postcondition: when we exit this method, dr->fResult should have some value
547// other than kUnknown.
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000548static void compute_diff(DiffRecord* dr,
549 DiffMetricProc diffFunction,
550 const int colorThreshold) {
epoger@google.com25d961c2012-02-02 20:50:36 +0000551 SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap);
552 SkAutoLockPixels alpWhite(*dr->fWhiteBitmap);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000553
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000554 const int w = dr->fComparisonBitmap->width();
555 const int h = dr->fComparisonBitmap->height();
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000556 int mismatchedPixels = 0;
557 int totalMismatchR = 0;
558 int totalMismatchG = 0;
559 int totalMismatchB = 0;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000560
561 if (w != dr->fBaseWidth || h != dr->fBaseHeight) {
epoger@google.com292aff62012-05-16 14:57:28 +0000562 dr->fResult = kDifferentSizes;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000563 return;
564 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000565 // Accumulate fractionally different pixels, then divide out
566 // # of pixels at the end.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000567 dr->fWeightedFraction = 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000568 for (int y = 0; y < h; y++) {
569 for (int x = 0; x < w; x++) {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000570 SkPMColor c0 = *dr->fBaseBitmap->getAddr32(x, y);
571 SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000572 SkPMColor trueDifference = compute_diff_pmcolor(c0, c1);
573 SkPMColor outputDifference = diffFunction(c0, c1);
574 uint32_t thisR = SkGetPackedR32(trueDifference);
575 uint32_t thisG = SkGetPackedG32(trueDifference);
576 uint32_t thisB = SkGetPackedB32(trueDifference);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000577 totalMismatchR += thisR;
578 totalMismatchG += thisG;
579 totalMismatchB += thisB;
580 // In HSV, value is defined as max RGB component.
581 int value = MAX3(thisR, thisG, thisB);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000582 dr->fWeightedFraction += ((float) value) / 255;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000583 if (thisR > dr->fMaxMismatchR) {
584 dr->fMaxMismatchR = thisR;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000585 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000586 if (thisG > dr->fMaxMismatchG) {
587 dr->fMaxMismatchG = thisG;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000588 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000589 if (thisB > dr->fMaxMismatchB) {
590 dr->fMaxMismatchB = thisB;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000591 }
592 if (!colors_match_thresholded(c0, c1, colorThreshold)) {
593 mismatchedPixels++;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000594 *dr->fDifferenceBitmap->getAddr32(x, y) = outputDifference;
epoger@google.com25d961c2012-02-02 20:50:36 +0000595 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000596 } else {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000597 *dr->fDifferenceBitmap->getAddr32(x, y) = 0;
epoger@google.com25d961c2012-02-02 20:50:36 +0000598 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000599 }
600 }
601 }
epoger@google.com292aff62012-05-16 14:57:28 +0000602 if (0 == mismatchedPixels) {
603 dr->fResult = kEqualPixels;
604 return;
605 }
606 dr->fResult = kDifferentPixels;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000607 int pixelCount = w * h;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000608 dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
609 dr->fWeightedFraction /= pixelCount;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000610 dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
611 dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
612 dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000613}
614
epoger@google.com71329d82012-08-16 13:42:13 +0000615/// Return a copy of the "input" string, within which we have replaced all instances
616/// of oldSubstring with newSubstring.
617///
618/// TODO: If we like this, we should move it into the core SkString implementation,
619/// adding more checks and ample test cases, and paying more attention to efficiency.
620static SkString replace_all(const SkString &input,
621 const char oldSubstring[], const char newSubstring[]) {
622 SkString output;
623 const char *input_cstr = input.c_str();
624 const char *first_char = input_cstr;
625 const char *match_char;
626 int oldSubstringLen = strlen(oldSubstring);
627 while (NULL != (match_char = strstr(first_char, oldSubstring))) {
628 output.append(first_char, (match_char - first_char));
629 output.append(newSubstring);
630 first_char = match_char + oldSubstringLen;
631 }
632 output.append(first_char);
633 return output;
634}
635
epoger@google.com25d961c2012-02-02 20:50:36 +0000636static SkString filename_to_derived_filename (const SkString& filename,
637 const char *suffix) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000638 SkString diffName (filename);
639 const char* cstring = diffName.c_str();
640 int dotOffset = strrchr(cstring, '.') - cstring;
641 diffName.remove(dotOffset, diffName.size() - dotOffset);
epoger@google.com25d961c2012-02-02 20:50:36 +0000642 diffName.append(suffix);
epoger@google.com71329d82012-08-16 13:42:13 +0000643
644 // In case we recursed into subdirectories, replace slashes with something else
645 // so the diffs will all be written into a single flat directory.
646 diffName = replace_all(diffName, PATH_DIV_STR, "_");
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000647 return diffName;
648}
649
epoger@google.com25d961c2012-02-02 20:50:36 +0000650/// Given a image filename, returns the name of the file containing the
651/// associated difference image.
652static SkString filename_to_diff_filename (const SkString& filename) {
653 return filename_to_derived_filename(filename, "-diff.png");
654}
655
656/// Given a image filename, returns the name of the file containing the
657/// "white" difference image.
658static SkString filename_to_white_filename (const SkString& filename) {
659 return filename_to_derived_filename(filename, "-white.png");
660}
661
scroggo@google.com4f65a772012-11-19 16:30:08 +0000662class AutoCreateAndReleaseBitmaps {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000663
scroggo@google.com4f65a772012-11-19 16:30:08 +0000664public:
665 AutoCreateAndReleaseBitmaps(DiffRecord* drp)
666 : fDrp(drp) {
667 SkASSERT(drp != NULL);
668 drp->fBaseBitmap = SkNEW(SkBitmap);
669 drp->fComparisonBitmap = SkNEW(SkBitmap);
670 drp->fDifferenceBitmap = SkNEW(SkBitmap);
671 drp->fWhiteBitmap = SkNEW(SkBitmap);
672 }
673 ~AutoCreateAndReleaseBitmaps() {
674 SkDELETE(fDrp->fBaseBitmap);
675 fDrp->fBaseBitmap = NULL;
676 SkDELETE(fDrp->fComparisonBitmap);
677 fDrp->fComparisonBitmap = NULL;
678 SkDELETE(fDrp->fDifferenceBitmap);
679 fDrp->fDifferenceBitmap = NULL;
680 SkDELETE(fDrp->fWhiteBitmap);
681 fDrp->fWhiteBitmap = NULL;
682 }
683
684private:
685 DiffRecord* fDrp;
686};
tomhudson@google.com7d042802011-07-14 13:15:55 +0000687
epoger@google.coma5f406e2012-05-01 13:26:16 +0000688/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com7d042802011-07-14 13:15:55 +0000689static void create_and_write_diff_image(DiffRecord* drp,
690 DiffMetricProc dmp,
691 const int colorThreshold,
692 const SkString& outputDir,
693 const SkString& filename) {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000694 const int w = drp->fBaseWidth;
695 const int h = drp->fBaseHeight;
696 drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
697 drp->fDifferenceBitmap->allocPixels();
epoger@google.com25d961c2012-02-02 20:50:36 +0000698 drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
699 drp->fWhiteBitmap->allocPixels();
tomhudson@google.com7d042802011-07-14 13:15:55 +0000700
epoger@google.com66008522012-05-16 17:40:57 +0000701 SkASSERT(kUnknown == drp->fResult);
702 compute_diff(drp, dmp, colorThreshold);
703 SkASSERT(kUnknown != drp->fResult);
704
705 if ((kDifferentPixels == drp->fResult) && !outputDir.isEmpty()) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000706 SkString differencePath (outputDir);
707 differencePath.append(filename_to_diff_filename(filename));
708 write_bitmap(differencePath, drp->fDifferenceBitmap);
709 SkString whitePath (outputDir);
710 whitePath.append(filename_to_white_filename(filename));
711 write_bitmap(whitePath, drp->fWhiteBitmap);
712 }
tomhudson@google.com4e305982011-07-13 17:42:46 +0000713}
714
epoger@google.coma5f406e2012-05-01 13:26:16 +0000715/// Returns true if string contains any of these substrings.
716static bool string_contains_any_of(const SkString& string,
717 const StringArray& substrings) {
718 for (int i = 0; i < substrings.count(); i++) {
719 if (string.contains(substrings[i]->c_str())) {
720 return true;
721 }
722 }
723 return false;
724}
725
epoger@google.com71329d82012-08-16 13:42:13 +0000726/// Internal (potentially recursive) implementation of get_file_list.
727static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir,
728 const StringArray& matchSubstrings,
729 const StringArray& nomatchSubstrings,
730 bool recurseIntoSubdirs, FileArray *files) {
731 bool isSubDirEmpty = subDir.isEmpty();
732 SkString dir(rootDir);
733 if (!isSubDirEmpty) {
734 dir.append(PATH_DIV_STR);
735 dir.append(subDir);
736 }
737
738 // Iterate over files (not directories) within dir.
739 SkOSFile::Iter fileIterator(dir.c_str());
740 SkString fileName;
741 while (fileIterator.next(&fileName, false)) {
742 if (fileName.startsWith(".")) {
743 continue;
744 }
745 SkString pathRelativeToRootDir(subDir);
746 if (!isSubDirEmpty) {
747 pathRelativeToRootDir.append(PATH_DIV_STR);
748 }
749 pathRelativeToRootDir.append(fileName);
750 if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) &&
751 !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
752 files->push(new SkString(pathRelativeToRootDir));
753 }
754 }
755
756 // Recurse into any non-ignored subdirectories.
757 if (recurseIntoSubdirs) {
758 SkOSFile::Iter dirIterator(dir.c_str());
759 SkString dirName;
760 while (dirIterator.next(&dirName, true)) {
761 if (dirName.startsWith(".")) {
762 continue;
763 }
764 SkString pathRelativeToRootDir(subDir);
765 if (!isSubDirEmpty) {
766 pathRelativeToRootDir.append(PATH_DIV_STR);
767 }
768 pathRelativeToRootDir.append(dirName);
769 if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
770 get_file_list_subdir(rootDir, pathRelativeToRootDir,
771 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
772 files);
773 }
774 }
775 }
776}
777
778/// Iterate over dir and get all files whose filename:
779/// - matches any of the substrings in matchSubstrings, but...
780/// - DOES NOT match any of the substrings in nomatchSubstrings
781/// - DOES NOT start with a dot (.)
782/// Adds the matching files to the list in *files.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000783static void get_file_list(const SkString& dir,
784 const StringArray& matchSubstrings,
785 const StringArray& nomatchSubstrings,
epoger@google.com71329d82012-08-16 13:42:13 +0000786 bool recurseIntoSubdirs, FileArray *files) {
787 get_file_list_subdir(dir, SkString(""),
788 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
789 files);
epoger@google.com5fd53852012-03-22 18:20:06 +0000790}
791
792static void release_file_list(FileArray *files) {
793 files->deleteAll();
794}
795
796/// Comparison routines for qsort, sort by file names.
797static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
798 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
799}
800
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000801/// Creates difference images, returns the number that have a 0 metric.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000802/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000803static void create_diff_images (DiffMetricProc dmp,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000804 const int colorThreshold,
805 RecordArray* differences,
806 const SkString& baseDir,
807 const SkString& comparisonDir,
808 const SkString& outputDir,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000809 const StringArray& matchSubstrings,
810 const StringArray& nomatchSubstrings,
epoger@google.com71329d82012-08-16 13:42:13 +0000811 bool recurseIntoSubdirs,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000812 DiffSummary* summary) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000813 SkASSERT(!baseDir.isEmpty());
814 SkASSERT(!comparisonDir.isEmpty());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000815
epoger@google.com5fd53852012-03-22 18:20:06 +0000816 FileArray baseFiles;
817 FileArray comparisonFiles;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000818
epoger@google.com71329d82012-08-16 13:42:13 +0000819 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
820 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000821 &comparisonFiles);
epoger@google.com5fd53852012-03-22 18:20:06 +0000822
epoger@google.coma5f406e2012-05-01 13:26:16 +0000823 if (!baseFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000824 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
825 SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000826 }
827 if (!comparisonFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000828 qsort(comparisonFiles.begin(), comparisonFiles.count(),
829 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000830 }
epoger@google.com66008522012-05-16 17:40:57 +0000831
epoger@google.com5fd53852012-03-22 18:20:06 +0000832 int i = 0;
833 int j = 0;
834
835 while (i < baseFiles.count() &&
836 j < comparisonFiles.count()) {
837
tomhudson@google.com4e305982011-07-13 17:42:46 +0000838 SkString basePath (baseDir);
epoger@google.com5fd53852012-03-22 18:20:06 +0000839 basePath.append(*baseFiles[i]);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000840 SkString comparisonPath (comparisonDir);
epoger@google.com5fd53852012-03-22 18:20:06 +0000841 comparisonPath.append(*comparisonFiles[j]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000842
epoger@google.com5fd53852012-03-22 18:20:06 +0000843 DiffRecord *drp = NULL;
844 int v = strcmp(baseFiles[i]->c_str(),
845 comparisonFiles[j]->c_str());
846
847 if (v < 0) {
848 // in baseDir, but not in comparisonDir
epoger@google.com292aff62012-05-16 14:57:28 +0000849 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath,
850 kComparisonMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000851 ++i;
852 } else if (v > 0) {
853 // in comparisonDir, but not in baseDir
epoger@google.com292aff62012-05-16 14:57:28 +0000854 drp = new DiffRecord(*comparisonFiles[j], basePath, comparisonPath,
855 kBaseMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000856 ++j;
857 } else {
epoger@google.com46256ea2012-05-22 13:45:35 +0000858 // Found the same filename in both baseDir and comparisonDir.
epoger@google.com5fd53852012-03-22 18:20:06 +0000859 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath);
epoger@google.com46256ea2012-05-22 13:45:35 +0000860 SkASSERT(kUnknown == drp->fResult);
epoger@google.com5fd53852012-03-22 18:20:06 +0000861
tomhudson@google.com8afae612012-08-14 15:03:35 +0000862 SkData* baseFileBits = NULL;
863 SkData* comparisonFileBits = NULL;
epoger@google.com46256ea2012-05-22 13:45:35 +0000864 if (NULL == (baseFileBits = read_file(basePath.c_str()))) {
865 SkDebugf("WARNING: couldn't read base file <%s>\n",
866 basePath.c_str());
867 drp->fResult = kBaseMissing;
tomhudson@google.com8afae612012-08-14 15:03:35 +0000868 } else if (NULL == (comparisonFileBits = read_file(comparisonPath.c_str()))) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000869 SkDebugf("WARNING: couldn't read comparison file <%s>\n",
870 comparisonPath.c_str());
871 drp->fResult = kComparisonMissing;
872 } else {
873 if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
874 drp->fResult = kEqualBits;
epoger@google.com46256ea2012-05-22 13:45:35 +0000875 } else {
scroggo@google.com4f65a772012-11-19 16:30:08 +0000876 AutoCreateAndReleaseBitmaps createBitmaps(drp);
877 if (get_bitmaps(baseFileBits, comparisonFileBits, drp)) {
878 create_and_write_diff_image(drp, dmp, colorThreshold,
879 outputDir, *baseFiles[i]);
880 } else {
881 drp->fResult = kDifferentOther;
882 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000883 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000884 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000885 if (baseFileBits) {
886 baseFileBits->unref();
887 }
888 if (comparisonFileBits) {
889 comparisonFileBits->unref();
890 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000891 ++i;
892 ++j;
893 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000894 SkASSERT(kUnknown != drp->fResult);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000895 differences->push(drp);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000896 summary->add(drp);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000897 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000898
899 for (; i < baseFiles.count(); ++i) {
900 // files only in baseDir
901 SkString basePath (baseDir);
902 basePath.append(*baseFiles[i]);
903 SkString comparisonPath;
904 DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath,
epoger@google.com292aff62012-05-16 14:57:28 +0000905 comparisonPath, kComparisonMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000906 differences->push(drp);
907 summary->add(drp);
908 }
909
910 for (; j < comparisonFiles.count(); ++j) {
911 // files only in comparisonDir
912 SkString basePath;
913 SkString comparisonPath(comparisonDir);
914 comparisonPath.append(*comparisonFiles[j]);
915 DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath,
epoger@google.com292aff62012-05-16 14:57:28 +0000916 comparisonPath, kBaseMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000917 differences->push(drp);
918 summary->add(drp);
919 }
920
921 release_file_list(&baseFiles);
922 release_file_list(&comparisonFiles);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000923}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000924
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000925/// Make layout more consistent by scaling image to 240 height, 360 width,
926/// or natural size, whichever is smallest.
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000927static int compute_image_height (int height, int width) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000928 int retval = 240;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000929 if (height < retval) {
930 retval = height;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000931 }
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000932 float scale = (float) retval / height;
933 if (width * scale > 360) {
934 scale = (float) 360 / width;
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000935 retval = static_cast<int>(height * scale);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000936 }
937 return retval;
938}
939
epoger@google.com25d961c2012-02-02 20:50:36 +0000940static void print_table_header (SkFILEWStream* stream,
941 const int matchCount,
942 const int colorThreshold,
943 const RecordArray& differences,
944 const SkString &baseDir,
epoger@google.coma2b793c2012-05-15 14:58:53 +0000945 const SkString &comparisonDir,
946 bool doOutputDate=false) {
epoger@google.com25d961c2012-02-02 20:50:36 +0000947 stream->writeText("<table>\n");
948 stream->writeText("<tr><th>");
jkleinert@google.comed01f122012-08-30 22:05:53 +0000949 stream->writeText("select image</th>\n<th>");
epoger@google.coma2b793c2012-05-15 14:58:53 +0000950 if (doOutputDate) {
951 SkTime::DateTime dt;
952 SkTime::GetDateTime(&dt);
953 stream->writeText("SkDiff run at ");
954 stream->writeDecAsText(dt.fHour);
955 stream->writeText(":");
956 if (dt.fMinute < 10) {
957 stream->writeText("0");
958 }
959 stream->writeDecAsText(dt.fMinute);
960 stream->writeText(":");
961 if (dt.fSecond < 10) {
962 stream->writeText("0");
963 }
964 stream->writeDecAsText(dt.fSecond);
965 stream->writeText("<br>");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000966 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000967 stream->writeDecAsText(matchCount);
968 stream->writeText(" of ");
969 stream->writeDecAsText(differences.count());
970 stream->writeText(" images matched ");
971 if (colorThreshold == 0) {
972 stream->writeText("exactly");
973 } else {
974 stream->writeText("within ");
975 stream->writeDecAsText(colorThreshold);
976 stream->writeText(" color units per component");
977 }
978 stream->writeText(".<br>");
epoger@google.com25d961c2012-02-02 20:50:36 +0000979 stream->writeText("</th>\n<th>");
980 stream->writeText("every different pixel shown in white");
981 stream->writeText("</th>\n<th>");
982 stream->writeText("color difference at each pixel");
epoger@google.com46a45962012-07-12 18:16:02 +0000983 stream->writeText("</th>\n<th>baseDir: ");
epoger@google.com25d961c2012-02-02 20:50:36 +0000984 stream->writeText(baseDir.c_str());
epoger@google.com46a45962012-07-12 18:16:02 +0000985 stream->writeText("</th>\n<th>comparisonDir: ");
epoger@google.com25d961c2012-02-02 20:50:36 +0000986 stream->writeText(comparisonDir.c_str());
987 stream->writeText("</th>\n");
988 stream->writeText("</tr>\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000989}
990
991static void print_pixel_count (SkFILEWStream* stream,
992 const DiffRecord& diff) {
993 stream->writeText("<br>(");
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000994 stream->writeDecAsText(static_cast<int>(diff.fFractionDifference *
995 diff.fBaseWidth *
996 diff.fBaseHeight));
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000997 stream->writeText(" pixels)");
tomhudson@google.com5b325292011-05-24 19:41:13 +0000998/*
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000999 stream->writeDecAsText(diff.fWeightedFraction *
tomhudson@google.com9b540ce2011-08-02 14:10:04 +00001000 diff.fBaseWidth *
1001 diff.fBaseHeight);
tomhudson@google.com5b325292011-05-24 19:41:13 +00001002 stream->writeText(" weighted pixels)");
1003*/
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001004}
1005
jkleinert@google.comed01f122012-08-30 22:05:53 +00001006static void print_checkbox_cell (SkFILEWStream* stream,
1007 const DiffRecord& diff) {
1008 stream->writeText("<td><input type=\"checkbox\" name=\"");
1009 stream->writeText(diff.fFilename.c_str());
1010 stream->writeText("\" checked=\"yes\"></td>");
1011}
1012
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001013static void print_label_cell (SkFILEWStream* stream,
1014 const DiffRecord& diff) {
epoger@google.com46256ea2012-05-22 13:45:35 +00001015 char metricBuf [20];
1016
1017 stream->writeText("<td><b>");
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001018 stream->writeText(diff.fFilename.c_str());
epoger@google.com46256ea2012-05-22 13:45:35 +00001019 stream->writeText("</b><br>");
epoger@google.com292aff62012-05-16 14:57:28 +00001020 switch (diff.fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +00001021 case kEqualBits:
1022 SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here");
1023 return;
1024 case kEqualPixels:
1025 SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here");
epoger@google.com5fd53852012-03-22 18:20:06 +00001026 return;
epoger@google.com292aff62012-05-16 14:57:28 +00001027 case kDifferentSizes:
epoger@google.com46256ea2012-05-22 13:45:35 +00001028 stream->writeText("Image sizes differ</td>");
1029 return;
1030 case kDifferentPixels:
1031 sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference);
1032 stream->writeText(metricBuf);
1033 stream->writeText(" of pixels differ");
1034 stream->writeText("\n (");
1035 sprintf(metricBuf, "%12.4f%%", 100 * diff.fWeightedFraction);
1036 stream->writeText(metricBuf);
1037 stream->writeText(" weighted)");
1038 // Write the actual number of pixels that differ if it's < 1%
1039 if (diff.fFractionDifference < 0.01) {
1040 print_pixel_count(stream, diff);
1041 }
1042 stream->writeText("<br>Average color mismatch ");
1043 stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR,
1044 diff.fAverageMismatchG,
1045 diff.fAverageMismatchB)));
1046 stream->writeText("<br>Max color mismatch ");
1047 stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
1048 diff.fMaxMismatchG,
1049 diff.fMaxMismatchB));
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +00001050 stream->writeText("</td>");
epoger@google.com46256ea2012-05-22 13:45:35 +00001051 break;
1052 case kDifferentOther:
1053 stream->writeText("Files differ; unable to parse one or both files</td>");
1054 return;
1055 case kBaseMissing:
1056 stream->writeText("Missing from baseDir</td>");
1057 return;
1058 case kComparisonMissing:
1059 stream->writeText("Missing from comparisonDir</td>");
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +00001060 return;
epoger@google.com292aff62012-05-16 14:57:28 +00001061 default:
epoger@google.com46256ea2012-05-22 13:45:35 +00001062 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
1063 return;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +00001064 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001065}
1066
1067static void print_image_cell (SkFILEWStream* stream,
tomhudson@google.com4e305982011-07-13 17:42:46 +00001068 const SkString& path,
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001069 int height) {
1070 stream->writeText("<td><a href=\"");
tomhudson@google.com4e305982011-07-13 17:42:46 +00001071 stream->writeText(path.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001072 stream->writeText("\"><img src=\"");
tomhudson@google.com4e305982011-07-13 17:42:46 +00001073 stream->writeText(path.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001074 stream->writeText("\" height=\"");
1075 stream->writeDecAsText(height);
1076 stream->writeText("px\"></a></td>");
1077}
1078
caryclark@google.com3dd45912012-06-06 12:11:10 +00001079#if 0 // UNUSED
epoger@google.com01f78702012-04-12 16:32:04 +00001080static void print_text_cell (SkFILEWStream* stream, const char* text) {
1081 stream->writeText("<td align=center>");
1082 if (NULL != text) {
1083 stream->writeText(text);
1084 }
1085 stream->writeText("</td>");
1086}
caryclark@google.com3dd45912012-06-06 12:11:10 +00001087#endif
epoger@google.com01f78702012-04-12 16:32:04 +00001088
epoger@google.com5fd53852012-03-22 18:20:06 +00001089static void print_diff_with_missing_file(SkFILEWStream* stream,
1090 DiffRecord& diff,
1091 const SkString& relativePath) {
1092 stream->writeText("<tr>\n");
jkleinert@google.comed01f122012-08-30 22:05:53 +00001093 print_checkbox_cell(stream, diff);
epoger@google.com5fd53852012-03-22 18:20:06 +00001094 print_label_cell(stream, diff);
1095 stream->writeText("<td>N/A</td>");
1096 stream->writeText("<td>N/A</td>");
epoger@google.com292aff62012-05-16 14:57:28 +00001097 if (kBaseMissing != diff.fResult) {
epoger@google.com5fd53852012-03-22 18:20:06 +00001098 int h, w;
1099 if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) {
1100 stream->writeText("<td>N/A</td>");
1101 } else {
1102 int height = compute_image_height(h, w);
1103 if (!diff.fBasePath.startsWith(PATH_DIV_STR)) {
1104 diff.fBasePath.prepend(relativePath);
1105 }
1106 print_image_cell(stream, diff.fBasePath, height);
1107 }
1108 } else {
1109 stream->writeText("<td>N/A</td>");
1110 }
epoger@google.com292aff62012-05-16 14:57:28 +00001111 if (kComparisonMissing != diff.fResult) {
epoger@google.com5fd53852012-03-22 18:20:06 +00001112 int h, w;
1113 if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) {
1114 stream->writeText("<td>N/A</td>");
1115 } else {
1116 int height = compute_image_height(h, w);
1117 if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) {
1118 diff.fComparisonPath.prepend(relativePath);
1119 }
1120 print_image_cell(stream, diff.fComparisonPath, height);
1121 }
1122 } else {
1123 stream->writeText("<td>N/A</td>");
1124 }
1125 stream->writeText("</tr>\n");
1126 stream->flush();
1127}
1128
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001129static void print_diff_page (const int matchCount,
1130 const int colorThreshold,
1131 const RecordArray& differences,
1132 const SkString& baseDir,
1133 const SkString& comparisonDir,
1134 const SkString& outputDir) {
1135
epoger@google.coma5f406e2012-05-01 13:26:16 +00001136 SkASSERT(!baseDir.isEmpty());
1137 SkASSERT(!comparisonDir.isEmpty());
1138 SkASSERT(!outputDir.isEmpty());
1139
tomhudson@google.com5b325292011-05-24 19:41:13 +00001140 SkString outputPath (outputDir);
1141 outputPath.append("index.html");
1142 //SkFILEWStream outputStream ("index.html");
1143 SkFILEWStream outputStream (outputPath.c_str());
1144
tomhudson@google.com4e305982011-07-13 17:42:46 +00001145 // Need to convert paths from relative-to-cwd to relative-to-outputDir
tomhudson@google.com5b325292011-05-24 19:41:13 +00001146 // FIXME this doesn't work if there are '..' inside the outputDir
bsalomon@google.comf1478f82012-07-30 19:16:02 +00001147
epoger@google.com21ecba82012-08-08 13:47:39 +00001148 bool isPathAbsolute = false;
1149 // On Windows or Linux, a path starting with PATH_DIV_CHAR is absolute.
1150 if (outputDir.size() > 0 && PATH_DIV_CHAR == outputDir[0]) {
1151 isPathAbsolute = true;
1152 }
bsalomon@google.comf1478f82012-07-30 19:16:02 +00001153#ifdef SK_BUILD_FOR_WIN32
epoger@google.com21ecba82012-08-08 13:47:39 +00001154 // On Windows, absolute paths can also start with "x:", where x is any
1155 // drive letter.
1156 if (outputDir.size() > 1 && ':' == outputDir[1]) {
1157 isPathAbsolute = true;
1158 }
bsalomon@google.comf1478f82012-07-30 19:16:02 +00001159#endif
epoger@google.com21ecba82012-08-08 13:47:39 +00001160
tomhudson@google.com5b325292011-05-24 19:41:13 +00001161 SkString relativePath;
epoger@google.com21ecba82012-08-08 13:47:39 +00001162 if (!isPathAbsolute) {
bsalomon@google.comf1478f82012-07-30 19:16:02 +00001163 unsigned int ui;
1164 for (ui = 0; ui < outputDir.size(); ui++) {
1165 if (outputDir[ui] == PATH_DIV_CHAR) {
1166 relativePath.append(".." PATH_DIV_STR);
1167 }
tomhudson@google.com5b325292011-05-24 19:41:13 +00001168 }
1169 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001170
jkleinert@google.comed01f122012-08-30 22:05:53 +00001171 outputStream.writeText(
1172 "<html>\n<head>\n"
1173 "<script src=\"https://ajax.googleapis.com/ajax/"
1174 "libs/jquery/1.7.2/jquery.min.js\"></script>\n"
1175 "<script type=\"text/javascript\">\n"
1176 "function generateCheckedList() {\n"
1177 "var boxes = $(\":checkbox:checked\");\n"
1178 "var fileCmdLineString = '';\n"
1179 "var fileMultiLineString = '';\n"
1180 "for (var i = 0; i < boxes.length; i++) {\n"
1181 "fileMultiLineString += boxes[i].name + '<br>';\n"
skia.committer@gmail.com11f86922012-08-31 17:14:46 +00001182 "fileCmdLineString += boxes[i].name + '&nbsp;';\n"
jkleinert@google.comed01f122012-08-30 22:05:53 +00001183 "}\n"
1184 "$(\"#checkedList\").html(fileCmdLineString + "
1185 "'<br><br>' + fileMultiLineString);\n"
1186 "}\n"
1187 "</script>\n</head>\n<body>\n");
epoger@google.com25d961c2012-02-02 20:50:36 +00001188 print_table_header(&outputStream, matchCount, colorThreshold, differences,
1189 baseDir, comparisonDir);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001190 int i;
1191 for (i = 0; i < differences.count(); i++) {
1192 DiffRecord* diff = differences[i];
epoger@google.com5fd53852012-03-22 18:20:06 +00001193
epoger@google.com292aff62012-05-16 14:57:28 +00001194 switch (diff->fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +00001195 // Cases in which there is no diff to report.
1196 case kEqualBits:
epoger@google.com292aff62012-05-16 14:57:28 +00001197 case kEqualPixels:
1198 continue;
epoger@google.com46256ea2012-05-22 13:45:35 +00001199 // Cases in which we want a detailed pixel diff.
1200 case kDifferentPixels:
1201 break;
1202 // Cases in which the files differed, but we can't display the diff.
1203 case kDifferentSizes:
1204 case kDifferentOther:
epoger@google.com292aff62012-05-16 14:57:28 +00001205 case kBaseMissing:
epoger@google.com292aff62012-05-16 14:57:28 +00001206 case kComparisonMissing:
epoger@google.com5fd53852012-03-22 18:20:06 +00001207 print_diff_with_missing_file(&outputStream, *diff, relativePath);
1208 continue;
epoger@google.com292aff62012-05-16 14:57:28 +00001209 default:
epoger@google.com46256ea2012-05-22 13:45:35 +00001210 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
1211 continue;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001212 }
epoger@google.com5fd53852012-03-22 18:20:06 +00001213
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001214 if (!diff->fBasePath.startsWith(PATH_DIV_STR)) {
tomhudson@google.com4e305982011-07-13 17:42:46 +00001215 diff->fBasePath.prepend(relativePath);
1216 }
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001217 if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) {
tomhudson@google.com4e305982011-07-13 17:42:46 +00001218 diff->fComparisonPath.prepend(relativePath);
1219 }
epoger@google.com5fd53852012-03-22 18:20:06 +00001220
tomhudson@google.com9b540ce2011-08-02 14:10:04 +00001221 int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001222 outputStream.writeText("<tr>\n");
jkleinert@google.comed01f122012-08-30 22:05:53 +00001223 print_checkbox_cell(&outputStream, *diff);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001224 print_label_cell(&outputStream, *diff);
epoger@google.com46256ea2012-05-22 13:45:35 +00001225 print_image_cell(&outputStream,
1226 filename_to_white_filename(diff->fFilename), height);
1227 print_image_cell(&outputStream,
1228 filename_to_diff_filename(diff->fFilename), height);
epoger@google.com25d961c2012-02-02 20:50:36 +00001229 print_image_cell(&outputStream, diff->fBasePath, height);
tomhudson@google.com4e305982011-07-13 17:42:46 +00001230 print_image_cell(&outputStream, diff->fComparisonPath, height);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001231 outputStream.writeText("</tr>\n");
1232 outputStream.flush();
1233 }
jkleinert@google.comed01f122012-08-30 22:05:53 +00001234 outputStream.writeText(
1235 "</table>\n"
1236 "<input type=\"button\" "
1237 "onclick=\"generateCheckedList()\" "
1238 "value=\"Create Rebaseline List\">\n"
1239 "<div id=\"checkedList\"></div>\n"
1240 "</body>\n</html>\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001241 outputStream.flush();
1242}
1243
1244static void usage (char * argv0) {
1245 SkDebugf("Skia baseline image diff tool\n");
epoger@google.coma5f406e2012-05-01 13:26:16 +00001246 SkDebugf("\n"
1247"Usage: \n"
1248" %s <baseDir> <comparisonDir> [outputDir] \n"
epoger@google.coma611c3e2012-05-18 20:10:06 +00001249, argv0, argv0);
tomhudson@google.com7d042802011-07-14 13:15:55 +00001250 SkDebugf(
epoger@google.com46a45962012-07-12 18:16:02 +00001251"\nArguments:"
epoger@google.comdfbf24e2012-07-13 21:22:02 +00001252"\n --failonresult <result>: After comparing all file pairs, exit with nonzero"
1253"\n return code (number of file pairs yielding this"
1254"\n result) if any file pairs yielded this result."
1255"\n This flag may be repeated, in which case the"
1256"\n return code will be the number of fail pairs"
1257"\n yielding ANY of these results."
epoger@google.com46a45962012-07-12 18:16:02 +00001258"\n --help: display this info"
epoger@google.com46a45962012-07-12 18:16:02 +00001259"\n --listfilenames: list all filenames for each result type in stdout"
epoger@google.comdfbf24e2012-07-13 21:22:02 +00001260"\n --match <substring>: compare files whose filenames contain this substring;"
1261"\n if unspecified, compare ALL files."
1262"\n this flag may be repeated."
epoger@google.com46a45962012-07-12 18:16:02 +00001263"\n --nodiffs: don't write out image diffs or index.html, just generate"
1264"\n report on stdout"
epoger@google.comdfbf24e2012-07-13 21:22:02 +00001265"\n --nomatch <substring>: regardless of --match, DO NOT compare files whose"
1266"\n filenames contain this substring."
1267"\n this flag may be repeated."
epoger@google.com46a45962012-07-12 18:16:02 +00001268"\n --noprintdirs: do not print the directories used."
epoger@google.com71329d82012-08-16 13:42:13 +00001269"\n --norecurse: do not recurse into subdirectories."
epoger@google.com46a45962012-07-12 18:16:02 +00001270"\n --sortbymaxmismatch: sort by worst color channel mismatch;"
1271"\n break ties with -sortbymismatch"
1272"\n --sortbymismatch: sort by average color channel mismatch"
1273"\n --threshold <n>: only report differences > n (per color channel) [default 0]"
1274"\n --weighted: sort by # pixels different weighted by color difference"
1275"\n"
1276"\n baseDir: directory to read baseline images from."
1277"\n comparisonDir: directory to read comparison images from"
1278"\n outputDir: directory to write difference images and index.html to;"
1279"\n defaults to comparisonDir"
1280"\n"
1281"\nIf no sort is specified, it will sort by fraction of pixels mismatching."
1282"\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001283}
1284
epoger@google.com70044cc2012-07-12 18:37:55 +00001285const int kNoError = 0;
1286const int kGenericError = -1;
epoger@google.com46a45962012-07-12 18:16:02 +00001287
caryclark@google.com5987f582012-10-02 18:33:14 +00001288int tool_main(int argc, char** argv);
1289int tool_main(int argc, char** argv) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001290 DiffMetricProc diffProc = compute_diff_pmcolor;
epoger@google.com28060e72012-06-28 16:47:34 +00001291 int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001292
1293 // Maximum error tolerated in any one color channel in any one pixel before
1294 // a difference is reported.
1295 int colorThreshold = 0;
1296 SkString baseDir;
1297 SkString comparisonDir;
1298 SkString outputDir;
epoger@google.comdfbf24e2012-07-13 21:22:02 +00001299
epoger@google.coma5f406e2012-05-01 13:26:16 +00001300 StringArray matchSubstrings;
1301 StringArray nomatchSubstrings;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001302
epoger@google.coma5f406e2012-05-01 13:26:16 +00001303 bool generateDiffs = true;
epoger@google.com46a45962012-07-12 18:16:02 +00001304 bool listFilenames = false;
epoger@google.com71329d82012-08-16 13:42:13 +00001305 bool printDirNames = true;
1306 bool recurseIntoSubdirs = true;
tomhudson@google.com7d042802011-07-14 13:15:55 +00001307
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001308 RecordArray differences;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001309 DiffSummary summary;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001310
epoger@google.com3af4ff42012-07-19 17:35:04 +00001311 bool failOnResultType[kNumResultTypes];
1312 for (int i = 0; i < kNumResultTypes; i++) {
1313 failOnResultType[i] = false;
1314 }
1315
epoger@google.coma5f406e2012-05-01 13:26:16 +00001316 int i;
1317 int numUnflaggedArguments = 0;
1318 for (i = 1; i < argc; i++) {
epoger@google.comdfbf24e2012-07-13 21:22:02 +00001319 if (!strcmp(argv[i], "--failonresult")) {
1320 Result type = getResultByName(argv[++i]);
epoger@google.com3af4ff42012-07-19 17:35:04 +00001321 failOnResultType[type] = true;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001322 continue;
1323 }
epoger@google.com46a45962012-07-12 18:16:02 +00001324 if (!strcmp(argv[i], "--help")) {
1325 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +00001326 return kNoError;
epoger@google.com46a45962012-07-12 18:16:02 +00001327 }
1328 if (!strcmp(argv[i], "--listfilenames")) {
1329 listFilenames = true;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001330 continue;
1331 }
1332 if (!strcmp(argv[i], "--match")) {
1333 matchSubstrings.push(new SkString(argv[++i]));
1334 continue;
1335 }
epoger@google.com46a45962012-07-12 18:16:02 +00001336 if (!strcmp(argv[i], "--nodiffs")) {
1337 generateDiffs = false;
1338 continue;
1339 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001340 if (!strcmp(argv[i], "--nomatch")) {
1341 nomatchSubstrings.push(new SkString(argv[++i]));
1342 continue;
1343 }
epoger@google.com46a45962012-07-12 18:16:02 +00001344 if (!strcmp(argv[i], "--noprintdirs")) {
epoger@google.com71329d82012-08-16 13:42:13 +00001345 printDirNames = false;
1346 continue;
1347 }
1348 if (!strcmp(argv[i], "--norecurse")) {
1349 recurseIntoSubdirs = false;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001350 continue;
1351 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001352 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
epoger@google.com28060e72012-06-28 16:47:34 +00001353 sortProc = compare<CompareDiffMaxMismatches>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001354 continue;
1355 }
epoger@google.com46a45962012-07-12 18:16:02 +00001356 if (!strcmp(argv[i], "--sortbymismatch")) {
1357 sortProc = compare<CompareDiffMeanMismatches>;
tomhudson@google.com5b325292011-05-24 19:41:13 +00001358 continue;
1359 }
epoger@google.com46a45962012-07-12 18:16:02 +00001360 if (!strcmp(argv[i], "--threshold")) {
1361 colorThreshold = atoi(argv[++i]);
1362 continue;
1363 }
1364 if (!strcmp(argv[i], "--weighted")) {
1365 sortProc = compare<CompareDiffWeighted>;
keyar@chromium.orga6318192012-07-09 21:01:50 +00001366 continue;
1367 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001368 if (argv[i][0] != '-') {
epoger@google.coma5f406e2012-05-01 13:26:16 +00001369 switch (numUnflaggedArguments++) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001370 case 0:
1371 baseDir.set(argv[i]);
1372 continue;
1373 case 1:
1374 comparisonDir.set(argv[i]);
1375 continue;
1376 case 2:
1377 outputDir.set(argv[i]);
1378 continue;
1379 default:
epoger@google.coma5f406e2012-05-01 13:26:16 +00001380 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001381 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +00001382 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001383 }
1384 }
1385
1386 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
1387 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +00001388 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001389 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001390
epoger@google.coma5f406e2012-05-01 13:26:16 +00001391 if (numUnflaggedArguments == 2) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001392 outputDir = comparisonDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001393 } else if (numUnflaggedArguments != 3) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001394 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +00001395 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001396 }
1397
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001398 if (!baseDir.endsWith(PATH_DIV_STR)) {
1399 baseDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001400 }
epoger@google.com71329d82012-08-16 13:42:13 +00001401 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +00001402 printf("baseDir is [%s]\n", baseDir.c_str());
1403 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001404
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001405 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
1406 comparisonDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001407 }
epoger@google.com71329d82012-08-16 13:42:13 +00001408 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +00001409 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
1410 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001411
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001412 if (!outputDir.endsWith(PATH_DIV_STR)) {
1413 outputDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001414 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001415 if (generateDiffs) {
epoger@google.com71329d82012-08-16 13:42:13 +00001416 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +00001417 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
1418 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001419 } else {
epoger@google.com71329d82012-08-16 13:42:13 +00001420 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +00001421 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
1422 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001423 outputDir.set("");
1424 }
1425
epoger@google.comda4af242012-06-25 18:45:50 +00001426 // If no matchSubstrings were specified, match ALL strings
1427 // (except for whatever nomatchSubstrings were specified, if any).
epoger@google.coma5f406e2012-05-01 13:26:16 +00001428 if (matchSubstrings.isEmpty()) {
1429 matchSubstrings.push(new SkString(""));
1430 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001431
epoger@google.coma611c3e2012-05-18 20:10:06 +00001432 create_diff_images(diffProc, colorThreshold, &differences,
1433 baseDir, comparisonDir, outputDir,
epoger@google.com71329d82012-08-16 13:42:13 +00001434 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &summary);
epoger@google.com3af4ff42012-07-19 17:35:04 +00001435 summary.print(listFilenames, failOnResultType);
tomhudson@google.com7d042802011-07-14 13:15:55 +00001436
1437 if (differences.count()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001438 qsort(differences.begin(), differences.count(),
1439 sizeof(DiffRecord*), sortProc);
tomhudson@google.com7d042802011-07-14 13:15:55 +00001440 }
epoger@google.com66008522012-05-16 17:40:57 +00001441
epoger@google.coma5f406e2012-05-01 13:26:16 +00001442 if (generateDiffs) {
1443 print_diff_page(summary.fNumMatches, colorThreshold, differences,
1444 baseDir, comparisonDir, outputDir);
1445 }
epoger@google.com76222c02012-05-31 15:12:09 +00001446
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001447 for (i = 0; i < differences.count(); i++) {
1448 delete differences[i];
1449 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001450 matchSubstrings.deleteAll();
1451 nomatchSubstrings.deleteAll();
epoger@google.combe6188d2012-05-31 15:13:45 +00001452
epoger@google.comdfbf24e2012-07-13 21:22:02 +00001453 int num_failing_results = 0;
epoger@google.com3af4ff42012-07-19 17:35:04 +00001454 for (int i = 0; i < kNumResultTypes; i++) {
1455 if (failOnResultType[i]) {
1456 num_failing_results += summary.fResultsOfType[i].count();
1457 }
epoger@google.com46a45962012-07-12 18:16:02 +00001458 }
epoger@google.com28659882012-07-16 18:01:06 +00001459
1460 // On Linux (and maybe other platforms too), any results outside of the
1461 // range [0...255] are wrapped (mod 256). Do the conversion ourselves, to
1462 // make sure that we only return 0 when there were no failures.
1463 return (num_failing_results > 255) ? 255 : num_failing_results;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001464}
caryclark@google.com5987f582012-10-02 18:33:14 +00001465
1466#if !defined SK_BUILD_FOR_IOS
1467int main(int argc, char * const argv[]) {
1468 return tool_main(argc, (char**) argv);
1469}
1470#endif