blob: 287523de157b8f64bf03cc4fa96d96feecf6f7f4 [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"
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000013#include "SkTypes.h"
14
bungeman@google.com16b2dd02013-10-11 19:19:10 +000015#include <stdio.h>
16
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000017/// If outputDir.isEmpty(), don't write out diff files.
18static 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
bungemanfe917272016-10-13 17:36:40 -040036 sk_sp<SkData> baseFileBits = read_file(drp->fBase.fFullPath.c_str());
bsalomon49f085d2014-09-05 13:34:00 -070037 if (baseFileBits) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000038 drp->fBase.fStatus = DiffResource::kRead_Status;
39 }
bungemanfe917272016-10-13 17:36:40 -040040 sk_sp<SkData> comparisonFileBits = read_file(drp->fComparison.fFullPath.c_str());
bsalomon49f085d2014-09-05 13:34:00 -070041 if (comparisonFileBits) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000042 drp->fComparison.fStatus = DiffResource::kRead_Status;
43 }
halcanary96fcdcc2015-08-27 07:41:13 -070044 if (nullptr == baseFileBits || nullptr == comparisonFileBits) {
45 if (nullptr == baseFileBits) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000046 drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
47 }
halcanary96fcdcc2015-08-27 07:41:13 -070048 if (nullptr == comparisonFileBits) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000049 drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
50 }
51 drp->fResult = DiffRecord::kCouldNotCompare_Result;
52 return;
53 }
54
bungemanfe917272016-10-13 17:36:40 -040055 if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000056 drp->fResult = DiffRecord::kEqualBits_Result;
57 return;
58 }
59
msarett667433f2016-03-17 07:17:54 -070060 get_bitmap(baseFileBits, drp->fBase, false);
61 get_bitmap(comparisonFileBits, drp->fComparison, false);
bungeman@google.come3c8ddf2012-12-05 20:13:12 +000062 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
79static 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
111const int kNoError = 0;
112const int kGenericError = -1;
113
114int tool_main(int argc, char** argv);
115int tool_main(int argc, char** argv) {
116 DiffMetricProc diffProc = compute_diff_pmcolor;
117
118 // Maximum error tolerated in any one color channel in any one pixel before
119 // a difference is reported.
120 int colorThreshold = 0;
121 SkString baseFile;
122 SkString baseLabel;
123 SkString comparisonFile;
124 SkString comparisonLabel;
125 SkString outputDir;
126
127 bool listFilenames = false;
128
129 bool failOnResultType[DiffRecord::kResultCount];
130 for (int i = 0; i < DiffRecord::kResultCount; i++) {
131 failOnResultType[i] = false;
132 }
133
134 bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
135 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
136 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
137 failOnStatusType[base][comparison] = false;
138 }
139 }
140
141 int i;
142 int numUnflaggedArguments = 0;
143 int numLabelArguments = 0;
144 for (i = 1; i < argc; i++) {
145 if (!strcmp(argv[i], "--failonresult")) {
146 if (argc == ++i) {
147 SkDebugf("failonresult expects one argument.\n");
148 continue;
149 }
150 DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
151 if (type != DiffRecord::kResultCount) {
152 failOnResultType[type] = true;
153 } else {
154 SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
155 }
156 continue;
157 }
158 if (!strcmp(argv[i], "--failonstatus")) {
159 if (argc == ++i) {
160 SkDebugf("failonstatus missing base status.\n");
161 continue;
162 }
163 bool baseStatuses[DiffResource::kStatusCount];
164 if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
165 SkDebugf("unrecognized base status <%s>\n", argv[i]);
166 }
167
168 if (argc == ++i) {
169 SkDebugf("failonstatus missing comparison status.\n");
170 continue;
171 }
172 bool comparisonStatuses[DiffResource::kStatusCount];
173 if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
174 SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
175 }
176
177 for (int base = 0; base < DiffResource::kStatusCount; ++base) {
178 for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
179 failOnStatusType[base][comparison] |=
180 baseStatuses[base] && comparisonStatuses[comparison];
181 }
182 }
183 continue;
184 }
185 if (!strcmp(argv[i], "--help")) {
186 usage(argv[0]);
187 return kNoError;
188 }
189 if (!strcmp(argv[i], "--listfilenames")) {
190 listFilenames = true;
191 continue;
192 }
193 if (!strcmp(argv[i], "--outputdir")) {
194 if (argc == ++i) {
195 SkDebugf("outputdir expects one argument.\n");
196 continue;
197 }
198 outputDir.set(argv[i]);
199 continue;
200 }
201 if (!strcmp(argv[i], "--threshold")) {
202 colorThreshold = atoi(argv[++i]);
203 continue;
204 }
205 if (!strcmp(argv[i], "-u")) {
206 //we don't produce unified diffs, ignore parameter to work with svn diff
207 continue;
208 }
209 if (!strcmp(argv[i], "-L")) {
210 if (argc == ++i) {
211 SkDebugf("label expects one argument.\n");
212 continue;
213 }
214 switch (numLabelArguments++) {
215 case 0:
216 baseLabel.set(argv[i]);
217 continue;
218 case 1:
219 comparisonLabel.set(argv[i]);
220 continue;
221 default:
222 SkDebugf("extra label argument <%s>\n", argv[i]);
223 usage(argv[0]);
224 return kGenericError;
225 }
226 continue;
227 }
228 if (argv[i][0] != '-') {
229 switch (numUnflaggedArguments++) {
230 case 0:
231 baseFile.set(argv[i]);
232 continue;
233 case 1:
234 comparisonFile.set(argv[i]);
235 continue;
236 default:
237 SkDebugf("extra unflagged argument <%s>\n", argv[i]);
238 usage(argv[0]);
239 return kGenericError;
240 }
241 }
242
243 SkDebugf("Unrecognized argument <%s>\n", argv[i]);
244 usage(argv[0]);
245 return kGenericError;
246 }
247
248 if (numUnflaggedArguments != 2) {
249 usage(argv[0]);
250 return kGenericError;
251 }
252
253 if (listFilenames) {
254 printf("Base file is [%s]\n", baseFile.c_str());
255 }
256
257 if (listFilenames) {
258 printf("Comparison file is [%s]\n", comparisonFile.c_str());
259 }
260
261 if (outputDir.isEmpty()) {
262 if (listFilenames) {
263 printf("Not writing any diffs. No output dir specified.\n");
264 }
265 } else {
266 if (!outputDir.endsWith(PATH_DIV_STR)) {
267 outputDir.append(PATH_DIV_STR);
268 }
269 if (listFilenames) {
270 printf("Writing diffs. Output dir is [%s]\n", outputDir.c_str());
271 }
272 }
273
274 // Some obscure documentation about diff/patch labels:
275 //
276 // Posix says the format is: <filename><tab><date>
277 // It also states that if a filename contains <tab> or <newline>
278 // the result is implementation defined
279 //
280 // Svn diff --diff-cmd provides labels of the form: <filename><tab><revision>
281 //
282 // Git diff --ext-diff does not supply arguments compatible with diff.
283 // However, it does provide the filename directly.
284 // skimagediff_git.sh: skimagediff %2 %5 -L "%1\t(%3)" -L "%1\t(%6)"
285 //
286 // Git difftool sets $LOCAL, $REMOTE, $MERGED, and $BASE instead of command line parameters.
287 // difftool.<>.cmd: skimagediff $LOCAL $REMOTE -L "$MERGED\t(local)" -L "$MERGED\t(remote)"
288 //
289 // Diff will write any specified label verbatim. Without a specified label diff will write
290 // <filename><tab><date>
291 // However, diff will encode the filename as a cstring if the filename contains
292 // Any of <space> or <double quote>
293 // A char less than 32
bungeman@google.com5b7bac52013-02-04 16:24:44 +0000294 // Any escapable character \\, \a, \b, \t, \n, \v, \f, \r
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000295 //
296 // Patch decodes:
297 // If first <non-white-space> is <double quote>, parse filename from cstring.
298 // If there is a <tab> after the first <non-white-space>, filename is
299 // [first <non-white-space>, the next run of <white-space> with an embedded <tab>).
300 // Otherwise the filename is [first <non-space>, the next <white-space>).
301 //
302 // The filename /dev/null means the file does not exist (used in adds and deletes).
303
304 // Considering the above, skimagediff will consider the contents of a -L parameter as
305 // <filename>(\t<specifier>)?
306 SkString outputFile;
307
308 if (baseLabel.isEmpty()) {
309 baseLabel.set(baseFile);
310 outputFile = baseLabel;
311 } else {
312 const char* baseLabelCstr = baseLabel.c_str();
313 const char* tab = strchr(baseLabelCstr, '\t');
halcanary96fcdcc2015-08-27 07:41:13 -0700314 if (nullptr == tab) {
bungeman@google.come3c8ddf2012-12-05 20:13:12 +0000315 outputFile = baseLabel;
316 } else {
317 outputFile.set(baseLabelCstr, tab - baseLabelCstr);
318 }
319 }
320 if (comparisonLabel.isEmpty()) {
321 comparisonLabel.set(comparisonFile);
322 }
323 printf("Base: %s\n", baseLabel.c_str());
324 printf("Comparison: %s\n", comparisonLabel.c_str());
325
326 DiffRecord dr;
327 create_diff_images(diffProc, colorThreshold, baseFile, comparisonFile, outputDir, outputFile,
328 &dr);
329
330 if (DiffResource::isStatusFailed(dr.fBase.fStatus)) {
331 printf("Base %s.\n", DiffResource::getStatusDescription(dr.fBase.fStatus));
332 }
333 if (DiffResource::isStatusFailed(dr.fComparison.fStatus)) {
334 printf("Comparison %s.\n", DiffResource::getStatusDescription(dr.fComparison.fStatus));
335 }
336 printf("Base and Comparison %s.\n", DiffRecord::getResultDescription(dr.fResult));
337
338 if (DiffRecord::kDifferentPixels_Result == dr.fResult) {
339 printf("%.4f%% of pixels differ", 100 * dr.fFractionDifference);
340 printf(" (%.4f%% weighted)", 100 * dr.fWeightedFraction);
341 if (dr.fFractionDifference < 0.01) {
342 printf(" %d pixels", static_cast<int>(dr.fFractionDifference *
343 dr.fBase.fBitmap.width() *
344 dr.fBase.fBitmap.height()));
345 }
346
347 printf("\nAverage color mismatch: ");
348 printf("%d", static_cast<int>(MAX3(dr.fAverageMismatchR,
349 dr.fAverageMismatchG,
350 dr.fAverageMismatchB)));
351 printf("\nMax color mismatch: ");
352 printf("%d", MAX3(dr.fMaxMismatchR,
353 dr.fMaxMismatchG,
354 dr.fMaxMismatchB));
355 printf("\n");
356 }
357 printf("\n");
358
359 int num_failing_results = 0;
360 if (failOnResultType[dr.fResult]) {
361 ++num_failing_results;
362 }
363 if (failOnStatusType[dr.fBase.fStatus][dr.fComparison.fStatus]) {
364 ++num_failing_results;
365 }
366
367 return num_failing_results;
368}
369
370#if !defined SK_BUILD_FOR_IOS
371int main(int argc, char * const argv[]) {
372 return tool_main(argc, (char**) argv);
373}
374#endif