diff options
author | bungeman@google.com <bungeman@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81> | 2012-12-05 20:13:12 +0000 |
---|---|---|
committer | bungeman@google.com <bungeman@google.com@2bbb7eff-a529-9590-31e7-b0007b416f81> | 2012-12-05 20:13:12 +0000 |
commit | e3c8ddfd03fdf587b4d8400718ae4bb6e9aa8b6d (patch) | |
tree | df186fda7d4b430c3af0dd403c44bec88cc0bd4d | |
parent | b959a9cbd9517e07e7997ea9f42838333d533021 (diff) |
Update skdiff.
https://codereview.appspot.com/6850115/
git-svn-id: http://skia.googlecode.com/svn/trunk@6681 2bbb7eff-a529-9590-31e7-b0007b416f81
-rw-r--r-- | gyp/tools.gyp | 24 | ||||
-rw-r--r-- | tools/skdiff.cpp | 221 | ||||
-rw-r--r-- | tools/skdiff.h | 265 | ||||
-rw-r--r-- | tools/skdiff_html.cpp | 300 | ||||
-rw-r--r-- | tools/skdiff_html.h | 21 | ||||
-rw-r--r-- | tools/skdiff_image.cpp | 375 | ||||
-rw-r--r-- | tools/skdiff_main.cpp | 1275 | ||||
-rw-r--r-- | tools/skdiff_utils.cpp | 186 | ||||
-rw-r--r-- | tools/skdiff_utils.h | 53 | ||||
-rwxr-xr-x | tools/tests/run.sh | 2 | ||||
-rw-r--r-- | tools/tests/skdiff/identical-bits-or-pixels/output-expected/stdout | 4 | ||||
-rw-r--r-- | tools/tests/skdiff/identical-bits/output-expected/stdout | 4 | ||||
-rw-r--r-- | tools/tests/skdiff/test1/output-expected/index.html | 20 | ||||
-rw-r--r-- | tools/tests/skdiff/test1/output-expected/stdout | 16 | ||||
-rw-r--r-- | tools/tests/skdiff/test2/output-expected/command_line | 2 | ||||
-rw-r--r-- | tools/tests/skdiff/test2/output-expected/stdout | 10 |
16 files changed, 1764 insertions, 1014 deletions
diff --git a/gyp/tools.gyp b/gyp/tools.gyp index e7c22415ae..8a31eaa817 100644 --- a/gyp/tools.gyp +++ b/gyp/tools.gyp @@ -28,7 +28,31 @@ 'target_name': 'skdiff', 'type': 'executable', 'sources': [ + '../tools/skdiff.cpp', + '../tools/skdiff.h', + '../tools/skdiff_html.cpp', + '../tools/skdiff_html.h', '../tools/skdiff_main.cpp', + '../tools/skdiff_utils.cpp', + '../tools/skdiff_utils.h', + ], + 'dependencies': [ + 'skia_base_libs.gyp:skia_base_libs', + 'effects.gyp:effects', + 'images.gyp:images', + ], + }, + { + 'target_name': 'skimagediff', + 'type': 'executable', + 'sources': [ + '../tools/skdiff.cpp', + '../tools/skdiff.h', + '../tools/skdiff_html.cpp', + '../tools/skdiff_html.h', + '../tools/skdiff_image.cpp', + '../tools/skdiff_utils.cpp', + '../tools/skdiff_utils.h', ], 'dependencies': [ 'skia_base_libs.gyp:skia_base_libs', diff --git a/tools/skdiff.cpp b/tools/skdiff.cpp new file mode 100644 index 0000000000..a1783a45de --- /dev/null +++ b/tools/skdiff.cpp @@ -0,0 +1,221 @@ +/* + * 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<DiffRecord::Result>(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<DiffResource::Status>(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<Status>(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 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 trueDifference = compute_diff_pmcolor(c0, c1); + SkPMColor outputDifference = diffFunction(c0, c1); + uint32_t thisR = SkGetPackedR32(trueDifference); + uint32_t thisG = SkGetPackedG32(trueDifference); + uint32_t thisB = SkGetPackedB32(trueDifference); + 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 (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->fAverageMismatchR = ((float) totalMismatchR) / pixelCount; + dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount; + dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount; +} diff --git a/tools/skdiff.h b/tools/skdiff.h new file mode 100644 index 0000000000..b9e69ced85 --- /dev/null +++ b/tools/skdiff.h @@ -0,0 +1,265 @@ +/* + * 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 "SkTDArray.h" + +#if 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) + , fAverageMismatchR(0) + , fAverageMismatchG(0) + , fAverageMismatchB(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 fAverageMismatchR; + float fAverageMismatchG; + float fAverageMismatchB; + + uint32_t fMaxMismatchR; + uint32_t fMaxMismatchG; + uint32_t fMaxMismatchB; + + /// Which category of diff result. + Result fResult; +}; + +typedef SkTDArray<DiffRecord*> RecordArray; + +/// A wrapper for any sortProc (comparison routine) which applies a first-order +/// sort beforehand, and a tiebreaker if the sortProc returns 0. +template<typename T> static int compare(const void* untyped_lhs, const void* untyped_rhs) { + const DiffRecord* lhs = *reinterpret_cast<DiffRecord* const *>(untyped_lhs); + const DiffRecord* rhs = *reinterpret_cast<DiffRecord* const *>(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_html.cpp b/tools/skdiff_html.cpp new file mode 100644 index 0000000000..85d8777712 --- /dev/null +++ b/tools/skdiff_html.cpp @@ -0,0 +1,300 @@ +/* + * 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<int>(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("<table>\n"); + stream->writeText("<tr><th>"); + stream->writeText("select image</th>\n<th>"); + 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("<br>"); + } + 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(".<br>"); + stream->writeText("</th>\n<th>"); + stream->writeText("every different pixel shown in white"); + stream->writeText("</th>\n<th>"); + stream->writeText("color difference at each pixel"); + stream->writeText("</th>\n<th>baseDir: "); + stream->writeText(baseDir.c_str()); + stream->writeText("</th>\n<th>comparisonDir: "); + stream->writeText(comparisonDir.c_str()); + stream->writeText("</th>\n"); + stream->writeText("</tr>\n"); +} + +static void print_pixel_count(SkFILEWStream* stream, const DiffRecord& diff) { + stream->writeText("<br>("); + stream->writeDecAsText(static_cast<int>(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("<td><input type=\"checkbox\" name=\""); + stream->writeText(diff.fBase.fFilename.c_str()); + stream->writeText("\" checked=\"yes\"></td>"); +} + +static void print_label_cell(SkFILEWStream* stream, const DiffRecord& diff) { + char metricBuf [20]; + + stream->writeText("<td><b>"); + stream->writeText(diff.fBase.fFilename.c_str()); + stream->writeText("</b><br>"); + 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</td>"); + 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("<br>Average color mismatch "); + stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR, + diff.fAverageMismatchG, + diff.fAverageMismatchB))); + stream->writeText("<br>Max color mismatch "); + stream->writeDecAsText(MAX3(diff.fMaxMismatchR, + diff.fMaxMismatchG, + diff.fMaxMismatchB)); + stream->writeText("</td>"); + break; + case DiffRecord::kCouldNotCompare_Result: + stream->writeText("Could not compare.<br>base: "); + stream->writeText(DiffResource::getStatusDescription(diff.fBase.fStatus)); + stream->writeText("<br>comparison: "); + stream->writeText(DiffResource::getStatusDescription(diff.fComparison.fStatus)); + stream->writeText("</td>"); + return; + default: + SkDEBUGFAIL("encountered DiffRecord with unknown result type"); + return; + } +} + +static void print_image_cell(SkFILEWStream* stream, const SkString& path, int height) { + stream->writeText("<td><a href=\""); + stream->writeText(path.c_str()); + stream->writeText("\"><img src=\""); + stream->writeText(path.c_str()); + stream->writeText("\" height=\""); + stream->writeDecAsText(height); + stream->writeText("px\"></a></td>"); +} + +static void print_link_cell(SkFILEWStream* stream, const SkString& path, const char* text) { + stream->writeText("<td><a href=\""); + stream->writeText(path.c_str()); + stream->writeText("\">"); + stream->writeText(text); + stream->writeText("</a></td>"); +} + +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("<td>N/A</td>"); + 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("<tr>\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("</tr>\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( + "<html>\n<head>\n" + "<script src=\"https://ajax.googleapis.com/ajax/" + "libs/jquery/1.7.2/jquery.min.js\"></script>\n" + "<script type=\"text/javascript\">\n" + "function generateCheckedList() {\n" + "var boxes = $(\":checkbox:checked\");\n" + "var fileCmdLineString = '';\n" + "var fileMultiLineString = '';\n" + "for (var i = 0; i < boxes.length; i++) {\n" + "fileMultiLineString += boxes[i].name + '<br>';\n" + "fileCmdLineString += boxes[i].name + ' ';\n" + "}\n" + "$(\"#checkedList\").html(fileCmdLineString + " + "'<br><br>' + fileMultiLineString);\n" + "}\n" + "</script>\n</head>\n<body>\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( + "</table>\n" + "<input type=\"button\" " + "onclick=\"generateCheckedList()\" " + "value=\"Create Rebaseline List\">\n" + "<div id=\"checkedList\"></div>\n" + "</body>\n</html>\n"); + outputStream.flush(); +} diff --git a/tools/skdiff_html.h b/tools/skdiff_html.h new file mode 100644 index 0000000000..5e87101f94 --- /dev/null +++ b/tools/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
\ No newline at end of file diff --git a/tools/skdiff_image.cpp b/tools/skdiff_image.cpp new file mode 100644 index 0000000000..14875874c2 --- /dev/null +++ b/tools/skdiff_image.cpp @@ -0,0 +1,375 @@ +/* + * 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 "SkImageDecoder.h" +#include "SkImageEncoder.h" +#include "SkOSFile.h" +#include "SkTDArray.h" +#include "SkTemplates.h" +#include "SkTypes.h" + +/// 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; + + SkAutoDataUnref baseFileBits(read_file(drp->fBase.fFullPath.c_str())); + if (NULL != baseFileBits) { + drp->fBase.fStatus = DiffResource::kRead_Status; + } + SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str())); + if (NULL != comparisonFileBits) { + drp->fComparison.fStatus = DiffResource::kRead_Status; + } + if (NULL == baseFileBits || NULL == comparisonFileBits) { + if (NULL == baseFileBits) { + drp->fBase.fStatus = DiffResource::kCouldNotRead_Status; + } + if (NULL == comparisonFileBits) { + drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status; + } + drp->fResult = DiffRecord::kCouldNotCompare_Result; + return; + } + + if (are_buffers_equal(baseFileBits, comparisonFileBits)) { + drp->fResult = DiffRecord::kEqualBits_Result; + return; + } + + get_bitmap(baseFileBits, drp->fBase, SkImageDecoder::kDecodePixels_Mode); + get_bitmap(comparisonFileBits, drp->fComparison, SkImageDecoder::kDecodePixels_Mode); + 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 <baseFile> <comparisonFile>\n" , argv0); + SkDebugf( +"\nArguments:" +"\n --failonresult <result>: 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 <baseStatus> <comparisonStatus>: 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 <n>: 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 \"<filename>(\t<specifier>)?\"." +"\n The base <filename> 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: <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 (NULL == 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; +} + +#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_main.cpp b/tools/skdiff_main.cpp index 3bd14c6cc0..b9b00386e9 100644 --- a/tools/skdiff_main.cpp +++ b/tools/skdiff_main.cpp @@ -1,11 +1,13 @@ - /* * 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 "SkColorPriv.h" +#include "skdiff.h" +#include "skdiff_html.h" +#include "skdiff_utils.h" +#include "SkBitmap.h" #include "SkData.h" #include "SkImageDecoder.h" #include "SkImageEncoder.h" @@ -13,7 +15,6 @@ #include "SkStream.h" #include "SkTDArray.h" #include "SkTemplates.h" -#include "SkTime.h" #include "SkTSearch.h" #include "SkTypes.h" @@ -31,159 +32,25 @@ * Returns zero exit code if all images match across baseDir and comparisonDir. */ -#if SK_BUILD_FOR_WIN32 - #define PATH_DIV_STR "\\" - #define PATH_DIV_CHAR '\\' -#else - #define PATH_DIV_STR "/" - #define PATH_DIV_CHAR '/' -#endif - -// Result of comparison for each pair of files. -// Listed from "better" to "worse", for sorting of results. -enum Result { - kEqualBits, - kEqualPixels, - kDifferentPixels, - kDifferentSizes, - kDifferentOther, - kComparisonMissing, - kBaseMissing, - kUnknown, - // - kNumResultTypes // NOT A VALID VALUE--used to set up arrays. Must be last. -}; - -// Returns the Result with this name. -// If there is no Result with this name, returns kNumResultTypes. -// TODO: Is there a better return value for the fall-through case? -static Result getResultByName(const char *name) { - if (0 == strcmp("EqualBits", name)) { - return kEqualBits; - } - if (0 == strcmp("EqualPixels", name)) { - return kEqualPixels; - } - if (0 == strcmp("DifferentPixels", name)) { - return kDifferentPixels; - } - if (0 == strcmp("DifferentSizes", name)) { - return kDifferentSizes; - } - if (0 == strcmp("DifferentOther", name)) { - return kDifferentOther; - } - if (0 == strcmp("ComparisonMissing", name)) { - return kComparisonMissing; - } - if (0 == strcmp("BaseMissing", name)) { - return kBaseMissing; - } - if (0 == strcmp("Unknown", name)) { - return kUnknown; - } - return kNumResultTypes; -} - -// Returns a text description of the given Result type. -static const char *getResultDescription(Result result) { - switch (result) { - case kEqualBits: - return "contain exactly the same bits"; - case kEqualPixels: - return "contain the same pixel values, but not the same bits"; - case kDifferentPixels: - return "have identical dimensions but some differing pixels"; - case kDifferentSizes: - return "have differing dimensions"; - case kDifferentOther: - return "contain different bits and are not parsable images"; - case kBaseMissing: - return "missing from baseDir"; - case kComparisonMissing: - return "missing from comparisonDir"; - case kUnknown: - return "not compared yet"; - default: - return NULL; - } -} - -struct DiffRecord { - DiffRecord (const SkString filename, - const SkString basePath, - const SkString comparisonPath, - const Result result = kUnknown) - : fFilename (filename) - , fBasePath (basePath) - , fComparisonPath (comparisonPath) - , fBaseBitmap (NULL) - , fComparisonBitmap (NULL) - , fDifferenceBitmap (NULL) - , fWhiteBitmap (NULL) - , fBaseHeight (0) - , fBaseWidth (0) - , fFractionDifference (0) - , fWeightedFraction (0) - , fAverageMismatchR (0) - , fAverageMismatchG (0) - , fAverageMismatchB (0) - , fMaxMismatchR (0) - , fMaxMismatchG (0) - , fMaxMismatchB (0) - , fResult(result) { - }; - - SkString fFilename; - SkString fBasePath; - SkString fComparisonPath; - - SkBitmap* fBaseBitmap; - SkBitmap* fComparisonBitmap; - SkBitmap* fDifferenceBitmap; - SkBitmap* fWhiteBitmap; - - int fBaseHeight; - int fBaseWidth; - - /// 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 fAverageMismatchR; - float fAverageMismatchG; - float fAverageMismatchB; - - uint32_t fMaxMismatchR; - uint32_t fMaxMismatchG; - uint32_t fMaxMismatchB; - - /// Which category of diff result. - Result fResult; -}; - -#define MAX2(a,b) (((b) < (a)) ? (a) : (b)) -#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c))) - -const SkPMColor PMCOLOR_WHITE = SkPreMultiplyColor(SK_ColorWHITE); -const SkPMColor PMCOLOR_BLACK = SkPreMultiplyColor(SK_ColorBLACK); - typedef SkTDArray<SkString*> StringArray; typedef StringArray FileArray; struct DiffSummary { DiffSummary () - : fNumMatches (0) - , fNumMismatches (0) - , fMaxMismatchV (0) - , fMaxMismatchPercent (0) { }; + : fNumMatches(0) + , fNumMismatches(0) + , fMaxMismatchV(0) + , fMaxMismatchPercent(0) { }; ~DiffSummary() { - for (int i = 0; i < kNumResultTypes; i++) { + 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; @@ -191,7 +58,48 @@ struct DiffSummary { uint32_t fMaxMismatchV; float fMaxMismatchPercent; - FileArray fResultsOfType[kNumResultTypes]; + FileArray fResultsOfType[DiffRecord::kResultCount]; + FileArray fStatusOfType[DiffResource::kStatusCount][DiffResource::kStatusCount]; + + 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<Status>(base); + for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) { + Status comparisonStatus = static_cast<Status>(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) { @@ -206,16 +114,22 @@ struct DiffSummary { printf("\n"); } - void print(bool listFilenames, bool failOnResultType[kNumResultTypes]) { + 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 < kNumResultTypes; resultInt++) { - Result result = static_cast<Result>(resultInt); + for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) { + DiffRecord::Result result = static_cast<DiffRecord::Result>(resultInt); if (failOnResultType[result]) { printf("[*] "); } else { printf("[_] "); } - printContents(fResultsOfType[result], getResultDescription(result), listFilenames); + 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); @@ -228,18 +142,27 @@ struct DiffSummary { void add (DiffRecord* drp) { uint32_t mismatchValue; - fResultsOfType[drp->fResult].push(new SkString(drp->fFilename)); + 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 kEqualBits: + case DiffRecord::kEqualBits_Result: fNumMatches++; break; - case kEqualPixels: + case DiffRecord::kEqualPixels_Result: fNumMatches++; break; - case kDifferentSizes: + case DiffRecord::kDifferentSizes_Result: fNumMismatches++; break; - case kDifferentPixels: + case DiffRecord::kDifferentPixels_Result: fNumMismatches++; if (drp->fFractionDifference * 100 > fMaxMismatchPercent) { fMaxMismatchPercent = drp->fFractionDifference * 100; @@ -250,16 +173,12 @@ struct DiffSummary { fMaxMismatchV = mismatchValue; } break; - case kDifferentOther: - fNumMismatches++; - break; - case kBaseMissing: - fNumMismatches++; - break; - case kComparisonMissing: + case DiffRecord::kCouldNotCompare_Result: fNumMismatches++; + fStatusOfType[drp->fBase.fStatus][drp->fComparison.fStatus].push( + new SkString(drp->fBase.fFilename)); break; - case kUnknown: + case DiffRecord::kUnknown_Result: SkDEBUGFAIL("adding uncategorized DiffRecord"); break; default: @@ -269,449 +188,6 @@ struct DiffSummary { } }; -typedef SkTDArray<DiffRecord*> RecordArray; - -/// A wrapper for any sortProc (comparison routine) which applies a first-order -/// sort beforehand, and a tiebreaker if the sortProc returns 0. -template<typename T> -static int compare(const void* untyped_lhs, const void* untyped_rhs) { - const DiffRecord* lhs = *reinterpret_cast<DiffRecord* const*>(untyped_lhs); - const DiffRecord* rhs = *reinterpret_cast<DiffRecord* const*>(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->fFilename.c_str(), rhs->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); - -#if 0 // UNUSED -static void expand_and_copy (int width, int height, SkBitmap** dest) { - SkBitmap* temp = new SkBitmap (); - temp->reset(); - temp->setConfig((*dest)->config(), width, height); - temp->allocPixels(); - (*dest)->copyPixelsTo(temp->getPixels(), temp->getSize(), - temp->rowBytes()); - *dest = temp; -} -#endif - -/// Returns true if the two buffers passed in are both non-NULL, and include -/// exactly the same byte values (and identical lengths). -static bool are_buffers_equal(SkData* skdata1, SkData* skdata2) { - if ((NULL == skdata1) || (NULL == skdata2)) { - return false; - } - if (skdata1->size() != skdata2->size()) { - return false; - } - return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size())); -} - -/// Reads the file at the given path and returns its complete contents as an -/// SkData object (or returns NULL on error). -static SkData* read_file(const char* file_path) { - SkFILEStream fileStream(file_path); - if (!fileStream.isValid()) { - SkDebugf("WARNING: could not open file <%s> for reading\n", file_path); - return NULL; - } - size_t bytesInFile = fileStream.getLength(); - size_t bytesLeftToRead = bytesInFile; - - void* bufferStart = sk_malloc_throw(bytesInFile); - char* bufferPointer = (char*)bufferStart; - while (bytesLeftToRead > 0) { - size_t bytesReadThisTime = fileStream.read( - bufferPointer, bytesLeftToRead); - if (0 == bytesReadThisTime) { - SkDebugf("WARNING: error reading from <%s>\n", file_path); - sk_free(bufferStart); - return NULL; - } - bytesLeftToRead -= bytesReadThisTime; - bufferPointer += bytesReadThisTime; - } - return SkData::NewFromMalloc(bufferStart, bytesInFile); -} - -/// Decodes binary contents of baseFile and comparisonFile into -/// diffRecord->fBaseBitmap and diffRecord->fComparisonBitmap. -/// Returns true if that succeeds. -static bool get_bitmaps (SkData* baseFileContents, - SkData* comparisonFileContents, - DiffRecord* diffRecord) { - SkMemoryStream compareStream(comparisonFileContents->data(), - comparisonFileContents->size()); - SkMemoryStream baseStream(baseFileContents->data(), - baseFileContents->size()); - - SkImageDecoder* codec = SkImageDecoder::Factory(&baseStream); - if (NULL == codec) { - SkDebugf("ERROR: no codec found for basePath <%s>\n", - diffRecord->fBasePath.c_str()); - return false; - } - - // In debug, the DLL will automatically be unloaded when this is deleted, - // but that shouldn't be a problem in release mode. - SkAutoTDelete<SkImageDecoder> ad(codec); - - baseStream.rewind(); - if (!codec->decode(&baseStream, diffRecord->fBaseBitmap, - SkBitmap::kARGB_8888_Config, - SkImageDecoder::kDecodePixels_Mode)) { - SkDebugf("ERROR: codec failed for basePath <%s>\n", - diffRecord->fBasePath.c_str()); - return false; - } - - diffRecord->fBaseWidth = diffRecord->fBaseBitmap->width(); - diffRecord->fBaseHeight = diffRecord->fBaseBitmap->height(); - - if (!codec->decode(&compareStream, diffRecord->fComparisonBitmap, - SkBitmap::kARGB_8888_Config, - SkImageDecoder::kDecodePixels_Mode)) { - SkDebugf("ERROR: codec failed for comparisonPath <%s>\n", - diffRecord->fComparisonPath.c_str()); - return false; - } - - return true; -} - -static bool get_bitmap_height_width(const SkString& path, - int *height, int *width) { - SkFILEStream stream(path.c_str()); - if (!stream.isValid()) { - SkDebugf("ERROR: couldn't open file <%s>\n", - path.c_str()); - return false; - } - - SkImageDecoder* codec = SkImageDecoder::Factory(&stream); - if (NULL == codec) { - SkDebugf("ERROR: no codec found for <%s>\n", - path.c_str()); - return false; - } - - SkAutoTDelete<SkImageDecoder> ad(codec); - SkBitmap bm; - - stream.rewind(); - if (!codec->decode(&stream, &bm, - SkBitmap::kARGB_8888_Config, - SkImageDecoder::kDecodePixels_Mode)) { - SkDebugf("ERROR: codec failed for <%s>\n", - path.c_str()); - return false; - } - - *height = bm.height(); - *width = bm.width(); - - return true; -} - -// from gm - 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); - } - } -} - -// from gm -static bool write_bitmap(const SkString& path, const SkBitmap* bitmap) { - SkBitmap copy; - bitmap->copyTo(©, SkBitmap::kARGB_8888_Config); - force_all_opaque(copy); - return SkImageEncoder::EncodeFile(path.c_str(), copy, - SkImageEncoder::kPNG_Type, 100); -} - -// 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)); -} - -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)); -} - -// based on gm -// Postcondition: when we exit this method, dr->fResult should have some value -// other than kUnknown. -static void compute_diff(DiffRecord* dr, - DiffMetricProc diffFunction, - const int colorThreshold) { - SkAutoLockPixels alpDiff(*dr->fDifferenceBitmap); - SkAutoLockPixels alpWhite(*dr->fWhiteBitmap); - - const int w = dr->fComparisonBitmap->width(); - const int h = dr->fComparisonBitmap->height(); - int mismatchedPixels = 0; - int totalMismatchR = 0; - int totalMismatchG = 0; - int totalMismatchB = 0; - - if (w != dr->fBaseWidth || h != dr->fBaseHeight) { - dr->fResult = kDifferentSizes; - return; - } - // 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->fBaseBitmap->getAddr32(x, y); - SkPMColor c1 = *dr->fComparisonBitmap->getAddr32(x, y); - SkPMColor trueDifference = compute_diff_pmcolor(c0, c1); - SkPMColor outputDifference = diffFunction(c0, c1); - uint32_t thisR = SkGetPackedR32(trueDifference); - uint32_t thisG = SkGetPackedG32(trueDifference); - uint32_t thisB = SkGetPackedB32(trueDifference); - 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 (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->fDifferenceBitmap->getAddr32(x, y) = outputDifference; - *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_WHITE; - } else { - *dr->fDifferenceBitmap->getAddr32(x, y) = 0; - *dr->fWhiteBitmap->getAddr32(x, y) = PMCOLOR_BLACK; - } - } - } - if (0 == mismatchedPixels) { - dr->fResult = kEqualPixels; - return; - } - dr->fResult = kDifferentPixels; - int pixelCount = w * h; - dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount; - dr->fWeightedFraction /= pixelCount; - dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount; - dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount; - dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount; -} - -/// 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; - int oldSubstringLen = strlen(oldSubstring); - while (NULL != (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(); - int 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; -} - -/// Given a image filename, returns the name of the file containing the -/// associated difference image. -static SkString filename_to_diff_filename (const SkString& filename) { - return filename_to_derived_filename(filename, "-diff.png"); -} - -/// Given a image filename, returns the name of the file containing the -/// "white" difference image. -static SkString filename_to_white_filename (const SkString& filename) { - return filename_to_derived_filename(filename, "-white.png"); -} - -class AutoCreateAndReleaseBitmaps { - -public: - AutoCreateAndReleaseBitmaps(DiffRecord* drp) - : fDrp(drp) { - SkASSERT(drp != NULL); - drp->fBaseBitmap = SkNEW(SkBitmap); - drp->fComparisonBitmap = SkNEW(SkBitmap); - drp->fDifferenceBitmap = SkNEW(SkBitmap); - drp->fWhiteBitmap = SkNEW(SkBitmap); - } - ~AutoCreateAndReleaseBitmaps() { - SkDELETE(fDrp->fBaseBitmap); - fDrp->fBaseBitmap = NULL; - SkDELETE(fDrp->fComparisonBitmap); - fDrp->fComparisonBitmap = NULL; - SkDELETE(fDrp->fDifferenceBitmap); - fDrp->fDifferenceBitmap = NULL; - SkDELETE(fDrp->fWhiteBitmap); - fDrp->fWhiteBitmap = NULL; - } - -private: - DiffRecord* fDrp; -}; - -/// If outputDir.isEmpty(), don't write out diff files. -static void create_and_write_diff_image(DiffRecord* drp, - DiffMetricProc dmp, - const int colorThreshold, - const SkString& outputDir, - const SkString& filename) { - const int w = drp->fBaseWidth; - const int h = drp->fBaseHeight; - drp->fDifferenceBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h); - drp->fDifferenceBitmap->allocPixels(); - drp->fWhiteBitmap->setConfig(SkBitmap::kARGB_8888_Config, w, h); - drp->fWhiteBitmap->allocPixels(); - - SkASSERT(kUnknown == drp->fResult); - compute_diff(drp, dmp, colorThreshold); - SkASSERT(kUnknown != drp->fResult); - - if ((kDifferentPixels == drp->fResult) && !outputDir.isEmpty()) { - SkString differencePath (outputDir); - differencePath.append(filename_to_diff_filename(filename)); - write_bitmap(differencePath, drp->fDifferenceBitmap); - SkString whitePath (outputDir); - whitePath.append(filename_to_white_filename(filename)); - write_bitmap(whitePath, drp->fWhiteBitmap); - } -} - /// Returns true if string contains any of these substrings. static bool string_contains_any_of(const SkString& string, const StringArray& substrings) { @@ -798,6 +274,40 @@ 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 != NULL); + } + ~AutoReleasePixels() { + fDrp->fBase.fBitmap.setPixelRef(NULL); + fDrp->fComparison.fBitmap.setPixelRef(NULL); + fDrp->fDifference.fBitmap.setPixelRef(NULL); + fDrp->fWhite.fBitmap.setPixelRef(NULL); + } + +private: + DiffRecord* fDrp; +}; + +static void get_bounds(DiffResource& resource, const char* name) { + if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) { + SkAutoDataUnref fileBits(read_file(resource.fFullPath.c_str())); + if (NULL == fileBits) { + SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str()); + resource.fStatus = DiffResource::kCouldNotRead_Status; + } else { + get_bitmap(fileBits, resource, SkImageDecoder::kDecodeBounds_Mode); + } + } +} + +static void get_bounds(DiffRecord& drp) { + get_bounds(drp.fBase, "base"); + get_bounds(drp.fComparison, "comparison"); +} + /// 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, @@ -809,6 +319,7 @@ static void create_diff_images (DiffMetricProc dmp, const StringArray& matchSubstrings, const StringArray& nomatchSubstrings, bool recurseIntoSubdirs, + bool getBounds, DiffSummary* summary) { SkASSERT(!baseDir.isEmpty()); SkASSERT(!comparisonDir.isEmpty()); @@ -835,85 +346,143 @@ static void create_diff_images (DiffMetricProc dmp, while (i < baseFiles.count() && j < comparisonFiles.count()) { - SkString basePath (baseDir); - basePath.append(*baseFiles[i]); - SkString comparisonPath (comparisonDir); - comparisonPath.append(*comparisonFiles[j]); + SkString basePath(baseDir); + SkString comparisonPath(comparisonDir); - DiffRecord *drp = NULL; - int v = strcmp(baseFiles[i]->c_str(), - comparisonFiles[j]->c_str()); + 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 = new DiffRecord(*baseFiles[i], basePath, comparisonPath, - kComparisonMissing); + 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; + ++i; } else if (v > 0) { // in comparisonDir, but not in baseDir - drp = new DiffRecord(*comparisonFiles[j], basePath, comparisonPath, - kBaseMissing); + 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; + ++j; } else { // Found the same filename in both baseDir and comparisonDir. - drp = new DiffRecord(*baseFiles[i], basePath, comparisonPath); - SkASSERT(kUnknown == drp->fResult); - - SkData* baseFileBits = NULL; - SkData* comparisonFileBits = NULL; - if (NULL == (baseFileBits = read_file(basePath.c_str()))) { - SkDebugf("WARNING: couldn't read base file <%s>\n", - basePath.c_str()); - drp->fResult = kBaseMissing; - } else if (NULL == (comparisonFileBits = read_file(comparisonPath.c_str()))) { - SkDebugf("WARNING: couldn't read comparison file <%s>\n", - comparisonPath.c_str()); - drp->fResult = kComparisonMissing; + 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; + + SkAutoDataUnref baseFileBits(read_file(drp->fBase.fFullPath.c_str())); + if (NULL != baseFileBits) { + drp->fBase.fStatus = DiffResource::kRead_Status; + } + SkAutoDataUnref comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str())); + if (NULL != comparisonFileBits) { + drp->fComparison.fStatus = DiffResource::kRead_Status; + } + if (NULL == baseFileBits || NULL == comparisonFileBits) { + if (NULL == baseFileBits) { + drp->fBase.fStatus = DiffResource::kCouldNotRead_Status; + } + if (NULL == comparisonFileBits) { + drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status; + } + drp->fResult = DiffRecord::kCouldNotCompare_Result; + + } else if (are_buffers_equal(baseFileBits, comparisonFileBits)) { + drp->fResult = DiffRecord::kEqualBits_Result; + } else { - if (are_buffers_equal(baseFileBits, comparisonFileBits)) { - drp->fResult = kEqualBits; + AutoReleasePixels arp(drp); + get_bitmap(baseFileBits, drp->fBase, SkImageDecoder::kDecodePixels_Mode); + get_bitmap(comparisonFileBits, drp->fComparison, + SkImageDecoder::kDecodePixels_Mode); + 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 { - AutoCreateAndReleaseBitmaps createBitmaps(drp); - if (get_bitmaps(baseFileBits, comparisonFileBits, drp)) { - create_and_write_diff_image(drp, dmp, colorThreshold, - outputDir, *baseFiles[i]); - } else { - drp->fResult = kDifferentOther; - } + drp->fResult = DiffRecord::kCouldNotCompare_Result; } } - if (baseFileBits) { - baseFileBits->unref(); - } - if (comparisonFileBits) { - comparisonFileBits->unref(); - } + ++i; ++j; } - SkASSERT(kUnknown != drp->fResult); + + 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 - SkString basePath (baseDir); - basePath.append(*baseFiles[i]); - SkString comparisonPath; - DiffRecord *drp = new DiffRecord(*baseFiles[i], basePath, - comparisonPath, kComparisonMissing); + 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 - SkString basePath; - SkString comparisonPath(comparisonDir); - comparisonPath.append(*comparisonFiles[j]); - DiffRecord *drp = new DiffRecord(*comparisonFiles[j], basePath, - comparisonPath, kBaseMissing); + 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); } @@ -922,331 +491,11 @@ static void create_diff_images (DiffMetricProc dmp, release_file_list(&comparisonFiles); } -/// 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<int>(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("<table>\n"); - stream->writeText("<tr><th>"); - stream->writeText("select image</th>\n<th>"); - 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("<br>"); - } - stream->writeDecAsText(matchCount); - stream->writeText(" of "); - stream->writeDecAsText(differences.count()); - stream->writeText(" images matched "); - if (colorThreshold == 0) { - stream->writeText("exactly"); - } else { - stream->writeText("within "); - stream->writeDecAsText(colorThreshold); - stream->writeText(" color units per component"); - } - stream->writeText(".<br>"); - stream->writeText("</th>\n<th>"); - stream->writeText("every different pixel shown in white"); - stream->writeText("</th>\n<th>"); - stream->writeText("color difference at each pixel"); - stream->writeText("</th>\n<th>baseDir: "); - stream->writeText(baseDir.c_str()); - stream->writeText("</th>\n<th>comparisonDir: "); - stream->writeText(comparisonDir.c_str()); - stream->writeText("</th>\n"); - stream->writeText("</tr>\n"); -} - -static void print_pixel_count (SkFILEWStream* stream, - const DiffRecord& diff) { - stream->writeText("<br>("); - stream->writeDecAsText(static_cast<int>(diff.fFractionDifference * - diff.fBaseWidth * - diff.fBaseHeight)); - 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("<td><input type=\"checkbox\" name=\""); - stream->writeText(diff.fFilename.c_str()); - stream->writeText("\" checked=\"yes\"></td>"); -} - -static void print_label_cell (SkFILEWStream* stream, - const DiffRecord& diff) { - char metricBuf [20]; - - stream->writeText("<td><b>"); - stream->writeText(diff.fFilename.c_str()); - stream->writeText("</b><br>"); - switch (diff.fResult) { - case kEqualBits: - SkDEBUGFAIL("should not encounter DiffRecord with kEqualBits here"); - return; - case kEqualPixels: - SkDEBUGFAIL("should not encounter DiffRecord with kEqualPixels here"); - return; - case kDifferentSizes: - stream->writeText("Image sizes differ</td>"); - return; - case kDifferentPixels: - sprintf(metricBuf, "%12.4f%%", 100 * diff.fFractionDifference); - stream->writeText(metricBuf); - stream->writeText(" of pixels differ"); - stream->writeText("\n ("); - sprintf(metricBuf, "%12.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("<br>Average color mismatch "); - stream->writeDecAsText(static_cast<int>(MAX3(diff.fAverageMismatchR, - diff.fAverageMismatchG, - diff.fAverageMismatchB))); - stream->writeText("<br>Max color mismatch "); - stream->writeDecAsText(MAX3(diff.fMaxMismatchR, - diff.fMaxMismatchG, - diff.fMaxMismatchB)); - stream->writeText("</td>"); - break; - case kDifferentOther: - stream->writeText("Files differ; unable to parse one or both files</td>"); - return; - case kBaseMissing: - stream->writeText("Missing from baseDir</td>"); - return; - case kComparisonMissing: - stream->writeText("Missing from comparisonDir</td>"); - return; - default: - SkDEBUGFAIL("encountered DiffRecord with unknown result type"); - return; - } -} - -static void print_image_cell (SkFILEWStream* stream, - const SkString& path, - int height) { - stream->writeText("<td><a href=\""); - stream->writeText(path.c_str()); - stream->writeText("\"><img src=\""); - stream->writeText(path.c_str()); - stream->writeText("\" height=\""); - stream->writeDecAsText(height); - stream->writeText("px\"></a></td>"); -} - -#if 0 // UNUSED -static void print_text_cell (SkFILEWStream* stream, const char* text) { - stream->writeText("<td align=center>"); - if (NULL != text) { - stream->writeText(text); - } - stream->writeText("</td>"); -} -#endif - -static void print_diff_with_missing_file(SkFILEWStream* stream, - DiffRecord& diff, - const SkString& relativePath) { - stream->writeText("<tr>\n"); - print_checkbox_cell(stream, diff); - print_label_cell(stream, diff); - stream->writeText("<td>N/A</td>"); - stream->writeText("<td>N/A</td>"); - if (kBaseMissing != diff.fResult) { - int h, w; - if (!get_bitmap_height_width(diff.fBasePath, &h, &w)) { - stream->writeText("<td>N/A</td>"); - } else { - int height = compute_image_height(h, w); - if (!diff.fBasePath.startsWith(PATH_DIV_STR)) { - diff.fBasePath.prepend(relativePath); - } - print_image_cell(stream, diff.fBasePath, height); - } - } else { - stream->writeText("<td>N/A</td>"); - } - if (kComparisonMissing != diff.fResult) { - int h, w; - if (!get_bitmap_height_width(diff.fComparisonPath, &h, &w)) { - stream->writeText("<td>N/A</td>"); - } else { - int height = compute_image_height(h, w); - if (!diff.fComparisonPath.startsWith(PATH_DIV_STR)) { - diff.fComparisonPath.prepend(relativePath); - } - print_image_cell(stream, diff.fComparisonPath, height); - } - } else { - stream->writeText("<td>N/A</td>"); - } - stream->writeText("</tr>\n"); - stream->flush(); -} - -static 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( - "<html>\n<head>\n" - "<script src=\"https://ajax.googleapis.com/ajax/" - "libs/jquery/1.7.2/jquery.min.js\"></script>\n" - "<script type=\"text/javascript\">\n" - "function generateCheckedList() {\n" - "var boxes = $(\":checkbox:checked\");\n" - "var fileCmdLineString = '';\n" - "var fileMultiLineString = '';\n" - "for (var i = 0; i < boxes.length; i++) {\n" - "fileMultiLineString += boxes[i].name + '<br>';\n" - "fileCmdLineString += boxes[i].name + ' ';\n" - "}\n" - "$(\"#checkedList\").html(fileCmdLineString + " - "'<br><br>' + fileMultiLineString);\n" - "}\n" - "</script>\n</head>\n<body>\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 kEqualBits: - case kEqualPixels: - continue; - // Cases in which we want a detailed pixel diff. - case kDifferentPixels: - break; - // Cases in which the files differed, but we can't display the diff. - case kDifferentSizes: - case kDifferentOther: - case kBaseMissing: - case kComparisonMissing: - print_diff_with_missing_file(&outputStream, *diff, relativePath); - continue; - default: - SkDEBUGFAIL("encountered DiffRecord with unknown result type"); - continue; - } - - if (!diff->fBasePath.startsWith(PATH_DIV_STR)) { - diff->fBasePath.prepend(relativePath); - } - if (!diff->fComparisonPath.startsWith(PATH_DIV_STR)) { - diff->fComparisonPath.prepend(relativePath); - } - - int height = compute_image_height(diff->fBaseHeight, diff->fBaseWidth); - outputStream.writeText("<tr>\n"); - print_checkbox_cell(&outputStream, *diff); - print_label_cell(&outputStream, *diff); - print_image_cell(&outputStream, - filename_to_white_filename(diff->fFilename), height); - print_image_cell(&outputStream, - filename_to_diff_filename(diff->fFilename), height); - print_image_cell(&outputStream, diff->fBasePath, height); - print_image_cell(&outputStream, diff->fComparisonPath, height); - outputStream.writeText("</tr>\n"); - outputStream.flush(); - } - outputStream.writeText( - "</table>\n" - "<input type=\"button\" " - "onclick=\"generateCheckedList()\" " - "value=\"Create Rebaseline List\">\n" - "<div id=\"checkedList\"></div>\n" - "</body>\n</html>\n"); - outputStream.flush(); -} - static void usage (char * argv0) { SkDebugf("Skia baseline image diff tool\n"); SkDebugf("\n" "Usage: \n" -" %s <baseDir> <comparisonDir> [outputDir] \n" -, argv0, argv0); +" %s <baseDir> <comparisonDir> [outputDir] \n", argv0); SkDebugf( "\nArguments:" "\n --failonresult <result>: After comparing all file pairs, exit with nonzero" @@ -1255,6 +504,8 @@ static void usage (char * argv0) { "\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 <baseStatus> <comparisonStatus>: 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 <substring>: compare files whose filenames contain this substring;" @@ -1308,17 +559,59 @@ int tool_main(int argc, char** argv) { RecordArray differences; DiffSummary summary; - bool failOnResultType[kNumResultTypes]; - for (int i = 0; i < kNumResultTypes; i++) { + 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")) { - Result type = getResultByName(argv[++i]); - failOnResultType[type] = true; + 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")) { @@ -1431,8 +724,9 @@ int tool_main(int argc, char** argv) { create_diff_images(diffProc, colorThreshold, &differences, baseDir, comparisonDir, outputDir, - matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &summary); - summary.print(listFilenames, failOnResultType); + matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs, + &summary); + summary.print(listFilenames, failOnResultType, failOnStatusType); if (differences.count()) { qsort(differences.begin(), differences.count(), @@ -1451,11 +745,20 @@ int tool_main(int argc, char** argv) { nomatchSubstrings.deleteAll(); int num_failing_results = 0; - for (int i = 0; i < kNumResultTypes; i++) { + 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 diff --git a/tools/skdiff_utils.cpp b/tools/skdiff_utils.cpp new file mode 100644 index 0000000000..0eb405aaa5 --- /dev/null +++ b/tools/skdiff_utils.cpp @@ -0,0 +1,186 @@ +/* + * 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 "SkImageDecoder.h" +#include "SkImageEncoder.h" +#include "SkStream.h" +#include "SkTemplates.h" +#include "SkTypes.h" + +bool are_buffers_equal(SkData* skdata1, SkData* skdata2) { + if ((NULL == skdata1) || (NULL == skdata2)) { + return false; + } + if (skdata1->size() != skdata2->size()) { + return false; + } + return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size())); +} + +SkData* read_file(const char* file_path) { + SkFILEStream fileStream(file_path); + if (!fileStream.isValid()) { + SkDebugf("WARNING: could not open file <%s> for reading\n", file_path); + return NULL; + } + size_t bytesInFile = fileStream.getLength(); + size_t bytesLeftToRead = bytesInFile; + + void* bufferStart = sk_malloc_throw(bytesInFile); + char* bufferPointer = (char*)bufferStart; + while (bytesLeftToRead > 0) { + size_t bytesReadThisTime = fileStream.read(bufferPointer, bytesLeftToRead); + if (0 == bytesReadThisTime) { + SkDebugf("WARNING: error reading from <%s>\n", file_path); + sk_free(bufferStart); + return NULL; + } + bytesLeftToRead -= bytesReadThisTime; + bufferPointer += bytesReadThisTime; + } + return SkData::NewFromMalloc(bufferStart, bytesInFile); +} + +bool get_bitmap(SkData* fileBits, DiffResource& resource, SkImageDecoder::Mode mode) { + SkMemoryStream stream(fileBits->data(), fileBits->size()); + + SkImageDecoder* codec = SkImageDecoder::Factory(&stream); + if (NULL == codec) { + SkDebugf("ERROR: no codec found for <%s>\n", resource.fFullPath.c_str()); + resource.fStatus = DiffResource::kCouldNotDecode_Status; + return false; + } + + // In debug, the DLL will automatically be unloaded when this is deleted, + // but that shouldn't be a problem in release mode. + SkAutoTDelete<SkImageDecoder> ad(codec); + + stream.rewind(); + if (!codec->decode(&stream, &resource.fBitmap, SkBitmap::kARGB_8888_Config, mode)) { + 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(©, SkBitmap::kARGB_8888_Config); + 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; + int oldSubstringLen = strlen(oldSubstring); + while (NULL != (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(); + int 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.setConfig(SkBitmap::kARGB_8888_Config, w, h); + drp->fDifference.fBitmap.allocPixels(); + + drp->fWhite.fBitmap.setConfig(SkBitmap::kARGB_8888_Config, w, h); + drp->fWhite.fBitmap.allocPixels(); + + 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_utils.h b/tools/skdiff_utils.h new file mode 100644 index 0000000000..bd496b3388 --- /dev/null +++ b/tools/skdiff_utils.h @@ -0,0 +1,53 @@ +/* + * 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" +#include "SkImageDecoder.h" + +class SkBitmap; +class SkData; +class SkString; + +/** Returns true if the two buffers passed in are both non-NULL, + * 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 NULL on error). + */ +SkData* read_file(const char* file_path); + +/** Decodes the fileBits into the resource.fBitmap. Returns false on failure. */ +bool get_bitmap(SkData* fileBits, DiffResource& resource, SkImageDecoder::Mode mode); + +/** 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 diff --git a/tools/tests/run.sh b/tools/tests/run.sh index 3e3619147f..1acb124017 100755 --- a/tools/tests/run.sh +++ b/tools/tests/run.sh @@ -59,7 +59,7 @@ skdiff_test "$SKDIFF_TESTDIR/baseDir $SKDIFF_TESTDIR/comparisonDir" "$SKDIFF_TES # baseDir or comparisonDir) # - list filenames with each result type to stdout # - don't generate HTML output files -skdiff_test "--failonresult DifferentPixels --failonresult DifferentSizes --failonresult DifferentOther --failonresult Unknown --listfilenames --nodiffs $SKDIFF_TESTDIR/baseDir $SKDIFF_TESTDIR/comparisonDir" "$SKDIFF_TESTDIR/test2" +skdiff_test "--failonresult DifferentPixels --failonresult DifferentSizes --failonresult Unknown --failonstatus CouldNotDecode,CouldNotRead any --failonstatus any CouldNotDecode,CouldNotRead --listfilenames --nodiffs $SKDIFF_TESTDIR/baseDir $SKDIFF_TESTDIR/comparisonDir" "$SKDIFF_TESTDIR/test2" # Run skdiff over just the files that have identical bits. skdiff_test "--nodiffs --match identical-bits $SKDIFF_TESTDIR/baseDir $SKDIFF_TESTDIR/comparisonDir" "$SKDIFF_TESTDIR/identical-bits" diff --git a/tools/tests/skdiff/identical-bits-or-pixels/output-expected/stdout b/tools/tests/skdiff/identical-bits-or-pixels/output-expected/stdout index 38575a5769..a0800cff8d 100644 --- a/tools/tests/skdiff/identical-bits-or-pixels/output-expected/stdout +++ b/tools/tests/skdiff/identical-bits-or-pixels/output-expected/stdout @@ -7,9 +7,7 @@ compared 3 file pairs: [_] 1 file pairs contain the same pixel values, but not the same bits [_] 0 file pairs have identical dimensions but some differing pixels [_] 0 file pairs have differing dimensions -[_] 0 file pairs contain different bits and are not parsable images -[_] 0 file pairs missing from comparisonDir -[_] 0 file pairs missing from baseDir +[_] 0 file pairs could not be compared [_] 0 file pairs not compared yet (results marked with [*] will cause nonzero return value) diff --git a/tools/tests/skdiff/identical-bits/output-expected/stdout b/tools/tests/skdiff/identical-bits/output-expected/stdout index 8e27568b33..d05c1cc709 100644 --- a/tools/tests/skdiff/identical-bits/output-expected/stdout +++ b/tools/tests/skdiff/identical-bits/output-expected/stdout @@ -7,9 +7,7 @@ compared 2 file pairs: [_] 0 file pairs contain the same pixel values, but not the same bits [_] 0 file pairs have identical dimensions but some differing pixels [_] 0 file pairs have differing dimensions -[_] 0 file pairs contain different bits and are not parsable images -[_] 0 file pairs missing from comparisonDir -[_] 0 file pairs missing from baseDir +[_] 0 file pairs could not be compared [_] 0 file pairs not compared yet (results marked with [*] will cause nonzero return value) diff --git a/tools/tests/skdiff/test1/output-expected/index.html b/tools/tests/skdiff/test1/output-expected/index.html index ed321e243a..a3e192e482 100644 --- a/tools/tests/skdiff/test1/output-expected/index.html +++ b/tools/tests/skdiff/test1/output-expected/index.html @@ -17,32 +17,32 @@ $("#checkedList").html(fileCmdLineString + '<br><br>' + fileMultiLineString); <body> <table> <tr><th>select image</th> -<th>3 of 12 images matched exactly.<br></th> +<th>3 of 12 diffs matched exactly.<br></th> <th>every different pixel shown in white</th> <th>color difference at each pixel</th> <th>baseDir: tools/tests/skdiff/baseDir/</th> <th>comparisonDir: tools/tests/skdiff/comparisonDir/</th> </tr> <tr> -<td><input type="checkbox" name="missing-files/missing-from-baseDir.png" checked="yes"></td><td><b>missing-files/missing-from-baseDir.png</b><br>Missing from baseDir</td><td>N/A</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/missing-files/missing-from-baseDir.png"><img src="../../../../../tools/tests/skdiff/comparisonDir/missing-files/missing-from-baseDir.png" height="240px"></a></td></tr> +<td><input type="checkbox" name="different-bits/different-bits-unknown-format.xyz" checked="yes"></td><td><b>different-bits/different-bits-unknown-format.xyz</b><br>Could not compare.<br>base: could not be decoded<br>comparison: could not be decoded</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/baseDir/different-bits/different-bits-unknown-format.xyz">N/A</a></td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/different-bits/different-bits-unknown-format.xyz">N/A</a></td></tr> <tr> -<td><input type="checkbox" name="missing-files/missing-from-baseDir.xyz" checked="yes"></td><td><b>missing-files/missing-from-baseDir.xyz</b><br>Missing from baseDir</td><td>N/A</td><td>N/A</td><td>N/A</td><td>N/A</td></tr> +<td><input type="checkbox" name="missing-files/missing-from-baseDir.png" checked="yes"></td><td><b>missing-files/missing-from-baseDir.png</b><br>Could not compare.<br>base: not found<br>comparison: decoded</td><td>N/A</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/missing-files/missing-from-baseDir.png"><img src="../../../../../tools/tests/skdiff/comparisonDir/missing-files/missing-from-baseDir.png" height="240px"></a></td></tr> <tr> -<td><input type="checkbox" name="missing-files/missing-from-comparisonDir.png" checked="yes"></td><td><b>missing-files/missing-from-comparisonDir.png</b><br>Missing from comparisonDir</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/baseDir/missing-files/missing-from-comparisonDir.png"><img src="../../../../../tools/tests/skdiff/baseDir/missing-files/missing-from-comparisonDir.png" height="240px"></a></td><td>N/A</td></tr> +<td><input type="checkbox" name="missing-files/missing-from-baseDir.xyz" checked="yes"></td><td><b>missing-files/missing-from-baseDir.xyz</b><br>Could not compare.<br>base: not found<br>comparison: could not be decoded</td><td>N/A</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/missing-files/missing-from-baseDir.xyz">N/A</a></td></tr> <tr> -<td><input type="checkbox" name="missing-files/missing-from-comparisonDir.xyz" checked="yes"></td><td><b>missing-files/missing-from-comparisonDir.xyz</b><br>Missing from comparisonDir</td><td>N/A</td><td>N/A</td><td>N/A</td><td>N/A</td></tr> +<td><input type="checkbox" name="missing-files/missing-from-comparisonDir.png" checked="yes"></td><td><b>missing-files/missing-from-comparisonDir.png</b><br>Could not compare.<br>base: decoded<br>comparison: not found</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/baseDir/missing-files/missing-from-comparisonDir.png"><img src="../../../../../tools/tests/skdiff/baseDir/missing-files/missing-from-comparisonDir.png" height="240px"></a></td><td>N/A</td></tr> <tr> -<td><input type="checkbox" name="different-bits/different-bits-unknown-format.xyz" checked="yes"></td><td><b>different-bits/different-bits-unknown-format.xyz</b><br>Files differ; unable to parse one or both files</td><td>N/A</td><td>N/A</td><td>N/A</td><td>N/A</td></tr> +<td><input type="checkbox" name="missing-files/missing-from-comparisonDir.xyz" checked="yes"></td><td><b>missing-files/missing-from-comparisonDir.xyz</b><br>Could not compare.<br>base: could not be decoded<br>comparison: not found</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/baseDir/missing-files/missing-from-comparisonDir.xyz">N/A</a></td><td>N/A</td></tr> <tr> <td><input type="checkbox" name="different-bits/slightly-different-sizes.png" checked="yes"></td><td><b>different-bits/slightly-different-sizes.png</b><br>Image sizes differ</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/baseDir/different-bits/slightly-different-sizes.png"><img src="../../../../../tools/tests/skdiff/baseDir/different-bits/slightly-different-sizes.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/different-bits/slightly-different-sizes.png"><img src="../../../../../tools/tests/skdiff/comparisonDir/different-bits/slightly-different-sizes.png" height="240px"></a></td></tr> <tr> <td><input type="checkbox" name="different-bits/very-different-sizes.png" checked="yes"></td><td><b>different-bits/very-different-sizes.png</b><br>Image sizes differ</td><td>N/A</td><td>N/A</td><td><a href="../../../../../tools/tests/skdiff/baseDir/different-bits/very-different-sizes.png"><img src="../../../../../tools/tests/skdiff/baseDir/different-bits/very-different-sizes.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/different-bits/very-different-sizes.png"><img src="../../../../../tools/tests/skdiff/comparisonDir/different-bits/very-different-sizes.png" height="128px"></a></td></tr> <tr> -<td><input type="checkbox" name="different-bits/very-different-pixels-same-size.png" checked="yes"></td><td><b>different-bits/very-different-pixels-same-size.png</b><br> 97.9926% of pixels differ - ( 42.8911% weighted)<br>Average color mismatch 89<br>Max color mismatch 239</td><td><a href="different-bits_very-different-pixels-same-size-white.png"><img src="different-bits_very-different-pixels-same-size-white.png" height="240px"></a></td><td><a href="different-bits_very-different-pixels-same-size-diff.png"><img src="different-bits_very-different-pixels-same-size-diff.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/baseDir/different-bits/very-different-pixels-same-size.png"><img src="../../../../../tools/tests/skdiff/baseDir/different-bits/very-different-pixels-same-size.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/different-bits/very-different-pixels-same-size.png"><img src="../../../../../tools/tests/skdiff/comparisonDir/different-bits/very-different-pixels-same-size.png" height="240px"></a></td></tr> +<td><input type="checkbox" name="different-bits/very-different-pixels-same-size.png" checked="yes"></td><td><b>different-bits/very-different-pixels-same-size.png</b><br>97.9926% of pixels differ + (42.8911% weighted)<br>Average color mismatch 89<br>Max color mismatch 239</td><td><a href="different-bits_very-different-pixels-same-size-white.png"><img src="different-bits_very-different-pixels-same-size-white.png" height="240px"></a></td><td><a href="different-bits_very-different-pixels-same-size-diff.png"><img src="different-bits_very-different-pixels-same-size-diff.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/baseDir/different-bits/very-different-pixels-same-size.png"><img src="../../../../../tools/tests/skdiff/baseDir/different-bits/very-different-pixels-same-size.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/different-bits/very-different-pixels-same-size.png"><img src="../../../../../tools/tests/skdiff/comparisonDir/different-bits/very-different-pixels-same-size.png" height="240px"></a></td></tr> <tr> -<td><input type="checkbox" name="different-bits/slightly-different-pixels-same-size.png" checked="yes"></td><td><b>different-bits/slightly-different-pixels-same-size.png</b><br> 0.6630% of pixels differ - ( 0.1904% weighted)<br>(2164 pixels)<br>Average color mismatch 0<br>Max color mismatch 213</td><td><a href="different-bits_slightly-different-pixels-same-size-white.png"><img src="different-bits_slightly-different-pixels-same-size-white.png" height="240px"></a></td><td><a href="different-bits_slightly-different-pixels-same-size-diff.png"><img src="different-bits_slightly-different-pixels-same-size-diff.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/baseDir/different-bits/slightly-different-pixels-same-size.png"><img src="../../../../../tools/tests/skdiff/baseDir/different-bits/slightly-different-pixels-same-size.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/different-bits/slightly-different-pixels-same-size.png"><img src="../../../../../tools/tests/skdiff/comparisonDir/different-bits/slightly-different-pixels-same-size.png" height="240px"></a></td></tr> +<td><input type="checkbox" name="different-bits/slightly-different-pixels-same-size.png" checked="yes"></td><td><b>different-bits/slightly-different-pixels-same-size.png</b><br>0.6630% of pixels differ + (0.1904% weighted)<br>(2164 pixels)<br>Average color mismatch 0<br>Max color mismatch 213</td><td><a href="different-bits_slightly-different-pixels-same-size-white.png"><img src="different-bits_slightly-different-pixels-same-size-white.png" height="240px"></a></td><td><a href="different-bits_slightly-different-pixels-same-size-diff.png"><img src="different-bits_slightly-different-pixels-same-size-diff.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/baseDir/different-bits/slightly-different-pixels-same-size.png"><img src="../../../../../tools/tests/skdiff/baseDir/different-bits/slightly-different-pixels-same-size.png" height="240px"></a></td><td><a href="../../../../../tools/tests/skdiff/comparisonDir/different-bits/slightly-different-pixels-same-size.png"><img src="../../../../../tools/tests/skdiff/comparisonDir/different-bits/slightly-different-pixels-same-size.png" height="240px"></a></td></tr> </table> <input type="button" onclick="generateCheckedList()" value="Create Rebaseline List"> <div id="checkedList"></div> diff --git a/tools/tests/skdiff/test1/output-expected/stdout b/tools/tests/skdiff/test1/output-expected/stdout index 3208fc4b56..2b8b2d403f 100644 --- a/tools/tests/skdiff/test1/output-expected/stdout +++ b/tools/tests/skdiff/test1/output-expected/stdout @@ -1,8 +1,9 @@ -ERROR: no codec found for basePath <tools/tests/skdiff/baseDir/different-bits/different-bits-unknown-format.xyz> -ERROR: no codec found for <tools/tests/skdiff/comparisonDir/missing-files/missing-from-baseDir.xyz> -ERROR: no codec found for <tools/tests/skdiff/baseDir/missing-files/missing-from-comparisonDir.xyz> ERROR: no codec found for <tools/tests/skdiff/baseDir/different-bits/different-bits-unknown-format.xyz> ERROR: no codec found for <tools/tests/skdiff/comparisonDir/different-bits/different-bits-unknown-format.xyz> +ERROR: no codec found for <tools/tests/skdiff/baseDir/identical-bits/identical-bits-unknown-format.xyz> +ERROR: no codec found for <tools/tests/skdiff/comparisonDir/identical-bits/identical-bits-unknown-format.xyz> +ERROR: no codec found for <tools/tests/skdiff/comparisonDir/missing-files/missing-from-baseDir.xyz> +ERROR: no codec found for <tools/tests/skdiff/baseDir/missing-files/missing-from-comparisonDir.xyz> baseDir is [tools/tests/skdiff/baseDir/] comparisonDir is [tools/tests/skdiff/comparisonDir/] writing diffs to outputDir is [tools/tests/skdiff/test1/output-actual/] @@ -12,9 +13,12 @@ compared 12 file pairs: [_] 1 file pairs contain the same pixel values, but not the same bits [_] 2 file pairs have identical dimensions but some differing pixels [_] 2 file pairs have differing dimensions -[_] 1 file pairs contain different bits and are not parsable images -[_] 2 file pairs missing from comparisonDir -[_] 2 file pairs missing from baseDir +[_] 5 file pairs could not be compared + [_] 1 file pairs decoded in baseDir and not found in comparisonDir + [_] 1 file pairs could not be decoded in baseDir and could not be decoded in comparisonDir + [_] 1 file pairs could not be decoded in baseDir and not found in comparisonDir + [_] 1 file pairs not found in baseDir and decoded in comparisonDir + [_] 1 file pairs not found in baseDir and could not be decoded in comparisonDir [_] 0 file pairs not compared yet (results marked with [*] will cause nonzero return value) diff --git a/tools/tests/skdiff/test2/output-expected/command_line b/tools/tests/skdiff/test2/output-expected/command_line index 97724dfbd6..2d8cc5716f 100644 --- a/tools/tests/skdiff/test2/output-expected/command_line +++ b/tools/tests/skdiff/test2/output-expected/command_line @@ -1 +1 @@ -out/Debug/skdiff --failonresult DifferentPixels --failonresult DifferentSizes --failonresult DifferentOther --failonresult Unknown --listfilenames --nodiffs tools/tests/skdiff/baseDir tools/tests/skdiff/comparisonDir tools/tests/skdiff/test2/output-actual +out/Debug/skdiff --failonresult DifferentPixels --failonresult DifferentSizes --failonresult Unknown --failonstatus CouldNotDecode,CouldNotRead any --failonstatus any CouldNotDecode,CouldNotRead --listfilenames --nodiffs tools/tests/skdiff/baseDir tools/tests/skdiff/comparisonDir tools/tests/skdiff/test2/output-actual diff --git a/tools/tests/skdiff/test2/output-expected/stdout b/tools/tests/skdiff/test2/output-expected/stdout index ef198e78cf..6aa33b8fd4 100644 --- a/tools/tests/skdiff/test2/output-expected/stdout +++ b/tools/tests/skdiff/test2/output-expected/stdout @@ -1,4 +1,5 @@ -ERROR: no codec found for basePath <tools/tests/skdiff/baseDir/different-bits/different-bits-unknown-format.xyz> +ERROR: no codec found for <tools/tests/skdiff/baseDir/different-bits/different-bits-unknown-format.xyz> +ERROR: no codec found for <tools/tests/skdiff/comparisonDir/different-bits/different-bits-unknown-format.xyz> baseDir is [tools/tests/skdiff/baseDir/] comparisonDir is [tools/tests/skdiff/comparisonDir/] not writing any diffs to outputDir [tools/tests/skdiff/test2/output-actual/] @@ -8,9 +9,10 @@ compared 12 file pairs: [_] 1 file pairs contain the same pixel values, but not the same bits: different-bits/different-bits-identical-pixels.png [*] 2 file pairs have identical dimensions but some differing pixels: different-bits/slightly-different-pixels-same-size.png different-bits/very-different-pixels-same-size.png [*] 2 file pairs have differing dimensions: different-bits/slightly-different-sizes.png different-bits/very-different-sizes.png -[*] 1 file pairs contain different bits and are not parsable images: different-bits/different-bits-unknown-format.xyz -[_] 2 file pairs missing from comparisonDir: missing-files/missing-from-comparisonDir.png missing-files/missing-from-comparisonDir.xyz -[_] 2 file pairs missing from baseDir: missing-files/missing-from-baseDir.png missing-files/missing-from-baseDir.xyz +[_] 5 file pairs could not be compared: different-bits/different-bits-unknown-format.xyz missing-files/missing-from-baseDir.png missing-files/missing-from-baseDir.xyz missing-files/missing-from-comparisonDir.png missing-files/missing-from-comparisonDir.xyz + [*] 1 file pairs could not be decoded in baseDir and could not be decoded in comparisonDir: different-bits/different-bits-unknown-format.xyz + [_] 2 file pairs found in baseDir and not found in comparisonDir: missing-files/missing-from-comparisonDir.png missing-files/missing-from-comparisonDir.xyz + [_] 2 file pairs not found in baseDir and found in comparisonDir: missing-files/missing-from-baseDir.png missing-files/missing-from-baseDir.xyz [*] 0 file pairs not compared yet: (results marked with [*] will cause nonzero return value) |