blob: df07844734c4f02494f3dfda86a22c4e89e9a7c9 [file] [log] [blame]
epoger@google.comec3ed6a2011-07-28 14:26:00 +00001/*
2 * Copyright 2011 Google Inc.
3 *
4 * Use of this source code is governed by a BSD-style license that can be
5 * found in the LICENSE file.
6 */
bungeman@google.come3c8ddf2012-12-05 20:13:12 +00007#include "skdiff.h"
8#include "skdiff_html.h"
9#include "skdiff_utils.h"
10#include "SkBitmap.h"
epoger@google.com46256ea2012-05-22 13:45:35 +000011#include "SkData.h"
zachr@google.com6ef5a852013-06-18 21:23:31 +000012#include "SkForceLinking.h"
tomhudson@google.com4b33d282011-04-27 15:39:30 +000013#include "SkImageDecoder.h"
14#include "SkImageEncoder.h"
15#include "SkOSFile.h"
16#include "SkStream.h"
17#include "SkTDArray.h"
18#include "SkTemplates.h"
tomhudson@google.com4b33d282011-04-27 15:39:30 +000019#include "SkTSearch.h"
20#include "SkTypes.h"
21
zachr@google.com6ef5a852013-06-18 21:23:31 +000022__SK_FORCE_IMAGE_DECODER_LINKING;
23
tomhudson@google.com4b33d282011-04-27 15:39:30 +000024/**
25 * skdiff
26 *
27 * Given three directory names, expects to find identically-named files in
28 * each of the first two; the first are treated as a set of baseline,
29 * the second a set of variant images, and a diff image is written into the
30 * third directory for each pair.
tomhudson@google.com7d042802011-07-14 13:15:55 +000031 * Creates an index.html in the current third directory to compare each
tomhudson@google.com4b33d282011-04-27 15:39:30 +000032 * pair that does not match exactly.
epoger@google.com71329d82012-08-16 13:42:13 +000033 * Recursively descends directories, unless run with --norecurse.
epoger@google.combe6188d2012-05-31 15:13:45 +000034 *
35 * Returns zero exit code if all images match across baseDir and comparisonDir.
tomhudson@google.com4b33d282011-04-27 15:39:30 +000036 */
37
epoger@google.coma5f406e2012-05-01 13:26:16 +000038typedef SkTDArray<SkString*> StringArray;
39typedef StringArray FileArray;
epoger@google.com5fd53852012-03-22 18:20:06 +000040
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000041struct DiffSummary {
42 DiffSummary ()
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000043 : fNumMatches(0)
44 , fNumMismatches(0)
45 , fMaxMismatchV(0)
46 , fMaxMismatchPercent(0) { };
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000047
epoger@google.com5fd53852012-03-22 18:20:06 +000048 ~DiffSummary() {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000049 for (int i = 0; i < DiffRecord::kResultCount; ++i) {
epoger@google.com76222c02012-05-31 15:12:09 +000050 fResultsOfType[i].deleteAll();
51 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000052 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
53 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
54 fStatusOfType[base][comparison].deleteAll();
55 }
skia.committer@gmail.com0264fb42012-12-06 02:01:25 +000056 }
epoger@google.com5fd53852012-03-22 18:20:06 +000057 }
58
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000059 uint32_t fNumMatches;
60 uint32_t fNumMismatches;
61 uint32_t fMaxMismatchV;
62 float fMaxMismatchPercent;
63
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000064 FileArray fResultsOfType[DiffRecord::kResultCount];
65 FileArray fStatusOfType[DiffResource::kStatusCount][DiffResource::kStatusCount];
66
67 void printContents(const FileArray& fileArray,
68 const char* baseStatus, const char* comparisonStatus,
69 bool listFilenames) {
70 int n = fileArray.count();
71 printf("%d file pairs %s in baseDir and %s in comparisonDir",
72 n, baseStatus, comparisonStatus);
73 if (listFilenames) {
74 printf(": ");
75 for (int i = 0; i < n; ++i) {
76 printf("%s ", fileArray[i]->c_str());
77 }
78 }
79 printf("\n");
80 }
81
82 void printStatus(bool listFilenames,
83 bool failOnStatusType[DiffResource::kStatusCount]
84 [DiffResource::kStatusCount]) {
85 typedef DiffResource::Status Status;
86
87 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
88 Status baseStatus = static_cast<Status>(base);
89 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
90 Status comparisonStatus = static_cast<Status>(comparison);
91 const FileArray& fileArray = fStatusOfType[base][comparison];
92 if (fileArray.count() > 0) {
93 if (failOnStatusType[base][comparison]) {
94 printf(" [*] ");
95 } else {
96 printf(" [_] ");
97 }
98 printContents(fileArray,
99 DiffResource::getStatusDescription(baseStatus),
100 DiffResource::getStatusDescription(comparisonStatus),
101 listFilenames);
102 }
103 }
104 }
105 }
epoger@google.com76222c02012-05-31 15:12:09 +0000106
epoger@google.com3af4ff42012-07-19 17:35:04 +0000107 // Print a line about the contents of this FileArray to stdout.
epoger@google.com46a45962012-07-12 18:16:02 +0000108 void printContents(const FileArray& fileArray, const char* headerText, bool listFilenames) {
epoger@google.com76222c02012-05-31 15:12:09 +0000109 int n = fileArray.count();
epoger@google.com3af4ff42012-07-19 17:35:04 +0000110 printf("%d file pairs %s", n, headerText);
111 if (listFilenames) {
112 printf(": ");
113 for (int i = 0; i < n; ++i) {
114 printf("%s ", fileArray[i]->c_str());
epoger@google.com76222c02012-05-31 15:12:09 +0000115 }
116 }
epoger@google.com3af4ff42012-07-19 17:35:04 +0000117 printf("\n");
epoger@google.com76222c02012-05-31 15:12:09 +0000118 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000119
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000120 void print(bool listFilenames, bool failOnResultType[DiffRecord::kResultCount],
121 bool failOnStatusType[DiffResource::kStatusCount]
122 [DiffResource::kStatusCount]) {
epoger@google.com3af4ff42012-07-19 17:35:04 +0000123 printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches);
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000124 for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) {
125 DiffRecord::Result result = static_cast<DiffRecord::Result>(resultInt);
epoger@google.com3af4ff42012-07-19 17:35:04 +0000126 if (failOnResultType[result]) {
127 printf("[*] ");
128 } else {
129 printf("[_] ");
130 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000131 printContents(fResultsOfType[result], DiffRecord::getResultDescription(result),
132 listFilenames);
133 if (DiffRecord::kCouldNotCompare_Result == result) {
134 printStatus(listFilenames, failOnStatusType);
135 }
epoger@google.com46a45962012-07-12 18:16:02 +0000136 }
epoger@google.com3af4ff42012-07-19 17:35:04 +0000137 printf("(results marked with [*] will cause nonzero return value)\n");
138 printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000139 if (fNumMismatches > 0) {
140 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
epoger@google.com46a45962012-07-12 18:16:02 +0000141 printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000142 }
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000143 }
144
145 void add (DiffRecord* drp) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000146 uint32_t mismatchValue;
epoger@google.com292aff62012-05-16 14:57:28 +0000147
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000148 if (drp->fBase.fFilename.equals(drp->fComparison.fFilename)) {
149 fResultsOfType[drp->fResult].push(new SkString(drp->fBase.fFilename));
150 } else {
151 SkString* blame = new SkString("(");
152 blame->append(drp->fBase.fFilename);
153 blame->append(", ");
154 blame->append(drp->fComparison.fFilename);
155 blame->append(")");
156 fResultsOfType[drp->fResult].push(blame);
157 }
epoger@google.com292aff62012-05-16 14:57:28 +0000158 switch (drp->fResult) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000159 case DiffRecord::kEqualBits_Result:
epoger@google.com46256ea2012-05-22 13:45:35 +0000160 fNumMatches++;
161 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000162 case DiffRecord::kEqualPixels_Result:
epoger@google.com292aff62012-05-16 14:57:28 +0000163 fNumMatches++;
164 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000165 case DiffRecord::kDifferentSizes_Result:
epoger@google.com5fd53852012-03-22 18:20:06 +0000166 fNumMismatches++;
epoger@google.com292aff62012-05-16 14:57:28 +0000167 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000168 case DiffRecord::kDifferentPixels_Result:
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000169 fNumMismatches++;
170 if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
171 fMaxMismatchPercent = drp->fFractionDifference * 100;
172 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000173 mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
174 drp->fMaxMismatchB);
175 if (mismatchValue > fMaxMismatchV) {
176 fMaxMismatchV = mismatchValue;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000177 }
epoger@google.com292aff62012-05-16 14:57:28 +0000178 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000179 case DiffRecord::kCouldNotCompare_Result:
epoger@google.com46256ea2012-05-22 13:45:35 +0000180 fNumMismatches++;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000181 fStatusOfType[drp->fBase.fStatus][drp->fComparison.fStatus].push(
182 new SkString(drp->fBase.fFilename));
epoger@google.com46256ea2012-05-22 13:45:35 +0000183 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000184 case DiffRecord::kUnknown_Result:
epoger@google.com46256ea2012-05-22 13:45:35 +0000185 SkDEBUGFAIL("adding uncategorized DiffRecord");
186 break;
187 default:
188 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
189 break;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000190 }
191 }
192};
193
epoger@google.coma5f406e2012-05-01 13:26:16 +0000194/// Returns true if string contains any of these substrings.
195static bool string_contains_any_of(const SkString& string,
196 const StringArray& substrings) {
197 for (int i = 0; i < substrings.count(); i++) {
198 if (string.contains(substrings[i]->c_str())) {
199 return true;
200 }
201 }
202 return false;
203}
204
epoger@google.com71329d82012-08-16 13:42:13 +0000205/// Internal (potentially recursive) implementation of get_file_list.
206static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir,
207 const StringArray& matchSubstrings,
208 const StringArray& nomatchSubstrings,
209 bool recurseIntoSubdirs, FileArray *files) {
210 bool isSubDirEmpty = subDir.isEmpty();
211 SkString dir(rootDir);
212 if (!isSubDirEmpty) {
213 dir.append(PATH_DIV_STR);
214 dir.append(subDir);
215 }
216
217 // Iterate over files (not directories) within dir.
218 SkOSFile::Iter fileIterator(dir.c_str());
219 SkString fileName;
220 while (fileIterator.next(&fileName, false)) {
221 if (fileName.startsWith(".")) {
222 continue;
223 }
224 SkString pathRelativeToRootDir(subDir);
225 if (!isSubDirEmpty) {
226 pathRelativeToRootDir.append(PATH_DIV_STR);
227 }
228 pathRelativeToRootDir.append(fileName);
229 if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) &&
230 !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
231 files->push(new SkString(pathRelativeToRootDir));
232 }
233 }
234
235 // Recurse into any non-ignored subdirectories.
236 if (recurseIntoSubdirs) {
237 SkOSFile::Iter dirIterator(dir.c_str());
238 SkString dirName;
239 while (dirIterator.next(&dirName, true)) {
240 if (dirName.startsWith(".")) {
241 continue;
242 }
243 SkString pathRelativeToRootDir(subDir);
244 if (!isSubDirEmpty) {
245 pathRelativeToRootDir.append(PATH_DIV_STR);
246 }
247 pathRelativeToRootDir.append(dirName);
248 if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
249 get_file_list_subdir(rootDir, pathRelativeToRootDir,
250 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
251 files);
252 }
253 }
254 }
255}
256
257/// Iterate over dir and get all files whose filename:
258/// - matches any of the substrings in matchSubstrings, but...
259/// - DOES NOT match any of the substrings in nomatchSubstrings
260/// - DOES NOT start with a dot (.)
261/// Adds the matching files to the list in *files.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000262static void get_file_list(const SkString& dir,
263 const StringArray& matchSubstrings,
264 const StringArray& nomatchSubstrings,
epoger@google.com71329d82012-08-16 13:42:13 +0000265 bool recurseIntoSubdirs, FileArray *files) {
266 get_file_list_subdir(dir, SkString(""),
267 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
268 files);
epoger@google.com5fd53852012-03-22 18:20:06 +0000269}
270
271static void release_file_list(FileArray *files) {
272 files->deleteAll();
273}
274
275/// Comparison routines for qsort, sort by file names.
276static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
277 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
278}
279
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000280class AutoReleasePixels {
281public:
282 AutoReleasePixels(DiffRecord* drp)
283 : fDrp(drp) {
284 SkASSERT(drp != NULL);
285 }
286 ~AutoReleasePixels() {
287 fDrp->fBase.fBitmap.setPixelRef(NULL);
288 fDrp->fComparison.fBitmap.setPixelRef(NULL);
289 fDrp->fDifference.fBitmap.setPixelRef(NULL);
290 fDrp->fWhite.fBitmap.setPixelRef(NULL);
291 }
292
293private:
294 DiffRecord* fDrp;
295};
296
297static void get_bounds(DiffResource& resource, const char* name) {
298 if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) {
299 SkAutoDataUnref fileBits(read_file(resource.fFullPath.c_str()));
300 if (NULL == fileBits) {
301 SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str());
302 resource.fStatus = DiffResource::kCouldNotRead_Status;
303 } else {
304 get_bitmap(fileBits, resource, SkImageDecoder::kDecodeBounds_Mode);
305 }
306 }
307}
308
309static void get_bounds(DiffRecord& drp) {
310 get_bounds(drp.fBase, "base");
311 get_bounds(drp.fComparison, "comparison");
312}
313
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000314#ifdef SK_OS_WIN
315#define ANSI_COLOR_RED ""
316#define ANSI_COLOR_GREEN ""
317#define ANSI_COLOR_YELLOW ""
318#define ANSI_COLOR_RESET ""
319#else
320#define ANSI_COLOR_RED "\x1b[31m"
321#define ANSI_COLOR_GREEN "\x1b[32m"
322#define ANSI_COLOR_YELLOW "\x1b[33m"
323#define ANSI_COLOR_RESET "\x1b[0m"
324#endif
325
326#define VERBOSE_STATUS(status,color,filename) if (verbose) printf( "[ " color " %10s " ANSI_COLOR_RESET " ] %s\n", status, filename->c_str())
327
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000328/// Creates difference images, returns the number that have a 0 metric.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000329/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000330static void create_diff_images (DiffMetricProc dmp,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000331 const int colorThreshold,
332 RecordArray* differences,
333 const SkString& baseDir,
334 const SkString& comparisonDir,
335 const SkString& outputDir,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000336 const StringArray& matchSubstrings,
337 const StringArray& nomatchSubstrings,
epoger@google.com71329d82012-08-16 13:42:13 +0000338 bool recurseIntoSubdirs,
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000339 bool getBounds,
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000340 bool verbose,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000341 DiffSummary* summary) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000342 SkASSERT(!baseDir.isEmpty());
343 SkASSERT(!comparisonDir.isEmpty());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000344
epoger@google.com5fd53852012-03-22 18:20:06 +0000345 FileArray baseFiles;
346 FileArray comparisonFiles;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000347
epoger@google.com71329d82012-08-16 13:42:13 +0000348 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
349 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000350 &comparisonFiles);
epoger@google.com5fd53852012-03-22 18:20:06 +0000351
epoger@google.coma5f406e2012-05-01 13:26:16 +0000352 if (!baseFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000353 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
354 SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000355 }
356 if (!comparisonFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000357 qsort(comparisonFiles.begin(), comparisonFiles.count(),
358 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000359 }
epoger@google.com66008522012-05-16 17:40:57 +0000360
epoger@google.com5fd53852012-03-22 18:20:06 +0000361 int i = 0;
362 int j = 0;
363
364 while (i < baseFiles.count() &&
365 j < comparisonFiles.count()) {
366
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000367 SkString basePath(baseDir);
368 SkString comparisonPath(comparisonDir);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000369
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000370 DiffRecord *drp = new DiffRecord;
371 int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
epoger@google.com5fd53852012-03-22 18:20:06 +0000372
373 if (v < 0) {
374 // in baseDir, but not in comparisonDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000375 drp->fResult = DiffRecord::kCouldNotCompare_Result;
376
377 basePath.append(*baseFiles[i]);
378 comparisonPath.append(*baseFiles[i]);
379
380 drp->fBase.fFilename = *baseFiles[i];
381 drp->fBase.fFullPath = basePath;
382 drp->fBase.fStatus = DiffResource::kExists_Status;
383
384 drp->fComparison.fFilename = *baseFiles[i];
385 drp->fComparison.fFullPath = comparisonPath;
386 drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
387
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000388 VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]);
389
epoger@google.com5fd53852012-03-22 18:20:06 +0000390 ++i;
391 } else if (v > 0) {
392 // in comparisonDir, but not in baseDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000393 drp->fResult = DiffRecord::kCouldNotCompare_Result;
394
395 basePath.append(*comparisonFiles[j]);
396 comparisonPath.append(*comparisonFiles[j]);
397
398 drp->fBase.fFilename = *comparisonFiles[j];
399 drp->fBase.fFullPath = basePath;
400 drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
401
402 drp->fComparison.fFilename = *comparisonFiles[j];
403 drp->fComparison.fFullPath = comparisonPath;
404 drp->fComparison.fStatus = DiffResource::kExists_Status;
405
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000406 VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]);
407
epoger@google.com5fd53852012-03-22 18:20:06 +0000408 ++j;
409 } else {
epoger@google.com46256ea2012-05-22 13:45:35 +0000410 // Found the same filename in both baseDir and comparisonDir.
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000411 SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
epoger@google.com5fd53852012-03-22 18:20:06 +0000412
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000413 basePath.append(*baseFiles[i]);
414 comparisonPath.append(*comparisonFiles[j]);
415
416 drp->fBase.fFilename = *baseFiles[i];
417 drp->fBase.fFullPath = basePath;
418 drp->fBase.fStatus = DiffResource::kExists_Status;
419
420 drp->fComparison.fFilename = *comparisonFiles[j];
421 drp->fComparison.fFullPath = comparisonPath;
422 drp->fComparison.fStatus = DiffResource::kExists_Status;
423
424 SkAutoDataUnref baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
bsalomon49f085d2014-09-05 13:34:00 -0700425 if (baseFileBits) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000426 drp->fBase.fStatus = DiffResource::kRead_Status;
427 }
428 SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
bsalomon49f085d2014-09-05 13:34:00 -0700429 if (comparisonFileBits) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000430 drp->fComparison.fStatus = DiffResource::kRead_Status;
431 }
432 if (NULL == baseFileBits || NULL == comparisonFileBits) {
433 if (NULL == baseFileBits) {
434 drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000435 VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]);
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000436 }
437 if (NULL == comparisonFileBits) {
438 drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000439 VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]);
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000440 }
441 drp->fResult = DiffRecord::kCouldNotCompare_Result;
442
443 } else if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
444 drp->fResult = DiffRecord::kEqualBits_Result;
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000445 VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]);
epoger@google.com46256ea2012-05-22 13:45:35 +0000446 } else {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000447 AutoReleasePixels arp(drp);
448 get_bitmap(baseFileBits, drp->fBase, SkImageDecoder::kDecodePixels_Mode);
449 get_bitmap(comparisonFileBits, drp->fComparison,
450 SkImageDecoder::kDecodePixels_Mode);
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000451 VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]);
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000452 if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
453 DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
454 create_and_write_diff_image(drp, dmp, colorThreshold,
455 outputDir, drp->fBase.fFilename);
epoger@google.com46256ea2012-05-22 13:45:35 +0000456 } else {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000457 drp->fResult = DiffRecord::kCouldNotCompare_Result;
epoger@google.com46256ea2012-05-22 13:45:35 +0000458 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000459 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000460
epoger@google.com5fd53852012-03-22 18:20:06 +0000461 ++i;
462 ++j;
463 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000464
465 if (getBounds) {
466 get_bounds(*drp);
467 }
468 SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000469 differences->push(drp);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000470 summary->add(drp);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000471 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000472
473 for (; i < baseFiles.count(); ++i) {
474 // files only in baseDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000475 DiffRecord *drp = new DiffRecord();
476 drp->fBase.fFilename = *baseFiles[i];
477 drp->fBase.fFullPath = baseDir;
478 drp->fBase.fFullPath.append(drp->fBase.fFilename);
479 drp->fBase.fStatus = DiffResource::kExists_Status;
480
481 drp->fComparison.fFilename = *baseFiles[i];
482 drp->fComparison.fFullPath = comparisonDir;
483 drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
484 drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
485
486 drp->fResult = DiffRecord::kCouldNotCompare_Result;
487 if (getBounds) {
488 get_bounds(*drp);
489 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000490 differences->push(drp);
491 summary->add(drp);
492 }
493
494 for (; j < comparisonFiles.count(); ++j) {
495 // files only in comparisonDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000496 DiffRecord *drp = new DiffRecord();
497 drp->fBase.fFilename = *comparisonFiles[j];
498 drp->fBase.fFullPath = baseDir;
499 drp->fBase.fFullPath.append(drp->fBase.fFilename);
500 drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
501
502 drp->fComparison.fFilename = *comparisonFiles[j];
503 drp->fComparison.fFullPath = comparisonDir;
504 drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
505 drp->fComparison.fStatus = DiffResource::kExists_Status;
506
507 drp->fResult = DiffRecord::kCouldNotCompare_Result;
508 if (getBounds) {
509 get_bounds(*drp);
510 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000511 differences->push(drp);
512 summary->add(drp);
513 }
514
515 release_file_list(&baseFiles);
516 release_file_list(&comparisonFiles);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000517}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000518
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000519static void usage (char * argv0) {
520 SkDebugf("Skia baseline image diff tool\n");
epoger@google.coma5f406e2012-05-01 13:26:16 +0000521 SkDebugf("\n"
522"Usage: \n"
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000523" %s <baseDir> <comparisonDir> [outputDir] \n", argv0);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000524 SkDebugf(
epoger@google.com46a45962012-07-12 18:16:02 +0000525"\nArguments:"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000526"\n --failonresult <result>: After comparing all file pairs, exit with nonzero"
527"\n return code (number of file pairs yielding this"
528"\n result) if any file pairs yielded this result."
529"\n This flag may be repeated, in which case the"
530"\n return code will be the number of fail pairs"
531"\n yielding ANY of these results."
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000532"\n --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
533"\n code if any file pairs yielded this status."
epoger@google.com46a45962012-07-12 18:16:02 +0000534"\n --help: display this info"
epoger@google.com46a45962012-07-12 18:16:02 +0000535"\n --listfilenames: list all filenames for each result type in stdout"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000536"\n --match <substring>: compare files whose filenames contain this substring;"
537"\n if unspecified, compare ALL files."
538"\n this flag may be repeated."
epoger@google.com46a45962012-07-12 18:16:02 +0000539"\n --nodiffs: don't write out image diffs or index.html, just generate"
540"\n report on stdout"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000541"\n --nomatch <substring>: regardless of --match, DO NOT compare files whose"
542"\n filenames contain this substring."
543"\n this flag may be repeated."
epoger@google.com46a45962012-07-12 18:16:02 +0000544"\n --noprintdirs: do not print the directories used."
epoger@google.com71329d82012-08-16 13:42:13 +0000545"\n --norecurse: do not recurse into subdirectories."
epoger@google.com46a45962012-07-12 18:16:02 +0000546"\n --sortbymaxmismatch: sort by worst color channel mismatch;"
547"\n break ties with -sortbymismatch"
548"\n --sortbymismatch: sort by average color channel mismatch"
549"\n --threshold <n>: only report differences > n (per color channel) [default 0]"
550"\n --weighted: sort by # pixels different weighted by color difference"
551"\n"
552"\n baseDir: directory to read baseline images from."
553"\n comparisonDir: directory to read comparison images from"
554"\n outputDir: directory to write difference images and index.html to;"
555"\n defaults to comparisonDir"
556"\n"
557"\nIf no sort is specified, it will sort by fraction of pixels mismatching."
558"\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000559}
560
epoger@google.com70044cc2012-07-12 18:37:55 +0000561const int kNoError = 0;
562const int kGenericError = -1;
epoger@google.com46a45962012-07-12 18:16:02 +0000563
caryclark@google.com5987f582012-10-02 18:33:14 +0000564int tool_main(int argc, char** argv);
565int tool_main(int argc, char** argv) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000566 DiffMetricProc diffProc = compute_diff_pmcolor;
epoger@google.com28060e72012-06-28 16:47:34 +0000567 int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000568
569 // Maximum error tolerated in any one color channel in any one pixel before
570 // a difference is reported.
571 int colorThreshold = 0;
572 SkString baseDir;
573 SkString comparisonDir;
574 SkString outputDir;
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000575
epoger@google.coma5f406e2012-05-01 13:26:16 +0000576 StringArray matchSubstrings;
577 StringArray nomatchSubstrings;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000578
epoger@google.coma5f406e2012-05-01 13:26:16 +0000579 bool generateDiffs = true;
epoger@google.com46a45962012-07-12 18:16:02 +0000580 bool listFilenames = false;
epoger@google.com71329d82012-08-16 13:42:13 +0000581 bool printDirNames = true;
582 bool recurseIntoSubdirs = true;
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000583 bool verbose = false;
tomhudson@google.com7d042802011-07-14 13:15:55 +0000584
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000585 RecordArray differences;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000586 DiffSummary summary;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000587
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000588 bool failOnResultType[DiffRecord::kResultCount];
589 for (int i = 0; i < DiffRecord::kResultCount; i++) {
epoger@google.com3af4ff42012-07-19 17:35:04 +0000590 failOnResultType[i] = false;
591 }
592
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000593 bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
594 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
595 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
596 failOnStatusType[base][comparison] = false;
597 }
598 }
599
epoger@google.coma5f406e2012-05-01 13:26:16 +0000600 int i;
601 int numUnflaggedArguments = 0;
602 for (i = 1; i < argc; i++) {
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000603 if (!strcmp(argv[i], "--failonresult")) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000604 if (argc == ++i) {
605 SkDebugf("failonresult expects one argument.\n");
606 continue;
607 }
608 DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
609 if (type != DiffRecord::kResultCount) {
610 failOnResultType[type] = true;
611 } else {
612 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
613 }
614 continue;
615 }
616 if (!strcmp(argv[i], "--failonstatus")) {
617 if (argc == ++i) {
618 SkDebugf("failonstatus missing base status.\n");
619 continue;
620 }
621 bool baseStatuses[DiffResource::kStatusCount];
622 if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
623 SkDebugf("unrecognized base status <%s>\n", argv[i]);
624 }
625
626 if (argc == ++i) {
627 SkDebugf("failonstatus missing comparison status.\n");
628 continue;
629 }
630 bool comparisonStatuses[DiffResource::kStatusCount];
631 if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
632 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
633 }
634
635 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
636 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
637 failOnStatusType[base][comparison] |=
638 baseStatuses[base] && comparisonStatuses[comparison];
639 }
640 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000641 continue;
642 }
epoger@google.com46a45962012-07-12 18:16:02 +0000643 if (!strcmp(argv[i], "--help")) {
644 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000645 return kNoError;
epoger@google.com46a45962012-07-12 18:16:02 +0000646 }
647 if (!strcmp(argv[i], "--listfilenames")) {
648 listFilenames = true;
epoger@google.coma5f406e2012-05-01 13:26:16 +0000649 continue;
650 }
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000651 if (!strcmp(argv[i], "--verbose")) {
652 verbose = true;
653 continue;
654 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000655 if (!strcmp(argv[i], "--match")) {
656 matchSubstrings.push(new SkString(argv[++i]));
657 continue;
658 }
epoger@google.com46a45962012-07-12 18:16:02 +0000659 if (!strcmp(argv[i], "--nodiffs")) {
660 generateDiffs = false;
661 continue;
662 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000663 if (!strcmp(argv[i], "--nomatch")) {
664 nomatchSubstrings.push(new SkString(argv[++i]));
665 continue;
666 }
epoger@google.com46a45962012-07-12 18:16:02 +0000667 if (!strcmp(argv[i], "--noprintdirs")) {
epoger@google.com71329d82012-08-16 13:42:13 +0000668 printDirNames = false;
669 continue;
670 }
671 if (!strcmp(argv[i], "--norecurse")) {
672 recurseIntoSubdirs = false;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000673 continue;
674 }
tomhudson@google.com7d042802011-07-14 13:15:55 +0000675 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
epoger@google.com28060e72012-06-28 16:47:34 +0000676 sortProc = compare<CompareDiffMaxMismatches>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000677 continue;
678 }
epoger@google.com46a45962012-07-12 18:16:02 +0000679 if (!strcmp(argv[i], "--sortbymismatch")) {
680 sortProc = compare<CompareDiffMeanMismatches>;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000681 continue;
682 }
epoger@google.com46a45962012-07-12 18:16:02 +0000683 if (!strcmp(argv[i], "--threshold")) {
684 colorThreshold = atoi(argv[++i]);
685 continue;
686 }
687 if (!strcmp(argv[i], "--weighted")) {
688 sortProc = compare<CompareDiffWeighted>;
keyar@chromium.orga6318192012-07-09 21:01:50 +0000689 continue;
690 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000691 if (argv[i][0] != '-') {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000692 switch (numUnflaggedArguments++) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000693 case 0:
694 baseDir.set(argv[i]);
695 continue;
696 case 1:
697 comparisonDir.set(argv[i]);
698 continue;
699 case 2:
700 outputDir.set(argv[i]);
701 continue;
702 default:
epoger@google.coma5f406e2012-05-01 13:26:16 +0000703 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000704 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000705 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000706 }
707 }
708
709 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
710 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000711 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000712 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000713
epoger@google.coma5f406e2012-05-01 13:26:16 +0000714 if (numUnflaggedArguments == 2) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000715 outputDir = comparisonDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +0000716 } else if (numUnflaggedArguments != 3) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000717 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000718 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000719 }
720
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000721 if (!baseDir.endsWith(PATH_DIV_STR)) {
722 baseDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000723 }
epoger@google.com71329d82012-08-16 13:42:13 +0000724 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000725 printf("baseDir is [%s]\n", baseDir.c_str());
726 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000727
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000728 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
729 comparisonDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000730 }
epoger@google.com71329d82012-08-16 13:42:13 +0000731 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000732 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
733 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000734
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000735 if (!outputDir.endsWith(PATH_DIV_STR)) {
736 outputDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000737 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000738 if (generateDiffs) {
epoger@google.com71329d82012-08-16 13:42:13 +0000739 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000740 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
741 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000742 } else {
epoger@google.com71329d82012-08-16 13:42:13 +0000743 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000744 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
745 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000746 outputDir.set("");
747 }
748
epoger@google.comda4af242012-06-25 18:45:50 +0000749 // If no matchSubstrings were specified, match ALL strings
750 // (except for whatever nomatchSubstrings were specified, if any).
epoger@google.coma5f406e2012-05-01 13:26:16 +0000751 if (matchSubstrings.isEmpty()) {
752 matchSubstrings.push(new SkString(""));
753 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000754
epoger@google.coma611c3e2012-05-18 20:10:06 +0000755 create_diff_images(diffProc, colorThreshold, &differences,
756 baseDir, comparisonDir, outputDir,
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000757 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
commit-bot@chromium.org93d7bb62014-05-28 18:26:00 +0000758 verbose, &summary);
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000759 summary.print(listFilenames, failOnResultType, failOnStatusType);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000760
761 if (differences.count()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000762 qsort(differences.begin(), differences.count(),
763 sizeof(DiffRecord*), sortProc);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000764 }
epoger@google.com66008522012-05-16 17:40:57 +0000765
epoger@google.coma5f406e2012-05-01 13:26:16 +0000766 if (generateDiffs) {
767 print_diff_page(summary.fNumMatches, colorThreshold, differences,
768 baseDir, comparisonDir, outputDir);
769 }
epoger@google.com76222c02012-05-31 15:12:09 +0000770
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000771 for (i = 0; i < differences.count(); i++) {
772 delete differences[i];
773 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000774 matchSubstrings.deleteAll();
775 nomatchSubstrings.deleteAll();
epoger@google.combe6188d2012-05-31 15:13:45 +0000776
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000777 int num_failing_results = 0;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000778 for (int i = 0; i < DiffRecord::kResultCount; i++) {
epoger@google.com3af4ff42012-07-19 17:35:04 +0000779 if (failOnResultType[i]) {
780 num_failing_results += summary.fResultsOfType[i].count();
781 }
epoger@google.com46a45962012-07-12 18:16:02 +0000782 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000783 if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
784 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
785 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
786 if (failOnStatusType[base][comparison]) {
787 num_failing_results += summary.fStatusOfType[base][comparison].count();
788 }
789 }
790 }
791 }
epoger@google.com28659882012-07-16 18:01:06 +0000792
793 // On Linux (and maybe other platforms too), any results outside of the
794 // range [0...255] are wrapped (mod 256). Do the conversion ourselves, to
795 // make sure that we only return 0 when there were no failures.
796 return (num_failing_results > 255) ? 255 : num_failing_results;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000797}
caryclark@google.com5987f582012-10-02 18:33:14 +0000798
799#if !defined SK_BUILD_FOR_IOS
800int main(int argc, char * const argv[]) {
801 return tool_main(argc, (char**) argv);
802}
803#endif