/**
 * This function parses the given filename and stores the results in the given
 * families array.
 */
static void parseConfigFile(const char *filename, SkTDArray<FontFamily*> &families) {

    FILE* file = NULL;

#if !defined(SK_BUILD_FOR_ANDROID_FRAMEWORK)
    // if we are using a version of Android prior to Android 4.2 (JellyBean MR1
    // at API Level 17) then we need to look for files with a different suffix.
    char sdkVersion[PROP_VALUE_MAX];
    __system_property_get("ro.build.version.sdk", sdkVersion);
    const int sdkVersionInt = atoi(sdkVersion);

    if (0 != *sdkVersion && sdkVersionInt < 17) {
        SkString basename;
        SkString updatedFilename;
        SkString locale = SkFontConfigParser::GetLocale();

        basename.set(filename);
        // Remove the .xml suffix. We'll add it back in a moment.
        if (basename.endsWith(".xml")) {
            basename.resize(basename.size()-4);
        }
        // Try first with language and region
        updatedFilename.printf("%s-%s.xml", basename.c_str(), locale.c_str());
        file = fopen(updatedFilename.c_str(), "r");
        if (!file) {
            // If not found, try next with just language
            updatedFilename.printf("%s-%.2s.xml", basename.c_str(), locale.c_str());
            file = fopen(updatedFilename.c_str(), "r");
        }
    }
#endif

    if (NULL == file) {
        file = fopen(filename, "r");
    }

    // Some of the files we attempt to parse (in particular, /vendor/etc/fallback_fonts.xml)
    // are optional - failure here is okay because one of these optional files may not exist.
    if (NULL == file) {
        return;
    }

    XML_Parser parser = XML_ParserCreate(NULL);
    FamilyData *familyData = new FamilyData(&parser, families);
    XML_SetUserData(parser, familyData);
    XML_SetElementHandler(parser, startElementHandler, endElementHandler);

    char buffer[512];
    bool done = false;
    while (!done) {
        fgets(buffer, sizeof(buffer), file);
        int len = strlen(buffer);
        if (feof(file) != 0) {
            done = true;
        }
        XML_Parse(parser, buffer, len, done);
    }
    XML_ParserFree(parser);
    fclose(file);
}
/**
 * Use the current system locale (language and region) to open the best matching
 * customization. For example, when the language is Japanese, the sequence might be:
 *      /system/etc/fallback_fonts-ja-JP.xml
 *      /system/etc/fallback_fonts-ja.xml
 *      /system/etc/fallback_fonts.xml
 */
FILE* openLocalizedFile(const char* origname) {
    FILE* file = 0;

#if !defined(SK_BUILD_FOR_ANDROID_NDK)
    SkString basename;
    SkString filename;
    char language[3] = "";
    char region[3] = "";

    basename.set(origname);
    // Remove the .xml suffix. We'll add it back in a moment.
    if (basename.endsWith(".xml")) {
        basename.resize(basename.size()-4);
    }
    getLocale(language, region);
    // Try first with language and region
    filename.printf("%s-%s-%s.xml", basename.c_str(), language, region);
    file = fopen(filename.c_str(), "r");
    if (!file) {
        // If not found, try next with just language
        filename.printf("%s-%s.xml", basename.c_str(), language);
        file = fopen(filename.c_str(), "r");
    }
#endif

    if (!file) {
        // If still not found, try just the original name
        file = fopen(origname, "r");
    }
    return file;
}
/**
 * Use the current system locale (language and region) to open the best matching
 * customization. For example, when the language is Japanese, the sequence might be:
 *      /system/etc/fallback_fonts-ja-JP.xml
 *      /system/etc/fallback_fonts-ja.xml
 *      /system/etc/fallback_fonts.xml
 */
FILE* openLocalizedFile(const char* origname) {
    FILE* file = 0;
    SkString basename;
    SkString filename;
    AndroidLocale locale;

    basename.set(origname);
    // Remove the .xml suffix. We'll add it back in a moment.
    if (basename.endsWith(".xml")) {
        basename.resize(basename.size()-4);
    }
    getLocale(locale);
    // Try first with language and region
    filename.printf("%s-%s-%s.xml", basename.c_str(), locale.language, locale.region);
    file = fopen(filename.c_str(), "r");
    if (!file) {
        // If not found, try next with just language
        filename.printf("%s-%s.xml", basename.c_str(), locale.language);
        file = fopen(filename.c_str(), "r");

        if (!file) {
            // If still not found, try just the original name
            file = fopen(origname, "r");
        }
    }
    return file;
}
Beispiel #4
0
/**
 *  Test SkOSPath::Join, SkOSPath::Basename, and SkOSPath::Dirname.
 *  Will use SkOSPath::Join to append filename to dir, test that it works correctly,
 *  and tests using SkOSPath::Basename on the result.
 *  @param reporter Reporter for test conditions.
 *  @param dir String representing the path to a folder. May or may not
 *      end with SkOSPath::SEPARATOR.
 *  @param filename String representing the basename of a file. Must NOT
 *      contain SkOSPath::SEPARATOR.
 */
