blob: 9d0bcf55e9d92c64fee2628993b50671dae9fdab [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
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000314/// Creates difference images, returns the number that have a 0 metric.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000315/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000316static void create_diff_images (DiffMetricProc dmp,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000317 const int colorThreshold,
318 RecordArray* differences,
319 const SkString& baseDir,
320 const SkString& comparisonDir,
321 const SkString& outputDir,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000322 const StringArray& matchSubstrings,
323 const StringArray& nomatchSubstrings,
epoger@google.com71329d82012-08-16 13:42:13 +0000324 bool recurseIntoSubdirs,
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000325 bool getBounds,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000326 DiffSummary* summary) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000327 SkASSERT(!baseDir.isEmpty());
328 SkASSERT(!comparisonDir.isEmpty());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000329
epoger@google.com5fd53852012-03-22 18:20:06 +0000330 FileArray baseFiles;
331 FileArray comparisonFiles;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000332
epoger@google.com71329d82012-08-16 13:42:13 +0000333 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
334 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000335 &comparisonFiles);
epoger@google.com5fd53852012-03-22 18:20:06 +0000336
epoger@google.coma5f406e2012-05-01 13:26:16 +0000337 if (!baseFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000338 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
339 SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000340 }
341 if (!comparisonFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000342 qsort(comparisonFiles.begin(), comparisonFiles.count(),
343 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000344 }
epoger@google.com66008522012-05-16 17:40:57 +0000345
epoger@google.com5fd53852012-03-22 18:20:06 +0000346 int i = 0;
347 int j = 0;
348
349 while (i < baseFiles.count() &&
350 j < comparisonFiles.count()) {
351
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000352 SkString basePath(baseDir);
353 SkString comparisonPath(comparisonDir);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000354
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000355 DiffRecord *drp = new DiffRecord;
356 int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
epoger@google.com5fd53852012-03-22 18:20:06 +0000357
358 if (v < 0) {
359 // in baseDir, but not in comparisonDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000360 drp->fResult = DiffRecord::kCouldNotCompare_Result;
361
362 basePath.append(*baseFiles[i]);
363 comparisonPath.append(*baseFiles[i]);
364
365 drp->fBase.fFilename = *baseFiles[i];
366 drp->fBase.fFullPath = basePath;
367 drp->fBase.fStatus = DiffResource::kExists_Status;
368
369 drp->fComparison.fFilename = *baseFiles[i];
370 drp->fComparison.fFullPath = comparisonPath;
371 drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
372
epoger@google.com5fd53852012-03-22 18:20:06 +0000373 ++i;
374 } else if (v > 0) {
375 // in comparisonDir, but not in baseDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000376 drp->fResult = DiffRecord::kCouldNotCompare_Result;
377
378 basePath.append(*comparisonFiles[j]);
379 comparisonPath.append(*comparisonFiles[j]);
380
381 drp->fBase.fFilename = *comparisonFiles[j];
382 drp->fBase.fFullPath = basePath;
383 drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
384
385 drp->fComparison.fFilename = *comparisonFiles[j];
386 drp->fComparison.fFullPath = comparisonPath;
387 drp->fComparison.fStatus = DiffResource::kExists_Status;
388
epoger@google.com5fd53852012-03-22 18:20:06 +0000389 ++j;
390 } else {
epoger@google.com46256ea2012-05-22 13:45:35 +0000391 // Found the same filename in both baseDir and comparisonDir.
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000392 SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
epoger@google.com5fd53852012-03-22 18:20:06 +0000393
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000394 basePath.append(*baseFiles[i]);
395 comparisonPath.append(*comparisonFiles[j]);
396
397 drp->fBase.fFilename = *baseFiles[i];
398 drp->fBase.fFullPath = basePath;
399 drp->fBase.fStatus = DiffResource::kExists_Status;
400
401 drp->fComparison.fFilename = *comparisonFiles[j];
402 drp->fComparison.fFullPath = comparisonPath;
403 drp->fComparison.fStatus = DiffResource::kExists_Status;
404
405 SkAutoDataUnref baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
406 if (NULL != baseFileBits) {
407 drp->fBase.fStatus = DiffResource::kRead_Status;
408 }
409 SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
410 if (NULL != comparisonFileBits) {
411 drp->fComparison.fStatus = DiffResource::kRead_Status;
412 }
413 if (NULL == baseFileBits || NULL == comparisonFileBits) {
414 if (NULL == baseFileBits) {
415 drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
416 }
417 if (NULL == comparisonFileBits) {
418 drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
419 }
420 drp->fResult = DiffRecord::kCouldNotCompare_Result;
421
422 } else if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
423 drp->fResult = DiffRecord::kEqualBits_Result;
424
epoger@google.com46256ea2012-05-22 13:45:35 +0000425 } else {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000426 AutoReleasePixels arp(drp);
427 get_bitmap(baseFileBits, drp->fBase, SkImageDecoder::kDecodePixels_Mode);
428 get_bitmap(comparisonFileBits, drp->fComparison,
429 SkImageDecoder::kDecodePixels_Mode);
430 if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
431 DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
432 create_and_write_diff_image(drp, dmp, colorThreshold,
433 outputDir, drp->fBase.fFilename);
epoger@google.com46256ea2012-05-22 13:45:35 +0000434 } else {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000435 drp->fResult = DiffRecord::kCouldNotCompare_Result;
epoger@google.com46256ea2012-05-22 13:45:35 +0000436 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000437 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000438
epoger@google.com5fd53852012-03-22 18:20:06 +0000439 ++i;
440 ++j;
441 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000442
443 if (getBounds) {
444 get_bounds(*drp);
445 }
446 SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000447 differences->push(drp);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000448 summary->add(drp);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000449 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000450
451 for (; i < baseFiles.count(); ++i) {
452 // files only in baseDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000453 DiffRecord *drp = new DiffRecord();
454 drp->fBase.fFilename = *baseFiles[i];
455 drp->fBase.fFullPath = baseDir;
456 drp->fBase.fFullPath.append(drp->fBase.fFilename);
457 drp->fBase.fStatus = DiffResource::kExists_Status;
458
459 drp->fComparison.fFilename = *baseFiles[i];
460 drp->fComparison.fFullPath = comparisonDir;
461 drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
462 drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
463
464 drp->fResult = DiffRecord::kCouldNotCompare_Result;
465 if (getBounds) {
466 get_bounds(*drp);
467 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000468 differences->push(drp);
469 summary->add(drp);
470 }
471
472 for (; j < comparisonFiles.count(); ++j) {
473 // files only in comparisonDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000474 DiffRecord *drp = new DiffRecord();
475 drp->fBase.fFilename = *comparisonFiles[j];
476 drp->fBase.fFullPath = baseDir;
477 drp->fBase.fFullPath.append(drp->fBase.fFilename);
478 drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
479
480 drp->fComparison.fFilename = *comparisonFiles[j];
481 drp->fComparison.fFullPath = comparisonDir;
482 drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
483 drp->fComparison.fStatus = DiffResource::kExists_Status;
484
485 drp->fResult = DiffRecord::kCouldNotCompare_Result;
486 if (getBounds) {
487 get_bounds(*drp);
488 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000489 differences->push(drp);
490 summary->add(drp);
491 }
492
493 release_file_list(&baseFiles);
494 release_file_list(&comparisonFiles);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000495}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000496
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000497static void usage (char * argv0) {
498 SkDebugf("Skia baseline image diff tool\n");
epoger@google.coma5f406e2012-05-01 13:26:16 +0000499 SkDebugf("\n"
500"Usage: \n"
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000501" %s <baseDir> <comparisonDir> [outputDir] \n", argv0);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000502 SkDebugf(
epoger@google.com46a45962012-07-12 18:16:02 +0000503"\nArguments:"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000504"\n --failonresult <result>: After comparing all file pairs, exit with nonzero"
505"\n return code (number of file pairs yielding this"
506"\n result) if any file pairs yielded this result."
507"\n This flag may be repeated, in which case the"
508"\n return code will be the number of fail pairs"
509"\n yielding ANY of these results."
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000510"\n --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
511"\n code if any file pairs yielded this status."
epoger@google.com46a45962012-07-12 18:16:02 +0000512"\n --help: display this info"
epoger@google.com46a45962012-07-12 18:16:02 +0000513"\n --listfilenames: list all filenames for each result type in stdout"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000514"\n --match <substring>: compare files whose filenames contain this substring;"
515"\n if unspecified, compare ALL files."
516"\n this flag may be repeated."
epoger@google.com46a45962012-07-12 18:16:02 +0000517"\n --nodiffs: don't write out image diffs or index.html, just generate"
518"\n report on stdout"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000519"\n --nomatch <substring>: regardless of --match, DO NOT compare files whose"
520"\n filenames contain this substring."
521"\n this flag may be repeated."
epoger@google.com46a45962012-07-12 18:16:02 +0000522"\n --noprintdirs: do not print the directories used."
epoger@google.com71329d82012-08-16 13:42:13 +0000523"\n --norecurse: do not recurse into subdirectories."
epoger@google.com46a45962012-07-12 18:16:02 +0000524"\n --sortbymaxmismatch: sort by worst color channel mismatch;"
525"\n break ties with -sortbymismatch"
526"\n --sortbymismatch: sort by average color channel mismatch"
527"\n --threshold <n>: only report differences > n (per color channel) [default 0]"
528"\n --weighted: sort by # pixels different weighted by color difference"
529"\n"
530"\n baseDir: directory to read baseline images from."
531"\n comparisonDir: directory to read comparison images from"
532"\n outputDir: directory to write difference images and index.html to;"
533"\n defaults to comparisonDir"
534"\n"
535"\nIf no sort is specified, it will sort by fraction of pixels mismatching."
536"\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000537}
538
epoger@google.com70044cc2012-07-12 18:37:55 +0000539const int kNoError = 0;
540const int kGenericError = -1;
epoger@google.com46a45962012-07-12 18:16:02 +0000541
caryclark@google.com5987f582012-10-02 18:33:14 +0000542int tool_main(int argc, char** argv);
543int tool_main(int argc, char** argv) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000544 DiffMetricProc diffProc = compute_diff_pmcolor;
epoger@google.com28060e72012-06-28 16:47:34 +0000545 int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000546
547 // Maximum error tolerated in any one color channel in any one pixel before
548 // a difference is reported.
549 int colorThreshold = 0;
550 SkString baseDir;
551 SkString comparisonDir;
552 SkString outputDir;
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000553
epoger@google.coma5f406e2012-05-01 13:26:16 +0000554 StringArray matchSubstrings;
555 StringArray nomatchSubstrings;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000556
epoger@google.coma5f406e2012-05-01 13:26:16 +0000557 bool generateDiffs = true;
epoger@google.com46a45962012-07-12 18:16:02 +0000558 bool listFilenames = false;
epoger@google.com71329d82012-08-16 13:42:13 +0000559 bool printDirNames = true;
560 bool recurseIntoSubdirs = true;
tomhudson@google.com7d042802011-07-14 13:15:55 +0000561
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000562 RecordArray differences;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000563 DiffSummary summary;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000564
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000565 bool failOnResultType[DiffRecord::kResultCount];
566 for (int i = 0; i < DiffRecord::kResultCount; i++) {
epoger@google.com3af4ff42012-07-19 17:35:04 +0000567 failOnResultType[i] = false;
568 }
569
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000570 bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
571 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
572 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
573 failOnStatusType[base][comparison] = false;
574 }
575 }
576
epoger@google.coma5f406e2012-05-01 13:26:16 +0000577 int i;
578 int numUnflaggedArguments = 0;
579 for (i = 1; i < argc; i++) {
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000580 if (!strcmp(argv[i], "--failonresult")) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000581 if (argc == ++i) {
582 SkDebugf("failonresult expects one argument.\n");
583 continue;
584 }
585 DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
586 if (type != DiffRecord::kResultCount) {
587 failOnResultType[type] = true;
588 } else {
589 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
590 }
591 continue;
592 }
593 if (!strcmp(argv[i], "--failonstatus")) {
594 if (argc == ++i) {
595 SkDebugf("failonstatus missing base status.\n");
596 continue;
597 }
598 bool baseStatuses[DiffResource::kStatusCount];
599 if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
600 SkDebugf("unrecognized base status <%s>\n", argv[i]);
601 }
602
603 if (argc == ++i) {
604 SkDebugf("failonstatus missing comparison status.\n");
605 continue;
606 }
607 bool comparisonStatuses[DiffResource::kStatusCount];
608 if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
609 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
610 }
611
612 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
613 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
614 failOnStatusType[base][comparison] |=
615 baseStatuses[base] && comparisonStatuses[comparison];
616 }
617 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000618 continue;
619 }
epoger@google.com46a45962012-07-12 18:16:02 +0000620 if (!strcmp(argv[i], "--help")) {
621 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000622 return kNoError;
epoger@google.com46a45962012-07-12 18:16:02 +0000623 }
624 if (!strcmp(argv[i], "--listfilenames")) {
625 listFilenames = true;
epoger@google.coma5f406e2012-05-01 13:26:16 +0000626 continue;
627 }
628 if (!strcmp(argv[i], "--match")) {
629 matchSubstrings.push(new SkString(argv[++i]));
630 continue;
631 }
epoger@google.com46a45962012-07-12 18:16:02 +0000632 if (!strcmp(argv[i], "--nodiffs")) {
633 generateDiffs = false;
634 continue;
635 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000636 if (!strcmp(argv[i], "--nomatch")) {
637 nomatchSubstrings.push(new SkString(argv[++i]));
638 continue;
639 }
epoger@google.com46a45962012-07-12 18:16:02 +0000640 if (!strcmp(argv[i], "--noprintdirs")) {
epoger@google.com71329d82012-08-16 13:42:13 +0000641 printDirNames = false;
642 continue;
643 }
644 if (!strcmp(argv[i], "--norecurse")) {
645 recurseIntoSubdirs = false;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000646 continue;
647 }
tomhudson@google.com7d042802011-07-14 13:15:55 +0000648 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
epoger@google.com28060e72012-06-28 16:47:34 +0000649 sortProc = compare<CompareDiffMaxMismatches>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000650 continue;
651 }
epoger@google.com46a45962012-07-12 18:16:02 +0000652 if (!strcmp(argv[i], "--sortbymismatch")) {
653 sortProc = compare<CompareDiffMeanMismatches>;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000654 continue;
655 }
epoger@google.com46a45962012-07-12 18:16:02 +0000656 if (!strcmp(argv[i], "--threshold")) {
657 colorThreshold = atoi(argv[++i]);
658 continue;
659 }
660 if (!strcmp(argv[i], "--weighted")) {
661 sortProc = compare<CompareDiffWeighted>;
keyar@chromium.orga6318192012-07-09 21:01:50 +0000662 continue;
663 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000664 if (argv[i][0] != '-') {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000665 switch (numUnflaggedArguments++) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000666 case 0:
667 baseDir.set(argv[i]);
668 continue;
669 case 1:
670 comparisonDir.set(argv[i]);
671 continue;
672 case 2:
673 outputDir.set(argv[i]);
674 continue;
675 default:
epoger@google.coma5f406e2012-05-01 13:26:16 +0000676 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000677 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000678 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000679 }
680 }
681
682 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
683 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000684 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000685 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000686
epoger@google.coma5f406e2012-05-01 13:26:16 +0000687 if (numUnflaggedArguments == 2) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000688 outputDir = comparisonDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +0000689 } else if (numUnflaggedArguments != 3) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000690 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000691 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000692 }
693
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000694 if (!baseDir.endsWith(PATH_DIV_STR)) {
695 baseDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000696 }
epoger@google.com71329d82012-08-16 13:42:13 +0000697 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000698 printf("baseDir is [%s]\n", baseDir.c_str());
699 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000700
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000701 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
702 comparisonDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000703 }
epoger@google.com71329d82012-08-16 13:42:13 +0000704 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000705 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
706 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000707
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000708 if (!outputDir.endsWith(PATH_DIV_STR)) {
709 outputDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000710 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000711 if (generateDiffs) {
epoger@google.com71329d82012-08-16 13:42:13 +0000712 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000713 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
714 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000715 } else {
epoger@google.com71329d82012-08-16 13:42:13 +0000716 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000717 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
718 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000719 outputDir.set("");
720 }
721
epoger@google.comda4af242012-06-25 18:45:50 +0000722 // If no matchSubstrings were specified, match ALL strings
723 // (except for whatever nomatchSubstrings were specified, if any).
epoger@google.coma5f406e2012-05-01 13:26:16 +0000724 if (matchSubstrings.isEmpty()) {
725 matchSubstrings.push(new SkString(""));
726 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000727
epoger@google.coma611c3e2012-05-18 20:10:06 +0000728 create_diff_images(diffProc, colorThreshold, &differences,
729 baseDir, comparisonDir, outputDir,
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000730 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
731 &summary);
732 summary.print(listFilenames, failOnResultType, failOnStatusType);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000733
734 if (differences.count()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000735 qsort(differences.begin(), differences.count(),
736 sizeof(DiffRecord*), sortProc);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000737 }
epoger@google.com66008522012-05-16 17:40:57 +0000738
epoger@google.coma5f406e2012-05-01 13:26:16 +0000739 if (generateDiffs) {
740 print_diff_page(summary.fNumMatches, colorThreshold, differences,
741 baseDir, comparisonDir, outputDir);
742 }
epoger@google.com76222c02012-05-31 15:12:09 +0000743
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000744 for (i = 0; i < differences.count(); i++) {
745 delete differences[i];
746 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000747 matchSubstrings.deleteAll();
748 nomatchSubstrings.deleteAll();
epoger@google.combe6188d2012-05-31 15:13:45 +0000749
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000750 int num_failing_results = 0;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000751 for (int i = 0; i < DiffRecord::kResultCount; i++) {
epoger@google.com3af4ff42012-07-19 17:35:04 +0000752 if (failOnResultType[i]) {
753 num_failing_results += summary.fResultsOfType[i].count();
754 }
epoger@google.com46a45962012-07-12 18:16:02 +0000755 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000756 if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
757 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
758 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
759 if (failOnStatusType[base][comparison]) {
760 num_failing_results += summary.fStatusOfType[base][comparison].count();
761 }
762 }
763 }
764 }
epoger@google.com28659882012-07-16 18:01:06 +0000765
766 // On Linux (and maybe other platforms too), any results outside of the
767 // range [0...255] are wrapped (mod 256). Do the conversion ourselves, to
768 // make sure that we only return 0 when there were no failures.
769 return (num_failing_results > 255) ? 255 : num_failing_results;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000770}
caryclark@google.com5987f582012-10-02 18:33:14 +0000771
772#if !defined SK_BUILD_FOR_IOS
773int main(int argc, char * const argv[]) {
774 return tool_main(argc, (char**) argv);
775}
776#endif