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.
21static 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 (baseFileBits) {
41        drp->fBase.fStatus = DiffResource::kRead_Status;
42    }
43    SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
44    if (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
82static 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
114const int kNoError = 0;
115const int kGenericError = -1;
116
117int tool_main(int argc, char** argv);
118int 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
374int main(int argc, char * const argv[]) {
375    return tool_main(argc, (char**) argv);
376}
377#endif
378