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