blob: 086fb09a5bfb8648e3b8dab8bb6a0ad964b5aeb7 [file] [log] [blame]
bungeman@google.come3c8ddf2012-12-05 20:13:12 +00001/*
2 * Copyright 2012 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 */
7#include "skdiff.h"
8#include "skdiff_utils.h"
9#include "SkBitmap.h"
10#include "SkData.h"
11#include "SkImageDecoder.h"
12#include "SkImageEncoder.h"
13#include "SkOSFile.h"
14#include "SkTDArray.h"
15#include "SkTemplates.h"
16#include "SkTypes.h"
17
18/// If outputDir.isEmpty(), don't write out diff files.
19static void create_diff_images (DiffMetricProc dmp,
20 const int colorThreshold,
21 const SkString& baseFile,
22 const SkString& comparisonFile,
23 const SkString& outputDir,
24 const SkString& outputFilename,
25 DiffRecord* drp) {
26 SkASSERT(!baseFile.isEmpty());
27 SkASSERT(!comparisonFile.isEmpty());
28
29 drp->fBase.fFilename = baseFile;
30 drp->fBase.fFullPath = baseFile;
31 drp->fBase.fStatus = DiffResource::kSpecified_Status;
32
33 drp->fComparison.fFilename = comparisonFile;
34 drp->fComparison.fFullPath = comparisonFile;
35 drp->fComparison.fStatus = DiffResource::kSpecified_Status;
36
37 SkAutoDataUnref baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
38 if (NULL != baseFileBits) {
39 drp->fBase.fStatus = DiffResource::kRead_Status;
40 }
41 SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
42 if (NULL != comparisonFileBits) {
43 drp->fComparison.fStatus = DiffResource::kRead_Status;
44 }
45 if (NULL == baseFileBits || NULL == comparisonFileBits) {
46 if (NULL == baseFileBits) {
47 drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
48 }
49 if (NULL == comparisonFileBits) {
50 drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
51 }
52 drp->fResult = DiffRecord::kCouldNotCompare_Result;
53 return;
54 }
55
56 if (are_buffers_equal(baseFileBits, comparisonFileBits)) {
57 drp->fResult = DiffRecord::kEqualBits_Result;
58 return;
59 }
60
61 get_bitmap(baseFileBits, drp->fBase, SkImageDecoder::kDecodePixels_Mode);
62 get_bitmap(comparisonFileBits, drp->fComparison, SkImageDecoder::kDecodePixels_Mode);
63 if (DiffResource::kDecoded_Status != drp->fBase.fStatus ||
64 DiffResource::kDecoded_Status != drp->fComparison.fStatus)
65 {
66 drp->fResult = DiffRecord::kCouldNotCompare_Result;
67 return;
68 }
69
70 create_and_write_diff_image(drp, dmp, colorThreshold, outputDir, outputFilename);
71 //TODO: copy fBase.fFilename and fComparison.fFilename to outputDir
72 // svn and git often present tmp files to diff tools which are promptly deleted
73
74 //TODO: serialize drp to outputDir
75 // write a tool to deserialize them and call print_diff_page
76
77 SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
78}
79
80static void usage (char * argv0) {
81 SkDebugf("Skia image diff tool\n");
82 SkDebugf("\n"
83"Usage: \n"
84" %s <baseFile> <comparisonFile>\n" , argv0);
85 SkDebugf(
86"\nArguments:"
87"\n --failonresult <result>: After comparing all file pairs, exit with nonzero"
88"\n return code (number of file pairs yielding this"
89"\n result) if any file pairs yielded this result."
90"\n This flag may be repeated, in which case the"
91"\n return code will be the number of fail pairs"
92"\n yielding ANY of these results."
93"\n --failonstatus <baseStatus> <comparisonStatus>: exit with nonzero return"
94"\n code if any file pairs yeilded this status."
95"\n --help: display this info"
96"\n --listfilenames: list all filenames for each result type in stdout"
97"\n --nodiffs: don't write out image diffs, just generate report on stdout"
98"\n --outputdir: directory to write difference images"
99"\n --threshold <n>: only report differences > n (per color channel) [default 0]"
100"\n -u: ignored. Recognized for compatibility with svn diff."
101"\n -L: first occurrence label for base, second occurrence label for comparison."
102"\n Labels must be of the form \"<filename>(\t<specifier>)?\"."
103"\n The base <filename> will be used to create files in outputdir."
104"\n"
105"\n baseFile: baseline image file."
106"\n comparisonFile: comparison image file"
107"\n"
108"\nIf no sort is specified, it will sort by fraction of pixels mismatching."
109"\n");
110}
111
112const int kNoError = 0;
113const int kGenericError = -1;
114
115int tool_main(int argc, char** argv);
116int tool_main(int argc, char** argv) {
117 DiffMetricProc diffProc = compute_diff_pmcolor;
118
119 // Maximum error tolerated in any one color channel in any one pixel before
120 // a difference is reported.
121 int colorThreshold = 0;
122 SkString baseFile;
123 SkString baseLabel;
124 SkString comparisonFile;
125 SkString comparisonLabel;
126 SkString outputDir;
127
128 bool listFilenames = false;
129
130 bool failOnResultType[DiffRecord::kResultCount];
131 for (int i = 0; i < DiffRecord::kResultCount; i++) {
132 failOnResultType[i] = false;
133 }
134
135 bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
136 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
137 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
138 failOnStatusType[base][comparison] = false;
139 }
140 }
141
142 int i;
143 int numUnflaggedArguments = 0;
144 int numLabelArguments = 0;
145 for (i = 1; i < argc; i++) {
146 if (!strcmp(argv[i], "--failonresult")) {
147 if (argc == ++i) {
148 SkDebugf("failonresult expects one argument.\n");
149 continue;
150 }
151 DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
152 if (type != DiffRecord::kResultCount) {
153 failOnResultType[type] = true;
154 } else {
155 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
156 }
157 continue;
158 }
159 if (!strcmp(argv[i], "--failonstatus")) {
160 if (argc == ++i) {
161 SkDebugf("failonstatus missing base status.\n");
162 continue;
163 }
164 bool baseStatuses[DiffResource::kStatusCount];
165 if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
166 SkDebugf("unrecognized base status <%s>\n", argv[i]);
167 }
168
169 if (argc == ++i) {
170 SkDebugf("failonstatus missing comparison status.\n");
171 continue;
172 }
173 bool comparisonStatuses[DiffResource::kStatusCount];
174 if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
175 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
176 }
177
178 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
179 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
180 failOnStatusType[base][comparison] |=
181 baseStatuses[base] && comparisonStatuses[comparison];
182 }
183 }
184 continue;
185 }
186 if (!strcmp(argv[i], "--help")) {
187 usage(argv[0]);
188 return kNoError;
189 }
190 if (!strcmp(argv[i], "--listfilenames")) {
191 listFilenames = true;
192 continue;
193 }
194 if (!strcmp(argv[i], "--outputdir")) {
195 if (argc == ++i) {
196 SkDebugf("outputdir expects one argument.\n");
197 continue;
198 }
199 outputDir.set(argv[i]);
200 continue;
201 }
202 if (!strcmp(argv[i], "--threshold")) {
203 colorThreshold = atoi(argv[++i]);
204 continue;
205 }
206 if (!strcmp(argv[i], "-u")) {
207 //we don't produce unified diffs, ignore parameter to work with svn diff
208 continue;
209 }
210 if (!strcmp(argv[i], "-L")) {
211 if (argc == ++i) {
212 SkDebugf("label expects one argument.\n");
213 continue;
214 }
215 switch (numLabelArguments++) {
216 case 0:
217 baseLabel.set(argv[i]);
218 continue;
219 case 1:
220 comparisonLabel.set(argv[i]);
221 continue;
222 default:
223 SkDebugf("extra label argument <%s>\n", argv[i]);
224 usage(argv[0]);
225 return kGenericError;
226 }
227 continue;
228 }
229 if (argv[i][0] != '-') {
230 switch (numUnflaggedArguments++) {
231 case 0:
232 baseFile.set(argv[i]);
233 continue;
234 case 1:
235 comparisonFile.set(argv[i]);
236 continue;
237 default:
238 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
239 usage(argv[0]);
240 return kGenericError;
241 }
242 }
243
244 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
245 usage(argv[0]);
246 return kGenericError;
247 }
248
249 if (numUnflaggedArguments != 2) {
250 usage(argv[0]);
251 return kGenericError;
252 }
253
254 if (listFilenames) {
255 printf("Base file is [%s]\n", baseFile.c_str());
256 }
257
258 if (listFilenames) {
259 printf("Comparison file is [%s]\n", comparisonFile.c_str());
260 }
261
262 if (outputDir.isEmpty()) {
263 if (listFilenames) {
264 printf("Not writing any diffs. No output dir specified.\n");
265 }
266 } else {
267 if (!outputDir.endsWith(PATH_DIV_STR)) {
268 outputDir.append(PATH_DIV_STR);
269 }
270 if (listFilenames) {
271 printf("Writing diffs. Output dir is [%s]\n", outputDir.c_str());
272 }
273 }
274
275 // Some obscure documentation about diff/patch labels:
276 //
277 // Posix says the format is: <filename><tab><date>
278 // It also states that if a filename contains <tab> or <newline>
279 // the result is implementation defined
280 //
281 // Svn diff --diff-cmd provides labels of the form: <filename><tab><revision>
282 //
283 // Git diff --ext-diff does not supply arguments compatible with diff.
284 // However, it does provide the filename directly.
285 // skimagediff_git.sh: skimagediff %2 %5 -L "%1\t(%3)" -L "%1\t(%6)"
286 //
287 // Git difftool sets $LOCAL, $REMOTE, $MERGED, and $BASE instead of command line parameters.
288 // difftool.<>.cmd: skimagediff $LOCAL $REMOTE -L "$MERGED\t(local)" -L "$MERGED\t(remote)"
289 //
290 // Diff will write any specified label verbatim. Without a specified label diff will write
291 // <filename><tab><date>
292 // However, diff will encode the filename as a cstring if the filename contains
293 // Any of <space> or <double quote>
294 // A char less than 32
bungeman@google.com5b7bac52013-02-04 16:24:44 +0000295 // Any escapable character \\, \a, \b, \t, \n, \v, \f, \r
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000296 //
297 // Patch decodes:
298 // If first <non-white-space> is <double quote>, parse filename from cstring.
299 // If there is a <tab> after the first <non-white-space>, filename is
300 // [first <non-white-space>, the next run of <white-space> with an embedded <tab>).
301 // Otherwise the filename is [first <non-space>, the next <white-space>).
302 //
303 // The filename /dev/null means the file does not exist (used in adds and deletes).
304
305 // Considering the above, skimagediff will consider the contents of a -L parameter as
306 // <filename>(\t<specifier>)?
307 SkString outputFile;
308
309 if (baseLabel.isEmpty()) {
310 baseLabel.set(baseFile);
311 outputFile = baseLabel;
312 } else {
313 const char* baseLabelCstr = baseLabel.c_str();
314 const char* tab = strchr(baseLabelCstr, '\t');
315 if (NULL == tab) {
316 outputFile = baseLabel;
317 } else {
318 outputFile.set(baseLabelCstr, tab - baseLabelCstr);
319 }
320 }
321 if (comparisonLabel.isEmpty()) {
322 comparisonLabel.set(comparisonFile);
323 }
324 printf("Base: %s\n", baseLabel.c_str());
325 printf("Comparison: %s\n", comparisonLabel.c_str());
326
327 DiffRecord dr;
328 create_diff_images(diffProc, colorThreshold, baseFile, comparisonFile, outputDir, outputFile,
329 &dr);
330
331 if (DiffResource::isStatusFailed(dr.fBase.fStatus)) {
332 printf("Base %s.\n", DiffResource::getStatusDescription(dr.fBase.fStatus));
333 }
334 if (DiffResource::isStatusFailed(dr.fComparison.fStatus)) {
335 printf("Comparison %s.\n", DiffResource::getStatusDescription(dr.fComparison.fStatus));
336 }
337 printf("Base and Comparison %s.\n", DiffRecord::getResultDescription(dr.fResult));
338
339 if (DiffRecord::kDifferentPixels_Result == dr.fResult) {
340 printf("%.4f%% of pixels differ", 100 * dr.fFractionDifference);
341 printf(" (%.4f%% weighted)", 100 * dr.fWeightedFraction);
342 if (dr.fFractionDifference < 0.01) {
343 printf(" %d pixels", static_cast<int>(dr.fFractionDifference *
344 dr.fBase.fBitmap.width() *
345 dr.fBase.fBitmap.height()));
346 }
347
348 printf("\nAverage color mismatch: ");
349 printf("%d", static_cast<int>(MAX3(dr.fAverageMismatchR,
350 dr.fAverageMismatchG,
351 dr.fAverageMismatchB)));
352 printf("\nMax color mismatch: ");
353 printf("%d", MAX3(dr.fMaxMismatchR,
354 dr.fMaxMismatchG,
355 dr.fMaxMismatchB));
356 printf("\n");
357 }
358 printf("\n");
359
360 int num_failing_results = 0;
361 if (failOnResultType[dr.fResult]) {
362 ++num_failing_results;
363 }
364 if (failOnStatusType[dr.fBase.fStatus][dr.fComparison.fStatus]) {
365 ++num_failing_results;
366 }
367
368 return num_failing_results;
369}
370
371#if !defined SK_BUILD_FOR_IOS
372int main(int argc, char * const argv[]) {
373 return tool_main(argc, (char**) argv);
374}
375#endif