From fe9172705791848ebfe5b5630973b7e67305f64c Mon Sep 17 00:00:00 2001 From: bungeman Date: Thu, 13 Oct 2016 17:36:40 -0400 Subject: Move skdiff tool and add to gn build. I really wanted this today, so I got it working again. Change-Id: I1a37d48d4806198b55c59d1df5ff15a03500195f Reviewed-on: https://skia-review.googlesource.com/3383 Commit-Queue: Ben Wagner Reviewed-by: Mike Klein --- tools/skdiff/skdiff.cpp | 228 +++++++++++ tools/skdiff/skdiff.h | 272 +++++++++++++ tools/skdiff/skdiff_html.cpp | 313 +++++++++++++++ tools/skdiff/skdiff_html.h | 21 + tools/skdiff/skdiff_image.cpp | 374 ++++++++++++++++++ tools/skdiff/skdiff_main.cpp | 862 ++++++++++++++++++++++++++++++++++++++++++ tools/skdiff/skdiff_utils.cpp | 179 +++++++++ tools/skdiff/skdiff_utils.h | 52 +++ 8 files changed, 2301 insertions(+) create mode 100644 tools/skdiff/skdiff.cpp create mode 100644 tools/skdiff/skdiff.h create mode 100644 tools/skdiff/skdiff_html.cpp create mode 100644 tools/skdiff/skdiff_html.h create mode 100644 tools/skdiff/skdiff_image.cpp create mode 100644 tools/skdiff/skdiff_main.cpp create mode 100644 tools/skdiff/skdiff_utils.cpp create mode 100644 tools/skdiff/skdiff_utils.h (limited to 'tools/skdiff') diff --git a/tools/skdiff/skdiff.cpp b/tools/skdiff/skdiff.cpp new file mode 100644 index 0000000000..ae6d72cd7a --- /dev/null +++ b/tools/skdiff/skdiff.cpp @@ -0,0 +1,228 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "skdiff.h" +#include "SkBitmap.h" +#include "SkColor.h" +#include "SkColorPriv.h" +#include "SkTypes.h" + +/*static*/ char const * const DiffRecord::ResultNames[DiffRecord::kResultCount] = { + "EqualBits", + "EqualPixels", + "DifferentPixels", + "DifferentSizes", + "CouldNotCompare", + "Unknown", +}; + +DiffRecord::Result DiffRecord::getResultByName(const char *name) { + for (int result = 0; result < DiffRecord::kResultCount; ++result) { + if (0 == strcmp(DiffRecord::ResultNames[result], name)) { + return static_cast(result); + } + } + return DiffRecord::kResultCount; +} + +static char const * const ResultDescriptions[DiffRecord::kResultCount] = { + "contain exactly the same bits", + "contain the same pixel values, but not the same bits", + "have identical dimensions but some differing pixels", + "have differing dimensions", + "could not be compared", + "not compared yet", +}; + +const char* DiffRecord::getResultDescription(DiffRecord::Result result) { + return ResultDescriptions[result]; +} + +/*static*/ char const * const DiffResource::StatusNames[DiffResource::kStatusCount] = { + "Decoded", + "CouldNotDecode", + + "Read", + "CouldNotRead", + + "Exists", + "DoesNotExist", + + "Specified", + "Unspecified", + + "Unknown", +}; + +DiffResource::Status DiffResource::getStatusByName(const char *name) { + for (int status = 0; status < DiffResource::kStatusCount; ++status) { + if (0 == strcmp(DiffResource::StatusNames[status], name)) { + return static_cast(status); + } + } + return DiffResource::kStatusCount; +} + +static char const * const StatusDescriptions[DiffResource::kStatusCount] = { + "decoded", + "could not be decoded", + + "read", + "could not be read", + + "found", + "not found", + + "specified", + "unspecified", + + "unknown", +}; + +const char* DiffResource::getStatusDescription(DiffResource::Status status) { + return StatusDescriptions[status]; +} + +bool DiffResource::isStatusFailed(DiffResource::Status status) { + return DiffResource::kCouldNotDecode_Status == status || + DiffResource::kCouldNotRead_Status == status || + DiffResource::kDoesNotExist_Status == status || + DiffResource::kUnspecified_Status == status || + DiffResource::kUnknown_Status == status; +} + +bool DiffResource::getMatchingStatuses(char* selector, bool statuses[kStatusCount]) { + if (!strcmp(selector, "any")) { + for (int statusIndex = 0; statusIndex < kStatusCount; ++statusIndex) { + statuses[statusIndex] = true; + } + return true; + } + + for (int statusIndex = 0; statusIndex < kStatusCount; ++statusIndex) { + statuses[statusIndex] = false; + } + + static const char kDelimiterChar = ','; + bool understood = true; + while (true) { + char* delimiterPtr = strchr(selector, kDelimiterChar); + + if (delimiterPtr) { + *delimiterPtr = '\0'; + } + + if (!strcmp(selector, "failed")) { + for (int statusIndex = 0; statusIndex < kStatusCount; ++statusIndex) { + Status status = static_cast(statusIndex); + statuses[statusIndex] |= isStatusFailed(status); + } + } else { + Status status = getStatusByName(selector); + if (status == kStatusCount) { + understood = false; + } else { + statuses[status] = true; + } + } + + if (!delimiterPtr) { + break; + } + + *delimiterPtr = kDelimiterChar; + selector = delimiterPtr + 1; + } + return understood; +} + +static inline bool colors_match_thresholded(SkPMColor c0, SkPMColor c1, const int threshold) { + int da = SkGetPackedA32(c0) - SkGetPackedA32(c1); + int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1); + int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1); + int db = SkGetPackedB32(c0) - SkGetPackedB32(c1); + + return ((SkAbs32(da) <= threshold) && + (SkAbs32(dr) <= threshold) && + (SkAbs32(dg) <= threshold) && + (SkAbs32(db) <= threshold)); +} + +const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE); +const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK); + +void compute_diff(DiffRecord* dr, DiffMetricProc diffFunction, const int colorThreshold) { + const int w = dr->fComparison.fBitmap.width(); + const int h = dr->fComparison.fBitmap.height(); + if (w != dr->fBase.fBitmap.width() || h != dr->fBase.fBitmap.height()) { + dr->fResult = DiffRecord::kDifferentSizes_Result; + return; + } + + SkAutoLockPixels alpDiff(dr->fDifference.fBitmap); + SkAutoLockPixels alpWhite(dr->fWhite.fBitmap); + int mismatchedPixels = 0; + int totalMismatchA = 0; + int totalMismatchR = 0; + int totalMismatchG = 0; + int totalMismatchB = 0; + + // Accumulate fractionally different pixels, then divide out + // # of pixels at the end. + dr->fWeightedFraction = 0; + for (int y = 0; y < h; y++) { + for (int x = 0; x < w; x++) { + SkPMColor c0 = *dr->fBase.fBitmap.getAddr32(x, y); + SkPMColor c1 = *dr->fComparison.fBitmap.getAddr32(x, y); + SkPMColor outputDifference = diffFunction(c0, c1); + uint32_t thisA = SkAbs32(SkGetPackedA32(c0) - SkGetPackedA32(c1)); + uint32_t thisR = SkAbs32(SkGetPackedR32(c0) - SkGetPackedR32(c1)); + uint32_t thisG = SkAbs32(SkGetPackedG32(c0) - SkGetPackedG32(c1)); + uint32_t thisB = SkAbs32(SkGetPackedB32(c0) - SkGetPackedB32(c1)); + totalMismatchA += thisA; + totalMismatchR += thisR; + totalMismatchG += thisG; + totalMismatchB += thisB; + // In HSV, value is defined as max RGB component. + int value = MAX3(thisR, thisG, thisB); + dr->fWeightedFraction += ((float) value) / 255; + if (thisA > dr->fMaxMismatchA) { + dr->fMaxMismatchA = thisA; + } + if (thisR > dr->fMaxMismatchR) { + dr->fMaxMismatchR = thisR; + } + if (thisG > dr->fMaxMismatchG) { + dr->fMaxMismatchG = thisG; + } + if (thisB > dr->fMaxMismatchB) { + dr->fMaxMismatchB = thisB; + } + if (!colors_match_thresholded(c0, c1, colorThreshold)) { + mismatchedPixels++; + *dr->fDifference.fBitmap.getAddr32(x, y) = outputDifference; + *dr->fWhite.fBitmap.getAddr32(x, y) = PMCOLOR_WHITE; + } else { + *dr->fDifference.fBitmap.getAddr32(x, y) = 0; + *dr->fWhite.fBitmap.getAddr32(x, y) = PMCOLOR_BLACK; + } + } + } + if (0 == mismatchedPixels) { + dr->fResult = DiffRecord::kEqualPixels_Result; + return; + } + dr->fResult = DiffRecord::kDifferentPixels_Result; + int pixelCount = w * h; + dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount; + dr->fWeightedFraction /= pixelCount; + dr->fTotalMismatchA = totalMismatchA; + dr->fAverageMismatchA = ((float) totalMismatchA) / pixelCount; + dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount; + dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount; + dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount; +} diff --git a/tools/skdiff/skdiff.h b/tools/skdiff/skdiff.h new file mode 100644 index 0000000000..6bdaadc28d --- /dev/null +++ b/tools/skdiff/skdiff.h @@ -0,0 +1,272 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skdiff_DEFINED +#define skdiff_DEFINED + +#include "SkBitmap.h" +#include "SkColor.h" +#include "SkColorPriv.h" +#include "SkString.h" +#include "../private/SkTDArray.h" + +#if defined(SK_BUILD_FOR_WIN32) + #define PATH_DIV_STR "\\" + #define PATH_DIV_CHAR '\\' +#else + #define PATH_DIV_STR "/" + #define PATH_DIV_CHAR '/' +#endif + +#define MAX2(a,b) (((b) < (a)) ? (a) : (b)) +#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c))) + + +struct DiffResource { + enum Status { + /** The resource was specified, exists, read, and decoded. */ + kDecoded_Status, + /** The resource was specified, exists, read, but could not be decoded. */ + kCouldNotDecode_Status, + + /** The resource was specified, exists, and read. */ + kRead_Status, + /** The resource was specified, exists, but could not be read. */ + kCouldNotRead_Status, + + /** The resource was specified and exists. */ + kExists_Status, + /** The resource was specified, but does not exist. */ + kDoesNotExist_Status, + + /** The resource was specified. */ + kSpecified_Status, + /** The resource was not specified. */ + kUnspecified_Status, + + /** Nothing is yet known about the resource. */ + kUnknown_Status, + + /** NOT A VALID VALUE -- used to set up arrays and to represent an unknown value. */ + kStatusCount + }; + static char const * const StatusNames[DiffResource::kStatusCount]; + + /** Returns the Status with this name. + * If there is no Status with this name, returns kStatusCount. + */ + static Status getStatusByName(const char *name); + + /** Returns a text description of the given Status type. */ + static const char *getStatusDescription(Status status); + + /** Returns true if the Status indicates some kind of failure. */ + static bool isStatusFailed(Status status); + + /** Sets statuses[i] if it is implied by selector, unsets it if not. + * Selector may be a comma delimited list of status names, "any", or "failed". + * Returns true if the selector was entirely understood, false otherwise. + */ + static bool getMatchingStatuses(char* selector, bool statuses[kStatusCount]); + + DiffResource() : fFilename(), fFullPath(), fBitmap(), fStatus(kUnknown_Status) { } + + /** If isEmpty() indicates no filename available. */ + SkString fFilename; + /** If isEmpty() indicates no path available. */ + SkString fFullPath; + /** If empty() indicates the bitmap could not be created. */ + SkBitmap fBitmap; + Status fStatus; +}; + +struct DiffRecord { + + // Result of comparison for each pair of files. + // Listed from "better" to "worse", for sorting of results. + enum Result { + kEqualBits_Result, + kEqualPixels_Result, + kDifferentPixels_Result, + kDifferentSizes_Result, + kCouldNotCompare_Result, + kUnknown_Result, + + kResultCount // NOT A VALID VALUE--used to set up arrays. Must be last. + }; + static char const * const ResultNames[DiffRecord::kResultCount]; + + /** Returns the Result with this name. + * If there is no Result with this name, returns kResultCount. + */ + static Result getResultByName(const char *name); + + /** Returns a text description of the given Result type. */ + static const char *getResultDescription(Result result); + + DiffRecord() + : fBase() + , fComparison() + , fDifference() + , fWhite() + , fFractionDifference(0) + , fWeightedFraction(0) + , fAverageMismatchA(0) + , fAverageMismatchR(0) + , fAverageMismatchG(0) + , fAverageMismatchB(0) + , fTotalMismatchA(0) + , fMaxMismatchA(0) + , fMaxMismatchR(0) + , fMaxMismatchG(0) + , fMaxMismatchB(0) + , fResult(kUnknown_Result) { + } + + DiffResource fBase; + DiffResource fComparison; + DiffResource fDifference; + DiffResource fWhite; + + /// Arbitrary floating-point metric to be used to sort images from most + /// to least different from baseline; values of 0 will be omitted from the + /// summary webpage. + float fFractionDifference; + float fWeightedFraction; + + float fAverageMismatchA; + float fAverageMismatchR; + float fAverageMismatchG; + float fAverageMismatchB; + + uint32_t fTotalMismatchA; + + uint32_t fMaxMismatchA; + uint32_t fMaxMismatchR; + uint32_t fMaxMismatchG; + uint32_t fMaxMismatchB; + + /// Which category of diff result. + Result fResult; +}; + +typedef SkTDArray RecordArray; + +/// A wrapper for any sortProc (comparison routine) which applies a first-order +/// sort beforehand, and a tiebreaker if the sortProc returns 0. +template static int compare(const void* untyped_lhs, const void* untyped_rhs) { + const DiffRecord* lhs = *reinterpret_cast(untyped_lhs); + const DiffRecord* rhs = *reinterpret_cast(untyped_rhs); + + // First-order sort... these comparisons should be applied before comparing + // pixel values, no matter what. + if (lhs->fResult != rhs->fResult) { + return (lhs->fResult < rhs->fResult) ? 1 : -1; + } + + // Passed first-order sort, so call the pixel comparison routine. + int result = T::comparePixels(lhs, rhs); + if (result != 0) { + return result; + } + + // Tiebreaker... if we got to this point, we don't really care + // which order they are sorted in, but let's at least be consistent. + return strcmp(lhs->fBase.fFilename.c_str(), rhs->fBase.fFilename.c_str()); +} + +/// Comparison routine for qsort; sorts by fFractionDifference +/// from largest to smallest. +class CompareDiffMetrics { +public: + static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) { + if (lhs->fFractionDifference < rhs->fFractionDifference) { + return 1; + } + if (rhs->fFractionDifference < lhs->fFractionDifference) { + return -1; + } + return 0; + } +}; + +class CompareDiffWeighted { +public: + static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) { + if (lhs->fWeightedFraction < rhs->fWeightedFraction) { + return 1; + } + if (lhs->fWeightedFraction > rhs->fWeightedFraction) { + return -1; + } + return 0; + } +}; + +/// Comparison routine for qsort; sorts by max(fAverageMismatch{RGB}) +/// from largest to smallest. +class CompareDiffMeanMismatches { +public: + static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) { + float leftValue = MAX3(lhs->fAverageMismatchR, + lhs->fAverageMismatchG, + lhs->fAverageMismatchB); + float rightValue = MAX3(rhs->fAverageMismatchR, + rhs->fAverageMismatchG, + rhs->fAverageMismatchB); + if (leftValue < rightValue) { + return 1; + } + if (rightValue < leftValue) { + return -1; + } + return 0; + } +}; + +/// Comparison routine for qsort; sorts by max(fMaxMismatch{RGB}) +/// from largest to smallest. +class CompareDiffMaxMismatches { +public: + static int comparePixels(const DiffRecord* lhs, const DiffRecord* rhs) { + uint32_t leftValue = MAX3(lhs->fMaxMismatchR, + lhs->fMaxMismatchG, + lhs->fMaxMismatchB); + uint32_t rightValue = MAX3(rhs->fMaxMismatchR, + rhs->fMaxMismatchG, + rhs->fMaxMismatchB); + if (leftValue < rightValue) { + return 1; + } + if (rightValue < leftValue) { + return -1; + } + + return CompareDiffMeanMismatches::comparePixels(lhs, rhs); + } +}; + + +/// Parameterized routine to compute the color of a pixel in a difference image. +typedef SkPMColor (*DiffMetricProc)(SkPMColor, SkPMColor); + +// from gm +static inline SkPMColor compute_diff_pmcolor(SkPMColor c0, SkPMColor c1) { + int dr = SkGetPackedR32(c0) - SkGetPackedR32(c1); + int dg = SkGetPackedG32(c0) - SkGetPackedG32(c1); + int db = SkGetPackedB32(c0) - SkGetPackedB32(c1); + + return SkPackARGB32(0xFF, SkAbs32(dr), SkAbs32(dg), SkAbs32(db)); +} + +/** When finished, dr->fResult should have some value other than kUnknown_Result. + * Expects dr->fWhite.fBitmap and dr->fDifference.fBitmap to have the same bounds as + * dr->fBase.fBitmap and have a valid pixelref. + */ +void compute_diff(DiffRecord* dr, DiffMetricProc diffFunction, const int colorThreshold); + +#endif diff --git a/tools/skdiff/skdiff_html.cpp b/tools/skdiff/skdiff_html.cpp new file mode 100644 index 0000000000..6f3c3b09e1 --- /dev/null +++ b/tools/skdiff/skdiff_html.cpp @@ -0,0 +1,313 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "skdiff.h" +#include "skdiff_html.h" +#include "SkStream.h" +#include "SkTime.h" + +/// Make layout more consistent by scaling image to 240 height, 360 width, +/// or natural size, whichever is smallest. +static int compute_image_height(int height, int width) { + int retval = 240; + if (height < retval) { + retval = height; + } + float scale = (float) retval / height; + if (width * scale > 360) { + scale = (float) 360 / width; + retval = static_cast(height * scale); + } + return retval; +} + +static void print_table_header(SkFILEWStream* stream, + const int matchCount, + const int colorThreshold, + const RecordArray& differences, + const SkString &baseDir, + const SkString &comparisonDir, + bool doOutputDate = false) { + stream->writeText("\n"); + stream->writeText("\n\n\n\n\n\n"); + stream->writeText("\n"); +} + +static void print_pixel_count(SkFILEWStream* stream, const DiffRecord& diff) { + stream->writeText("
("); + stream->writeDecAsText(static_cast(diff.fFractionDifference * + diff.fBase.fBitmap.width() * + diff.fBase.fBitmap.height())); + stream->writeText(" pixels)"); +/* + stream->writeDecAsText(diff.fWeightedFraction * + diff.fBaseWidth * + diff.fBaseHeight); + stream->writeText(" weighted pixels)"); +*/ +} + +static void print_checkbox_cell(SkFILEWStream* stream, const DiffRecord& diff) { + stream->writeText(""); +} + +static void print_label_cell(SkFILEWStream* stream, const DiffRecord& diff) { + char metricBuf [20]; + + stream->writeText(""); + return; + case DiffRecord::kDifferentPixels_Result: + sprintf(metricBuf, "%.4f%%", 100 * diff.fFractionDifference); + stream->writeText(metricBuf); + stream->writeText(" of pixels differ"); + stream->writeText("\n ("); + sprintf(metricBuf, "%.4f%%", 100 * diff.fWeightedFraction); + stream->writeText(metricBuf); + stream->writeText(" weighted)"); + // Write the actual number of pixels that differ if it's < 1% + if (diff.fFractionDifference < 0.01) { + print_pixel_count(stream, diff); + } + stream->writeText("
"); + if (SkScalarRoundToInt(diff.fAverageMismatchA) > 0) { + stream->writeText("
Average alpha channel mismatch "); + stream->writeDecAsText(SkScalarRoundToInt(diff.fAverageMismatchA)); + } + + stream->writeText("
Max alpha channel mismatch "); + stream->writeDecAsText(SkScalarRoundToInt(diff.fMaxMismatchA)); + + stream->writeText("
Total alpha channel mismatch "); + stream->writeDecAsText(static_cast(diff.fTotalMismatchA)); + + stream->writeText("
"); + stream->writeText("
Average color mismatch "); + stream->writeDecAsText(SkScalarRoundToInt(MAX3(diff.fAverageMismatchR, + diff.fAverageMismatchG, + diff.fAverageMismatchB))); + stream->writeText("
Max color mismatch "); + stream->writeDecAsText(MAX3(diff.fMaxMismatchR, + diff.fMaxMismatchG, + diff.fMaxMismatchB)); + stream->writeText(""); + break; + case DiffRecord::kCouldNotCompare_Result: + stream->writeText("Could not compare.
base: "); + stream->writeText(DiffResource::getStatusDescription(diff.fBase.fStatus)); + stream->writeText("
comparison: "); + stream->writeText(DiffResource::getStatusDescription(diff.fComparison.fStatus)); + stream->writeText(""); + return; + default: + SkDEBUGFAIL("encountered DiffRecord with unknown result type"); + return; + } +} + +static void print_image_cell(SkFILEWStream* stream, const SkString& path, int height) { + stream->writeText("
"); +} + +static void print_link_cell(SkFILEWStream* stream, const SkString& path, const char* text) { + stream->writeText(""); +} + +static void print_diff_resource_cell(SkFILEWStream* stream, DiffResource& resource, + const SkString& relativePath, bool local) { + if (resource.fBitmap.empty()) { + if (DiffResource::kCouldNotDecode_Status == resource.fStatus) { + if (local && !resource.fFilename.isEmpty()) { + print_link_cell(stream, resource.fFilename, "N/A"); + return; + } + if (!resource.fFullPath.isEmpty()) { + if (!resource.fFullPath.startsWith(PATH_DIV_STR)) { + resource.fFullPath.prepend(relativePath); + } + print_link_cell(stream, resource.fFullPath, "N/A"); + return; + } + } + stream->writeText(""); + return; + } + + int height = compute_image_height(resource.fBitmap.height(), resource.fBitmap.width()); + if (local) { + print_image_cell(stream, resource.fFilename, height); + return; + } + if (!resource.fFullPath.startsWith(PATH_DIV_STR)) { + resource.fFullPath.prepend(relativePath); + } + print_image_cell(stream, resource.fFullPath, height); +} + +static void print_diff_row(SkFILEWStream* stream, DiffRecord& diff, const SkString& relativePath) { + stream->writeText("\n"); + print_checkbox_cell(stream, diff); + print_label_cell(stream, diff); + print_diff_resource_cell(stream, diff.fWhite, relativePath, true); + print_diff_resource_cell(stream, diff.fDifference, relativePath, true); + print_diff_resource_cell(stream, diff.fBase, relativePath, false); + print_diff_resource_cell(stream, diff.fComparison, relativePath, false); + stream->writeText("\n"); + stream->flush(); +} + +void print_diff_page(const int matchCount, + const int colorThreshold, + const RecordArray& differences, + const SkString& baseDir, + const SkString& comparisonDir, + const SkString& outputDir) { + + SkASSERT(!baseDir.isEmpty()); + SkASSERT(!comparisonDir.isEmpty()); + SkASSERT(!outputDir.isEmpty()); + + SkString outputPath(outputDir); + outputPath.append("index.html"); + //SkFILEWStream outputStream ("index.html"); + SkFILEWStream outputStream(outputPath.c_str()); + + // Need to convert paths from relative-to-cwd to relative-to-outputDir + // FIXME this doesn't work if there are '..' inside the outputDir + + bool isPathAbsolute = false; + // On Windows or Linux, a path starting with PATH_DIV_CHAR is absolute. + if (outputDir.size() > 0 && PATH_DIV_CHAR == outputDir[0]) { + isPathAbsolute = true; + } +#ifdef SK_BUILD_FOR_WIN32 + // On Windows, absolute paths can also start with "x:", where x is any + // drive letter. + if (outputDir.size() > 1 && ':' == outputDir[1]) { + isPathAbsolute = true; + } +#endif + + SkString relativePath; + if (!isPathAbsolute) { + unsigned int ui; + for (ui = 0; ui < outputDir.size(); ui++) { + if (outputDir[ui] == PATH_DIV_CHAR) { + relativePath.append(".." PATH_DIV_STR); + } + } + } + + outputStream.writeText( + "\n\n" + "\n" + "\n\n\n"); + print_table_header(&outputStream, matchCount, colorThreshold, differences, + baseDir, comparisonDir); + int i; + for (i = 0; i < differences.count(); i++) { + DiffRecord* diff = differences[i]; + + switch (diff->fResult) { + // Cases in which there is no diff to report. + case DiffRecord::kEqualBits_Result: + case DiffRecord::kEqualPixels_Result: + continue; + // Cases in which we want a detailed pixel diff. + case DiffRecord::kDifferentPixels_Result: + case DiffRecord::kDifferentSizes_Result: + case DiffRecord::kCouldNotCompare_Result: + print_diff_row(&outputStream, *diff, relativePath); + continue; + default: + SkDEBUGFAIL("encountered DiffRecord with unknown result type"); + continue; + } + } + outputStream.writeText( + "
"); + stream->writeText("select image"); + if (doOutputDate) { + SkTime::DateTime dt; + SkTime::GetDateTime(&dt); + stream->writeText("SkDiff run at "); + stream->writeDecAsText(dt.fHour); + stream->writeText(":"); + if (dt.fMinute < 10) { + stream->writeText("0"); + } + stream->writeDecAsText(dt.fMinute); + stream->writeText(":"); + if (dt.fSecond < 10) { + stream->writeText("0"); + } + stream->writeDecAsText(dt.fSecond); + stream->writeText("
"); + } + stream->writeDecAsText(matchCount); + stream->writeText(" of "); + stream->writeDecAsText(differences.count()); + stream->writeText(" diffs matched "); + if (colorThreshold == 0) { + stream->writeText("exactly"); + } else { + stream->writeText("within "); + stream->writeDecAsText(colorThreshold); + stream->writeText(" color units per component"); + } + stream->writeText(".
"); + stream->writeText("
"); + stream->writeText("every different pixel shown in white"); + stream->writeText(""); + stream->writeText("color difference at each pixel"); + stream->writeText("baseDir: "); + stream->writeText(baseDir.c_str()); + stream->writeText("comparisonDir: "); + stream->writeText(comparisonDir.c_str()); + stream->writeText("
writeText(diff.fBase.fFilename.c_str()); + stream->writeText("\" checked=\"yes\">"); + stream->writeText(diff.fBase.fFilename.c_str()); + stream->writeText("
"); + switch (diff.fResult) { + case DiffRecord::kEqualBits_Result: + SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here"); + return; + case DiffRecord::kEqualPixels_Result: + SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here"); + return; + case DiffRecord::kDifferentSizes_Result: + stream->writeText("Image sizes differ
writeText(path.c_str()); + stream->writeText("\">writeText(path.c_str()); + stream->writeText("\" height=\""); + stream->writeDecAsText(height); + stream->writeText("px\">writeText(path.c_str()); + stream->writeText("\">"); + stream->writeText(text); + stream->writeText("N/A
\n" + "\n" + "
\n" + "\n\n"); + outputStream.flush(); +} diff --git a/tools/skdiff/skdiff_html.h b/tools/skdiff/skdiff_html.h new file mode 100644 index 0000000000..eefbebf2fd --- /dev/null +++ b/tools/skdiff/skdiff_html.h @@ -0,0 +1,21 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skdiff_html_DEFINED +#define skdiff_html_DEFINED + +#include "skdiff.h" +class SkString; + +void print_diff_page(const int matchCount, + const int colorThreshold, + const RecordArray& differences, + const SkString& baseDir, + const SkString& comparisonDir, + const SkString& outputDir); + +#endif diff --git a/tools/skdiff/skdiff_image.cpp b/tools/skdiff/skdiff_image.cpp new file mode 100644 index 0000000000..287523de15 --- /dev/null +++ b/tools/skdiff/skdiff_image.cpp @@ -0,0 +1,374 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#include "skdiff.h" +#include "skdiff_utils.h" +#include "SkBitmap.h" +#include "SkData.h" +#include "SkImageEncoder.h" +#include "SkOSFile.h" +#include "SkTypes.h" + +#include + +/// If outputDir.isEmpty(), don't write out diff files. +static void create_diff_images (DiffMetricProc dmp, + const int colorThreshold, + const SkString& baseFile, + const SkString& comparisonFile, + const SkString& outputDir, + const SkString& outputFilename, + DiffRecord* drp) { + SkASSERT(!baseFile.isEmpty()); + SkASSERT(!comparisonFile.isEmpty()); + + drp->fBase.fFilename = baseFile; + drp->fBase.fFullPath = baseFile; + drp->fBase.fStatus = DiffResource::kSpecified_Status; + + drp->fComparison.fFilename = comparisonFile; + drp->fComparison.fFullPath = comparisonFile; + drp->fComparison.fStatus = DiffResource::kSpecified_Status; + + sk_sp baseFileBits = read_file(drp->fBase.fFullPath.c_str()); + if (baseFileBits) { + drp->fBase.fStatus = DiffResource::kRead_Status; + } + sk_sp comparisonFileBits = read_file(drp->fComparison.fFullPath.c_str()); + if (comparisonFileBits) { + drp->fComparison.fStatus = DiffResource::kRead_Status; + } + if (nullptr == baseFileBits || nullptr == comparisonFileBits) { + if (nullptr == baseFileBits) { + drp->fBase.fStatus = DiffResource::kCouldNotRead_Status; + } + if (nullptr == comparisonFileBits) { + drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status; + } + drp->fResult = DiffRecord::kCouldNotCompare_Result; + return; + } + + if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) { + drp->fResult = DiffRecord::kEqualBits_Result; + return; + } + + get_bitmap(baseFileBits, drp->fBase, false); + get_bitmap(comparisonFileBits, drp->fComparison, false); + if (DiffResource::kDecoded_Status != drp->fBase.fStatus || + DiffResource::kDecoded_Status != drp->fComparison.fStatus) + { + drp->fResult = DiffRecord::kCouldNotCompare_Result; + return; + } + + create_and_write_diff_image(drp, dmp, colorThreshold, outputDir, outputFilename); + //TODO: copy fBase.fFilename and fComparison.fFilename to outputDir + // svn and git often present tmp files to diff tools which are promptly deleted + + //TODO: serialize drp to outputDir + // write a tool to deserialize them and call print_diff_page + + SkASSERT(DiffRecord::kUnknown_Result != drp->fResult); +} + +static void usage (char * argv0) { + SkDebugf("Skia image diff tool\n"); + SkDebugf("\n" +"Usage: \n" +" %s \n" , argv0); + SkDebugf( +"\nArguments:" +"\n --failonresult : After comparing all file pairs, exit with nonzero" +"\n return code (number of file pairs yielding this" +"\n result) if any file pairs yielded this result." +"\n This flag may be repeated, in which case the" +"\n return code will be the number of fail pairs" +"\n yielding ANY of these results." +"\n --failonstatus : exit with nonzero return" +"\n code if any file pairs yeilded this status." +"\n --help: display this info" +"\n --listfilenames: list all filenames for each result type in stdout" +"\n --nodiffs: don't write out image diffs, just generate report on stdout" +"\n --outputdir: directory to write difference images" +"\n --threshold : only report differences > n (per color channel) [default 0]" +"\n -u: ignored. Recognized for compatibility with svn diff." +"\n -L: first occurrence label for base, second occurrence label for comparison." +"\n Labels must be of the form \"(\t)?\"." +"\n The base will be used to create files in outputdir." +"\n" +"\n baseFile: baseline image file." +"\n comparisonFile: comparison image file" +"\n" +"\nIf no sort is specified, it will sort by fraction of pixels mismatching." +"\n"); +} + +const int kNoError = 0; +const int kGenericError = -1; + +int tool_main(int argc, char** argv); +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: + // It also states that if a filename contains or + // the result is implementation defined + // + // Svn diff --diff-cmd provides labels of the form: + // + // 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 + // + // However, diff will encode the filename as a cstring if the filename contains + // Any of or + // A char less than 32 + // Any escapable character \\, \a, \b, \t, \n, \v, \f, \r + // + // Patch decodes: + // If first is , parse filename from cstring. + // If there is a after the first , filename is + // [first , the next run of with an embedded ). + // Otherwise the filename is [first , the next ). + // + // 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 + // (\t)? + 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(dr.fFractionDifference * + dr.fBase.fBitmap.width() * + dr.fBase.fBitmap.height())); + } + + printf("\nAverage color mismatch: "); + printf("%d", static_cast(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; +} + +#if !defined SK_BUILD_FOR_IOS +int main(int argc, char * const argv[]) { + return tool_main(argc, (char**) argv); +} +#endif diff --git a/tools/skdiff/skdiff_main.cpp b/tools/skdiff/skdiff_main.cpp new file mode 100644 index 0000000000..c51cd28d78 --- /dev/null +++ b/tools/skdiff/skdiff_main.cpp @@ -0,0 +1,862 @@ +/* + * Copyright 2011 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#include "skdiff.h" +#include "skdiff_html.h" +#include "skdiff_utils.h" +#include "SkBitmap.h" +#include "SkData.h" +#include "SkForceLinking.h" +#include "SkImageEncoder.h" +#include "SkOSFile.h" +#include "SkStream.h" +#include "../private/SkTDArray.h" +#include "../private/SkTSearch.h" + +#include + +__SK_FORCE_IMAGE_DECODER_LINKING; + +/** + * skdiff + * + * Given three directory names, expects to find identically-named files in + * each of the first two; the first are treated as a set of baseline, + * the second a set of variant images, and a diff image is written into the + * third directory for each pair. + * Creates an index.html in the current third directory to compare each + * pair that does not match exactly. + * Recursively descends directories, unless run with --norecurse. + * + * Returns zero exit code if all images match across baseDir and comparisonDir. + */ + +typedef SkTDArray StringArray; +typedef StringArray FileArray; + +static void add_unique_basename(StringArray* array, const SkString& filename) { + // trim off dirs + const char* src = filename.c_str(); + const char* trimmed = strrchr(src, SkPATH_SEPARATOR); + if (trimmed) { + trimmed += 1; // skip the separator + } else { + trimmed = src; + } + const char* end = strrchr(trimmed, '.'); + if (!end) { + end = trimmed + strlen(trimmed); + } + SkString result(trimmed, end - trimmed); + + // only add unique entries + for (int i = 0; i < array->count(); ++i) { + if (*array->getAt(i) == result) { + return; + } + } + *array->append() = new SkString(result); +} + +struct DiffSummary { + DiffSummary () + : fNumMatches(0) + , fNumMismatches(0) + , fMaxMismatchV(0) + , fMaxMismatchPercent(0) { } + + ~DiffSummary() { + for (int i = 0; i < DiffRecord::kResultCount; ++i) { + fResultsOfType[i].deleteAll(); + } + for (int base = 0; base < DiffResource::kStatusCount; ++base) { + for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { + fStatusOfType[base][comparison].deleteAll(); + } + } + } + + uint32_t fNumMatches; + uint32_t fNumMismatches; + uint32_t fMaxMismatchV; + float fMaxMismatchPercent; + + FileArray fResultsOfType[DiffRecord::kResultCount]; + FileArray fStatusOfType[DiffResource::kStatusCount][DiffResource::kStatusCount]; + + StringArray fFailedBaseNames[DiffRecord::kResultCount]; + + void printContents(const FileArray& fileArray, + const char* baseStatus, const char* comparisonStatus, + bool listFilenames) { + int n = fileArray.count(); + printf("%d file pairs %s in baseDir and %s in comparisonDir", + n, baseStatus, comparisonStatus); + if (listFilenames) { + printf(": "); + for (int i = 0; i < n; ++i) { + printf("%s ", fileArray[i]->c_str()); + } + } + printf("\n"); + } + + void printStatus(bool listFilenames, + bool failOnStatusType[DiffResource::kStatusCount] + [DiffResource::kStatusCount]) { + typedef DiffResource::Status Status; + + for (int base = 0; base < DiffResource::kStatusCount; ++base) { + Status baseStatus = static_cast(base); + for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { + Status comparisonStatus = static_cast(comparison); + const FileArray& fileArray = fStatusOfType[base][comparison]; + if (fileArray.count() > 0) { + if (failOnStatusType[base][comparison]) { + printf(" [*] "); + } else { + printf(" [_] "); + } + printContents(fileArray, + DiffResource::getStatusDescription(baseStatus), + DiffResource::getStatusDescription(comparisonStatus), + listFilenames); + } + } + } + } + + // Print a line about the contents of this FileArray to stdout. + void printContents(const FileArray& fileArray, const char* headerText, bool listFilenames) { + int n = fileArray.count(); + printf("%d file pairs %s", n, headerText); + if (listFilenames) { + printf(": "); + for (int i = 0; i < n; ++i) { + printf("%s ", fileArray[i]->c_str()); + } + } + printf("\n"); + } + + void print(bool listFilenames, bool failOnResultType[DiffRecord::kResultCount], + bool failOnStatusType[DiffResource::kStatusCount] + [DiffResource::kStatusCount]) { + printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches); + for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) { + DiffRecord::Result result = static_cast(resultInt); + if (failOnResultType[result]) { + printf("[*] "); + } else { + printf("[_] "); + } + printContents(fResultsOfType[result], DiffRecord::getResultDescription(result), + listFilenames); + if (DiffRecord::kCouldNotCompare_Result == result) { + printStatus(listFilenames, failOnStatusType); + } + } + printf("(results marked with [*] will cause nonzero return value)\n"); + printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches); + if (fNumMismatches > 0) { + printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV); + printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent); + } + } + + void printfFailingBaseNames(const char separator[]) { + for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) { + const StringArray& array = fFailedBaseNames[resultInt]; + if (array.count()) { + printf("%s [%d]%s", DiffRecord::ResultNames[resultInt], array.count(), separator); + for (int j = 0; j < array.count(); ++j) { + printf("%s%s", array[j]->c_str(), separator); + } + printf("\n"); + } + } + } + + void add (DiffRecord* drp) { + uint32_t mismatchValue; + + if (drp->fBase.fFilename.equals(drp->fComparison.fFilename)) { + fResultsOfType[drp->fResult].push(new SkString(drp->fBase.fFilename)); + } else { + SkString* blame = new SkString("("); + blame->append(drp->fBase.fFilename); + blame->append(", "); + blame->append(drp->fComparison.fFilename); + blame->append(")"); + fResultsOfType[drp->fResult].push(blame); + } + switch (drp->fResult) { + case DiffRecord::kEqualBits_Result: + fNumMatches++; + break; + case DiffRecord::kEqualPixels_Result: + fNumMatches++; + break; + case DiffRecord::kDifferentSizes_Result: + fNumMismatches++; + break; + case DiffRecord::kDifferentPixels_Result: + fNumMismatches++; + if (drp->fFractionDifference * 100 > fMaxMismatchPercent) { + fMaxMismatchPercent = drp->fFractionDifference * 100; + } + mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG, + drp->fMaxMismatchB); + if (mismatchValue > fMaxMismatchV) { + fMaxMismatchV = mismatchValue; + } + break; + case DiffRecord::kCouldNotCompare_Result: + fNumMismatches++; + fStatusOfType[drp->fBase.fStatus][drp->fComparison.fStatus].push( + new SkString(drp->fBase.fFilename)); + break; + case DiffRecord::kUnknown_Result: + SkDEBUGFAIL("adding uncategorized DiffRecord"); + break; + default: + SkDEBUGFAIL("adding DiffRecord with unhandled fResult value"); + break; + } + + switch (drp->fResult) { + case DiffRecord::kEqualBits_Result: + case DiffRecord::kEqualPixels_Result: + break; + default: + add_unique_basename(&fFailedBaseNames[drp->fResult], drp->fBase.fFilename); + break; + } + } +}; + +/// Returns true if string contains any of these substrings. +static bool string_contains_any_of(const SkString& string, + const StringArray& substrings) { + for (int i = 0; i < substrings.count(); i++) { + if (string.contains(substrings[i]->c_str())) { + return true; + } + } + return false; +} + +/// Internal (potentially recursive) implementation of get_file_list. +static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir, + const StringArray& matchSubstrings, + const StringArray& nomatchSubstrings, + bool recurseIntoSubdirs, FileArray *files) { + bool isSubDirEmpty = subDir.isEmpty(); + SkString dir(rootDir); + if (!isSubDirEmpty) { + dir.append(PATH_DIV_STR); + dir.append(subDir); + } + + // Iterate over files (not directories) within dir. + SkOSFile::Iter fileIterator(dir.c_str()); + SkString fileName; + while (fileIterator.next(&fileName, false)) { + if (fileName.startsWith(".")) { + continue; + } + SkString pathRelativeToRootDir(subDir); + if (!isSubDirEmpty) { + pathRelativeToRootDir.append(PATH_DIV_STR); + } + pathRelativeToRootDir.append(fileName); + if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) && + !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) { + files->push(new SkString(pathRelativeToRootDir)); + } + } + + // Recurse into any non-ignored subdirectories. + if (recurseIntoSubdirs) { + SkOSFile::Iter dirIterator(dir.c_str()); + SkString dirName; + while (dirIterator.next(&dirName, true)) { + if (dirName.startsWith(".")) { + continue; + } + SkString pathRelativeToRootDir(subDir); + if (!isSubDirEmpty) { + pathRelativeToRootDir.append(PATH_DIV_STR); + } + pathRelativeToRootDir.append(dirName); + if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) { + get_file_list_subdir(rootDir, pathRelativeToRootDir, + matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, + files); + } + } + } +} + +/// Iterate over dir and get all files whose filename: +/// - matches any of the substrings in matchSubstrings, but... +/// - DOES NOT match any of the substrings in nomatchSubstrings +/// - DOES NOT start with a dot (.) +/// Adds the matching files to the list in *files. +static void get_file_list(const SkString& dir, + const StringArray& matchSubstrings, + const StringArray& nomatchSubstrings, + bool recurseIntoSubdirs, FileArray *files) { + get_file_list_subdir(dir, SkString(""), + matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, + files); +} + +static void release_file_list(FileArray *files) { + files->deleteAll(); +} + +/// Comparison routines for qsort, sort by file names. +static int compare_file_name_metrics(SkString **lhs, SkString **rhs) { + return strcmp((*lhs)->c_str(), (*rhs)->c_str()); +} + +class AutoReleasePixels { +public: + AutoReleasePixels(DiffRecord* drp) + : fDrp(drp) { + SkASSERT(drp != nullptr); + } + ~AutoReleasePixels() { + fDrp->fBase.fBitmap.setPixelRef(nullptr); + fDrp->fComparison.fBitmap.setPixelRef(nullptr); + fDrp->fDifference.fBitmap.setPixelRef(nullptr); + fDrp->fWhite.fBitmap.setPixelRef(nullptr); + } + +private: + DiffRecord* fDrp; +}; + +static void get_bounds(DiffResource& resource, const char* name) { + if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) { + sk_sp fileBits(read_file(resource.fFullPath.c_str())); + if (fileBits) { + get_bitmap(fileBits, resource, true); + } else { + SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str()); + resource.fStatus = DiffResource::kCouldNotRead_Status; + } + } +} + +static void get_bounds(DiffRecord& drp) { + get_bounds(drp.fBase, "base"); + get_bounds(drp.fComparison, "comparison"); +} + +#ifdef SK_OS_WIN +#define ANSI_COLOR_RED "" +#define ANSI_COLOR_GREEN "" +#define ANSI_COLOR_YELLOW "" +#define ANSI_COLOR_RESET "" +#else +#define ANSI_COLOR_RED "\x1b[31m" +#define ANSI_COLOR_GREEN "\x1b[32m" +#define ANSI_COLOR_YELLOW "\x1b[33m" +#define ANSI_COLOR_RESET "\x1b[0m" +#endif + +#define VERBOSE_STATUS(status,color,filename) if (verbose) printf( "[ " color " %10s " ANSI_COLOR_RESET " ] %s\n", status, filename->c_str()) + +/// Creates difference images, returns the number that have a 0 metric. +/// If outputDir.isEmpty(), don't write out diff files. +static void create_diff_images (DiffMetricProc dmp, + const int colorThreshold, + RecordArray* differences, + const SkString& baseDir, + const SkString& comparisonDir, + const SkString& outputDir, + const StringArray& matchSubstrings, + const StringArray& nomatchSubstrings, + bool recurseIntoSubdirs, + bool getBounds, + bool verbose, + DiffSummary* summary) { + SkASSERT(!baseDir.isEmpty()); + SkASSERT(!comparisonDir.isEmpty()); + + FileArray baseFiles; + FileArray comparisonFiles; + + get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles); + get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, + &comparisonFiles); + + if (!baseFiles.isEmpty()) { + qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*), + SkCastForQSort(compare_file_name_metrics)); + } + if (!comparisonFiles.isEmpty()) { + qsort(comparisonFiles.begin(), comparisonFiles.count(), + sizeof(SkString*), SkCastForQSort(compare_file_name_metrics)); + } + + if (!outputDir.isEmpty()) { + sk_mkdir(outputDir.c_str()); + } + + int i = 0; + int j = 0; + + while (i < baseFiles.count() && + j < comparisonFiles.count()) { + + SkString basePath(baseDir); + SkString comparisonPath(comparisonDir); + + DiffRecord *drp = new DiffRecord; + int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str()); + + if (v < 0) { + // in baseDir, but not in comparisonDir + drp->fResult = DiffRecord::kCouldNotCompare_Result; + + basePath.append(*baseFiles[i]); + comparisonPath.append(*baseFiles[i]); + + drp->fBase.fFilename = *baseFiles[i]; + drp->fBase.fFullPath = basePath; + drp->fBase.fStatus = DiffResource::kExists_Status; + + drp->fComparison.fFilename = *baseFiles[i]; + drp->fComparison.fFullPath = comparisonPath; + drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status; + + VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]); + + ++i; + } else if (v > 0) { + // in comparisonDir, but not in baseDir + drp->fResult = DiffRecord::kCouldNotCompare_Result; + + basePath.append(*comparisonFiles[j]); + comparisonPath.append(*comparisonFiles[j]); + + drp->fBase.fFilename = *comparisonFiles[j]; + drp->fBase.fFullPath = basePath; + drp->fBase.fStatus = DiffResource::kDoesNotExist_Status; + + drp->fComparison.fFilename = *comparisonFiles[j]; + drp->fComparison.fFullPath = comparisonPath; + drp->fComparison.fStatus = DiffResource::kExists_Status; + + VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]); + + ++j; + } else { + // Found the same filename in both baseDir and comparisonDir. + SkASSERT(DiffRecord::kUnknown_Result == drp->fResult); + + basePath.append(*baseFiles[i]); + comparisonPath.append(*comparisonFiles[j]); + + drp->fBase.fFilename = *baseFiles[i]; + drp->fBase.fFullPath = basePath; + drp->fBase.fStatus = DiffResource::kExists_Status; + + drp->fComparison.fFilename = *comparisonFiles[j]; + drp->fComparison.fFullPath = comparisonPath; + drp->fComparison.fStatus = DiffResource::kExists_Status; + + sk_sp baseFileBits(read_file(drp->fBase.fFullPath.c_str())); + if (baseFileBits) { + drp->fBase.fStatus = DiffResource::kRead_Status; + } + sk_sp comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str())); + if (comparisonFileBits) { + drp->fComparison.fStatus = DiffResource::kRead_Status; + } + if (nullptr == baseFileBits || nullptr == comparisonFileBits) { + if (nullptr == baseFileBits) { + drp->fBase.fStatus = DiffResource::kCouldNotRead_Status; + VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]); + } + if (nullptr == comparisonFileBits) { + drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status; + VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]); + } + drp->fResult = DiffRecord::kCouldNotCompare_Result; + + } else if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) { + drp->fResult = DiffRecord::kEqualBits_Result; + VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]); + } else { + AutoReleasePixels arp(drp); + get_bitmap(baseFileBits, drp->fBase, false); + get_bitmap(comparisonFileBits, drp->fComparison, false); + VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]); + if (DiffResource::kDecoded_Status == drp->fBase.fStatus && + DiffResource::kDecoded_Status == drp->fComparison.fStatus) { + create_and_write_diff_image(drp, dmp, colorThreshold, + outputDir, drp->fBase.fFilename); + } else { + drp->fResult = DiffRecord::kCouldNotCompare_Result; + } + } + + ++i; + ++j; + } + + if (getBounds) { + get_bounds(*drp); + } + SkASSERT(DiffRecord::kUnknown_Result != drp->fResult); + differences->push(drp); + summary->add(drp); + } + + for (; i < baseFiles.count(); ++i) { + // files only in baseDir + DiffRecord *drp = new DiffRecord(); + drp->fBase.fFilename = *baseFiles[i]; + drp->fBase.fFullPath = baseDir; + drp->fBase.fFullPath.append(drp->fBase.fFilename); + drp->fBase.fStatus = DiffResource::kExists_Status; + + drp->fComparison.fFilename = *baseFiles[i]; + drp->fComparison.fFullPath = comparisonDir; + drp->fComparison.fFullPath.append(drp->fComparison.fFilename); + drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status; + + drp->fResult = DiffRecord::kCouldNotCompare_Result; + if (getBounds) { + get_bounds(*drp); + } + differences->push(drp); + summary->add(drp); + } + + for (; j < comparisonFiles.count(); ++j) { + // files only in comparisonDir + DiffRecord *drp = new DiffRecord(); + drp->fBase.fFilename = *comparisonFiles[j]; + drp->fBase.fFullPath = baseDir; + drp->fBase.fFullPath.append(drp->fBase.fFilename); + drp->fBase.fStatus = DiffResource::kDoesNotExist_Status; + + drp->fComparison.fFilename = *comparisonFiles[j]; + drp->fComparison.fFullPath = comparisonDir; + drp->fComparison.fFullPath.append(drp->fComparison.fFilename); + drp->fComparison.fStatus = DiffResource::kExists_Status; + + drp->fResult = DiffRecord::kCouldNotCompare_Result; + if (getBounds) { + get_bounds(*drp); + } + differences->push(drp); + summary->add(drp); + } + + release_file_list(&baseFiles); + release_file_list(&comparisonFiles); +} + +static void usage (char * argv0) { + SkDebugf("Skia baseline image diff tool\n"); + SkDebugf("\n" +"Usage: \n" +" %s [outputDir] \n", argv0); + SkDebugf( +"\nArguments:" +"\n --failonresult : After comparing all file pairs, exit with nonzero" +"\n return code (number of file pairs yielding this" +"\n result) if any file pairs yielded this result." +"\n This flag may be repeated, in which case the" +"\n return code will be the number of fail pairs" +"\n yielding ANY of these results." +"\n --failonstatus : exit with nonzero return" +"\n code if any file pairs yielded this status." +"\n --help: display this info" +"\n --listfilenames: list all filenames for each result type in stdout" +"\n --match : compare files whose filenames contain this substring;" +"\n if unspecified, compare ALL files." +"\n this flag may be repeated." +"\n --nodiffs: don't write out image diffs or index.html, just generate" +"\n report on stdout" +"\n --nomatch : regardless of --match, DO NOT compare files whose" +"\n filenames contain this substring." +"\n this flag may be repeated." +"\n --noprintdirs: do not print the directories used." +"\n --norecurse: do not recurse into subdirectories." +"\n --sortbymaxmismatch: sort by worst color channel mismatch;" +"\n break ties with -sortbymismatch" +"\n --sortbymismatch: sort by average color channel mismatch" +"\n --threshold : only report differences > n (per color channel) [default 0]" +"\n --weighted: sort by # pixels different weighted by color difference" +"\n" +"\n baseDir: directory to read baseline images from." +"\n comparisonDir: directory to read comparison images from" +"\n outputDir: directory to write difference images and index.html to;" +"\n defaults to comparisonDir" +"\n" +"\nIf no sort is specified, it will sort by fraction of pixels mismatching." +"\n"); +} + +const int kNoError = 0; +const int kGenericError = -1; + +int tool_main(int argc, char** argv); +int tool_main(int argc, char** argv) { + DiffMetricProc diffProc = compute_diff_pmcolor; + int (*sortProc)(const void*, const void*) = compare; + + // 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; + continue; + } + if (!strcmp(argv[i], "--sortbymismatch")) { + sortProc = compare; + continue; + } + if (!strcmp(argv[i], "--threshold")) { + colorThreshold = atoi(argv[++i]); + continue; + } + if (!strcmp(argv[i], "--weighted")) { + sortProc = compare; + 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; +} + +#if !defined SK_BUILD_FOR_IOS +int main(int argc, char * const argv[]) { + return tool_main(argc, (char**) argv); +} +#endif diff --git a/tools/skdiff/skdiff_utils.cpp b/tools/skdiff/skdiff_utils.cpp new file mode 100644 index 0000000000..609d75d265 --- /dev/null +++ b/tools/skdiff/skdiff_utils.cpp @@ -0,0 +1,179 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ +#include "skdiff.h" +#include "skdiff_utils.h" +#include "SkBitmap.h" +#include "SkCodec.h" +#include "SkData.h" +#include "SkImageEncoder.h" +#include "SkStream.h" +#include "SkTypes.h" + +#include + +bool are_buffers_equal(SkData* skdata1, SkData* skdata2) { + if ((nullptr == skdata1) || (nullptr == skdata2)) { + return false; + } + if (skdata1->size() != skdata2->size()) { + return false; + } + return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size())); +} + +sk_sp read_file(const char* file_path) { + sk_sp data(SkData::MakeFromFileName(file_path)); + if (!data) { + SkDebugf("WARNING: could not open file <%s> for reading\n", file_path); + } + return data; +} + +bool get_bitmap(sk_sp fileBits, DiffResource& resource, bool sizeOnly) { + SkAutoTDelete codec(SkCodec::NewFromData(fileBits)); + if (!codec) { + SkDebugf("ERROR: could not create codec for <%s>\n", resource.fFullPath.c_str()); + resource.fStatus = DiffResource::kCouldNotDecode_Status; + return false; + } + + if (!resource.fBitmap.setInfo(codec->getInfo().makeColorType(kN32_SkColorType))) { + SkDebugf("ERROR: could not set bitmap info for <%s>\n", resource.fFullPath.c_str()); + resource.fStatus = DiffResource::kCouldNotDecode_Status; + return false; + } + + if (sizeOnly) { + return true; + } + + if (!resource.fBitmap.tryAllocPixels()) { + SkDebugf("ERROR: could not allocate pixels for <%s>\n", resource.fFullPath.c_str()); + resource.fStatus = DiffResource::kCouldNotDecode_Status; + return false; + } + + if (SkCodec::kSuccess != codec->getPixels(resource.fBitmap.info(), + resource.fBitmap.getPixels(), resource.fBitmap.rowBytes())) { + SkDebugf("ERROR: codec failed for basePath <%s>\n", resource.fFullPath.c_str()); + resource.fStatus = DiffResource::kCouldNotDecode_Status; + return false; + } + + resource.fStatus = DiffResource::kDecoded_Status; + return true; +} + +/** Thanks to PNG, we need to force all pixels 100% opaque. */ +static void force_all_opaque(const SkBitmap& bitmap) { + SkAutoLockPixels lock(bitmap); + for (int y = 0; y < bitmap.height(); y++) { + for (int x = 0; x < bitmap.width(); x++) { + *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT); + } + } +} + +bool write_bitmap(const SkString& path, const SkBitmap& bitmap) { + SkBitmap copy; + bitmap.copyTo(©, kN32_SkColorType); + force_all_opaque(copy); + return SkImageEncoder::EncodeFile(path.c_str(), copy, + SkImageEncoder::kPNG_Type, 100); +} + +/// Return a copy of the "input" string, within which we have replaced all instances +/// of oldSubstring with newSubstring. +/// +/// TODO: If we like this, we should move it into the core SkString implementation, +/// adding more checks and ample test cases, and paying more attention to efficiency. +static SkString replace_all(const SkString &input, + const char oldSubstring[], const char newSubstring[]) { + SkString output; + const char *input_cstr = input.c_str(); + const char *first_char = input_cstr; + const char *match_char; + size_t oldSubstringLen = strlen(oldSubstring); + while ((match_char = strstr(first_char, oldSubstring))) { + output.append(first_char, (match_char - first_char)); + output.append(newSubstring); + first_char = match_char + oldSubstringLen; + } + output.append(first_char); + return output; +} + +static SkString filename_to_derived_filename(const SkString& filename, const char *suffix) { + SkString diffName (filename); + const char* cstring = diffName.c_str(); + size_t dotOffset = strrchr(cstring, '.') - cstring; + diffName.remove(dotOffset, diffName.size() - dotOffset); + diffName.append(suffix); + + // In case we recursed into subdirectories, replace slashes with something else + // so the diffs will all be written into a single flat directory. + diffName = replace_all(diffName, PATH_DIV_STR, "_"); + return diffName; +} + +SkString filename_to_diff_filename(const SkString& filename) { + return filename_to_derived_filename(filename, "-diff.png"); +} + +SkString filename_to_white_filename(const SkString& filename) { + return filename_to_derived_filename(filename, "-white.png"); +} + +void create_and_write_diff_image(DiffRecord* drp, + DiffMetricProc dmp, + const int colorThreshold, + const SkString& outputDir, + const SkString& filename) { + const int w = drp->fBase.fBitmap.width(); + const int h = drp->fBase.fBitmap.height(); + + if (w != drp->fComparison.fBitmap.width() || h != drp->fComparison.fBitmap.height()) { + drp->fResult = DiffRecord::kDifferentSizes_Result; + } else { + drp->fDifference.fBitmap.allocN32Pixels(w, h); + + drp->fWhite.fBitmap.allocN32Pixels(w, h); + + SkASSERT(DiffRecord::kUnknown_Result == drp->fResult); + compute_diff(drp, dmp, colorThreshold); + SkASSERT(DiffRecord::kUnknown_Result != drp->fResult); + } + + if (outputDir.isEmpty()) { + drp->fDifference.fStatus = DiffResource::kUnspecified_Status; + drp->fWhite.fStatus = DiffResource::kUnspecified_Status; + + } else { + drp->fDifference.fFilename = filename_to_diff_filename(filename); + drp->fDifference.fFullPath = outputDir; + drp->fDifference.fFullPath.append(drp->fDifference.fFilename); + drp->fDifference.fStatus = DiffResource::kSpecified_Status; + + drp->fWhite.fFilename = filename_to_white_filename(filename); + drp->fWhite.fFullPath = outputDir; + drp->fWhite.fFullPath.append(drp->fWhite.fFilename); + drp->fWhite.fStatus = DiffResource::kSpecified_Status; + + if (DiffRecord::kDifferentPixels_Result == drp->fResult) { + if (write_bitmap(drp->fDifference.fFullPath, drp->fDifference.fBitmap)) { + drp->fDifference.fStatus = DiffResource::kExists_Status; + } else { + drp->fDifference.fStatus = DiffResource::kDoesNotExist_Status; + } + if (write_bitmap(drp->fWhite.fFullPath, drp->fWhite.fBitmap)) { + drp->fWhite.fStatus = DiffResource::kExists_Status; + } else { + drp->fWhite.fStatus = DiffResource::kDoesNotExist_Status; + } + } + } +} diff --git a/tools/skdiff/skdiff_utils.h b/tools/skdiff/skdiff_utils.h new file mode 100644 index 0000000000..c799325e36 --- /dev/null +++ b/tools/skdiff/skdiff_utils.h @@ -0,0 +1,52 @@ +/* + * Copyright 2012 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef skdiff_utils_DEFINED +#define skdiff_utils_DEFINED + +#include "skdiff.h" + +class SkBitmap; +class SkData; +class SkString; + +/** Returns true if the two buffers passed in are both non-nullptr, + * have the same length, and contain exactly the same byte values. + */ +bool are_buffers_equal(SkData* skdata1, SkData* skdata2); + +/** Reads the file at the given path and returns its complete contents as an + * SkData object (or returns nullptr on error). + */ +sk_sp read_file(const char* file_path); + +/** Decodes the fileBits into the resource.fBitmap. Returns false on failure. */ +bool get_bitmap(sk_sp fileBits, DiffResource& resource, bool sizeOnly); + +/** Writes the bitmap as a PNG to the path specified. */ +bool write_bitmap(const SkString& path, const SkBitmap& bitmap); + +/** Given an image filename, returns the name of the file containing + * the associated difference image. + */ +SkString filename_to_diff_filename(const SkString& filename); + +/** Given an image filename, returns the name of the file containing + * the "white" difference image. + */ +SkString filename_to_white_filename(const SkString& filename); + +/** Calls compute_diff and handles the difference and white diff resources. + * If !outputDir.isEmpty(), writes out difference and white images. + */ +void create_and_write_diff_image(DiffRecord* drp, + DiffMetricProc dmp, + const int colorThreshold, + const SkString& outputDir, + const SkString& filename); + +#endif -- cgit v1.2.3