static void test_dir_with_file(skiatest::Reporter* reporter, SkString dir,
                               SkString filename) {
    // If filename contains SkOSPath::SEPARATOR, the tests will fail.
    SkASSERT(!filename.contains(SkOSPath::SEPARATOR));

    // Tests for SkOSPath::Join and SkOSPath::Basename

    // fullName should be "dir<SkOSPath::SEPARATOR>file"
    SkString fullName = SkOSPath::Join(dir.c_str(), filename.c_str());

    // fullName should be the combined size of dir and file, plus one if
    // dir did not include the final path separator.
    size_t expectedSize = dir.size() + filename.size();
    if (!dir.endsWith(SkOSPath::SEPARATOR) && !dir.isEmpty()) {
        expectedSize++;
    }
    REPORTER_ASSERT(reporter, fullName.size() == expectedSize);

    SkString basename = SkOSPath::Basename(fullName.c_str());
    SkString dirname = SkOSPath::Dirname(fullName.c_str());

    // basename should be the same as filename
    REPORTER_ASSERT(reporter, basename.equals(filename));

    // dirname should be the same as dir with any trailing seperators removed.
    // Except when the the string is just "/".
    SkString strippedDir = dir;
    while (strippedDir.size() > 2 && strippedDir[strippedDir.size() - 1] == SkOSPath::SEPARATOR) {
        strippedDir.remove(strippedDir.size() - 1, 1);
    }
    if (!dirname.equals(strippedDir)) {
        SkDebugf("OOUCH %s %s %s\n", dir.c_str(), strippedDir.c_str(), dirname.c_str());
    }
    REPORTER_ASSERT(reporter, dirname.equals(strippedDir));

    // basename will not contain a path separator
    REPORTER_ASSERT(reporter, !basename.contains(SkOSPath::SEPARATOR));

    // Now take the basename of filename, which should be the same as filename.
    basename = SkOSPath::Basename(filename.c_str());
    REPORTER_ASSERT(reporter, basename.equals(filename));
}
Beispiel #5
0
int tool_main(int argc, char** argv) {
    DiffMetricProc diffProc = compute_diff_pmcolor;
    int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;

    // Maximum error tolerated in any one color channel in any one pixel before
    // a difference is reported.
    int colorThreshold = 0;
    SkString baseDir;
    SkString comparisonDir;
    SkString outputDir;

    StringArray matchSubstrings;
    StringArray nomatchSubstrings;

    bool generateDiffs = true;
    bool listFilenames = false;
    bool printDirNames = true;
    bool recurseIntoSubdirs = true;
    bool verbose = false;
    bool listFailingBase = false;

    RecordArray differences;
    DiffSummary summary;

    bool failOnResultType[DiffRecord::kResultCount];
    for (int i = 0; i < DiffRecord::kResultCount; i++) {
        failOnResultType[i] = false;
    }

    bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
    for (int base = 0; base < DiffResource::kStatusCount; ++base) {
        for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
            failOnStatusType[base][comparison] = false;
        }
    }

    int i;
    int numUnflaggedArguments = 0;
    for (i = 1; i < argc; i++) {
        if (!strcmp(argv[i], "--failonresult")) {
            if (argc == ++i) {
                SkDebugf("failonresult expects one argument.\n");
                continue;
            }
            DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
            if (type != DiffRecord::kResultCount) {
                failOnResultType[type] = true;
            } else {
                SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
            }
            continue;
        }
        if (!strcmp(argv[i], "--failonstatus")) {
            if (argc == ++i) {
                SkDebugf("failonstatus missing base status.\n");
                continue;
            }
            bool baseStatuses[DiffResource::kStatusCount];
            if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
                SkDebugf("unrecognized base status <%s>\n", argv[i]);
            }

            if (argc == ++i) {
                SkDebugf("failonstatus missing comparison status.\n");
                continue;
            }
            bool comparisonStatuses[DiffResource::kStatusCount];
            if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
                SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
            }

            for (int base = 0; base < DiffResource::kStatusCount; ++base) {
                for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
                    failOnStatusType[base][comparison] |=
                        baseStatuses[base] && comparisonStatuses[comparison];
                }
            }
            continue;
        }
        if (!strcmp(argv[i], "--help")) {
            usage(argv[0]);
            return kNoError;
        }
        if (!strcmp(argv[i], "--listfilenames")) {
            listFilenames = true;
            continue;
        }
        if (!strcmp(argv[i], "--verbose")) {
            verbose = true;
            continue;
        }
        if (!strcmp(argv[i], "--match")) {
            matchSubstrings.push(new SkString(argv[++i]));
            continue;
        }
        if (!strcmp(argv[i], "--nodiffs")) {
            generateDiffs = false;
            continue;
        }
        if (!strcmp(argv[i], "--nomatch")) {
            nomatchSubstrings.push(new SkString(argv[++i]));
            continue;
        }
        if (!strcmp(argv[i], "--noprintdirs")) {
            printDirNames = false;
            continue;
        }
        if (!strcmp(argv[i], "--norecurse")) {
            recurseIntoSubdirs = false;
            continue;
        }
        if (!strcmp(argv[i], "--sortbymaxmismatch")) {
            sortProc = compare<CompareDiffMaxMismatches>;
            continue;
        }
        if (!strcmp(argv[i], "--sortbymismatch")) {
            sortProc = compare<CompareDiffMeanMismatches>;
            continue;
        }
        if (!strcmp(argv[i], "--threshold")) {
            colorThreshold = atoi(argv[++i]);
            continue;
        }
        if (!strcmp(argv[i], "--weighted")) {
            sortProc = compare<CompareDiffWeighted>;
            continue;
        }
        if (argv[i][0] != '-') {
            switch (numUnflaggedArguments++) {
                case 0:
                    baseDir.set(argv[i]);
                    continue;
                case 1:
                    comparisonDir.set(argv[i]);
                    continue;
                case 2:
                    outputDir.set(argv[i]);
                    continue;
                default:
                    SkDebugf("extra unflagged argument <%s>\n", argv[i]);
                    usage(argv[0]);
                    return kGenericError;
            }
        }
        if (!strcmp(argv[i], "--listFailingBase")) {
            listFailingBase = true;
            continue;
        }

        SkDebugf("Unrecognized argument <%s>\n", argv[i]);
        usage(argv[0]);
        return kGenericError;
    }

    if (numUnflaggedArguments == 2) {
        outputDir = comparisonDir;
    } else if (numUnflaggedArguments != 3) {
        usage(argv[0]);
        return kGenericError;
    }

    if (!baseDir.endsWith(PATH_DIV_STR)) {
        baseDir.append(PATH_DIV_STR);
    }
    if (printDirNames) {
        printf("baseDir is [%s]\n", baseDir.c_str());
    }

    if (!comparisonDir.endsWith(PATH_DIV_STR)) {
        comparisonDir.append(PATH_DIV_STR);
    }
    if (printDirNames) {
        printf("comparisonDir is [%s]\n", comparisonDir.c_str());
    }

    if (!outputDir.endsWith(PATH_DIV_STR)) {
        outputDir.append(PATH_DIV_STR);
    }
    if (generateDiffs) {
        if (printDirNames) {
            printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
        }
    } else {
        if (printDirNames) {
            printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
        }
        outputDir.set("");
    }

    // If no matchSubstrings were specified, match ALL strings
    // (except for whatever nomatchSubstrings were specified, if any).
    if (matchSubstrings.isEmpty()) {
        matchSubstrings.push(new SkString(""));
    }

    create_diff_images(diffProc, colorThreshold, &differences,
                       baseDir, comparisonDir, outputDir,
                       matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
                       verbose, &summary);
    summary.print(listFilenames, failOnResultType, failOnStatusType);

    if (listFailingBase) {
        summary.printfFailingBaseNames("\n");
    }

    if (differences.count()) {
        qsort(differences.begin(), differences.count(),
              sizeof(DiffRecord*), sortProc);
    }

    if (generateDiffs) {
        print_diff_page(summary.fNumMatches, colorThreshold, differences,
                        baseDir, comparisonDir, outputDir);
    }

    for (i = 0; i < differences.count(); i++) {
        delete differences[i];
    }
    matchSubstrings.deleteAll();
    nomatchSubstrings.deleteAll();

    int num_failing_results = 0;
    for (int i = 0; i < DiffRecord::kResultCount; i++) {
        if (failOnResultType[i]) {
            num_failing_results += summary.fResultsOfType[i].count();
        }
    }
    if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
        for (int base = 0; base < DiffResource::kStatusCount; ++base) {
            for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
                if (failOnStatusType[base][comparison]) {
                    num_failing_results += summary.fStatusOfType[base][comparison].count();
                }
            }
        }
    }

    // On Linux (and maybe other platforms too), any results outside of the
    // range [0...255] are wrapped (mod 256).  Do the conversion ourselves, to
    // make sure that we only return 0 when there were no failures.
    return (num_failing_results > 255) ? 255 : num_failing_results;
}
/**
 * Given either a SkStream or a SkData, try to decode the encoded
 * image using the specified options and report errors.
 */
