blob: 34fc9751b5260a583ce0d189649300631d359aad [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.
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 {
45 kEqualBits, // both files in the pair contain exactly the same bits
46 kEqualPixels, // not bitwise equal, but their pixels are exactly the same
epoger@google.com292aff62012-05-16 14:57:28 +000047 kDifferentPixels,// both are images we can parse, but with different pixels
epoger@google.com28060e72012-06-28 16:47:34 +000048 kDifferentSizes, // both are images we can parse, but of different sizes
epoger@google.com292aff62012-05-16 14:57:28 +000049 kDifferentOther, // files have different bits but are not parsable images
epoger@google.com292aff62012-05-16 14:57:28 +000050 kComparisonMissing,// missing from comparisonDir
epoger@google.com28060e72012-06-28 16:47:34 +000051 kBaseMissing, // missing from baseDir
epoger@google.com76222c02012-05-31 15:12:09 +000052 kUnknown, // not compared yet
53 //
54 kNumResultTypes // NOT A VALID VALUE--used to set up arrays. Must be last.
epoger@google.com292aff62012-05-16 14:57:28 +000055};
56
tomhudson@google.com4b33d282011-04-27 15:39:30 +000057struct DiffRecord {
tomhudson@google.com4e305982011-07-13 17:42:46 +000058 DiffRecord (const SkString filename,
59 const SkString basePath,
epoger@google.com5fd53852012-03-22 18:20:06 +000060 const SkString comparisonPath,
epoger@google.com292aff62012-05-16 14:57:28 +000061 const Result result = kUnknown)
tomhudson@google.com4b33d282011-04-27 15:39:30 +000062 : fFilename (filename)
tomhudson@google.com4e305982011-07-13 17:42:46 +000063 , fBasePath (basePath)
64 , fComparisonPath (comparisonPath)
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000065 , fBaseBitmap (new SkBitmap ())
66 , fComparisonBitmap (new SkBitmap ())
67 , fDifferenceBitmap (new SkBitmap ())
epoger@google.com25d961c2012-02-02 20:50:36 +000068 , fWhiteBitmap (new SkBitmap ())
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000069 , fBaseHeight (0)
70 , fBaseWidth (0)
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000071 , fFractionDifference (0)
72 , fWeightedFraction (0)
tomhudson@google.com4b33d282011-04-27 15:39:30 +000073 , fAverageMismatchR (0)
74 , fAverageMismatchG (0)
75 , fAverageMismatchB (0)
76 , fMaxMismatchR (0)
77 , fMaxMismatchG (0)
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +000078 , fMaxMismatchB (0)
epoger@google.com292aff62012-05-16 14:57:28 +000079 , fResult(result) {
tomhudson@google.com4e305982011-07-13 17:42:46 +000080 };
tomhudson@google.com4b33d282011-04-27 15:39:30 +000081
82 SkString fFilename;
tomhudson@google.com4e305982011-07-13 17:42:46 +000083 SkString fBasePath;
84 SkString fComparisonPath;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000085
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000086 SkBitmap* fBaseBitmap;
87 SkBitmap* fComparisonBitmap;
88 SkBitmap* fDifferenceBitmap;
epoger@google.com25d961c2012-02-02 20:50:36 +000089 SkBitmap* fWhiteBitmap;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000090
91 int fBaseHeight;
92 int fBaseWidth;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000093
94 /// Arbitrary floating-point metric to be used to sort images from most
95 /// to least different from baseline; values of 0 will be omitted from the
96 /// summary webpage.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000097 float fFractionDifference;
98 float fWeightedFraction;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000099
100 float fAverageMismatchR;
101 float fAverageMismatchG;
102 float fAverageMismatchB;
103
104 uint32_t fMaxMismatchR;
105 uint32_t fMaxMismatchG;
106 uint32_t fMaxMismatchB;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000107
epoger@google.com292aff62012-05-16 14:57:28 +0000108 /// Which category of diff result.
109 Result fResult;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000110};
111
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000112#define MAX2(a,b) (((b) < (a)) ? (a) : (b))
113#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
114
epoger@google.com25d961c2012-02-02 20:50:36 +0000115const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE);
116const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK);
117
epoger@google.coma5f406e2012-05-01 13:26:16 +0000118typedef SkTDArray<SkString*> StringArray;
119typedef StringArray FileArray;
epoger@google.com5fd53852012-03-22 18:20:06 +0000120
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000121struct DiffSummary {
122 DiffSummary ()
123 : fNumMatches (0)
124 , fNumMismatches (0)
125 , fMaxMismatchV (0)
126 , fMaxMismatchPercent (0) { };
127
epoger@google.com5fd53852012-03-22 18:20:06 +0000128 ~DiffSummary() {
epoger@google.com76222c02012-05-31 15:12:09 +0000129 for (int i = 0; i < kNumResultTypes; i++) {
130 fResultsOfType[i].deleteAll();
131 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000132 }
133
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000134 uint32_t fNumMatches;
135 uint32_t fNumMismatches;
136 uint32_t fMaxMismatchV;
137 float fMaxMismatchPercent;
138
epoger@google.com76222c02012-05-31 15:12:09 +0000139 FileArray fResultsOfType[kNumResultTypes];
140
141 // Print the contents of this FileArray, if any, to stdout.
142 // (If the FileArray is empty, print nothing.)
143 void printContents(const FileArray& fileArray, const char* headerText) {
144 int n = fileArray.count();
145 if (n > 0) {
146 printf("%s:\n", headerText);
147 for (int i = 0; i < n; ++i) {
148 printf("\t%s\n", fileArray[i]->c_str());
149 }
150 }
151 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000152
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000153 void print () {
epoger@google.com76222c02012-05-31 15:12:09 +0000154 printContents(fResultsOfType[kBaseMissing],
155 "Missing in baseDir");
156 printContents(fResultsOfType[kComparisonMissing],
157 "Missing in comparisonDir");
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000158 printf("%d of %d images matched.\n", fNumMatches,
159 fNumMatches + fNumMismatches);
160 if (fNumMismatches > 0) {
161 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
162 printf("Largest area mismatch was %.2f%% of pixels\n",
163 fMaxMismatchPercent);
164 }
165
166 }
167
168 void add (DiffRecord* drp) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000169 uint32_t mismatchValue;
epoger@google.com292aff62012-05-16 14:57:28 +0000170
epoger@google.com76222c02012-05-31 15:12:09 +0000171 fResultsOfType[drp->fResult].push(new SkString(drp->fFilename));
epoger@google.com292aff62012-05-16 14:57:28 +0000172 switch (drp->fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000173 case kEqualBits:
174 fNumMatches++;
175 break;
epoger@google.com292aff62012-05-16 14:57:28 +0000176 case kEqualPixels:
177 fNumMatches++;
178 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000179 case kDifferentSizes:
epoger@google.com5fd53852012-03-22 18:20:06 +0000180 fNumMismatches++;
epoger@google.com292aff62012-05-16 14:57:28 +0000181 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000182 case kDifferentPixels:
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000183 fNumMismatches++;
184 if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
185 fMaxMismatchPercent = drp->fFractionDifference * 100;
186 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000187 mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
188 drp->fMaxMismatchB);
189 if (mismatchValue > fMaxMismatchV) {
190 fMaxMismatchV = mismatchValue;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000191 }
epoger@google.com292aff62012-05-16 14:57:28 +0000192 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000193 case kDifferentOther:
194 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000195 break;
196 case kBaseMissing:
197 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000198 break;
199 case kComparisonMissing:
200 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000201 break;
202 case kUnknown:
203 SkDEBUGFAIL("adding uncategorized DiffRecord");
204 break;
205 default:
206 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
207 break;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000208 }
209 }
210};
211
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000212typedef SkTDArray<DiffRecord*> RecordArray;
213
epoger@google.com28060e72012-06-28 16:47:34 +0000214/// A wrapper for any sortProc (comparison routine) which applies a first-order
215/// sort beforehand, and a tiebreaker if the sortProc returns 0.
216template<typename T>
217static int compare(const void* untyped_lhs, const void* untyped_rhs) {
218 const DiffRecord* lhs = *reinterpret_cast<DiffRecord* const*>(untyped_lhs);
219 const DiffRecord* rhs = *reinterpret_cast<DiffRecord* const*>(untyped_rhs);
220
221 // First-order sort... these comparisons should be applied before comparing
222 // pixel values, no matter what.
223 if (lhs->fResult != rhs->fResult) {
224 return (lhs->fResult < rhs->fResult) ? 1 : -1;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000225 }
epoger@google.com28060e72012-06-28 16:47:34 +0000226
227 // Passed first-order sort, so call the pixel comparison routine.
228 int result = T::comparePixels(lhs, rhs);
229 if (result != 0) {
230 return result;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000231 }
epoger@google.com28060e72012-06-28 16:47:34 +0000232
233 // Tiebreaker... if we got to this point, we don't really care
234 // which order they are sorted in, but let's at least be consistent.
235 return strcmp(lhs->fFilename.c_str(), rhs->fFilename.c_str());
tomhudson@google.com5b325292011-05-24 19:41:13 +0000236}
237
epoger@google.com28060e72012-06-28 16:47:34 +0000238/// Comparison routine for qsort; sorts by fFractionDifference
239/// from largest to smallest.
240class CompareDiffMetrics {
241public:
242 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
243 if (lhs->fFractionDifference < rhs->fFractionDifference) {
244 return 1;
245 }
246 if (rhs->fFractionDifference < lhs->fFractionDifference) {
247 return -1;
248 }
249 return 0;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000250 }
epoger@google.com28060e72012-06-28 16:47:34 +0000251};
252
253class CompareDiffWeighted {
254public:
255 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
256 if (lhs->fWeightedFraction < rhs->fWeightedFraction) {
257 return 1;
258 }
259 if (lhs->fWeightedFraction > rhs->fWeightedFraction) {
260 return -1;
261 }
262 return 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000263 }
epoger@google.com28060e72012-06-28 16:47:34 +0000264};
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000265
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000266/// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
267/// from largest to smallest.
epoger@google.com28060e72012-06-28 16:47:34 +0000268class CompareDiffMeanMismatches {
269public:
270 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
271 float leftValue = MAX3(lhs->fAverageMismatchR,
272 lhs->fAverageMismatchG,
273 lhs->fAverageMismatchB);
274 float rightValue = MAX3(rhs->fAverageMismatchR,
275 rhs->fAverageMismatchG,
276 rhs->fAverageMismatchB);
277 if (leftValue < rightValue) {
278 return 1;
279 }
280 if (rightValue < leftValue) {
281 return -1;
282 }
283 return 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000284 }
epoger@google.com28060e72012-06-28 16:47:34 +0000285};
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000286
287/// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
288/// from largest to smallest.
epoger@google.com28060e72012-06-28 16:47:34 +0000289class CompareDiffMaxMismatches {
290public:
291 static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) {
292 uint32_t leftValue = MAX3(lhs->fMaxMismatchR,
293 lhs->fMaxMismatchG,
294 lhs->fMaxMismatchB);
295 uint32_t rightValue = MAX3(rhs->fMaxMismatchR,
296 rhs->fMaxMismatchG,
297 rhs->fMaxMismatchB);
298 if (leftValue < rightValue) {
299 return 1;
300 }
301 if (rightValue < leftValue) {
302 return -1;
303 }
304
305 return CompareDiffMeanMismatches::comparePixels(lhs, rhs);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000306 }
epoger@google.com28060e72012-06-28 16:47:34 +0000307};
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000308
309
310
311/// Parameterized routine to compute the color of a pixel in a difference image.
312typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
313
caryclark@google.com3dd45912012-06-06 12:11:10 +0000314#if 0 // UNUSED
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000315static void expand_and_copy (int width, int height, SkBitmap** dest) {
316 SkBitmap* temp = new SkBitmap ();
317 temp->reset();
318 temp->setConfig((*dest)->config(), width, height);
319 temp->allocPixels();
320 (*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(),
321 temp->rowBytes());
322 *dest = temp;
323}
caryclark@google.com3dd45912012-06-06 12:11:10 +0000324#endif
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000325
epoger@google.com46256ea2012-05-22 13:45:35 +0000326/// Returns true if the two buffers passed in are both non-NULL, and include
327/// exactly the same byte values (and identical lengths).
328static bool are_buffers_equal(SkData* skdata1, SkData* skdata2) {
329 if ((NULL == skdata1) || (NULL == skdata2)) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000330 return false;
331 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000332 if (skdata1->size() != skdata2->size()) {
333 return false;
334 }
335 return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size()));
336}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000337
epoger@google.com46256ea2012-05-22 13:45:35 +0000338/// Reads the file at the given path and returns its complete contents as an
339/// SkData object (or returns NULL on error).
340static SkData* read_file(const char* file_path) {
341 SkFILEStream fileStream(file_path);
342 if (!fileStream.isValid()) {
343 SkDebugf("WARNING: could not open file <%s> for reading\n", file_path);
344 return NULL;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000345 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000346 size_t bytesInFile = fileStream.getLength();
347 size_t bytesLeftToRead = bytesInFile;
348
349 void* bufferStart = sk_malloc_throw(bytesInFile);
350 char* bufferPointer = (char*)bufferStart;
351 while (bytesLeftToRead > 0) {
352 size_t bytesReadThisTime = fileStream.read(
353 bufferPointer, bytesLeftToRead);
354 if (0 == bytesReadThisTime) {
355 SkDebugf("WARNING: error reading from <%s>\n", file_path);
356 sk_free(bufferStart);
357 return NULL;
358 }
359 bytesLeftToRead -= bytesReadThisTime;
360 bufferPointer += bytesReadThisTime;
361 }
362 return SkData::NewFromMalloc(bufferStart, bytesInFile);
363}
364
365/// Decodes binary contents of baseFile and comparisonFile into
366/// diffRecord->fBaseBitmap and diffRecord->fComparisonBitmap.
367/// Returns true if that succeeds.
368static bool get_bitmaps (SkData* baseFileContents,
369 SkData* comparisonFileContents,
370 DiffRecord* diffRecord) {
371 SkMemoryStream compareStream(comparisonFileContents->data(),
372 comparisonFileContents->size());
373 SkMemoryStream baseStream(baseFileContents->data(),
374 baseFileContents->size());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000375
376 SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
377 if (NULL == codec) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000378 SkDebugf("ERROR: no codec found for basePath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000379 diffRecord->fBasePath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000380 return false;
381 }
382
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000383 // In debug, the DLL will automatically be unloaded when this is deleted,
384 // but that shouldn't be a problem in release mode.
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000385 SkAutoTDelete<SkImageDecoder> ad(codec);
386
387 baseStream.rewind();
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000388 if (!codec->decode(&baseStream, diffRecord->fBaseBitmap,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000389 SkBitmap::kARGB_8888_Config,
390 SkImageDecoder::kDecodePixels_Mode)) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000391 SkDebugf("ERROR: codec failed for basePath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000392 diffRecord->fBasePath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000393 return false;
394 }
395
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000396 diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width();
397 diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height();
398
399 if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000400 SkBitmap::kARGB_8888_Config,
401 SkImageDecoder::kDecodePixels_Mode)) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000402 SkDebugf("ERROR: codec failed for comparisonPath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000403 diffRecord->fComparisonPath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000404 return false;
405 }
406
407 return true;
408}
409
epoger@google.com5fd53852012-03-22 18:20:06 +0000410static bool get_bitmap_height_width(const SkString& path,
411 int *height, int *width) {
412 SkFILEStream stream(path.c_str());
413 if (!stream.isValid()) {
414 SkDebugf("ERROR: couldn't open file <%s>\n",
415 path.c_str());
416 return false;
417 }
418
419 SkImageDecoder* codec = SkImageDecoder::Factory(&stream);
420 if (NULL == codec) {
421 SkDebugf("ERROR: no codec found for <%s>\n",
422 path.c_str());
423 return false;
424 }
425
426 SkAutoTDelete<SkImageDecoder> ad(codec);
427 SkBitmap bm;
428
429 stream.rewind();
430 if (!codec->decode(&stream, &bm,
431 SkBitmap::kARGB_8888_Config,
432 SkImageDecoder::kDecodePixels_Mode)) {
433 SkDebugf("ERROR: codec failed for <%s>\n",
434 path.c_str());
435 return false;
436 }
437
438 *height = bm.height();
439 *width = bm.width();
440
441 return true;
442}
443
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000444// from gm - thanks to PNG, we need to force all pixels 100% opaque
445static void force_all_opaque(const SkBitmap& bitmap) {
446 SkAutoLockPixels lock(bitmap);
447 for (int y = 0; y < bitmap.height(); y++) {
448 for (int x = 0; x < bitmap.width(); x++) {
449 *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
450 }
451 }
452}
453
454// from gm
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000455static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000456 SkBitmap copy;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000457 bitmap->copyTo(&copy, SkBitmap::kARGB_8888_Config);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000458 force_all_opaque(copy);
459 return SkImageEncoder::EncodeFile(path.c_str(), copy,
460 SkImageEncoder::kPNG_Type, 100);
461}
462
463// from gm
464static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
465 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
466 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
467 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
468
469 return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
470}
471
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000472static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
473 const int threshold) {
474 int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
475 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
476 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
477 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
478
479 return ((SkAbs32(da) <= threshold) &&
480 (SkAbs32(dr) <= threshold) &&
481 (SkAbs32(dg) <= threshold) &&
482 (SkAbs32(db) <= threshold));
483}
484
485// based on gm
epoger@google.com66008522012-05-16 17:40:57 +0000486// Postcondition: when we exit this method, dr->fResult should have some value
487// other than kUnknown.
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000488static void compute_diff(DiffRecord* dr,
489 DiffMetricProc diffFunction,
490 const int colorThreshold) {
epoger@google.com25d961c2012-02-02 20:50:36 +0000491 SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap);
492 SkAutoLockPixels alpWhite(*dr->fWhiteBitmap);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000493
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000494 const int w = dr->fComparisonBitmap->width();
495 const int h = dr->fComparisonBitmap->height();
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000496 int mismatchedPixels = 0;
497 int totalMismatchR = 0;
498 int totalMismatchG = 0;
499 int totalMismatchB = 0;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000500
501 if (w != dr->fBaseWidth || h != dr->fBaseHeight) {
epoger@google.com292aff62012-05-16 14:57:28 +0000502 dr->fResult = kDifferentSizes;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000503 return;
504 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000505 // Accumulate fractionally different pixels, then divide out
506 // # of pixels at the end.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000507 dr->fWeightedFraction = 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000508 for (int y = 0; y < h; y++) {
509 for (int x = 0; x < w; x++) {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000510 SkPMColor c0 = *dr->fBaseBitmap->getAddr32(x, y);
511 SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000512 SkPMColor trueDifference = compute_diff_pmcolor(c0, c1);
513 SkPMColor outputDifference = diffFunction(c0, c1);
514 uint32_t thisR = SkGetPackedR32(trueDifference);
515 uint32_t thisG = SkGetPackedG32(trueDifference);
516 uint32_t thisB = SkGetPackedB32(trueDifference);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000517 totalMismatchR += thisR;
518 totalMismatchG += thisG;
519 totalMismatchB += thisB;
520 // In HSV, value is defined as max RGB component.
521 int value = MAX3(thisR, thisG, thisB);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000522 dr->fWeightedFraction += ((float) value) / 255;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000523 if (thisR > dr->fMaxMismatchR) {
524 dr->fMaxMismatchR = thisR;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000525 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000526 if (thisG > dr->fMaxMismatchG) {
527 dr->fMaxMismatchG = thisG;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000528 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000529 if (thisB > dr->fMaxMismatchB) {
530 dr->fMaxMismatchB = thisB;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000531 }
532 if (!colors_match_thresholded(c0, c1, colorThreshold)) {
533 mismatchedPixels++;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000534 *dr->fDifferenceBitmap->getAddr32(x, y) = outputDifference;
epoger@google.com25d961c2012-02-02 20:50:36 +0000535 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000536 } else {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000537 *dr->fDifferenceBitmap->getAddr32(x, y) = 0;
epoger@google.com25d961c2012-02-02 20:50:36 +0000538 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000539 }
540 }
541 }
epoger@google.com292aff62012-05-16 14:57:28 +0000542 if (0 == mismatchedPixels) {
543 dr->fResult = kEqualPixels;
544 return;
545 }
546 dr->fResult = kDifferentPixels;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000547 int pixelCount = w * h;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000548 dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
549 dr->fWeightedFraction /= pixelCount;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000550 dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
551 dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
552 dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000553}
554
epoger@google.com25d961c2012-02-02 20:50:36 +0000555static SkString filename_to_derived_filename (const SkString& filename,
556 const char *suffix) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000557 SkString diffName (filename);
558 const char* cstring = diffName.c_str();
559 int dotOffset = strrchr(cstring, '.') - cstring;
560 diffName.remove(dotOffset, diffName.size() - dotOffset);
epoger@google.com25d961c2012-02-02 20:50:36 +0000561 diffName.append(suffix);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000562 return diffName;
563}
564
epoger@google.com25d961c2012-02-02 20:50:36 +0000565/// Given a image filename, returns the name of the file containing the
566/// associated difference image.
567static SkString filename_to_diff_filename (const SkString& filename) {
568 return filename_to_derived_filename(filename, "-diff.png");
569}
570
571/// Given a image filename, returns the name of the file containing the
572/// "white" difference image.
573static SkString filename_to_white_filename (const SkString& filename) {
574 return filename_to_derived_filename(filename, "-white.png");
575}
576
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000577static void release_bitmaps(DiffRecord* drp) {
578 delete drp->fBaseBitmap;
579 drp->fBaseBitmap = NULL;
580 delete drp->fComparisonBitmap;
581 drp->fComparisonBitmap = NULL;
582 delete drp->fDifferenceBitmap;
583 drp->fDifferenceBitmap = NULL;
epoger@google.com25d961c2012-02-02 20:50:36 +0000584 delete drp->fWhiteBitmap;
585 drp->fWhiteBitmap = NULL;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000586}
587
tomhudson@google.com7d042802011-07-14 13:15:55 +0000588
epoger@google.coma5f406e2012-05-01 13:26:16 +0000589/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com7d042802011-07-14 13:15:55 +0000590static void create_and_write_diff_image(DiffRecord* drp,
591 DiffMetricProc dmp,
592 const int colorThreshold,
593 const SkString& outputDir,
594 const SkString& filename) {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000595 const int w = drp->fBaseWidth;
596 const int h = drp->fBaseHeight;
597 drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
598 drp->fDifferenceBitmap->allocPixels();
epoger@google.com25d961c2012-02-02 20:50:36 +0000599 drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
600 drp->fWhiteBitmap->allocPixels();
tomhudson@google.com7d042802011-07-14 13:15:55 +0000601
epoger@google.com66008522012-05-16 17:40:57 +0000602 SkASSERT(kUnknown == drp->fResult);
603 compute_diff(drp, dmp, colorThreshold);
604 SkASSERT(kUnknown != drp->fResult);
605
606 if ((kDifferentPixels == drp->fResult) && !outputDir.isEmpty()) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000607 SkString differencePath (outputDir);
608 differencePath.append(filename_to_diff_filename(filename));
609 write_bitmap(differencePath, drp->fDifferenceBitmap);
610 SkString whitePath (outputDir);
611 whitePath.append(filename_to_white_filename(filename));
612 write_bitmap(whitePath, drp->fWhiteBitmap);
613 }
epoger@google.com66008522012-05-16 17:40:57 +0000614
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000615 release_bitmaps(drp);
tomhudson@google.com4e305982011-07-13 17:42:46 +0000616}
617
epoger@google.coma5f406e2012-05-01 13:26:16 +0000618/// Returns true if string contains any of these substrings.
619static bool string_contains_any_of(const SkString& string,
620 const StringArray& substrings) {
621 for (int i = 0; i < substrings.count(); i++) {
622 if (string.contains(substrings[i]->c_str())) {
623 return true;
624 }
625 }
626 return false;
627}
628
629/// Iterate over dir and get all files that:
630/// - match any of the substrings in matchSubstrings, but...
631/// - DO NOT match any of the substrings in nomatchSubstrings
632/// Returns the list of files in *files.
633static void get_file_list(const SkString& dir,
634 const StringArray& matchSubstrings,
635 const StringArray& nomatchSubstrings,
636 FileArray *files) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000637 SkOSFile::Iter it(dir.c_str());
638 SkString filename;
639 while (it.next(&filename)) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000640 if (string_contains_any_of(filename, matchSubstrings) &&
641 !string_contains_any_of(filename, nomatchSubstrings)) {
642 files->push(new SkString(filename));
epoger@google.com5fd53852012-03-22 18:20:06 +0000643 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000644 }
645}
646
647static void release_file_list(FileArray *files) {
648 files->deleteAll();
649}
650
651/// Comparison routines for qsort, sort by file names.
652static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
653 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
654}
655
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000656/// Creates difference images, returns the number that have a 0 metric.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000657/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000658static void create_diff_images (DiffMetricProc dmp,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000659 const int colorThreshold,
660 RecordArray* differences,
661 const SkString& baseDir,
662 const SkString& comparisonDir,
663 const SkString& outputDir,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000664 const StringArray& matchSubstrings,
665 const StringArray& nomatchSubstrings,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000666 DiffSummary* summary) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000667 SkASSERT(!baseDir.isEmpty());
668 SkASSERT(!comparisonDir.isEmpty());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000669
epoger@google.com5fd53852012-03-22 18:20:06 +0000670 FileArray baseFiles;
671 FileArray comparisonFiles;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000672
epoger@google.coma5f406e2012-05-01 13:26:16 +0000673 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, &baseFiles);
674 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings,
675 &comparisonFiles);
epoger@google.com5fd53852012-03-22 18:20:06 +0000676
epoger@google.coma5f406e2012-05-01 13:26:16 +0000677 if (!baseFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000678 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
679 SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000680 }
681 if (!comparisonFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000682 qsort(comparisonFiles.begin(), comparisonFiles.count(),
683 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000684 }
epoger@google.com66008522012-05-16 17:40:57 +0000685
epoger@google.com5fd53852012-03-22 18:20:06 +0000686 int i = 0;
687 int j = 0;
688
689 while (i < baseFiles.count() &&
690 j < comparisonFiles.count()) {
691
tomhudson@google.com4e305982011-07-13 17:42:46 +0000692 SkString basePath (baseDir);
epoger@google.com5fd53852012-03-22 18:20:06 +0000693 basePath.append(*baseFiles[i]);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000694 SkString comparisonPath (comparisonDir);
epoger@google.com5fd53852012-03-22 18:20:06 +0000695 comparisonPath.append(*comparisonFiles[j]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000696
epoger@google.com5fd53852012-03-22 18:20:06 +0000697 DiffRecord *drp = NULL;
698 int v = strcmp(baseFiles[i]->c_str(),
699 comparisonFiles[j]->c_str());
700
701 if (v < 0) {
702 // in baseDir, but not in comparisonDir
epoger@google.com292aff62012-05-16 14:57:28 +0000703 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath,
704 kComparisonMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000705 ++i;
706 } else if (v > 0) {
707 // in comparisonDir, but not in baseDir
epoger@google.com292aff62012-05-16 14:57:28 +0000708 drp = new DiffRecord(*comparisonFiles[j], basePath, comparisonPath,
709 kBaseMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000710 ++j;
711 } else {
epoger@google.com46256ea2012-05-22 13:45:35 +0000712 // Found the same filename in both baseDir and comparisonDir.
epoger@google.com5fd53852012-03-22 18:20:06 +0000713 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath);
epoger@google.com46256ea2012-05-22 13:45:35 +0000714 SkASSERT(kUnknown == drp->fResult);
epoger@google.com5fd53852012-03-22 18:20:06 +0000715
epoger@google.com46256ea2012-05-22 13:45:35 +0000716 SkData* baseFileBits;
717 SkData* comparisonFileBits;
718 if (NULL == (baseFileBits = read_file(basePath.c_str()))) {
719 SkDebugf("WARNING: couldn't read base file <%s>\n",
720 basePath.c_str());
721 drp->fResult = kBaseMissing;
722 } else if (NULL == (comparisonFileBits = read_file(
723 comparisonPath.c_str()))) {
724 SkDebugf("WARNING: couldn't read comparison file <%s>\n",
725 comparisonPath.c_str());
726 drp->fResult = kComparisonMissing;
727 } else {
728 if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
729 drp->fResult = kEqualBits;
730 } else if (get_bitmaps(baseFileBits, comparisonFileBits, drp)) {
731 create_and_write_diff_image(drp, dmp, colorThreshold,
732 outputDir, *baseFiles[i]);
733 } else {
734 drp->fResult = kDifferentOther;
735 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000736 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000737 if (baseFileBits) {
738 baseFileBits->unref();
739 }
740 if (comparisonFileBits) {
741 comparisonFileBits->unref();
742 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000743 ++i;
744 ++j;
745 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000746 SkASSERT(kUnknown != drp->fResult);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000747 differences->push(drp);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000748 summary->add(drp);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000749 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000750
751 for (; i < baseFiles.count(); ++i) {
752 // files only in baseDir
753 SkString basePath (baseDir);
754 basePath.append(*baseFiles[i]);
755 SkString comparisonPath;
756 DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath,
epoger@google.com292aff62012-05-16 14:57:28 +0000757 comparisonPath, kComparisonMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000758 differences->push(drp);
759 summary->add(drp);
760 }
761
762 for (; j < comparisonFiles.count(); ++j) {
763 // files only in comparisonDir
764 SkString basePath;
765 SkString comparisonPath(comparisonDir);
766 comparisonPath.append(*comparisonFiles[j]);
767 DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath,
epoger@google.com292aff62012-05-16 14:57:28 +0000768 comparisonPath, kBaseMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000769 differences->push(drp);
770 summary->add(drp);
771 }
772
773 release_file_list(&baseFiles);
774 release_file_list(&comparisonFiles);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000775}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000776
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000777/// Make layout more consistent by scaling image to 240 height, 360 width,
778/// or natural size, whichever is smallest.
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000779static int compute_image_height (int height, int width) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000780 int retval = 240;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000781 if (height < retval) {
782 retval = height;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000783 }
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000784 float scale = (float) retval / height;
785 if (width * scale > 360) {
786 scale = (float) 360 / width;
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000787 retval = static_cast<int>(height * scale);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000788 }
789 return retval;
790}
791
epoger@google.com25d961c2012-02-02 20:50:36 +0000792static void print_table_header (SkFILEWStream* stream,
793 const int matchCount,
794 const int colorThreshold,
795 const RecordArray& differences,
796 const SkString &baseDir,
epoger@google.coma2b793c2012-05-15 14:58:53 +0000797 const SkString &comparisonDir,
798 bool doOutputDate=false) {
epoger@google.com25d961c2012-02-02 20:50:36 +0000799 stream->writeText("<table>\n");
800 stream->writeText("<tr><th>");
epoger@google.coma2b793c2012-05-15 14:58:53 +0000801 if (doOutputDate) {
802 SkTime::DateTime dt;
803 SkTime::GetDateTime(&dt);
804 stream->writeText("SkDiff run at ");
805 stream->writeDecAsText(dt.fHour);
806 stream->writeText(":");
807 if (dt.fMinute < 10) {
808 stream->writeText("0");
809 }
810 stream->writeDecAsText(dt.fMinute);
811 stream->writeText(":");
812 if (dt.fSecond < 10) {
813 stream->writeText("0");
814 }
815 stream->writeDecAsText(dt.fSecond);
816 stream->writeText("<br>");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000817 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000818 stream->writeDecAsText(matchCount);
819 stream->writeText(" of ");
820 stream->writeDecAsText(differences.count());
821 stream->writeText(" images matched ");
822 if (colorThreshold == 0) {
823 stream->writeText("exactly");
824 } else {
825 stream->writeText("within ");
826 stream->writeDecAsText(colorThreshold);
827 stream->writeText(" color units per component");
828 }
829 stream->writeText(".<br>");
epoger@google.com25d961c2012-02-02 20:50:36 +0000830 stream->writeText("</th>\n<th>");
831 stream->writeText("every different pixel shown in white");
832 stream->writeText("</th>\n<th>");
833 stream->writeText("color difference at each pixel");
834 stream->writeText("</th>\n<th>");
835 stream->writeText(baseDir.c_str());
836 stream->writeText("</th>\n<th>");
837 stream->writeText(comparisonDir.c_str());
838 stream->writeText("</th>\n");
839 stream->writeText("</tr>\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000840}
841
842static void print_pixel_count (SkFILEWStream* stream,
843 const DiffRecord& diff) {
844 stream->writeText("<br>(");
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000845 stream->writeDecAsText(static_cast<int>(diff.fFractionDifference *
846 diff.fBaseWidth *
847 diff.fBaseHeight));
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000848 stream->writeText(" pixels)");
tomhudson@google.com5b325292011-05-24 19:41:13 +0000849/*
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000850 stream->writeDecAsText(diff.fWeightedFraction *
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000851 diff.fBaseWidth *
852 diff.fBaseHeight);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000853 stream->writeText(" weighted pixels)");
854*/
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000855}
856
857static void print_label_cell (SkFILEWStream* stream,
858 const DiffRecord& diff) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000859 char metricBuf [20];
860
861 stream->writeText("<td><b>");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000862 stream->writeText(diff.fFilename.c_str());
epoger@google.com46256ea2012-05-22 13:45:35 +0000863 stream->writeText("</b><br>");
epoger@google.com292aff62012-05-16 14:57:28 +0000864 switch (diff.fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000865 case kEqualBits:
866 SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here");
867 return;
868 case kEqualPixels:
869 SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here");
epoger@google.com5fd53852012-03-22 18:20:06 +0000870 return;
epoger@google.com292aff62012-05-16 14:57:28 +0000871 case kDifferentSizes:
epoger@google.com46256ea2012-05-22 13:45:35 +0000872 stream->writeText("Image sizes differ</td>");
873 return;
874 case kDifferentPixels:
875 sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference);
876 stream->writeText(metricBuf);
877 stream->writeText(" of pixels differ");
878 stream->writeText("\n (");
879 sprintf(metricBuf, "%12.4f%%", 100 * diff.fWeightedFraction);
880 stream->writeText(metricBuf);
881 stream->writeText(" weighted)");
882 // Write the actual number of pixels that differ if it's < 1%
883 if (diff.fFractionDifference < 0.01) {
884 print_pixel_count(stream, diff);
885 }
886 stream->writeText("<br>Average color mismatch ");
887 stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR,
888 diff.fAverageMismatchG,
889 diff.fAverageMismatchB)));
890 stream->writeText("<br>Max color mismatch ");
891 stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
892 diff.fMaxMismatchG,
893 diff.fMaxMismatchB));
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000894 stream->writeText("</td>");
epoger@google.com46256ea2012-05-22 13:45:35 +0000895 break;
896 case kDifferentOther:
897 stream->writeText("Files differ; unable to parse one or both files</td>");
898 return;
899 case kBaseMissing:
900 stream->writeText("Missing from baseDir</td>");
901 return;
902 case kComparisonMissing:
903 stream->writeText("Missing from comparisonDir</td>");
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000904 return;
epoger@google.com292aff62012-05-16 14:57:28 +0000905 default:
epoger@google.com46256ea2012-05-22 13:45:35 +0000906 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
907 return;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000908 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000909}
910
911static void print_image_cell (SkFILEWStream* stream,
tomhudson@google.com4e305982011-07-13 17:42:46 +0000912 const SkString& path,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000913 int height) {
914 stream->writeText("<td><a href=\"");
tomhudson@google.com4e305982011-07-13 17:42:46 +0000915 stream->writeText(path.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000916 stream->writeText("\"><img src=\"");
tomhudson@google.com4e305982011-07-13 17:42:46 +0000917 stream->writeText(path.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000918 stream->writeText("\" height=\"");
919 stream->writeDecAsText(height);
920 stream->writeText("px\"></a></td>");
921}
922
caryclark@google.com3dd45912012-06-06 12:11:10 +0000923#if 0 // UNUSED
epoger@google.com01f78702012-04-12 16:32:04 +0000924static void print_text_cell (SkFILEWStream* stream, const char* text) {
925 stream->writeText("<td align=center>");
926 if (NULL != text) {
927 stream->writeText(text);
928 }
929 stream->writeText("</td>");
930}
caryclark@google.com3dd45912012-06-06 12:11:10 +0000931#endif
epoger@google.com01f78702012-04-12 16:32:04 +0000932
epoger@google.com5fd53852012-03-22 18:20:06 +0000933static void print_diff_with_missing_file(SkFILEWStream* stream,
934 DiffRecord& diff,
935 const SkString& relativePath) {
936 stream->writeText("<tr>\n");
937 print_label_cell(stream, diff);
938 stream->writeText("<td>N/A</td>");
939 stream->writeText("<td>N/A</td>");
epoger@google.com292aff62012-05-16 14:57:28 +0000940 if (kBaseMissing != diff.fResult) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000941 int h, w;
942 if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) {
943 stream->writeText("<td>N/A</td>");
944 } else {
945 int height = compute_image_height(h, w);
946 if (!diff.fBasePath.startsWith(PATH_DIV_STR)) {
947 diff.fBasePath.prepend(relativePath);
948 }
949 print_image_cell(stream, diff.fBasePath, height);
950 }
951 } else {
952 stream->writeText("<td>N/A</td>");
953 }
epoger@google.com292aff62012-05-16 14:57:28 +0000954 if (kComparisonMissing != diff.fResult) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000955 int h, w;
956 if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) {
957 stream->writeText("<td>N/A</td>");
958 } else {
959 int height = compute_image_height(h, w);
960 if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) {
961 diff.fComparisonPath.prepend(relativePath);
962 }
963 print_image_cell(stream, diff.fComparisonPath, height);
964 }
965 } else {
966 stream->writeText("<td>N/A</td>");
967 }
968 stream->writeText("</tr>\n");
969 stream->flush();
970}
971
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000972static void print_diff_page (const int matchCount,
973 const int colorThreshold,
974 const RecordArray& differences,
975 const SkString& baseDir,
976 const SkString& comparisonDir,
977 const SkString& outputDir) {
978
epoger@google.coma5f406e2012-05-01 13:26:16 +0000979 SkASSERT(!baseDir.isEmpty());
980 SkASSERT(!comparisonDir.isEmpty());
981 SkASSERT(!outputDir.isEmpty());
982
tomhudson@google.com5b325292011-05-24 19:41:13 +0000983 SkString outputPath (outputDir);
984 outputPath.append("index.html");
985 //SkFILEWStream outputStream ("index.html");
986 SkFILEWStream outputStream (outputPath.c_str());
987
tomhudson@google.com4e305982011-07-13 17:42:46 +0000988 // Need to convert paths from relative-to-cwd to relative-to-outputDir
tomhudson@google.com5b325292011-05-24 19:41:13 +0000989 // FIXME this doesn't work if there are '..' inside the outputDir
990 unsigned int ui;
991 SkString relativePath;
992 for (ui = 0; ui < outputDir.size(); ui++) {
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000993 if (outputDir[ui] == PATH_DIV_CHAR) {
994 relativePath.append(".." PATH_DIV_STR);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000995 }
996 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000997
998 outputStream.writeText("<html>\n<body>\n");
epoger@google.com25d961c2012-02-02 20:50:36 +0000999 print_table_header(&outputStream, matchCount, colorThreshold, differences,
1000 baseDir, comparisonDir);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001001 int i;
1002 for (i = 0; i < differences.count(); i++) {
1003 DiffRecord* diff = differences[i];
epoger@google.com5fd53852012-03-22 18:20:06 +00001004
epoger@google.com292aff62012-05-16 14:57:28 +00001005 switch (diff->fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +00001006 // Cases in which there is no diff to report.
1007 case kEqualBits:
epoger@google.com292aff62012-05-16 14:57:28 +00001008 case kEqualPixels:
1009 continue;
epoger@google.com46256ea2012-05-22 13:45:35 +00001010 // Cases in which we want a detailed pixel diff.
1011 case kDifferentPixels:
1012 break;
1013 // Cases in which the files differed, but we can't display the diff.
1014 case kDifferentSizes:
1015 case kDifferentOther:
epoger@google.com292aff62012-05-16 14:57:28 +00001016 case kBaseMissing:
epoger@google.com292aff62012-05-16 14:57:28 +00001017 case kComparisonMissing:
epoger@google.com5fd53852012-03-22 18:20:06 +00001018 print_diff_with_missing_file(&outputStream, *diff, relativePath);
1019 continue;
epoger@google.com292aff62012-05-16 14:57:28 +00001020 default:
epoger@google.com46256ea2012-05-22 13:45:35 +00001021 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
1022 continue;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001023 }
epoger@google.com5fd53852012-03-22 18:20:06 +00001024
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001025 if (!diff->fBasePath.startsWith(PATH_DIV_STR)) {
tomhudson@google.com4e305982011-07-13 17:42:46 +00001026 diff->fBasePath.prepend(relativePath);
1027 }
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001028 if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) {
tomhudson@google.com4e305982011-07-13 17:42:46 +00001029 diff->fComparisonPath.prepend(relativePath);
1030 }
epoger@google.com5fd53852012-03-22 18:20:06 +00001031
tomhudson@google.com9b540ce2011-08-02 14:10:04 +00001032 int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001033 outputStream.writeText("<tr>\n");
1034 print_label_cell(&outputStream, *diff);
epoger@google.com46256ea2012-05-22 13:45:35 +00001035 print_image_cell(&outputStream,
1036 filename_to_white_filename(diff->fFilename), height);
1037 print_image_cell(&outputStream,
1038 filename_to_diff_filename(diff->fFilename), height);
epoger@google.com25d961c2012-02-02 20:50:36 +00001039 print_image_cell(&outputStream, diff->fBasePath, height);
tomhudson@google.com4e305982011-07-13 17:42:46 +00001040 print_image_cell(&outputStream, diff->fComparisonPath, height);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001041 outputStream.writeText("</tr>\n");
1042 outputStream.flush();
1043 }
1044 outputStream.writeText("</table>\n");
1045 outputStream.writeText("</body>\n</html>\n");
1046 outputStream.flush();
1047}
1048
1049static void usage (char * argv0) {
1050 SkDebugf("Skia baseline image diff tool\n");
epoger@google.coma5f406e2012-05-01 13:26:16 +00001051 SkDebugf("\n"
1052"Usage: \n"
1053" %s <baseDir> <comparisonDir> [outputDir] \n"
epoger@google.coma611c3e2012-05-18 20:10:06 +00001054, argv0, argv0);
epoger@google.coma5f406e2012-05-01 13:26:16 +00001055 SkDebugf("\n"
1056"Arguments: \n"
1057" --nodiffs: don't write out image diffs or index.html, just generate \n"
1058" report on stdout \n"
1059" --threshold <n>: only report differences > n (per color channel) [default 0]\n"
1060" --match: compare files whose filenames contain this substring; if \n"
1061" unspecified, compare ALL files. \n"
1062" this flag may be repeated to add more matching substrings. \n"
1063" --nomatch: regardless of --match, DO NOT compare files whose filenames \n"
1064" contain this substring. \n"
1065" this flag may be repeated to add more forbidden substrings. \n"
tomhudson@google.com7d042802011-07-14 13:15:55 +00001066" --sortbymismatch: sort by average color channel mismatch\n");
1067 SkDebugf(
epoger@google.coma5f406e2012-05-01 13:26:16 +00001068" --sortbymaxmismatch: sort by worst color channel mismatch;\n"
1069" break ties with -sortbymismatch\n"
1070" [default sort is by fraction of pixels mismatching]\n");
tomhudson@google.com5b325292011-05-24 19:41:13 +00001071 SkDebugf(
tomhudson@google.com7d042802011-07-14 13:15:55 +00001072" --weighted: sort by # pixels different weighted by color difference\n");
1073 SkDebugf(
epoger@google.coma611c3e2012-05-18 20:10:06 +00001074" baseDir: directory to read baseline images from.\n");
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001075 SkDebugf(
epoger@google.coma5f406e2012-05-01 13:26:16 +00001076" comparisonDir: directory to read comparison images from\n");
1077 SkDebugf(
1078" outputDir: directory to write difference images and index.html to; \n"
epoger@google.coma611c3e2012-05-18 20:10:06 +00001079" defaults to comparisonDir \n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001080}
1081
1082int main (int argc, char ** argv) {
1083 DiffMetricProc diffProc = compute_diff_pmcolor;
epoger@google.com28060e72012-06-28 16:47:34 +00001084 int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001085
1086 // Maximum error tolerated in any one color channel in any one pixel before
1087 // a difference is reported.
1088 int colorThreshold = 0;
1089 SkString baseDir;
1090 SkString comparisonDir;
1091 SkString outputDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001092 StringArray matchSubstrings;
1093 StringArray nomatchSubstrings;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001094
epoger@google.coma5f406e2012-05-01 13:26:16 +00001095 bool generateDiffs = true;
tomhudson@google.com7d042802011-07-14 13:15:55 +00001096
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001097 RecordArray differences;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001098 DiffSummary summary;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001099
epoger@google.coma5f406e2012-05-01 13:26:16 +00001100 int i;
1101 int numUnflaggedArguments = 0;
1102 for (i = 1; i < argc; i++) {
tomhudson@google.com7d042802011-07-14 13:15:55 +00001103 if (!strcmp(argv[i], "--help")) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001104 usage(argv[0]);
1105 return 0;
1106 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001107 if (!strcmp(argv[i], "--nodiffs")) {
1108 generateDiffs = false;
1109 continue;
1110 }
1111 if (!strcmp(argv[i], "--threshold")) {
1112 colorThreshold = atoi(argv[++i]);
1113 continue;
1114 }
1115 if (!strcmp(argv[i], "--match")) {
1116 matchSubstrings.push(new SkString(argv[++i]));
1117 continue;
1118 }
1119 if (!strcmp(argv[i], "--nomatch")) {
1120 nomatchSubstrings.push(new SkString(argv[++i]));
1121 continue;
1122 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001123 if (!strcmp(argv[i], "--sortbymismatch")) {
epoger@google.com28060e72012-06-28 16:47:34 +00001124 sortProc = compare<CompareDiffMeanMismatches>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001125 continue;
1126 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001127 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
epoger@google.com28060e72012-06-28 16:47:34 +00001128 sortProc = compare<CompareDiffMaxMismatches>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001129 continue;
1130 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001131 if (!strcmp(argv[i], "--weighted")) {
epoger@google.com28060e72012-06-28 16:47:34 +00001132 sortProc = compare<CompareDiffWeighted>;
tomhudson@google.com5b325292011-05-24 19:41:13 +00001133 continue;
1134 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001135 if (argv[i][0] != '-') {
epoger@google.coma5f406e2012-05-01 13:26:16 +00001136 switch (numUnflaggedArguments++) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001137 case 0:
1138 baseDir.set(argv[i]);
1139 continue;
1140 case 1:
1141 comparisonDir.set(argv[i]);
1142 continue;
1143 case 2:
1144 outputDir.set(argv[i]);
1145 continue;
1146 default:
epoger@google.coma5f406e2012-05-01 13:26:16 +00001147 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001148 usage(argv[0]);
1149 return 0;
1150 }
1151 }
1152
1153 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
1154 usage(argv[0]);
1155 return 0;
1156 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001157
epoger@google.coma5f406e2012-05-01 13:26:16 +00001158 if (numUnflaggedArguments == 2) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001159 outputDir = comparisonDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001160 } else if (numUnflaggedArguments != 3) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001161 usage(argv[0]);
1162 return 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001163 }
1164
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001165 if (!baseDir.endsWith(PATH_DIV_STR)) {
1166 baseDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001167 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001168 printf("baseDir is [%s]\n", baseDir.c_str());
1169
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001170 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
1171 comparisonDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001172 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001173 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
1174
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001175 if (!outputDir.endsWith(PATH_DIV_STR)) {
1176 outputDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001177 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001178 if (generateDiffs) {
1179 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
1180 } else {
1181 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
1182 outputDir.set("");
1183 }
1184
epoger@google.comda4af242012-06-25 18:45:50 +00001185 // If no matchSubstrings were specified, match ALL strings
1186 // (except for whatever nomatchSubstrings were specified, if any).
epoger@google.coma5f406e2012-05-01 13:26:16 +00001187 if (matchSubstrings.isEmpty()) {
1188 matchSubstrings.push(new SkString(""));
1189 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001190
epoger@google.coma611c3e2012-05-18 20:10:06 +00001191 create_diff_images(diffProc, colorThreshold, &differences,
1192 baseDir, comparisonDir, outputDir,
1193 matchSubstrings, nomatchSubstrings, &summary);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001194 summary.print();
tomhudson@google.com7d042802011-07-14 13:15:55 +00001195
1196 if (differences.count()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001197 qsort(differences.begin(), differences.count(),
1198 sizeof(DiffRecord*), sortProc);
tomhudson@google.com7d042802011-07-14 13:15:55 +00001199 }
epoger@google.com66008522012-05-16 17:40:57 +00001200
epoger@google.coma5f406e2012-05-01 13:26:16 +00001201 if (generateDiffs) {
1202 print_diff_page(summary.fNumMatches, colorThreshold, differences,
1203 baseDir, comparisonDir, outputDir);
1204 }
epoger@google.com76222c02012-05-31 15:12:09 +00001205
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001206 for (i = 0; i < differences.count(); i++) {
1207 delete differences[i];
1208 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001209 matchSubstrings.deleteAll();
1210 nomatchSubstrings.deleteAll();
epoger@google.combe6188d2012-05-31 15:13:45 +00001211
1212 return summary.fNumMismatches;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001213}