blob: 30ecfa42e9996a8dd3887c818d993aa184cfe9b9 [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.com292aff62012-05-16 14:57:28 +000043enum Result {
44 kEqualBits, // both files in the pair contain exactly the same bits
45 kEqualPixels, // not bitwise equal, but their pixels are exactly the same
46 kDifferentSizes, // both are images we can parse, but of different sizes
47 kDifferentPixels,// both are images we can parse, but with different pixels
48 kDifferentOther, // files have different bits but are not parsable images
49 kBaseMissing, // missing from baseDir
50 kComparisonMissing,// missing from comparisonDir
epoger@google.com76222c02012-05-31 15:12:09 +000051 kUnknown, // not compared yet
52 //
53 kNumResultTypes // NOT A VALID VALUE--used to set up arrays. Must be last.
epoger@google.com292aff62012-05-16 14:57:28 +000054};
55
tomhudson@google.com4b33d282011-04-27 15:39:30 +000056struct DiffRecord {
tomhudson@google.com4e305982011-07-13 17:42:46 +000057 DiffRecord (const SkString filename,
58 const SkString basePath,
epoger@google.com5fd53852012-03-22 18:20:06 +000059 const SkString comparisonPath,
epoger@google.com292aff62012-05-16 14:57:28 +000060 const Result result = kUnknown)
tomhudson@google.com4b33d282011-04-27 15:39:30 +000061 : fFilename (filename)
tomhudson@google.com4e305982011-07-13 17:42:46 +000062 , fBasePath (basePath)
63 , fComparisonPath (comparisonPath)
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000064 , fBaseBitmap (new SkBitmap ())
65 , fComparisonBitmap (new SkBitmap ())
66 , fDifferenceBitmap (new SkBitmap ())
epoger@google.com25d961c2012-02-02 20:50:36 +000067 , fWhiteBitmap (new SkBitmap ())
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000068 , fBaseHeight (0)
69 , fBaseWidth (0)
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000070 , fFractionDifference (0)
71 , fWeightedFraction (0)
tomhudson@google.com4b33d282011-04-27 15:39:30 +000072 , fAverageMismatchR (0)
73 , fAverageMismatchG (0)
74 , fAverageMismatchB (0)
75 , fMaxMismatchR (0)
76 , fMaxMismatchG (0)
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +000077 , fMaxMismatchB (0)
epoger@google.com292aff62012-05-16 14:57:28 +000078 , fResult(result) {
tomhudson@google.com4e305982011-07-13 17:42:46 +000079 };
tomhudson@google.com4b33d282011-04-27 15:39:30 +000080
81 SkString fFilename;
tomhudson@google.com4e305982011-07-13 17:42:46 +000082 SkString fBasePath;
83 SkString fComparisonPath;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000084
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000085 SkBitmap* fBaseBitmap;
86 SkBitmap* fComparisonBitmap;
87 SkBitmap* fDifferenceBitmap;
epoger@google.com25d961c2012-02-02 20:50:36 +000088 SkBitmap* fWhiteBitmap;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +000089
90 int fBaseHeight;
91 int fBaseWidth;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000092
93 /// Arbitrary floating-point metric to be used to sort images from most
94 /// to least different from baseline; values of 0 will be omitted from the
95 /// summary webpage.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000096 float fFractionDifference;
97 float fWeightedFraction;
tomhudson@google.com4b33d282011-04-27 15:39:30 +000098
99 float fAverageMismatchR;
100 float fAverageMismatchG;
101 float fAverageMismatchB;
102
103 uint32_t fMaxMismatchR;
104 uint32_t fMaxMismatchG;
105 uint32_t fMaxMismatchB;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000106
epoger@google.com292aff62012-05-16 14:57:28 +0000107 /// Which category of diff result.
108 Result fResult;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000109};
110
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000111#define MAX2(a,b) (((b) < (a)) ? (a) : (b))
112#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
113
epoger@google.com25d961c2012-02-02 20:50:36 +0000114const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE);
115const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK);
116
epoger@google.coma5f406e2012-05-01 13:26:16 +0000117typedef SkTDArray<SkString*> StringArray;
118typedef StringArray FileArray;
epoger@google.com5fd53852012-03-22 18:20:06 +0000119
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000120struct DiffSummary {
121 DiffSummary ()
122 : fNumMatches (0)
123 , fNumMismatches (0)
124 , fMaxMismatchV (0)
125 , fMaxMismatchPercent (0) { };
126
epoger@google.com5fd53852012-03-22 18:20:06 +0000127 ~DiffSummary() {
epoger@google.com76222c02012-05-31 15:12:09 +0000128 for (int i = 0; i < kNumResultTypes; i++) {
129 fResultsOfType[i].deleteAll();
130 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000131 }
132
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000133 uint32_t fNumMatches;
134 uint32_t fNumMismatches;
135 uint32_t fMaxMismatchV;
136 float fMaxMismatchPercent;
137
epoger@google.com76222c02012-05-31 15:12:09 +0000138 FileArray fResultsOfType[kNumResultTypes];
139
140 // Print the contents of this FileArray, if any, to stdout.
141 // (If the FileArray is empty, print nothing.)
142 void printContents(const FileArray& fileArray, const char* headerText) {
143 int n = fileArray.count();
144 if (n > 0) {
145 printf("%s:\n", headerText);
146 for (int i = 0; i < n; ++i) {
147 printf("\t%s\n", fileArray[i]->c_str());
148 }
149 }
150 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000151
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000152 void print () {
epoger@google.com76222c02012-05-31 15:12:09 +0000153 printContents(fResultsOfType[kBaseMissing],
154 "Missing in baseDir");
155 printContents(fResultsOfType[kComparisonMissing],
156 "Missing in comparisonDir");
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000157 printf("%d of %d images matched.\n", fNumMatches,
158 fNumMatches + fNumMismatches);
159 if (fNumMismatches > 0) {
160 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
161 printf("Largest area mismatch was %.2f%% of pixels\n",
162 fMaxMismatchPercent);
163 }
164
165 }
166
167 void add (DiffRecord* drp) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000168 uint32_t mismatchValue;
epoger@google.com292aff62012-05-16 14:57:28 +0000169
epoger@google.com76222c02012-05-31 15:12:09 +0000170 fResultsOfType[drp->fResult].push(new SkString(drp->fFilename));
epoger@google.com292aff62012-05-16 14:57:28 +0000171 switch (drp->fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000172 case kEqualBits:
173 fNumMatches++;
174 break;
epoger@google.com292aff62012-05-16 14:57:28 +0000175 case kEqualPixels:
176 fNumMatches++;
177 break;
epoger@google.com46256ea2012-05-22 13:45:35 +0000178 case kDifferentSizes:
epoger@google.com5fd53852012-03-22 18:20:06 +0000179 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000180 drp->fFractionDifference = 2.0;// sort as if 200% of pixels differed
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++;
195 drp->fFractionDifference = 3.0;// sort as if 300% of pixels differed
196 break;
197 case kBaseMissing:
198 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000199 break;
200 case kComparisonMissing:
201 fNumMismatches++;
epoger@google.com46256ea2012-05-22 13:45:35 +0000202 break;
203 case kUnknown:
204 SkDEBUGFAIL("adding uncategorized DiffRecord");
205 break;
206 default:
207 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
208 break;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000209 }
210 }
211};
212
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000213typedef SkTDArray<DiffRecord*> RecordArray;
214
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000215/// Comparison routine for qsort; sorts by fFractionDifference
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000216/// from largest to smallest.
217static int compare_diff_metrics (DiffRecord** lhs, DiffRecord** rhs) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000218 if ((*lhs)->fFractionDifference < (*rhs)->fFractionDifference) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000219 return 1;
220 }
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000221 if ((*rhs)->fFractionDifference < (*lhs)->fFractionDifference) {
tomhudson@google.com5b325292011-05-24 19:41:13 +0000222 return -1;
223 }
224 return 0;
225}
226
227static int compare_diff_weighted (DiffRecord** lhs, DiffRecord** rhs) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000228 if ((*lhs)->fWeightedFraction < (*rhs)->fWeightedFraction) {
tomhudson@google.com5b325292011-05-24 19:41:13 +0000229 return 1;
230 }
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000231 if ((*lhs)->fWeightedFraction > (*rhs)->fWeightedFraction) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000232 return -1;
233 }
234 return 0;
235}
236
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000237/// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB})
238/// from largest to smallest.
239static int compare_diff_mean_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
240 float leftValue = MAX3((*lhs)->fAverageMismatchR,
241 (*lhs)->fAverageMismatchG,
242 (*lhs)->fAverageMismatchB);
243 float rightValue = MAX3((*rhs)->fAverageMismatchR,
244 (*rhs)->fAverageMismatchG,
245 (*rhs)->fAverageMismatchB);
246 if (leftValue < rightValue) {
247 return 1;
248 }
249 if (rightValue < leftValue) {
250 return -1;
251 }
252 return 0;
253}
254
255/// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB})
256/// from largest to smallest.
257static int compare_diff_max_mismatches (DiffRecord** lhs, DiffRecord** rhs) {
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000258 uint32_t leftValue = MAX3((*lhs)->fMaxMismatchR,
259 (*lhs)->fMaxMismatchG,
260 (*lhs)->fMaxMismatchB);
261 uint32_t rightValue = MAX3((*rhs)->fMaxMismatchR,
262 (*rhs)->fMaxMismatchG,
263 (*rhs)->fMaxMismatchB);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000264 if (leftValue < rightValue) {
265 return 1;
266 }
267 if (rightValue < leftValue) {
268 return -1;
269 }
270 return compare_diff_mean_mismatches(lhs, rhs);
271}
272
273
274
275/// Parameterized routine to compute the color of a pixel in a difference image.
276typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor);
277
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000278static void expand_and_copy (int width, int height, SkBitmap** dest) {
279 SkBitmap* temp = new SkBitmap ();
280 temp->reset();
281 temp->setConfig((*dest)->config(), width, height);
282 temp->allocPixels();
283 (*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(),
284 temp->rowBytes());
285 *dest = temp;
286}
287
epoger@google.com46256ea2012-05-22 13:45:35 +0000288/// Returns true if the two buffers passed in are both non-NULL, and include
289/// exactly the same byte values (and identical lengths).
290static bool are_buffers_equal(SkData* skdata1, SkData* skdata2) {
291 if ((NULL == skdata1) || (NULL == skdata2)) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000292 return false;
293 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000294 if (skdata1->size() != skdata2->size()) {
295 return false;
296 }
297 return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size()));
298}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000299
epoger@google.com46256ea2012-05-22 13:45:35 +0000300/// Reads the file at the given path and returns its complete contents as an
301/// SkData object (or returns NULL on error).
302static SkData* read_file(const char* file_path) {
303 SkFILEStream fileStream(file_path);
304 if (!fileStream.isValid()) {
305 SkDebugf("WARNING: could not open file <%s> for reading\n", file_path);
306 return NULL;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000307 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000308 size_t bytesInFile = fileStream.getLength();
309 size_t bytesLeftToRead = bytesInFile;
310
311 void* bufferStart = sk_malloc_throw(bytesInFile);
312 char* bufferPointer = (char*)bufferStart;
313 while (bytesLeftToRead > 0) {
314 size_t bytesReadThisTime = fileStream.read(
315 bufferPointer, bytesLeftToRead);
316 if (0 == bytesReadThisTime) {
317 SkDebugf("WARNING: error reading from <%s>\n", file_path);
318 sk_free(bufferStart);
319 return NULL;
320 }
321 bytesLeftToRead -= bytesReadThisTime;
322 bufferPointer += bytesReadThisTime;
323 }
324 return SkData::NewFromMalloc(bufferStart, bytesInFile);
325}
326
327/// Decodes binary contents of baseFile and comparisonFile into
328/// diffRecord->fBaseBitmap and diffRecord->fComparisonBitmap.
329/// Returns true if that succeeds.
330static bool get_bitmaps (SkData* baseFileContents,
331 SkData* comparisonFileContents,
332 DiffRecord* diffRecord) {
333 SkMemoryStream compareStream(comparisonFileContents->data(),
334 comparisonFileContents->size());
335 SkMemoryStream baseStream(baseFileContents->data(),
336 baseFileContents->size());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000337
338 SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream);
339 if (NULL == codec) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000340 SkDebugf("ERROR: no codec found for basePath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000341 diffRecord->fBasePath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000342 return false;
343 }
344
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000345 // In debug, the DLL will automatically be unloaded when this is deleted,
346 // but that shouldn't be a problem in release mode.
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000347 SkAutoTDelete<SkImageDecoder> ad(codec);
348
349 baseStream.rewind();
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000350 if (!codec->decode(&baseStream, diffRecord->fBaseBitmap,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000351 SkBitmap::kARGB_8888_Config,
352 SkImageDecoder::kDecodePixels_Mode)) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000353 SkDebugf("ERROR: codec failed for basePath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000354 diffRecord->fBasePath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000355 return false;
356 }
357
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000358 diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width();
359 diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height();
360
361 if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000362 SkBitmap::kARGB_8888_Config,
363 SkImageDecoder::kDecodePixels_Mode)) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000364 SkDebugf("ERROR: codec failed for comparisonPath <%s>\n",
tomhudson@google.com4e305982011-07-13 17:42:46 +0000365 diffRecord->fComparisonPath.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000366 return false;
367 }
368
369 return true;
370}
371
epoger@google.com5fd53852012-03-22 18:20:06 +0000372static bool get_bitmap_height_width(const SkString& path,
373 int *height, int *width) {
374 SkFILEStream stream(path.c_str());
375 if (!stream.isValid()) {
376 SkDebugf("ERROR: couldn't open file <%s>\n",
377 path.c_str());
378 return false;
379 }
380
381 SkImageDecoder* codec = SkImageDecoder::Factory(&stream);
382 if (NULL == codec) {
383 SkDebugf("ERROR: no codec found for <%s>\n",
384 path.c_str());
385 return false;
386 }
387
388 SkAutoTDelete<SkImageDecoder> ad(codec);
389 SkBitmap bm;
390
391 stream.rewind();
392 if (!codec->decode(&stream, &bm,
393 SkBitmap::kARGB_8888_Config,
394 SkImageDecoder::kDecodePixels_Mode)) {
395 SkDebugf("ERROR: codec failed for <%s>\n",
396 path.c_str());
397 return false;
398 }
399
400 *height = bm.height();
401 *width = bm.width();
402
403 return true;
404}
405
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000406// from gm - thanks to PNG, we need to force all pixels 100% opaque
407static void force_all_opaque(const SkBitmap& bitmap) {
408 SkAutoLockPixels lock(bitmap);
409 for (int y = 0; y < bitmap.height(); y++) {
410 for (int x = 0; x < bitmap.width(); x++) {
411 *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
412 }
413 }
414}
415
416// from gm
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000417static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000418 SkBitmap copy;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000419 bitmap->copyTo(&copy, SkBitmap::kARGB_8888_Config);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000420 force_all_opaque(copy);
421 return SkImageEncoder::EncodeFile(path.c_str(), copy,
422 SkImageEncoder::kPNG_Type, 100);
423}
424
425// from gm
426static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) {
427 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
428 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
429 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
430
431 return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db));
432}
433
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000434static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1,
435 const int threshold) {
436 int da = SkGetPackedA32(c0) - SkGetPackedA32(c1);
437 int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1);
438 int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1);
439 int db = SkGetPackedB32(c0) - SkGetPackedB32(c1);
440
441 return ((SkAbs32(da) <= threshold) &&
442 (SkAbs32(dr) <= threshold) &&
443 (SkAbs32(dg) <= threshold) &&
444 (SkAbs32(db) <= threshold));
445}
446
447// based on gm
epoger@google.com66008522012-05-16 17:40:57 +0000448// Postcondition: when we exit this method, dr->fResult should have some value
449// other than kUnknown.
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000450static void compute_diff(DiffRecord* dr,
451 DiffMetricProc diffFunction,
452 const int colorThreshold) {
epoger@google.com25d961c2012-02-02 20:50:36 +0000453 SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap);
454 SkAutoLockPixels alpWhite(*dr->fWhiteBitmap);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000455
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000456 const int w = dr->fComparisonBitmap->width();
457 const int h = dr->fComparisonBitmap->height();
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000458 int mismatchedPixels = 0;
459 int totalMismatchR = 0;
460 int totalMismatchG = 0;
461 int totalMismatchB = 0;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000462
463 if (w != dr->fBaseWidth || h != dr->fBaseHeight) {
epoger@google.com292aff62012-05-16 14:57:28 +0000464 dr->fResult = kDifferentSizes;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000465 return;
466 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000467 // Accumulate fractionally different pixels, then divide out
468 // # of pixels at the end.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000469 dr->fWeightedFraction = 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000470 for (int y = 0; y < h; y++) {
471 for (int x = 0; x < w; x++) {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000472 SkPMColor c0 = *dr->fBaseBitmap->getAddr32(x, y);
473 SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000474 SkPMColor trueDifference = compute_diff_pmcolor(c0, c1);
475 SkPMColor outputDifference = diffFunction(c0, c1);
476 uint32_t thisR = SkGetPackedR32(trueDifference);
477 uint32_t thisG = SkGetPackedG32(trueDifference);
478 uint32_t thisB = SkGetPackedB32(trueDifference);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000479 totalMismatchR += thisR;
480 totalMismatchG += thisG;
481 totalMismatchB += thisB;
482 // In HSV, value is defined as max RGB component.
483 int value = MAX3(thisR, thisG, thisB);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000484 dr->fWeightedFraction += ((float) value) / 255;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000485 if (thisR > dr->fMaxMismatchR) {
486 dr->fMaxMismatchR = thisR;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000487 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000488 if (thisG > dr->fMaxMismatchG) {
489 dr->fMaxMismatchG = thisG;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000490 }
tomhudson@google.com5b325292011-05-24 19:41:13 +0000491 if (thisB > dr->fMaxMismatchB) {
492 dr->fMaxMismatchB = thisB;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000493 }
494 if (!colors_match_thresholded(c0, c1, colorThreshold)) {
495 mismatchedPixels++;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000496 *dr->fDifferenceBitmap->getAddr32(x, y) = outputDifference;
epoger@google.com25d961c2012-02-02 20:50:36 +0000497 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000498 } else {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000499 *dr->fDifferenceBitmap->getAddr32(x, y) = 0;
epoger@google.com25d961c2012-02-02 20:50:36 +0000500 *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000501 }
502 }
503 }
epoger@google.com292aff62012-05-16 14:57:28 +0000504 if (0 == mismatchedPixels) {
505 dr->fResult = kEqualPixels;
506 return;
507 }
508 dr->fResult = kDifferentPixels;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000509 int pixelCount = w * h;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000510 dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
511 dr->fWeightedFraction /= pixelCount;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000512 dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
513 dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
514 dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000515}
516
epoger@google.com25d961c2012-02-02 20:50:36 +0000517static SkString filename_to_derived_filename (const SkString& filename,
518 const char *suffix) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000519 SkString diffName (filename);
520 const char* cstring = diffName.c_str();
521 int dotOffset = strrchr(cstring, '.') - cstring;
522 diffName.remove(dotOffset, diffName.size() - dotOffset);
epoger@google.com25d961c2012-02-02 20:50:36 +0000523 diffName.append(suffix);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000524 return diffName;
525}
526
epoger@google.com25d961c2012-02-02 20:50:36 +0000527/// Given a image filename, returns the name of the file containing the
528/// associated difference image.
529static SkString filename_to_diff_filename (const SkString& filename) {
530 return filename_to_derived_filename(filename, "-diff.png");
531}
532
533/// Given a image filename, returns the name of the file containing the
534/// "white" difference image.
535static SkString filename_to_white_filename (const SkString& filename) {
536 return filename_to_derived_filename(filename, "-white.png");
537}
538
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000539static void release_bitmaps(DiffRecord* drp) {
540 delete drp->fBaseBitmap;
541 drp->fBaseBitmap = NULL;
542 delete drp->fComparisonBitmap;
543 drp->fComparisonBitmap = NULL;
544 delete drp->fDifferenceBitmap;
545 drp->fDifferenceBitmap = NULL;
epoger@google.com25d961c2012-02-02 20:50:36 +0000546 delete drp->fWhiteBitmap;
547 drp->fWhiteBitmap = NULL;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000548}
549
tomhudson@google.com7d042802011-07-14 13:15:55 +0000550
epoger@google.coma5f406e2012-05-01 13:26:16 +0000551/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com7d042802011-07-14 13:15:55 +0000552static void create_and_write_diff_image(DiffRecord* drp,
553 DiffMetricProc dmp,
554 const int colorThreshold,
555 const SkString& outputDir,
556 const SkString& filename) {
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000557 const int w = drp->fBaseWidth;
558 const int h = drp->fBaseHeight;
559 drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
560 drp->fDifferenceBitmap->allocPixels();
epoger@google.com25d961c2012-02-02 20:50:36 +0000561 drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h);
562 drp->fWhiteBitmap->allocPixels();
tomhudson@google.com7d042802011-07-14 13:15:55 +0000563
epoger@google.com66008522012-05-16 17:40:57 +0000564 SkASSERT(kUnknown == drp->fResult);
565 compute_diff(drp, dmp, colorThreshold);
566 SkASSERT(kUnknown != drp->fResult);
567
568 if ((kDifferentPixels == drp->fResult) && !outputDir.isEmpty()) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000569 SkString differencePath (outputDir);
570 differencePath.append(filename_to_diff_filename(filename));
571 write_bitmap(differencePath, drp->fDifferenceBitmap);
572 SkString whitePath (outputDir);
573 whitePath.append(filename_to_white_filename(filename));
574 write_bitmap(whitePath, drp->fWhiteBitmap);
575 }
epoger@google.com66008522012-05-16 17:40:57 +0000576
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000577 release_bitmaps(drp);
tomhudson@google.com4e305982011-07-13 17:42:46 +0000578}
579
epoger@google.coma5f406e2012-05-01 13:26:16 +0000580/// Returns true if string contains any of these substrings.
581static bool string_contains_any_of(const SkString& string,
582 const StringArray& substrings) {
583 for (int i = 0; i < substrings.count(); i++) {
584 if (string.contains(substrings[i]->c_str())) {
585 return true;
586 }
587 }
588 return false;
589}
590
591/// Iterate over dir and get all files that:
592/// - match any of the substrings in matchSubstrings, but...
593/// - DO NOT match any of the substrings in nomatchSubstrings
594/// Returns the list of files in *files.
595static void get_file_list(const SkString& dir,
596 const StringArray& matchSubstrings,
597 const StringArray& nomatchSubstrings,
598 FileArray *files) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000599 SkOSFile::Iter it(dir.c_str());
600 SkString filename;
601 while (it.next(&filename)) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000602 if (string_contains_any_of(filename, matchSubstrings) &&
603 !string_contains_any_of(filename, nomatchSubstrings)) {
604 files->push(new SkString(filename));
epoger@google.com5fd53852012-03-22 18:20:06 +0000605 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000606 }
607}
608
609static void release_file_list(FileArray *files) {
610 files->deleteAll();
611}
612
613/// Comparison routines for qsort, sort by file names.
614static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
615 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
616}
617
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000618/// Creates difference images, returns the number that have a 0 metric.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000619/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000620static void create_diff_images (DiffMetricProc dmp,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000621 const int colorThreshold,
622 RecordArray* differences,
623 const SkString& baseDir,
624 const SkString& comparisonDir,
625 const SkString& outputDir,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000626 const StringArray& matchSubstrings,
627 const StringArray& nomatchSubstrings,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000628 DiffSummary* summary) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000629 SkASSERT(!baseDir.isEmpty());
630 SkASSERT(!comparisonDir.isEmpty());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000631
epoger@google.com5fd53852012-03-22 18:20:06 +0000632 FileArray baseFiles;
633 FileArray comparisonFiles;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000634
epoger@google.coma5f406e2012-05-01 13:26:16 +0000635 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, &baseFiles);
636 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings,
637 &comparisonFiles);
epoger@google.com5fd53852012-03-22 18:20:06 +0000638
epoger@google.coma5f406e2012-05-01 13:26:16 +0000639 if (!baseFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000640 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
641 SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000642 }
643 if (!comparisonFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000644 qsort(comparisonFiles.begin(), comparisonFiles.count(),
645 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000646 }
epoger@google.com66008522012-05-16 17:40:57 +0000647
epoger@google.com5fd53852012-03-22 18:20:06 +0000648 int i = 0;
649 int j = 0;
650
651 while (i < baseFiles.count() &&
652 j < comparisonFiles.count()) {
653
tomhudson@google.com4e305982011-07-13 17:42:46 +0000654 SkString basePath (baseDir);
epoger@google.com5fd53852012-03-22 18:20:06 +0000655 basePath.append(*baseFiles[i]);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000656 SkString comparisonPath (comparisonDir);
epoger@google.com5fd53852012-03-22 18:20:06 +0000657 comparisonPath.append(*comparisonFiles[j]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000658
epoger@google.com5fd53852012-03-22 18:20:06 +0000659 DiffRecord *drp = NULL;
660 int v = strcmp(baseFiles[i]->c_str(),
661 comparisonFiles[j]->c_str());
662
663 if (v < 0) {
664 // in baseDir, but not in comparisonDir
epoger@google.com292aff62012-05-16 14:57:28 +0000665 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath,
666 kComparisonMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000667 ++i;
668 } else if (v > 0) {
669 // in comparisonDir, but not in baseDir
epoger@google.com292aff62012-05-16 14:57:28 +0000670 drp = new DiffRecord(*comparisonFiles[j], basePath, comparisonPath,
671 kBaseMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000672 ++j;
673 } else {
epoger@google.com46256ea2012-05-22 13:45:35 +0000674 // Found the same filename in both baseDir and comparisonDir.
epoger@google.com5fd53852012-03-22 18:20:06 +0000675 drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath);
epoger@google.com46256ea2012-05-22 13:45:35 +0000676 SkASSERT(kUnknown == drp->fResult);
epoger@google.com5fd53852012-03-22 18:20:06 +0000677
epoger@google.com46256ea2012-05-22 13:45:35 +0000678 SkData* baseFileBits;
679 SkData* comparisonFileBits;
680 if (NULL == (baseFileBits = read_file(basePath.c_str()))) {
681 SkDebugf("WARNING: couldn't read base file <%s>\n",
682 basePath.c_str());
683 drp->fResult = kBaseMissing;
684 } else if (NULL == (comparisonFileBits = read_file(
685 comparisonPath.c_str()))) {
686 SkDebugf("WARNING: couldn't read comparison file <%s>\n",
687 comparisonPath.c_str());
688 drp->fResult = kComparisonMissing;
689 } else {
690 if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
691 drp->fResult = kEqualBits;
692 } else if (get_bitmaps(baseFileBits, comparisonFileBits, drp)) {
693 create_and_write_diff_image(drp, dmp, colorThreshold,
694 outputDir, *baseFiles[i]);
695 } else {
696 drp->fResult = kDifferentOther;
697 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000698 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000699 if (baseFileBits) {
700 baseFileBits->unref();
701 }
702 if (comparisonFileBits) {
703 comparisonFileBits->unref();
704 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000705 ++i;
706 ++j;
707 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000708 SkASSERT(kUnknown != drp->fResult);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000709 differences->push(drp);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000710 summary->add(drp);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000711 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000712
713 for (; i < baseFiles.count(); ++i) {
714 // files only in baseDir
715 SkString basePath (baseDir);
716 basePath.append(*baseFiles[i]);
717 SkString comparisonPath;
718 DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath,
epoger@google.com292aff62012-05-16 14:57:28 +0000719 comparisonPath, kComparisonMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000720 differences->push(drp);
721 summary->add(drp);
722 }
723
724 for (; j < comparisonFiles.count(); ++j) {
725 // files only in comparisonDir
726 SkString basePath;
727 SkString comparisonPath(comparisonDir);
728 comparisonPath.append(*comparisonFiles[j]);
729 DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath,
epoger@google.com292aff62012-05-16 14:57:28 +0000730 comparisonPath, kBaseMissing);
epoger@google.com5fd53852012-03-22 18:20:06 +0000731 differences->push(drp);
732 summary->add(drp);
733 }
734
735 release_file_list(&baseFiles);
736 release_file_list(&comparisonFiles);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000737}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000738
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000739/// Make layout more consistent by scaling image to 240 height, 360 width,
740/// or natural size, whichever is smallest.
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000741static int compute_image_height (int height, int width) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000742 int retval = 240;
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000743 if (height < retval) {
744 retval = height;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000745 }
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000746 float scale = (float) retval / height;
747 if (width * scale > 360) {
748 scale = (float) 360 / width;
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000749 retval = static_cast<int>(height * scale);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000750 }
751 return retval;
752}
753
epoger@google.com25d961c2012-02-02 20:50:36 +0000754static void print_table_header (SkFILEWStream* stream,
755 const int matchCount,
756 const int colorThreshold,
757 const RecordArray& differences,
758 const SkString &baseDir,
epoger@google.coma2b793c2012-05-15 14:58:53 +0000759 const SkString &comparisonDir,
760 bool doOutputDate=false) {
epoger@google.com25d961c2012-02-02 20:50:36 +0000761 stream->writeText("<table>\n");
762 stream->writeText("<tr><th>");
epoger@google.coma2b793c2012-05-15 14:58:53 +0000763 if (doOutputDate) {
764 SkTime::DateTime dt;
765 SkTime::GetDateTime(&dt);
766 stream->writeText("SkDiff run at ");
767 stream->writeDecAsText(dt.fHour);
768 stream->writeText(":");
769 if (dt.fMinute < 10) {
770 stream->writeText("0");
771 }
772 stream->writeDecAsText(dt.fMinute);
773 stream->writeText(":");
774 if (dt.fSecond < 10) {
775 stream->writeText("0");
776 }
777 stream->writeDecAsText(dt.fSecond);
778 stream->writeText("<br>");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000779 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000780 stream->writeDecAsText(matchCount);
781 stream->writeText(" of ");
782 stream->writeDecAsText(differences.count());
783 stream->writeText(" images matched ");
784 if (colorThreshold == 0) {
785 stream->writeText("exactly");
786 } else {
787 stream->writeText("within ");
788 stream->writeDecAsText(colorThreshold);
789 stream->writeText(" color units per component");
790 }
791 stream->writeText(".<br>");
epoger@google.com25d961c2012-02-02 20:50:36 +0000792 stream->writeText("</th>\n<th>");
793 stream->writeText("every different pixel shown in white");
794 stream->writeText("</th>\n<th>");
795 stream->writeText("color difference at each pixel");
796 stream->writeText("</th>\n<th>");
797 stream->writeText(baseDir.c_str());
798 stream->writeText("</th>\n<th>");
799 stream->writeText(comparisonDir.c_str());
800 stream->writeText("</th>\n");
801 stream->writeText("</tr>\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000802}
803
804static void print_pixel_count (SkFILEWStream* stream,
805 const DiffRecord& diff) {
806 stream->writeText("<br>(");
bsalomon@google.com8e06dab2011-10-07 20:03:39 +0000807 stream->writeDecAsText(static_cast<int>(diff.fFractionDifference *
808 diff.fBaseWidth *
809 diff.fBaseHeight));
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000810 stream->writeText(" pixels)");
tomhudson@google.com5b325292011-05-24 19:41:13 +0000811/*
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000812 stream->writeDecAsText(diff.fWeightedFraction *
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000813 diff.fBaseWidth *
814 diff.fBaseHeight);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000815 stream->writeText(" weighted pixels)");
816*/
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000817}
818
819static void print_label_cell (SkFILEWStream* stream,
820 const DiffRecord& diff) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000821 char metricBuf [20];
822
823 stream->writeText("<td><b>");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000824 stream->writeText(diff.fFilename.c_str());
epoger@google.com46256ea2012-05-22 13:45:35 +0000825 stream->writeText("</b><br>");
epoger@google.com292aff62012-05-16 14:57:28 +0000826 switch (diff.fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000827 case kEqualBits:
828 SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here");
829 return;
830 case kEqualPixels:
831 SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here");
epoger@google.com5fd53852012-03-22 18:20:06 +0000832 return;
epoger@google.com292aff62012-05-16 14:57:28 +0000833 case kDifferentSizes:
epoger@google.com46256ea2012-05-22 13:45:35 +0000834 stream->writeText("Image sizes differ</td>");
835 return;
836 case kDifferentPixels:
837 sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference);
838 stream->writeText(metricBuf);
839 stream->writeText(" of pixels differ");
840 stream->writeText("\n (");
841 sprintf(metricBuf, "%12.4f%%", 100 * diff.fWeightedFraction);
842 stream->writeText(metricBuf);
843 stream->writeText(" weighted)");
844 // Write the actual number of pixels that differ if it's < 1%
845 if (diff.fFractionDifference < 0.01) {
846 print_pixel_count(stream, diff);
847 }
848 stream->writeText("<br>Average color mismatch ");
849 stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR,
850 diff.fAverageMismatchG,
851 diff.fAverageMismatchB)));
852 stream->writeText("<br>Max color mismatch ");
853 stream->writeDecAsText(MAX3(diff.fMaxMismatchR,
854 diff.fMaxMismatchG,
855 diff.fMaxMismatchB));
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000856 stream->writeText("</td>");
epoger@google.com46256ea2012-05-22 13:45:35 +0000857 break;
858 case kDifferentOther:
859 stream->writeText("Files differ; unable to parse one or both files</td>");
860 return;
861 case kBaseMissing:
862 stream->writeText("Missing from baseDir</td>");
863 return;
864 case kComparisonMissing:
865 stream->writeText("Missing from comparisonDir</td>");
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000866 return;
epoger@google.com292aff62012-05-16 14:57:28 +0000867 default:
epoger@google.com46256ea2012-05-22 13:45:35 +0000868 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
869 return;
tomhudson@google.com8b08c3e2011-11-30 17:01:00 +0000870 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000871}
872
873static void print_image_cell (SkFILEWStream* stream,
tomhudson@google.com4e305982011-07-13 17:42:46 +0000874 const SkString& path,
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000875 int height) {
876 stream->writeText("<td><a href=\"");
tomhudson@google.com4e305982011-07-13 17:42:46 +0000877 stream->writeText(path.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000878 stream->writeText("\"><img src=\"");
tomhudson@google.com4e305982011-07-13 17:42:46 +0000879 stream->writeText(path.c_str());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000880 stream->writeText("\" height=\"");
881 stream->writeDecAsText(height);
882 stream->writeText("px\"></a></td>");
883}
884
epoger@google.com01f78702012-04-12 16:32:04 +0000885static void print_text_cell (SkFILEWStream* stream, const char* text) {
886 stream->writeText("<td align=center>");
887 if (NULL != text) {
888 stream->writeText(text);
889 }
890 stream->writeText("</td>");
891}
892
epoger@google.com5fd53852012-03-22 18:20:06 +0000893static void print_diff_with_missing_file(SkFILEWStream* stream,
894 DiffRecord& diff,
895 const SkString& relativePath) {
896 stream->writeText("<tr>\n");
897 print_label_cell(stream, diff);
898 stream->writeText("<td>N/A</td>");
899 stream->writeText("<td>N/A</td>");
epoger@google.com292aff62012-05-16 14:57:28 +0000900 if (kBaseMissing != diff.fResult) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000901 int h, w;
902 if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) {
903 stream->writeText("<td>N/A</td>");
904 } else {
905 int height = compute_image_height(h, w);
906 if (!diff.fBasePath.startsWith(PATH_DIV_STR)) {
907 diff.fBasePath.prepend(relativePath);
908 }
909 print_image_cell(stream, diff.fBasePath, height);
910 }
911 } else {
912 stream->writeText("<td>N/A</td>");
913 }
epoger@google.com292aff62012-05-16 14:57:28 +0000914 if (kComparisonMissing != diff.fResult) {
epoger@google.com5fd53852012-03-22 18:20:06 +0000915 int h, w;
916 if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) {
917 stream->writeText("<td>N/A</td>");
918 } else {
919 int height = compute_image_height(h, w);
920 if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) {
921 diff.fComparisonPath.prepend(relativePath);
922 }
923 print_image_cell(stream, diff.fComparisonPath, height);
924 }
925 } else {
926 stream->writeText("<td>N/A</td>");
927 }
928 stream->writeText("</tr>\n");
929 stream->flush();
930}
931
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000932static void print_diff_page (const int matchCount,
933 const int colorThreshold,
934 const RecordArray& differences,
935 const SkString& baseDir,
936 const SkString& comparisonDir,
937 const SkString& outputDir) {
938
epoger@google.coma5f406e2012-05-01 13:26:16 +0000939 SkASSERT(!baseDir.isEmpty());
940 SkASSERT(!comparisonDir.isEmpty());
941 SkASSERT(!outputDir.isEmpty());
942
tomhudson@google.com5b325292011-05-24 19:41:13 +0000943 SkString outputPath (outputDir);
944 outputPath.append("index.html");
945 //SkFILEWStream outputStream ("index.html");
946 SkFILEWStream outputStream (outputPath.c_str());
947
tomhudson@google.com4e305982011-07-13 17:42:46 +0000948 // Need to convert paths from relative-to-cwd to relative-to-outputDir
tomhudson@google.com5b325292011-05-24 19:41:13 +0000949 // FIXME this doesn't work if there are '..' inside the outputDir
950 unsigned int ui;
951 SkString relativePath;
952 for (ui = 0; ui < outputDir.size(); ui++) {
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000953 if (outputDir[ui] == PATH_DIV_CHAR) {
954 relativePath.append(".." PATH_DIV_STR);
tomhudson@google.com5b325292011-05-24 19:41:13 +0000955 }
956 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000957
958 outputStream.writeText("<html>\n<body>\n");
epoger@google.com25d961c2012-02-02 20:50:36 +0000959 print_table_header(&outputStream, matchCount, colorThreshold, differences,
960 baseDir, comparisonDir);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000961 int i;
962 for (i = 0; i < differences.count(); i++) {
963 DiffRecord* diff = differences[i];
epoger@google.com5fd53852012-03-22 18:20:06 +0000964
epoger@google.com292aff62012-05-16 14:57:28 +0000965 switch (diff->fResult) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000966 // Cases in which there is no diff to report.
967 case kEqualBits:
epoger@google.com292aff62012-05-16 14:57:28 +0000968 case kEqualPixels:
969 continue;
epoger@google.com46256ea2012-05-22 13:45:35 +0000970 // Cases in which we want a detailed pixel diff.
971 case kDifferentPixels:
972 break;
973 // Cases in which the files differed, but we can't display the diff.
974 case kDifferentSizes:
975 case kDifferentOther:
epoger@google.com292aff62012-05-16 14:57:28 +0000976 case kBaseMissing:
epoger@google.com292aff62012-05-16 14:57:28 +0000977 case kComparisonMissing:
epoger@google.com5fd53852012-03-22 18:20:06 +0000978 print_diff_with_missing_file(&outputStream, *diff, relativePath);
979 continue;
epoger@google.com292aff62012-05-16 14:57:28 +0000980 default:
epoger@google.com46256ea2012-05-22 13:45:35 +0000981 SkDEBUGFAIL("encountered DiffRecord with unknown result type");
982 continue;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000983 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000984
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000985 if (!diff->fBasePath.startsWith(PATH_DIV_STR)) {
tomhudson@google.com4e305982011-07-13 17:42:46 +0000986 diff->fBasePath.prepend(relativePath);
987 }
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000988 if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) {
tomhudson@google.com4e305982011-07-13 17:42:46 +0000989 diff->fComparisonPath.prepend(relativePath);
990 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000991
tomhudson@google.com9b540ce2011-08-02 14:10:04 +0000992 int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000993 outputStream.writeText("<tr>\n");
994 print_label_cell(&outputStream, *diff);
epoger@google.com46256ea2012-05-22 13:45:35 +0000995 print_image_cell(&outputStream,
996 filename_to_white_filename(diff->fFilename), height);
997 print_image_cell(&outputStream,
998 filename_to_diff_filename(diff->fFilename), height);
epoger@google.com25d961c2012-02-02 20:50:36 +0000999 print_image_cell(&outputStream, diff->fBasePath, height);
tomhudson@google.com4e305982011-07-13 17:42:46 +00001000 print_image_cell(&outputStream, diff->fComparisonPath, height);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001001 outputStream.writeText("</tr>\n");
1002 outputStream.flush();
1003 }
1004 outputStream.writeText("</table>\n");
1005 outputStream.writeText("</body>\n</html>\n");
1006 outputStream.flush();
1007}
1008
1009static void usage (char * argv0) {
1010 SkDebugf("Skia baseline image diff tool\n");
epoger@google.coma5f406e2012-05-01 13:26:16 +00001011 SkDebugf("\n"
1012"Usage: \n"
1013" %s <baseDir> <comparisonDir> [outputDir] \n"
epoger@google.coma611c3e2012-05-18 20:10:06 +00001014, argv0, argv0);
epoger@google.coma5f406e2012-05-01 13:26:16 +00001015 SkDebugf("\n"
1016"Arguments: \n"
1017" --nodiffs: don't write out image diffs or index.html, just generate \n"
1018" report on stdout \n"
1019" --threshold <n>: only report differences > n (per color channel) [default 0]\n"
1020" --match: compare files whose filenames contain this substring; if \n"
1021" unspecified, compare ALL files. \n"
1022" this flag may be repeated to add more matching substrings. \n"
1023" --nomatch: regardless of --match, DO NOT compare files whose filenames \n"
1024" contain this substring. \n"
1025" this flag may be repeated to add more forbidden substrings. \n"
tomhudson@google.com7d042802011-07-14 13:15:55 +00001026" --sortbymismatch: sort by average color channel mismatch\n");
1027 SkDebugf(
epoger@google.coma5f406e2012-05-01 13:26:16 +00001028" --sortbymaxmismatch: sort by worst color channel mismatch;\n"
1029" break ties with -sortbymismatch\n"
1030" [default sort is by fraction of pixels mismatching]\n");
tomhudson@google.com5b325292011-05-24 19:41:13 +00001031 SkDebugf(
tomhudson@google.com7d042802011-07-14 13:15:55 +00001032" --weighted: sort by # pixels different weighted by color difference\n");
1033 SkDebugf(
epoger@google.coma611c3e2012-05-18 20:10:06 +00001034" baseDir: directory to read baseline images from.\n");
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001035 SkDebugf(
epoger@google.coma5f406e2012-05-01 13:26:16 +00001036" comparisonDir: directory to read comparison images from\n");
1037 SkDebugf(
1038" outputDir: directory to write difference images and index.html to; \n"
epoger@google.coma611c3e2012-05-18 20:10:06 +00001039" defaults to comparisonDir \n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001040}
1041
1042int main (int argc, char ** argv) {
1043 DiffMetricProc diffProc = compute_diff_pmcolor;
reed@google.comc7a67cb2012-05-07 14:52:12 +00001044 int (*sortProc)(const void*, const void*) = SkCastForQSort(compare_diff_metrics);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001045
1046 // Maximum error tolerated in any one color channel in any one pixel before
1047 // a difference is reported.
1048 int colorThreshold = 0;
1049 SkString baseDir;
1050 SkString comparisonDir;
1051 SkString outputDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001052 StringArray matchSubstrings;
1053 StringArray nomatchSubstrings;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001054
epoger@google.coma5f406e2012-05-01 13:26:16 +00001055 bool generateDiffs = true;
tomhudson@google.com7d042802011-07-14 13:15:55 +00001056
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001057 RecordArray differences;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001058 DiffSummary summary;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001059
epoger@google.coma5f406e2012-05-01 13:26:16 +00001060 int i;
1061 int numUnflaggedArguments = 0;
1062 for (i = 1; i < argc; i++) {
tomhudson@google.com7d042802011-07-14 13:15:55 +00001063 if (!strcmp(argv[i], "--help")) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001064 usage(argv[0]);
1065 return 0;
1066 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001067 if (!strcmp(argv[i], "--nodiffs")) {
1068 generateDiffs = false;
1069 continue;
1070 }
1071 if (!strcmp(argv[i], "--threshold")) {
1072 colorThreshold = atoi(argv[++i]);
1073 continue;
1074 }
1075 if (!strcmp(argv[i], "--match")) {
1076 matchSubstrings.push(new SkString(argv[++i]));
1077 continue;
1078 }
1079 if (!strcmp(argv[i], "--nomatch")) {
1080 nomatchSubstrings.push(new SkString(argv[++i]));
1081 continue;
1082 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001083 if (!strcmp(argv[i], "--sortbymismatch")) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001084 sortProc = SkCastForQSort(compare_diff_mean_mismatches);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001085 continue;
1086 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001087 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001088 sortProc = SkCastForQSort(compare_diff_max_mismatches);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001089 continue;
1090 }
tomhudson@google.com7d042802011-07-14 13:15:55 +00001091 if (!strcmp(argv[i], "--weighted")) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001092 sortProc = SkCastForQSort(compare_diff_weighted);
tomhudson@google.com5b325292011-05-24 19:41:13 +00001093 continue;
1094 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001095 if (argv[i][0] != '-') {
epoger@google.coma5f406e2012-05-01 13:26:16 +00001096 switch (numUnflaggedArguments++) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001097 case 0:
1098 baseDir.set(argv[i]);
1099 continue;
1100 case 1:
1101 comparisonDir.set(argv[i]);
1102 continue;
1103 case 2:
1104 outputDir.set(argv[i]);
1105 continue;
1106 default:
epoger@google.coma5f406e2012-05-01 13:26:16 +00001107 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001108 usage(argv[0]);
1109 return 0;
1110 }
1111 }
1112
1113 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
1114 usage(argv[0]);
1115 return 0;
1116 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001117
epoger@google.coma5f406e2012-05-01 13:26:16 +00001118 if (numUnflaggedArguments == 2) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001119 outputDir = comparisonDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +00001120 } else if (numUnflaggedArguments != 3) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001121 usage(argv[0]);
1122 return 0;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001123 }
1124
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001125 if (!baseDir.endsWith(PATH_DIV_STR)) {
1126 baseDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001127 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001128 printf("baseDir is [%s]\n", baseDir.c_str());
1129
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001130 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
1131 comparisonDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001132 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001133 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
1134
bsalomon@google.com1a315fe2011-09-23 14:56:37 +00001135 if (!outputDir.endsWith(PATH_DIV_STR)) {
1136 outputDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001137 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001138 if (generateDiffs) {
1139 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
1140 } else {
1141 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
1142 outputDir.set("");
1143 }
1144
1145 // Default substring matching:
1146 // - No matter what, don't match any PDF files.
1147 // We may want to change this later, but for now this maintains the filter
1148 // that get_file_list() used to always apply.
1149 // - If no matchSubstrings were specified, match ALL strings.
1150 nomatchSubstrings.push(new SkString(".pdf"));
1151 if (matchSubstrings.isEmpty()) {
1152 matchSubstrings.push(new SkString(""));
1153 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001154
epoger@google.coma611c3e2012-05-18 20:10:06 +00001155 create_diff_images(diffProc, colorThreshold, &differences,
1156 baseDir, comparisonDir, outputDir,
1157 matchSubstrings, nomatchSubstrings, &summary);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +00001158 summary.print();
tomhudson@google.com7d042802011-07-14 13:15:55 +00001159
1160 if (differences.count()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +00001161 qsort(differences.begin(), differences.count(),
1162 sizeof(DiffRecord*), sortProc);
tomhudson@google.com7d042802011-07-14 13:15:55 +00001163 }
epoger@google.com66008522012-05-16 17:40:57 +00001164
epoger@google.coma5f406e2012-05-01 13:26:16 +00001165 if (generateDiffs) {
1166 print_diff_page(summary.fNumMatches, colorThreshold, differences,
1167 baseDir, comparisonDir, outputDir);
1168 }
epoger@google.com76222c02012-05-31 15:12:09 +00001169
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001170 for (i = 0; i < differences.count(); i++) {
1171 delete differences[i];
1172 }
epoger@google.coma5f406e2012-05-01 13:26:16 +00001173 matchSubstrings.deleteAll();
1174 nomatchSubstrings.deleteAll();
epoger@google.combe6188d2012-05-31 15:13:45 +00001175
1176 return summary.fNumMismatches;
tomhudson@google.com4b33d282011-04-27 15:39:30 +00001177}