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