static void test_options(skiatest::Reporter* reporter,
                         const SkDecodingImageGenerator::Options& opts,
                         SkStreamRewindable* encodedStream,
                         SkData* encodedData,
                         bool useData,
                         const SkString& path) {
    SkBitmap bm;
    bool success = false;
    if (useData) {
        if (NULL == encodedData) {
            return;
        }
        success = SkInstallDiscardablePixelRef(
            SkDecodingImageGenerator::Create(encodedData, opts), &bm);
    } else {
        if (NULL == encodedStream) {
            return;
        }
        success = SkInstallDiscardablePixelRef(
            SkDecodingImageGenerator::Create(encodedStream->duplicate(), opts), &bm);
    }
    if (!success) {
        if (opts.fUseRequestedColorType
            && (kARGB_4444_SkColorType == opts.fRequestedColorType)) {
            return;  // Ignore known conversion inabilities.
        }
        // If we get here, it's a failure and we will need more
        // information about why it failed.
        ERRORF(reporter, "Bounds decode failed [sampleSize=%d dither=%s "
               "colorType=%s %s]", opts.fSampleSize, yn(opts.fDitherImage),
               options_colorType(opts), path.c_str());
        return;
    }
    #if defined(SK_BUILD_FOR_ANDROID) || defined(SK_BUILD_FOR_UNIX)
    // Android is the only system that use Skia's image decoders in
    // production.  For now, we'll only verify that samplesize works
    // on systems where it already is known to work.
    REPORTER_ASSERT(reporter, check_rounding(bm.height(), kExpectedHeight,
                                             opts.fSampleSize));
    REPORTER_ASSERT(reporter, check_rounding(bm.width(), kExpectedWidth,
                                             opts.fSampleSize));
    // The ImageDecoder API doesn't guarantee that SampleSize does
    // anything at all, but the decoders that this test excercises all
    // produce an output size in the following range:
    //    (((sample_size * out_size) > (in_size - sample_size))
    //     && out_size <= SkNextPow2(((in_size - 1) / sample_size) + 1));
    #endif  // SK_BUILD_FOR_ANDROID || SK_BUILD_FOR_UNIX
    SkAutoLockPixels alp(bm);
    if (bm.getPixels() == NULL) {
        ERRORF(reporter, "Pixel decode failed [sampleSize=%d dither=%s "
               "colorType=%s %s]", opts.fSampleSize, yn(opts.fDitherImage),
               options_colorType(opts), path.c_str());
        return;
    }

    SkColorType requestedColorType = opts.fRequestedColorType;
    REPORTER_ASSERT(reporter,
                    (!opts.fUseRequestedColorType)
                    || (bm.colorType() == requestedColorType));

    // Condition under which we should check the decoding results:
    if ((kN32_SkColorType == bm.colorType())
        && (!path.endsWith(".jpg"))  // lossy
        && (opts.fSampleSize == 1)) {  // scaled
        const SkColor* correctPixels = kExpectedPixels;
        SkASSERT(bm.height() == kExpectedHeight);
        SkASSERT(bm.width() == kExpectedWidth);
        int pixelErrors = 0;
        for (int y = 0; y < bm.height(); ++y) {
            for (int x = 0; x < bm.width(); ++x) {
                if (*correctPixels != bm.getColor(x, y)) {
                    ++pixelErrors;
                }
                ++correctPixels;
            }
        }
        if (pixelErrors != 0) {
            ERRORF(reporter, "Pixel-level mismatch (%d of %d) "
                   "[sampleSize=%d dither=%s colorType=%s %s]",
                   pixelErrors, kExpectedHeight * kExpectedWidth,
                   opts.fSampleSize, yn(opts.fDitherImage),
                   options_colorType(opts), path.c_str());
        }
    }
}
int tool_main(int argc, char** argv) {
    DiffMetricProc diffProc = compute_diff_pmcolor;

    // Maximum error tolerated in any one color channel in any one pixel before
    // a difference is reported.
    int colorThreshold = 0;
    SkString baseFile;
    SkString baseLabel;
    SkString comparisonFile;
    SkString comparisonLabel;
    SkString outputDir;

    bool listFilenames = false;

    bool failOnResultType[DiffRecord::kResultCount];
    for (int i = 0; i < DiffRecord::kResultCount; i++) {
        failOnResultType[i] = false;
    }

    bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
    for (int base = 0; base < DiffResource::kStatusCount; ++base) {
        for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
            failOnStatusType[base][comparison] = false;
        }
    }

    int i;
    int numUnflaggedArguments = 0;
    int numLabelArguments = 0;
    for (i = 1; i < argc; i++) {
        if (!strcmp(argv[i], "--failonresult")) {
            if (argc == ++i) {
                SkDebugf("failonresult expects one argument.\n");
                continue;
            }
            DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
            if (type != DiffRecord::kResultCount) {
                failOnResultType[type] = true;
            } else {
                SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
            }
            continue;
        }
        if (!strcmp(argv[i], "--failonstatus")) {
            if (argc == ++i) {
                SkDebugf("failonstatus missing base status.\n");
                continue;
            }
            bool baseStatuses[DiffResource::kStatusCount];
            if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
                SkDebugf("unrecognized base status <%s>\n", argv[i]);
            }

            if (argc == ++i) {
                SkDebugf("failonstatus missing comparison status.\n");
                continue;
            }
            bool comparisonStatuses[DiffResource::kStatusCount];
            if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
                SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
            }

            for (int base = 0; base < DiffResource::kStatusCount; ++base) {
                for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
                    failOnStatusType[base][comparison] |=
                        baseStatuses[base] && comparisonStatuses[comparison];
                }
            }
            continue;
        }
        if (!strcmp(argv[i], "--help")) {
            usage(argv[0]);
            return kNoError;
        }
        if (!strcmp(argv[i], "--listfilenames")) {
            listFilenames = true;
            continue;
        }
        if (!strcmp(argv[i], "--outputdir")) {
            if (argc == ++i) {
                SkDebugf("outputdir expects one argument.\n");
                continue;
            }
            outputDir.set(argv[i]);
            continue;
        }
        if (!strcmp(argv[i], "--threshold")) {
            colorThreshold = atoi(argv[++i]);
            continue;
        }
        if (!strcmp(argv[i], "-u")) {
            //we don't produce unified diffs, ignore parameter to work with svn diff
            continue;
        }
        if (!strcmp(argv[i], "-L")) {
            if (argc == ++i) {
                SkDebugf("label expects one argument.\n");
                continue;
            }
            switch (numLabelArguments++) {
                case 0:
                    baseLabel.set(argv[i]);
                    continue;
                case 1:
                    comparisonLabel.set(argv[i]);
                    continue;
                default:
                    SkDebugf("extra label argument <%s>\n", argv[i]);
                    usage(argv[0]);
                    return kGenericError;
            }
            continue;
        }
        if (argv[i][0] != '-') {
            switch (numUnflaggedArguments++) {
                case 0:
                    baseFile.set(argv[i]);
                    continue;
                case 1:
                    comparisonFile.set(argv[i]);
                    continue;
                default:
                    SkDebugf("extra unflagged argument <%s>\n", argv[i]);
                    usage(argv[0]);
                    return kGenericError;
            }
        }

        SkDebugf("Unrecognized argument <%s>\n", argv[i]);
        usage(argv[0]);
        return kGenericError;
    }

    if (numUnflaggedArguments != 2) {
        usage(argv[0]);
        return kGenericError;
    }

    if (listFilenames) {
        printf("Base file is [%s]\n", baseFile.c_str());
    }

    if (listFilenames) {
        printf("Comparison file is [%s]\n", comparisonFile.c_str());
    }

    if (outputDir.isEmpty()) {
        if (listFilenames) {
            printf("Not writing any diffs. No output dir specified.\n");
        }
    } else {
        if (!outputDir.endsWith(PATH_DIV_STR)) {
            outputDir.append(PATH_DIV_STR);
        }
        if (listFilenames) {
            printf("Writing diffs. Output dir is [%s]\n", outputDir.c_str());
        }
    }

    // Some obscure documentation about diff/patch labels:
    //
    // Posix says the format is: <filename><tab><date>
    //     It also states that if a filename contains <tab> or <newline>
    //     the result is implementation defined
    //
    // Svn diff --diff-cmd provides labels of the form: <filename><tab><revision>
    //
    // Git diff --ext-diff does not supply arguments compatible with diff.
    //     However, it does provide the filename directly.
    //     skimagediff_git.sh: skimagediff %2 %5 -L "%1\t(%3)" -L "%1\t(%6)"
    //
    // Git difftool sets $LOCAL, $REMOTE, $MERGED, and $BASE instead of command line parameters.
    //     difftool.<>.cmd: skimagediff $LOCAL $REMOTE -L "$MERGED\t(local)" -L "$MERGED\t(remote)"
    //
    // Diff will write any specified label verbatim. Without a specified label diff will write
    //     <filename><tab><date>
    //     However, diff will encode the filename as a cstring if the filename contains
    //         Any of <space> or <double quote>
    //         A char less than 32
    //         Any escapable character \\, \a, \b, \t, \n, \v, \f, \r
    //
    // Patch decodes:
    //     If first <non-white-space> is <double quote>, parse filename from cstring.
    //     If there is a <tab> after the first <non-white-space>, filename is
    //         [first <non-white-space>, the next run of <white-space> with an embedded <tab>).
    //     Otherwise the filename is [first <non-space>, the next <white-space>).
    //
    // The filename /dev/null means the file does not exist (used in adds and deletes).

    // Considering the above, skimagediff will consider the contents of a -L parameter as
    //     <filename>(\t<specifier>)?
    SkString outputFile;

    if (baseLabel.isEmpty()) {
        baseLabel.set(baseFile);
        outputFile = baseLabel;
    } else {
        const char* baseLabelCstr = baseLabel.c_str();
        const char* tab = strchr(baseLabelCstr, '\t');
        if (nullptr == tab) {
            outputFile = baseLabel;
        } else {
            outputFile.set(baseLabelCstr, tab - baseLabelCstr);
        }
    }
    if (comparisonLabel.isEmpty()) {
        comparisonLabel.set(comparisonFile);
    }
    printf("Base:       %s\n", baseLabel.c_str());
    printf("Comparison: %s\n", comparisonLabel.c_str());

    DiffRecord dr;
    create_diff_images(diffProc, colorThreshold, baseFile, comparisonFile, outputDir, outputFile,
                       &dr);

    if (DiffResource::isStatusFailed(dr.fBase.fStatus)) {
        printf("Base %s.\n", DiffResource::getStatusDescription(dr.fBase.fStatus));
    }
    if (DiffResource::isStatusFailed(dr.fComparison.fStatus)) {
        printf("Comparison %s.\n", DiffResource::getStatusDescription(dr.fComparison.fStatus));
    }
    printf("Base and Comparison %s.\n", DiffRecord::getResultDescription(dr.fResult));

    if (DiffRecord::kDifferentPixels_Result == dr.fResult) {
        printf("%.4f%% of pixels differ", 100 * dr.fFractionDifference);
        printf(" (%.4f%%  weighted)", 100 * dr.fWeightedFraction);
        if (dr.fFractionDifference < 0.01) {
            printf(" %d pixels", static_cast<int>(dr.fFractionDifference *
                                                  dr.fBase.fBitmap.width() *
                                                  dr.fBase.fBitmap.height()));
        }

        printf("\nAverage color mismatch: ");
        printf("%d", static_cast<int>(MAX3(dr.fAverageMismatchR,
                                           dr.fAverageMismatchG,
                                           dr.fAverageMismatchB)));
        printf("\nMax color mismatch: ");
        printf("%d", MAX3(dr.fMaxMismatchR,
                          dr.fMaxMismatchG,
                          dr.fMaxMismatchB));
        printf("\n");
    }
    printf("\n");

    int num_failing_results = 0;
    if (failOnResultType[dr.fResult]) {
        ++num_failing_results;
    }
    if (failOnStatusType[dr.fBase.fStatus][dr.fComparison.fStatus]) {
        ++num_failing_results;
    }

    return num_failing_results;
}