Home | History | Annotate | Download | only in skdiff
      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.
     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 
     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 
    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