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.
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
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
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 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