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