blob: a9a1968fa533bdea50be2859ed43bb31280c3ed7 [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"
tomhudson@google.com4b33d282011-04-27 15:39:30 +000012#include "SkImageDecoder.h"
13#include "SkImageEncoder.h"
14#include "SkOSFile.h"
15#include "SkStream.h"
16#include "SkTDArray.h"
17#include "SkTemplates.h"
tomhudson@google.com4b33d282011-04-27 15:39:30 +000018#include "SkTSearch.h"
19#include "SkTypes.h"
20
21/**
22 * skdiff
23 *
24 * Given three directory names, expects to find identically-named files in
25 * each of the first two; the first are treated as a set of baseline,
26 * the second a set of variant images, and a diff image is written into the
27 * third directory for each pair.
tomhudson@google.com7d042802011-07-14 13:15:55 +000028 * Creates an index.html in the current third directory to compare each
tomhudson@google.com4b33d282011-04-27 15:39:30 +000029 * pair that does not match exactly.
epoger@google.com71329d82012-08-16 13:42:13 +000030 * Recursively descends directories, unless run with --norecurse.
epoger@google.combe6188d2012-05-31 15:13:45 +000031 *
32 * Returns zero exit code if all images match across baseDir and comparisonDir.
tomhudson@google.com4b33d282011-04-27 15:39:30 +000033 */
34
epoger@google.coma5f406e2012-05-01 13:26:16 +000035typedef SkTDArray<SkString*> StringArray;
36typedef StringArray FileArray;
epoger@google.com5fd53852012-03-22 18:20:06 +000037
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000038struct DiffSummary {
39 DiffSummary ()
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000040 : fNumMatches(0)
41 , fNumMismatches(0)
42 , fMaxMismatchV(0)
43 , fMaxMismatchPercent(0) { };
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000044
epoger@google.com5fd53852012-03-22 18:20:06 +000045 ~DiffSummary() {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000046 for (int i = 0; i < DiffRecord::kResultCount; ++i) {
epoger@google.com76222c02012-05-31 15:12:09 +000047 fResultsOfType[i].deleteAll();
48 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000049 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
50 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
51 fStatusOfType[base][comparison].deleteAll();
52 }
skia.committer@gmail.com0264fb42012-12-06 02:01:25 +000053 }
epoger@google.com5fd53852012-03-22 18:20:06 +000054 }
55
tomhudson@google.com9dc527b2011-06-09 15:47:10 +000056 uint32_t fNumMatches;
57 uint32_t fNumMismatches;
58 uint32_t fMaxMismatchV;
59 float fMaxMismatchPercent;
60
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000061 FileArray fResultsOfType[DiffRecord::kResultCount];
62 FileArray fStatusOfType[DiffResource::kStatusCount][DiffResource::kStatusCount];
63
64 void printContents(const FileArray& fileArray,
65 const char* baseStatus, const char* comparisonStatus,
66 bool listFilenames) {
67 int n = fileArray.count();
68 printf("%d file pairs %s in baseDir and %s in comparisonDir",
69 n, baseStatus, comparisonStatus);
70 if (listFilenames) {
71 printf(": ");
72 for (int i = 0; i < n; ++i) {
73 printf("%s ", fileArray[i]->c_str());
74 }
75 }
76 printf("\n");
77 }
78
79 void printStatus(bool listFilenames,
80 bool failOnStatusType[DiffResource::kStatusCount]
81 [DiffResource::kStatusCount]) {
82 typedef DiffResource::Status Status;
83
84 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
85 Status baseStatus = static_cast<Status>(base);
86 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
87 Status comparisonStatus = static_cast<Status>(comparison);
88 const FileArray& fileArray = fStatusOfType[base][comparison];
89 if (fileArray.count() > 0) {
90 if (failOnStatusType[base][comparison]) {
91 printf(" [*] ");
92 } else {
93 printf(" [_] ");
94 }
95 printContents(fileArray,
96 DiffResource::getStatusDescription(baseStatus),
97 DiffResource::getStatusDescription(comparisonStatus),
98 listFilenames);
99 }
100 }
101 }
102 }
epoger@google.com76222c02012-05-31 15:12:09 +0000103
epoger@google.com3af4ff42012-07-19 17:35:04 +0000104 // Print a line about the contents of this FileArray to stdout.
epoger@google.com46a45962012-07-12 18:16:02 +0000105 void printContents(const FileArray& fileArray, const char* headerText, bool listFilenames) {
epoger@google.com76222c02012-05-31 15:12:09 +0000106 int n = fileArray.count();
epoger@google.com3af4ff42012-07-19 17:35:04 +0000107 printf("%d file pairs %s", n, headerText);
108 if (listFilenames) {
109 printf(": ");
110 for (int i = 0; i < n; ++i) {
111 printf("%s ", fileArray[i]->c_str());
epoger@google.com76222c02012-05-31 15:12:09 +0000112 }
113 }
epoger@google.com3af4ff42012-07-19 17:35:04 +0000114 printf("\n");
epoger@google.com76222c02012-05-31 15:12:09 +0000115 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000116
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000117 void print(bool listFilenames, bool failOnResultType[DiffRecord::kResultCount],
118 bool failOnStatusType[DiffResource::kStatusCount]
119 [DiffResource::kStatusCount]) {
epoger@google.com3af4ff42012-07-19 17:35:04 +0000120 printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches);
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000121 for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) {
122 DiffRecord::Result result = static_cast<DiffRecord::Result>(resultInt);
epoger@google.com3af4ff42012-07-19 17:35:04 +0000123 if (failOnResultType[result]) {
124 printf("[*] ");
125 } else {
126 printf("[_] ");
127 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000128 printContents(fResultsOfType[result], DiffRecord::getResultDescription(result),
129 listFilenames);
130 if (DiffRecord::kCouldNotCompare_Result == result) {
131 printStatus(listFilenames, failOnStatusType);
132 }
epoger@google.com46a45962012-07-12 18:16:02 +0000133 }
epoger@google.com3af4ff42012-07-19 17:35:04 +0000134 printf("(results marked with [*] will cause nonzero return value)\n");
135 printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000136 if (fNumMismatches > 0) {
137 printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
epoger@google.com46a45962012-07-12 18:16:02 +0000138 printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000139 }
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000140 }
141
142 void add (DiffRecord* drp) {
epoger@google.com46256ea2012-05-22 13:45:35 +0000143 uint32_t mismatchValue;
epoger@google.com292aff62012-05-16 14:57:28 +0000144
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000145 if (drp->fBase.fFilename.equals(drp->fComparison.fFilename)) {
146 fResultsOfType[drp->fResult].push(new SkString(drp->fBase.fFilename));
147 } else {
148 SkString* blame = new SkString("(");
149 blame->append(drp->fBase.fFilename);
150 blame->append(", ");
151 blame->append(drp->fComparison.fFilename);
152 blame->append(")");
153 fResultsOfType[drp->fResult].push(blame);
154 }
epoger@google.com292aff62012-05-16 14:57:28 +0000155 switch (drp->fResult) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000156 case DiffRecord::kEqualBits_Result:
epoger@google.com46256ea2012-05-22 13:45:35 +0000157 fNumMatches++;
158 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000159 case DiffRecord::kEqualPixels_Result:
epoger@google.com292aff62012-05-16 14:57:28 +0000160 fNumMatches++;
161 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000162 case DiffRecord::kDifferentSizes_Result:
epoger@google.com5fd53852012-03-22 18:20:06 +0000163 fNumMismatches++;
epoger@google.com292aff62012-05-16 14:57:28 +0000164 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000165 case DiffRecord::kDifferentPixels_Result:
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000166 fNumMismatches++;
167 if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
168 fMaxMismatchPercent = drp->fFractionDifference * 100;
169 }
epoger@google.com46256ea2012-05-22 13:45:35 +0000170 mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
171 drp->fMaxMismatchB);
172 if (mismatchValue > fMaxMismatchV) {
173 fMaxMismatchV = mismatchValue;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000174 }
epoger@google.com292aff62012-05-16 14:57:28 +0000175 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000176 case DiffRecord::kCouldNotCompare_Result:
epoger@google.com46256ea2012-05-22 13:45:35 +0000177 fNumMismatches++;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000178 fStatusOfType[drp->fBase.fStatus][drp->fComparison.fStatus].push(
179 new SkString(drp->fBase.fFilename));
epoger@google.com46256ea2012-05-22 13:45:35 +0000180 break;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000181 case DiffRecord::kUnknown_Result:
epoger@google.com46256ea2012-05-22 13:45:35 +0000182 SkDEBUGFAIL("adding uncategorized DiffRecord");
183 break;
184 default:
185 SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
186 break;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000187 }
188 }
189};
190
epoger@google.coma5f406e2012-05-01 13:26:16 +0000191/// Returns true if string contains any of these substrings.
192static bool string_contains_any_of(const SkString& string,
193 const StringArray& substrings) {
194 for (int i = 0; i < substrings.count(); i++) {
195 if (string.contains(substrings[i]->c_str())) {
196 return true;
197 }
198 }
199 return false;
200}
201
epoger@google.com71329d82012-08-16 13:42:13 +0000202/// Internal (potentially recursive) implementation of get_file_list.
203static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir,
204 const StringArray& matchSubstrings,
205 const StringArray& nomatchSubstrings,
206 bool recurseIntoSubdirs, FileArray *files) {
207 bool isSubDirEmpty = subDir.isEmpty();
208 SkString dir(rootDir);
209 if (!isSubDirEmpty) {
210 dir.append(PATH_DIV_STR);
211 dir.append(subDir);
212 }
213
214 // Iterate over files (not directories) within dir.
215 SkOSFile::Iter fileIterator(dir.c_str());
216 SkString fileName;
217 while (fileIterator.next(&fileName, false)) {
218 if (fileName.startsWith(".")) {
219 continue;
220 }
221 SkString pathRelativeToRootDir(subDir);
222 if (!isSubDirEmpty) {
223 pathRelativeToRootDir.append(PATH_DIV_STR);
224 }
225 pathRelativeToRootDir.append(fileName);
226 if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) &&
227 !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
228 files->push(new SkString(pathRelativeToRootDir));
229 }
230 }
231
232 // Recurse into any non-ignored subdirectories.
233 if (recurseIntoSubdirs) {
234 SkOSFile::Iter dirIterator(dir.c_str());
235 SkString dirName;
236 while (dirIterator.next(&dirName, true)) {
237 if (dirName.startsWith(".")) {
238 continue;
239 }
240 SkString pathRelativeToRootDir(subDir);
241 if (!isSubDirEmpty) {
242 pathRelativeToRootDir.append(PATH_DIV_STR);
243 }
244 pathRelativeToRootDir.append(dirName);
245 if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
246 get_file_list_subdir(rootDir, pathRelativeToRootDir,
247 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
248 files);
249 }
250 }
251 }
252}
253
254/// Iterate over dir and get all files whose filename:
255/// - matches any of the substrings in matchSubstrings, but...
256/// - DOES NOT match any of the substrings in nomatchSubstrings
257/// - DOES NOT start with a dot (.)
258/// Adds the matching files to the list in *files.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000259static void get_file_list(const SkString& dir,
260 const StringArray& matchSubstrings,
261 const StringArray& nomatchSubstrings,
epoger@google.com71329d82012-08-16 13:42:13 +0000262 bool recurseIntoSubdirs, FileArray *files) {
263 get_file_list_subdir(dir, SkString(""),
264 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
265 files);
epoger@google.com5fd53852012-03-22 18:20:06 +0000266}
267
268static void release_file_list(FileArray *files) {
269 files->deleteAll();
270}
271
272/// Comparison routines for qsort, sort by file names.
273static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
274 return strcmp((*lhs)->c_str(), (*rhs)->c_str());
275}
276
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000277class AutoReleasePixels {
278public:
279 AutoReleasePixels(DiffRecord* drp)
280 : fDrp(drp) {
281 SkASSERT(drp != NULL);
282 }
283 ~AutoReleasePixels() {
284 fDrp->fBase.fBitmap.setPixelRef(NULL);
285 fDrp->fComparison.fBitmap.setPixelRef(NULL);
286 fDrp->fDifference.fBitmap.setPixelRef(NULL);
287 fDrp->fWhite.fBitmap.setPixelRef(NULL);
288 }
289
290private:
291 DiffRecord* fDrp;
292};
293
294static void get_bounds(DiffResource& resource, const char* name) {
295 if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) {
296 SkAutoDataUnref fileBits(read_file(resource.fFullPath.c_str()));
297 if (NULL == fileBits) {
298 SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str());
299 resource.fStatus = DiffResource::kCouldNotRead_Status;
300 } else {
301 get_bitmap(fileBits, resource, SkImageDecoder::kDecodeBounds_Mode);
302 }
303 }
304}
305
306static void get_bounds(DiffRecord& drp) {
307 get_bounds(drp.fBase, "base");
308 get_bounds(drp.fComparison, "comparison");
309}
310
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000311/// Creates difference images, returns the number that have a 0 metric.
epoger@google.coma5f406e2012-05-01 13:26:16 +0000312/// If outputDir.isEmpty(), don't write out diff files.
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000313static void create_diff_images (DiffMetricProc dmp,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000314 const int colorThreshold,
315 RecordArray* differences,
316 const SkString& baseDir,
317 const SkString& comparisonDir,
318 const SkString& outputDir,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000319 const StringArray& matchSubstrings,
320 const StringArray& nomatchSubstrings,
epoger@google.com71329d82012-08-16 13:42:13 +0000321 bool recurseIntoSubdirs,
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000322 bool getBounds,
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000323 DiffSummary* summary) {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000324 SkASSERT(!baseDir.isEmpty());
325 SkASSERT(!comparisonDir.isEmpty());
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000326
epoger@google.com5fd53852012-03-22 18:20:06 +0000327 FileArray baseFiles;
328 FileArray comparisonFiles;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000329
epoger@google.com71329d82012-08-16 13:42:13 +0000330 get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
331 get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
epoger@google.coma5f406e2012-05-01 13:26:16 +0000332 &comparisonFiles);
epoger@google.com5fd53852012-03-22 18:20:06 +0000333
epoger@google.coma5f406e2012-05-01 13:26:16 +0000334 if (!baseFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000335 qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
336 SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000337 }
338 if (!comparisonFiles.isEmpty()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000339 qsort(comparisonFiles.begin(), comparisonFiles.count(),
340 sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
epoger@google.coma5f406e2012-05-01 13:26:16 +0000341 }
epoger@google.com66008522012-05-16 17:40:57 +0000342
epoger@google.com5fd53852012-03-22 18:20:06 +0000343 int i = 0;
344 int j = 0;
345
346 while (i < baseFiles.count() &&
347 j < comparisonFiles.count()) {
348
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000349 SkString basePath(baseDir);
350 SkString comparisonPath(comparisonDir);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000351
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000352 DiffRecord *drp = new DiffRecord;
353 int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
epoger@google.com5fd53852012-03-22 18:20:06 +0000354
355 if (v < 0) {
356 // in baseDir, but not in comparisonDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000357 drp->fResult = DiffRecord::kCouldNotCompare_Result;
358
359 basePath.append(*baseFiles[i]);
360 comparisonPath.append(*baseFiles[i]);
361
362 drp->fBase.fFilename = *baseFiles[i];
363 drp->fBase.fFullPath = basePath;
364 drp->fBase.fStatus = DiffResource::kExists_Status;
365
366 drp->fComparison.fFilename = *baseFiles[i];
367 drp->fComparison.fFullPath = comparisonPath;
368 drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
369
epoger@google.com5fd53852012-03-22 18:20:06 +0000370 ++i;
371 } else if (v > 0) {
372 // in comparisonDir, but not in baseDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000373 drp->fResult = DiffRecord::kCouldNotCompare_Result;
374
375 basePath.append(*comparisonFiles[j]);
376 comparisonPath.append(*comparisonFiles[j]);
377
378 drp->fBase.fFilename = *comparisonFiles[j];
379 drp->fBase.fFullPath = basePath;
380 drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
381
382 drp->fComparison.fFilename = *comparisonFiles[j];
383 drp->fComparison.fFullPath = comparisonPath;
384 drp->fComparison.fStatus = DiffResource::kExists_Status;
385
epoger@google.com5fd53852012-03-22 18:20:06 +0000386 ++j;
387 } else {
epoger@google.com46256ea2012-05-22 13:45:35 +0000388 // Found the same filename in both baseDir and comparisonDir.
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000389 SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
epoger@google.com5fd53852012-03-22 18:20:06 +0000390
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000391 basePath.append(*baseFiles[i]);
392 comparisonPath.append(*comparisonFiles[j]);
393
394 drp->fBase.fFilename = *baseFiles[i];
395 drp->fBase.fFullPath = basePath;
396 drp->fBase.fStatus = DiffResource::kExists_Status;
397
398 drp->fComparison.fFilename = *comparisonFiles[j];
399 drp->fComparison.fFullPath = comparisonPath;
400 drp->fComparison.fStatus = DiffResource::kExists_Status;
401
402 SkAutoDataUnref baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
403 if (NULL != baseFileBits) {
404 drp->fBase.fStatus = DiffResource::kRead_Status;
405 }
406 SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
407 if (NULL != comparisonFileBits) {
408 drp->fComparison.fStatus = DiffResource::kRead_Status;
409 }
410 if (NULL == baseFileBits || NULL == comparisonFileBits) {
411 if (NULL == baseFileBits) {
412 drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
413 }
414 if (NULL == comparisonFileBits) {
415 drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
416 }
417 drp->fResult = DiffRecord::kCouldNotCompare_Result;
418
419 } else if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
420 drp->fResult = DiffRecord::kEqualBits_Result;
421
epoger@google.com46256ea2012-05-22 13:45:35 +0000422 } else {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000423 AutoReleasePixels arp(drp);
424 get_bitmap(baseFileBits, drp->fBase, SkImageDecoder::kDecodePixels_Mode);
425 get_bitmap(comparisonFileBits, drp->fComparison,
426 SkImageDecoder::kDecodePixels_Mode);
427 if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
428 DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
429 create_and_write_diff_image(drp, dmp, colorThreshold,
430 outputDir, drp->fBase.fFilename);
epoger@google.com46256ea2012-05-22 13:45:35 +0000431 } else {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000432 drp->fResult = DiffRecord::kCouldNotCompare_Result;
epoger@google.com46256ea2012-05-22 13:45:35 +0000433 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000434 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000435
epoger@google.com5fd53852012-03-22 18:20:06 +0000436 ++i;
437 ++j;
438 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000439
440 if (getBounds) {
441 get_bounds(*drp);
442 }
443 SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000444 differences->push(drp);
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000445 summary->add(drp);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000446 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000447
448 for (; i < baseFiles.count(); ++i) {
449 // files only in baseDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000450 DiffRecord *drp = new DiffRecord();
451 drp->fBase.fFilename = *baseFiles[i];
452 drp->fBase.fFullPath = baseDir;
453 drp->fBase.fFullPath.append(drp->fBase.fFilename);
454 drp->fBase.fStatus = DiffResource::kExists_Status;
455
456 drp->fComparison.fFilename = *baseFiles[i];
457 drp->fComparison.fFullPath = comparisonDir;
458 drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
459 drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
460
461 drp->fResult = DiffRecord::kCouldNotCompare_Result;
462 if (getBounds) {
463 get_bounds(*drp);
464 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000465 differences->push(drp);
466 summary->add(drp);
467 }
468
469 for (; j < comparisonFiles.count(); ++j) {
470 // files only in comparisonDir
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000471 DiffRecord *drp = new DiffRecord();
472 drp->fBase.fFilename = *comparisonFiles[j];
473 drp->fBase.fFullPath = baseDir;
474 drp->fBase.fFullPath.append(drp->fBase.fFilename);
475 drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
476
477 drp->fComparison.fFilename = *comparisonFiles[j];
478 drp->fComparison.fFullPath = comparisonDir;
479 drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
480 drp->fComparison.fStatus = DiffResource::kExists_Status;
481
482 drp->fResult = DiffRecord::kCouldNotCompare_Result;
483 if (getBounds) {
484 get_bounds(*drp);
485 }
epoger@google.com5fd53852012-03-22 18:20:06 +0000486 differences->push(drp);
487 summary->add(drp);
488 }
489
490 release_file_list(&baseFiles);
491 release_file_list(&comparisonFiles);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000492}
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000493
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000494static void usage (char * argv0) {
495 SkDebugf("Skia baseline image diff tool\n");
epoger@google.coma5f406e2012-05-01 13:26:16 +0000496 SkDebugf("\n"
497"Usage: \n"
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000498" %s <baseDir> <comparisonDir> [outputDir] \n", argv0);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000499 SkDebugf(
epoger@google.com46a45962012-07-12 18:16:02 +0000500"\nArguments:"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000501"\n --failonresult <result>: After comparing all file pairs, exit with nonzero"
502"\n return code (number of file pairs yielding this"
503"\n result) if any file pairs yielded this result."
504"\n This flag may be repeated, in which case the"
505"\n return code will be the number of fail pairs"
506"\n yielding ANY of these results."
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000507"\n --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
508"\n code if any file pairs yielded this status."
epoger@google.com46a45962012-07-12 18:16:02 +0000509"\n --help: display this info"
epoger@google.com46a45962012-07-12 18:16:02 +0000510"\n --listfilenames: list all filenames for each result type in stdout"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000511"\n --match <substring>: compare files whose filenames contain this substring;"
512"\n if unspecified, compare ALL files."
513"\n this flag may be repeated."
epoger@google.com46a45962012-07-12 18:16:02 +0000514"\n --nodiffs: don't write out image diffs or index.html, just generate"
515"\n report on stdout"
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000516"\n --nomatch <substring>: regardless of --match, DO NOT compare files whose"
517"\n filenames contain this substring."
518"\n this flag may be repeated."
epoger@google.com46a45962012-07-12 18:16:02 +0000519"\n --noprintdirs: do not print the directories used."
epoger@google.com71329d82012-08-16 13:42:13 +0000520"\n --norecurse: do not recurse into subdirectories."
epoger@google.com46a45962012-07-12 18:16:02 +0000521"\n --sortbymaxmismatch: sort by worst color channel mismatch;"
522"\n break ties with -sortbymismatch"
523"\n --sortbymismatch: sort by average color channel mismatch"
524"\n --threshold <n>: only report differences > n (per color channel) [default 0]"
525"\n --weighted: sort by # pixels different weighted by color difference"
526"\n"
527"\n baseDir: directory to read baseline images from."
528"\n comparisonDir: directory to read comparison images from"
529"\n outputDir: directory to write difference images and index.html to;"
530"\n defaults to comparisonDir"
531"\n"
532"\nIf no sort is specified, it will sort by fraction of pixels mismatching."
533"\n");
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000534}
535
epoger@google.com70044cc2012-07-12 18:37:55 +0000536const int kNoError = 0;
537const int kGenericError = -1;
epoger@google.com46a45962012-07-12 18:16:02 +0000538
caryclark@google.com5987f582012-10-02 18:33:14 +0000539int tool_main(int argc, char** argv);
540int tool_main(int argc, char** argv) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000541 DiffMetricProc diffProc = compute_diff_pmcolor;
epoger@google.com28060e72012-06-28 16:47:34 +0000542 int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000543
544 // Maximum error tolerated in any one color channel in any one pixel before
545 // a difference is reported.
546 int colorThreshold = 0;
547 SkString baseDir;
548 SkString comparisonDir;
549 SkString outputDir;
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000550
epoger@google.coma5f406e2012-05-01 13:26:16 +0000551 StringArray matchSubstrings;
552 StringArray nomatchSubstrings;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000553
epoger@google.coma5f406e2012-05-01 13:26:16 +0000554 bool generateDiffs = true;
epoger@google.com46a45962012-07-12 18:16:02 +0000555 bool listFilenames = false;
epoger@google.com71329d82012-08-16 13:42:13 +0000556 bool printDirNames = true;
557 bool recurseIntoSubdirs = true;
tomhudson@google.com7d042802011-07-14 13:15:55 +0000558
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000559 RecordArray differences;
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000560 DiffSummary summary;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000561
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000562 bool failOnResultType[DiffRecord::kResultCount];
563 for (int i = 0; i < DiffRecord::kResultCount; i++) {
epoger@google.com3af4ff42012-07-19 17:35:04 +0000564 failOnResultType[i] = false;
565 }
566
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000567 bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
568 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
569 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
570 failOnStatusType[base][comparison] = false;
571 }
572 }
573
epoger@google.coma5f406e2012-05-01 13:26:16 +0000574 int i;
575 int numUnflaggedArguments = 0;
576 for (i = 1; i < argc; i++) {
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000577 if (!strcmp(argv[i], "--failonresult")) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000578 if (argc == ++i) {
579 SkDebugf("failonresult expects one argument.\n");
580 continue;
581 }
582 DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
583 if (type != DiffRecord::kResultCount) {
584 failOnResultType[type] = true;
585 } else {
586 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
587 }
588 continue;
589 }
590 if (!strcmp(argv[i], "--failonstatus")) {
591 if (argc == ++i) {
592 SkDebugf("failonstatus missing base status.\n");
593 continue;
594 }
595 bool baseStatuses[DiffResource::kStatusCount];
596 if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
597 SkDebugf("unrecognized base status <%s>\n", argv[i]);
598 }
599
600 if (argc == ++i) {
601 SkDebugf("failonstatus missing comparison status.\n");
602 continue;
603 }
604 bool comparisonStatuses[DiffResource::kStatusCount];
605 if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
606 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
607 }
608
609 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
610 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
611 failOnStatusType[base][comparison] |=
612 baseStatuses[base] && comparisonStatuses[comparison];
613 }
614 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000615 continue;
616 }
epoger@google.com46a45962012-07-12 18:16:02 +0000617 if (!strcmp(argv[i], "--help")) {
618 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000619 return kNoError;
epoger@google.com46a45962012-07-12 18:16:02 +0000620 }
621 if (!strcmp(argv[i], "--listfilenames")) {
622 listFilenames = true;
epoger@google.coma5f406e2012-05-01 13:26:16 +0000623 continue;
624 }
625 if (!strcmp(argv[i], "--match")) {
626 matchSubstrings.push(new SkString(argv[++i]));
627 continue;
628 }
epoger@google.com46a45962012-07-12 18:16:02 +0000629 if (!strcmp(argv[i], "--nodiffs")) {
630 generateDiffs = false;
631 continue;
632 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000633 if (!strcmp(argv[i], "--nomatch")) {
634 nomatchSubstrings.push(new SkString(argv[++i]));
635 continue;
636 }
epoger@google.com46a45962012-07-12 18:16:02 +0000637 if (!strcmp(argv[i], "--noprintdirs")) {
epoger@google.com71329d82012-08-16 13:42:13 +0000638 printDirNames = false;
639 continue;
640 }
641 if (!strcmp(argv[i], "--norecurse")) {
642 recurseIntoSubdirs = false;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000643 continue;
644 }
tomhudson@google.com7d042802011-07-14 13:15:55 +0000645 if (!strcmp(argv[i], "--sortbymaxmismatch")) {
epoger@google.com28060e72012-06-28 16:47:34 +0000646 sortProc = compare<CompareDiffMaxMismatches>;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000647 continue;
648 }
epoger@google.com46a45962012-07-12 18:16:02 +0000649 if (!strcmp(argv[i], "--sortbymismatch")) {
650 sortProc = compare<CompareDiffMeanMismatches>;
tomhudson@google.com5b325292011-05-24 19:41:13 +0000651 continue;
652 }
epoger@google.com46a45962012-07-12 18:16:02 +0000653 if (!strcmp(argv[i], "--threshold")) {
654 colorThreshold = atoi(argv[++i]);
655 continue;
656 }
657 if (!strcmp(argv[i], "--weighted")) {
658 sortProc = compare<CompareDiffWeighted>;
keyar@chromium.orga6318192012-07-09 21:01:50 +0000659 continue;
660 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000661 if (argv[i][0] != '-') {
epoger@google.coma5f406e2012-05-01 13:26:16 +0000662 switch (numUnflaggedArguments++) {
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000663 case 0:
664 baseDir.set(argv[i]);
665 continue;
666 case 1:
667 comparisonDir.set(argv[i]);
668 continue;
669 case 2:
670 outputDir.set(argv[i]);
671 continue;
672 default:
epoger@google.coma5f406e2012-05-01 13:26:16 +0000673 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000674 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000675 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000676 }
677 }
678
679 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
680 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000681 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000682 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000683
epoger@google.coma5f406e2012-05-01 13:26:16 +0000684 if (numUnflaggedArguments == 2) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000685 outputDir = comparisonDir;
epoger@google.coma5f406e2012-05-01 13:26:16 +0000686 } else if (numUnflaggedArguments != 3) {
tomhudson@google.com9dc527b2011-06-09 15:47:10 +0000687 usage(argv[0]);
epoger@google.com70044cc2012-07-12 18:37:55 +0000688 return kGenericError;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000689 }
690
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000691 if (!baseDir.endsWith(PATH_DIV_STR)) {
692 baseDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000693 }
epoger@google.com71329d82012-08-16 13:42:13 +0000694 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000695 printf("baseDir is [%s]\n", baseDir.c_str());
696 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000697
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000698 if (!comparisonDir.endsWith(PATH_DIV_STR)) {
699 comparisonDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000700 }
epoger@google.com71329d82012-08-16 13:42:13 +0000701 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000702 printf("comparisonDir is [%s]\n", comparisonDir.c_str());
703 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000704
bsalomon@google.com1a315fe2011-09-23 14:56:37 +0000705 if (!outputDir.endsWith(PATH_DIV_STR)) {
706 outputDir.append(PATH_DIV_STR);
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000707 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000708 if (generateDiffs) {
epoger@google.com71329d82012-08-16 13:42:13 +0000709 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000710 printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
711 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000712 } else {
epoger@google.com71329d82012-08-16 13:42:13 +0000713 if (printDirNames) {
keyar@chromium.orga6318192012-07-09 21:01:50 +0000714 printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
715 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000716 outputDir.set("");
717 }
718
epoger@google.comda4af242012-06-25 18:45:50 +0000719 // If no matchSubstrings were specified, match ALL strings
720 // (except for whatever nomatchSubstrings were specified, if any).
epoger@google.coma5f406e2012-05-01 13:26:16 +0000721 if (matchSubstrings.isEmpty()) {
722 matchSubstrings.push(new SkString(""));
723 }
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000724
epoger@google.coma611c3e2012-05-18 20:10:06 +0000725 create_diff_images(diffProc, colorThreshold, &differences,
726 baseDir, comparisonDir, outputDir,
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000727 matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
728 &summary);
729 summary.print(listFilenames, failOnResultType, failOnStatusType);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000730
731 if (differences.count()) {
reed@google.comc7a67cb2012-05-07 14:52:12 +0000732 qsort(differences.begin(), differences.count(),
733 sizeof(DiffRecord*), sortProc);
tomhudson@google.com7d042802011-07-14 13:15:55 +0000734 }
epoger@google.com66008522012-05-16 17:40:57 +0000735
epoger@google.coma5f406e2012-05-01 13:26:16 +0000736 if (generateDiffs) {
737 print_diff_page(summary.fNumMatches, colorThreshold, differences,
738 baseDir, comparisonDir, outputDir);
739 }
epoger@google.com76222c02012-05-31 15:12:09 +0000740
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000741 for (i = 0; i < differences.count(); i++) {
742 delete differences[i];
743 }
epoger@google.coma5f406e2012-05-01 13:26:16 +0000744 matchSubstrings.deleteAll();
745 nomatchSubstrings.deleteAll();
epoger@google.combe6188d2012-05-31 15:13:45 +0000746
epoger@google.comdfbf24e2012-07-13 21:22:02 +0000747 int num_failing_results = 0;
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000748 for (int i = 0; i < DiffRecord::kResultCount; i++) {
epoger@google.com3af4ff42012-07-19 17:35:04 +0000749 if (failOnResultType[i]) {
750 num_failing_results += summary.fResultsOfType[i].count();
751 }
epoger@google.com46a45962012-07-12 18:16:02 +0000752 }
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000753 if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
754 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
755 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
756 if (failOnStatusType[base][comparison]) {
757 num_failing_results += summary.fStatusOfType[base][comparison].count();
758 }
759 }
760 }
761 }
epoger@google.com28659882012-07-16 18:01:06 +0000762
763 // On Linux (and maybe other platforms too), any results outside of the
764 // range [0...255] are wrapped (mod 256). Do the conversion ourselves, to
765 // make sure that we only return 0 when there were no failures.
766 return (num_failing_results > 255) ? 255 : num_failing_results;
tomhudson@google.com4b33d282011-04-27 15:39:30 +0000767}
caryclark@google.com5987f582012-10-02 18:33:14 +0000768
769#if !defined SK_BUILD_FOR_IOS
770int main(int argc, char * const argv[]) {
771 return tool_main(argc, (char**) argv);
772}
773#endif