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