aboutsummaryrefslogtreecommitdiffhomepage
path: root/tools/skdiff
diff options
context:
space:
mode:
authorGravatar bungeman <bungeman@google.com>2016-10-13 17:36:40 -0400
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2016-10-14 14:02:42 +0000
commitfe9172705791848ebfe5b5630973b7e67305f64c (patch)
tree15c823ed4d95e16e20126e9529d0104ba7886fe0 /tools/skdiff
parentc434f5122a2394639ddeb66a1ded456565b043cd (diff)
Move skdiff tool and add to gn build.
I really wanted this today, so I got it working again. Change-Id: I1a37d48d4806198b55c59d1df5ff15a03500195f Reviewed-on: https://skia-review.googlesource.com/3383 Commit-Queue: Ben Wagner <bungeman@google.com> Reviewed-by: Mike Klein <mtklein@chromium.org>
Diffstat (limited to 'tools/skdiff')
-rw-r--r--tools/skdiff/skdiff.cpp228
-rw-r--r--tools/skdiff/skdiff.h272
-rw-r--r--tools/skdiff/skdiff_html.cpp313
-rw-r--r--tools/skdiff/skdiff_html.h21
-rw-r--r--tools/skdiff/skdiff_image.cpp374
-rw-r--r--tools/skdiff/skdiff_main.cpp862
-rw-r--r--tools/skdiff/skdiff_utils.cpp179
-rw-r--r--tools/skdiff/skdiff_utils.h52
8 files changed, 2301 insertions, 0 deletions
diff --git a/tools/skdiff/skdiff.cpp b/tools/skdiff/skdiff.cpp
new file mode 100644
index 0000000000..ae6d72cd7a
--- /dev/null
+++ b/tools/skdiff/skdiff.cpp
@@ -0,0 +1,228 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "skdiff.h"
+#include "SkBitmap.h"
+#include "SkColor.h"
+#include "SkColorPriv.h"
+#include "SkTypes.h"
+
+/*static*/ char const * const DiffRecord::ResultNames[DiffRecord::kResultCount] = {
+ "EqualBits",
+ "EqualPixels",
+ "DifferentPixels",
+ "DifferentSizes",
+ "CouldNotCompare",
+ "Unknown",
+};
+
+DiffRecord::Result DiffRecord::getResultByName(const char *name) {
+ for (int result = 0; result < DiffRecord::kResultCount; ++result) {
+ if (0 == strcmp(DiffRecord::ResultNames[result], name)) {
+ return static_cast<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 totalMismatchA = 0;
+ int totalMismatchR = 0;
+ int totalMismatchG = 0;
+ int totalMismatchB = 0;
+
+ // Accumulate fractionally different pixels, then divide out
+ // # of pixels at the end.
+ dr->fWeightedFraction = 0;
+ for (int y = 0; y < h; y++) {
+ for (int x = 0; x < w; x++) {
+ SkPMColor c0 = *dr->fBase.fBitmap.getAddr32(x, y);
+ SkPMColor c1 = *dr->fComparison.fBitmap.getAddr32(x, y);
+ SkPMColor outputDifference = diffFunction(c0, c1);
+ uint32_t thisA = SkAbs32(SkGetPackedA32(c0) - SkGetPackedA32(c1));
+ uint32_t thisR = SkAbs32(SkGetPackedR32(c0) - SkGetPackedR32(c1));
+ uint32_t thisG = SkAbs32(SkGetPackedG32(c0) - SkGetPackedG32(c1));
+ uint32_t thisB = SkAbs32(SkGetPackedB32(c0) - SkGetPackedB32(c1));
+ totalMismatchA += thisA;
+ totalMismatchR += thisR;
+ totalMismatchG += thisG;
+ totalMismatchB += thisB;
+ // In HSV, value is defined as max RGB component.
+ int value = MAX3(thisR, thisG, thisB);
+ dr->fWeightedFraction += ((float) value) / 255;
+ if (thisA > dr->fMaxMismatchA) {
+ dr->fMaxMismatchA = thisA;
+ }
+ if (thisR > dr->fMaxMismatchR) {
+ dr->fMaxMismatchR = thisR;
+ }
+ if (thisG > dr->fMaxMismatchG) {
+ dr->fMaxMismatchG = thisG;
+ }
+ if (thisB > dr->fMaxMismatchB) {
+ dr->fMaxMismatchB = thisB;
+ }
+ if (!colors_match_thresholded(c0, c1, colorThreshold)) {
+ mismatchedPixels++;
+ *dr->fDifference.fBitmap.getAddr32(x, y) = outputDifference;
+ *dr->fWhite.fBitmap.getAddr32(x, y) = PMCOLOR_WHITE;
+ } else {
+ *dr->fDifference.fBitmap.getAddr32(x, y) = 0;
+ *dr->fWhite.fBitmap.getAddr32(x, y) = PMCOLOR_BLACK;
+ }
+ }
+ }
+ if (0 == mismatchedPixels) {
+ dr->fResult = DiffRecord::kEqualPixels_Result;
+ return;
+ }
+ dr->fResult = DiffRecord::kDifferentPixels_Result;
+ int pixelCount = w * h;
+ dr->fFractionDifference = ((float) mismatchedPixels) / pixelCount;
+ dr->fWeightedFraction /= pixelCount;
+ dr->fTotalMismatchA = totalMismatchA;
+ dr->fAverageMismatchA = ((float) totalMismatchA) / pixelCount;
+ dr->fAverageMismatchR = ((float) totalMismatchR) / pixelCount;
+ dr->fAverageMismatchG = ((float) totalMismatchG) / pixelCount;
+ dr->fAverageMismatchB = ((float) totalMismatchB) / pixelCount;
+}
diff --git a/tools/skdiff/skdiff.h b/tools/skdiff/skdiff.h
new file mode 100644
index 0000000000..6bdaadc28d
--- /dev/null
+++ b/tools/skdiff/skdiff.h
@@ -0,0 +1,272 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef skdiff_DEFINED
+#define skdiff_DEFINED
+
+#include "SkBitmap.h"
+#include "SkColor.h"
+#include "SkColorPriv.h"
+#include "SkString.h"
+#include "../private/SkTDArray.h"
+
+#if defined(SK_BUILD_FOR_WIN32)
+ #define PATH_DIV_STR "\\"
+ #define PATH_DIV_CHAR '\\'
+#else
+ #define PATH_DIV_STR "/"
+ #define PATH_DIV_CHAR '/'
+#endif
+
+#define MAX2(a,b) (((b) < (a)) ? (a) : (b))
+#define MAX3(a,b,c) (((b) < (a)) ? MAX2((a), (c)) : MAX2((b), (c)))
+
+
+struct DiffResource {
+ enum Status {
+ /** The resource was specified, exists, read, and decoded. */
+ kDecoded_Status,
+ /** The resource was specified, exists, read, but could not be decoded. */
+ kCouldNotDecode_Status,
+
+ /** The resource was specified, exists, and read. */
+ kRead_Status,
+ /** The resource was specified, exists, but could not be read. */
+ kCouldNotRead_Status,
+
+ /** The resource was specified and exists. */
+ kExists_Status,
+ /** The resource was specified, but does not exist. */
+ kDoesNotExist_Status,
+
+ /** The resource was specified. */
+ kSpecified_Status,
+ /** The resource was not specified. */
+ kUnspecified_Status,
+
+ /** Nothing is yet known about the resource. */
+ kUnknown_Status,
+
+ /** NOT A VALID VALUE -- used to set up arrays and to represent an unknown value. */
+ kStatusCount
+ };
+ static char const * const StatusNames[DiffResource::kStatusCount];
+
+ /** Returns the Status with this name.
+ * If there is no Status with this name, returns kStatusCount.
+ */
+ static Status getStatusByName(const char *name);
+
+ /** Returns a text description of the given Status type. */
+ static const char *getStatusDescription(Status status);
+
+ /** Returns true if the Status indicates some kind of failure. */
+ static bool isStatusFailed(Status status);
+
+ /** Sets statuses[i] if it is implied by selector, unsets it if not.
+ * Selector may be a comma delimited list of status names, "any", or "failed".
+ * Returns true if the selector was entirely understood, false otherwise.
+ */
+ static bool getMatchingStatuses(char* selector, bool statuses[kStatusCount]);
+
+ DiffResource() : fFilename(), fFullPath(), fBitmap(), fStatus(kUnknown_Status) { }
+
+ /** If isEmpty() indicates no filename available. */
+ SkString fFilename;
+ /** If isEmpty() indicates no path available. */
+ SkString fFullPath;
+ /** If empty() indicates the bitmap could not be created. */
+ SkBitmap fBitmap;
+ Status fStatus;
+};
+
+struct DiffRecord {
+
+ // Result of comparison for each pair of files.
+ // Listed from "better" to "worse", for sorting of results.
+ enum Result {
+ kEqualBits_Result,
+ kEqualPixels_Result,
+ kDifferentPixels_Result,
+ kDifferentSizes_Result,
+ kCouldNotCompare_Result,
+ kUnknown_Result,
+
+ kResultCount // NOT A VALID VALUE--used to set up arrays. Must be last.
+ };
+ static char const * const ResultNames[DiffRecord::kResultCount];
+
+ /** Returns the Result with this name.
+ * If there is no Result with this name, returns kResultCount.
+ */
+ static Result getResultByName(const char *name);
+
+ /** Returns a text description of the given Result type. */
+ static const char *getResultDescription(Result result);
+
+ DiffRecord()
+ : fBase()
+ , fComparison()
+ , fDifference()
+ , fWhite()
+ , fFractionDifference(0)
+ , fWeightedFraction(0)
+ , fAverageMismatchA(0)
+ , fAverageMismatchR(0)
+ , fAverageMismatchG(0)
+ , fAverageMismatchB(0)
+ , fTotalMismatchA(0)
+ , fMaxMismatchA(0)
+ , fMaxMismatchR(0)
+ , fMaxMismatchG(0)
+ , fMaxMismatchB(0)
+ , fResult(kUnknown_Result) {
+ }
+
+ DiffResource fBase;
+ DiffResource fComparison;
+ DiffResource fDifference;
+ DiffResource fWhite;
+
+ /// Arbitrary floating-point metric to be used to sort images from most
+ /// to least different from baseline; values of 0 will be omitted from the
+ /// summary webpage.
+ float fFractionDifference;
+ float fWeightedFraction;
+
+ float fAverageMismatchA;
+ float fAverageMismatchR;
+ float fAverageMismatchG;
+ float fAverageMismatchB;
+
+ uint32_t fTotalMismatchA;
+
+ uint32_t fMaxMismatchA;
+ uint32_t fMaxMismatchR;
+ uint32_t fMaxMismatchG;
+ uint32_t fMaxMismatchB;
+
+ /// Which category of diff result.
+ Result fResult;
+};
+
+typedef SkTDArray<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/skdiff_html.cpp b/tools/skdiff/skdiff_html.cpp
new file mode 100644
index 0000000000..6f3c3b09e1
--- /dev/null
+++ b/tools/skdiff/skdiff_html.cpp
@@ -0,0 +1,313 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "skdiff.h"
+#include "skdiff_html.h"
+#include "SkStream.h"
+#include "SkTime.h"
+
+/// Make layout more consistent by scaling image to 240 height, 360 width,
+/// or natural size, whichever is smallest.
+static int compute_image_height(int height, int width) {
+ int retval = 240;
+ if (height < retval) {
+ retval = height;
+ }
+ float scale = (float) retval / height;
+ if (width * scale > 360) {
+ scale = (float) 360 / width;
+ retval = static_cast<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>");
+ if (SkScalarRoundToInt(diff.fAverageMismatchA) > 0) {
+ stream->writeText("<br>Average alpha channel mismatch ");
+ stream->writeDecAsText(SkScalarRoundToInt(diff.fAverageMismatchA));
+ }
+
+ stream->writeText("<br>Max alpha channel mismatch ");
+ stream->writeDecAsText(SkScalarRoundToInt(diff.fMaxMismatchA));
+
+ stream->writeText("<br>Total alpha channel mismatch ");
+ stream->writeDecAsText(static_cast<int>(diff.fTotalMismatchA));
+
+ stream->writeText("<br>");
+ stream->writeText("<br>Average color mismatch ");
+ stream->writeDecAsText(SkScalarRoundToInt(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 + '&nbsp;';\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/skdiff_html.h b/tools/skdiff/skdiff_html.h
new file mode 100644
index 0000000000..eefbebf2fd
--- /dev/null
+++ b/tools/skdiff/skdiff_html.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef skdiff_html_DEFINED
+#define skdiff_html_DEFINED
+
+#include "skdiff.h"
+class SkString;
+
+void print_diff_page(const int matchCount,
+ const int colorThreshold,
+ const RecordArray& differences,
+ const SkString& baseDir,
+ const SkString& comparisonDir,
+ const SkString& outputDir);
+
+#endif
diff --git a/tools/skdiff/skdiff_image.cpp b/tools/skdiff/skdiff_image.cpp
new file mode 100644
index 0000000000..287523de15
--- /dev/null
+++ b/tools/skdiff/skdiff_image.cpp
@@ -0,0 +1,374 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#include "skdiff.h"
+#include "skdiff_utils.h"
+#include "SkBitmap.h"
+#include "SkData.h"
+#include "SkImageEncoder.h"
+#include "SkOSFile.h"
+#include "SkTypes.h"
+
+#include <stdio.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;
+
+ sk_sp<SkData> baseFileBits = read_file(drp->fBase.fFullPath.c_str());
+ if (baseFileBits) {
+ drp->fBase.fStatus = DiffResource::kRead_Status;
+ }
+ sk_sp<SkData> comparisonFileBits = read_file(drp->fComparison.fFullPath.c_str());
+ if (comparisonFileBits) {
+ drp->fComparison.fStatus = DiffResource::kRead_Status;
+ }
+ if (nullptr == baseFileBits || nullptr == comparisonFileBits) {
+ if (nullptr == baseFileBits) {
+ drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
+ }
+ if (nullptr == comparisonFileBits) {
+ drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
+ }
+ drp->fResult = DiffRecord::kCouldNotCompare_Result;
+ return;
+ }
+
+ if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) {
+ drp->fResult = DiffRecord::kEqualBits_Result;
+ return;
+ }
+
+ get_bitmap(baseFileBits, drp->fBase, false);
+ get_bitmap(comparisonFileBits, drp->fComparison, false);
+ if (DiffResource::kDecoded_Status != drp->fBase.fStatus ||
+ DiffResource::kDecoded_Status != drp->fComparison.fStatus)
+ {
+ drp->fResult = DiffRecord::kCouldNotCompare_Result;
+ return;
+ }
+
+ create_and_write_diff_image(drp, dmp, colorThreshold, outputDir, outputFilename);
+ //TODO: copy fBase.fFilename and fComparison.fFilename to outputDir
+ // svn and git often present tmp files to diff tools which are promptly deleted
+
+ //TODO: serialize drp to outputDir
+ // write a tool to deserialize them and call print_diff_page
+
+ SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
+}
+
+static void usage (char * argv0) {
+ SkDebugf("Skia image diff tool\n");
+ SkDebugf("\n"
+"Usage: \n"
+" %s <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 (nullptr == tab) {
+ outputFile = baseLabel;
+ } else {
+ outputFile.set(baseLabelCstr, tab - baseLabelCstr);
+ }
+ }
+ if (comparisonLabel.isEmpty()) {
+ comparisonLabel.set(comparisonFile);
+ }
+ printf("Base: %s\n", baseLabel.c_str());
+ printf("Comparison: %s\n", comparisonLabel.c_str());
+
+ DiffRecord dr;
+ create_diff_images(diffProc, colorThreshold, baseFile, comparisonFile, outputDir, outputFile,
+ &dr);
+
+ if (DiffResource::isStatusFailed(dr.fBase.fStatus)) {
+ printf("Base %s.\n", DiffResource::getStatusDescription(dr.fBase.fStatus));
+ }
+ if (DiffResource::isStatusFailed(dr.fComparison.fStatus)) {
+ printf("Comparison %s.\n", DiffResource::getStatusDescription(dr.fComparison.fStatus));
+ }
+ printf("Base and Comparison %s.\n", DiffRecord::getResultDescription(dr.fResult));
+
+ if (DiffRecord::kDifferentPixels_Result == dr.fResult) {
+ printf("%.4f%% of pixels differ", 100 * dr.fFractionDifference);
+ printf(" (%.4f%% weighted)", 100 * dr.fWeightedFraction);
+ if (dr.fFractionDifference < 0.01) {
+ printf(" %d pixels", static_cast<int>(dr.fFractionDifference *
+ dr.fBase.fBitmap.width() *
+ dr.fBase.fBitmap.height()));
+ }
+
+ printf("\nAverage color mismatch: ");
+ printf("%d", static_cast<int>(MAX3(dr.fAverageMismatchR,
+ dr.fAverageMismatchG,
+ dr.fAverageMismatchB)));
+ printf("\nMax color mismatch: ");
+ printf("%d", MAX3(dr.fMaxMismatchR,
+ dr.fMaxMismatchG,
+ dr.fMaxMismatchB));
+ printf("\n");
+ }
+ printf("\n");
+
+ int num_failing_results = 0;
+ if (failOnResultType[dr.fResult]) {
+ ++num_failing_results;
+ }
+ if (failOnStatusType[dr.fBase.fStatus][dr.fComparison.fStatus]) {
+ ++num_failing_results;
+ }
+
+ return num_failing_results;
+}
+
+#if !defined SK_BUILD_FOR_IOS
+int main(int argc, char * const argv[]) {
+ return tool_main(argc, (char**) argv);
+}
+#endif
diff --git a/tools/skdiff/skdiff_main.cpp b/tools/skdiff/skdiff_main.cpp
new file mode 100644
index 0000000000..c51cd28d78
--- /dev/null
+++ b/tools/skdiff/skdiff_main.cpp
@@ -0,0 +1,862 @@
+/*
+ * Copyright 2011 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#include "skdiff.h"
+#include "skdiff_html.h"
+#include "skdiff_utils.h"
+#include "SkBitmap.h"
+#include "SkData.h"
+#include "SkForceLinking.h"
+#include "SkImageEncoder.h"
+#include "SkOSFile.h"
+#include "SkStream.h"
+#include "../private/SkTDArray.h"
+#include "../private/SkTSearch.h"
+
+#include <stdlib.h>
+
+__SK_FORCE_IMAGE_DECODER_LINKING;
+
+/**
+ * skdiff
+ *
+ * Given three directory names, expects to find identically-named files in
+ * each of the first two; the first are treated as a set of baseline,
+ * the second a set of variant images, and a diff image is written into the
+ * third directory for each pair.
+ * Creates an index.html in the current third directory to compare each
+ * pair that does not match exactly.
+ * Recursively descends directories, unless run with --norecurse.
+ *
+ * Returns zero exit code if all images match across baseDir and comparisonDir.
+ */
+
+typedef SkTDArray<SkString*> StringArray;
+typedef StringArray FileArray;
+
+static void add_unique_basename(StringArray* array, const SkString& filename) {
+ // trim off dirs
+ const char* src = filename.c_str();
+ const char* trimmed = strrchr(src, SkPATH_SEPARATOR);
+ if (trimmed) {
+ trimmed += 1; // skip the separator
+ } else {
+ trimmed = src;
+ }
+ const char* end = strrchr(trimmed, '.');
+ if (!end) {
+ end = trimmed + strlen(trimmed);
+ }
+ SkString result(trimmed, end - trimmed);
+
+ // only add unique entries
+ for (int i = 0; i < array->count(); ++i) {
+ if (*array->getAt(i) == result) {
+ return;
+ }
+ }
+ *array->append() = new SkString(result);
+}
+
+struct DiffSummary {
+ DiffSummary ()
+ : fNumMatches(0)
+ , fNumMismatches(0)
+ , fMaxMismatchV(0)
+ , fMaxMismatchPercent(0) { }
+
+ ~DiffSummary() {
+ for (int i = 0; i < DiffRecord::kResultCount; ++i) {
+ fResultsOfType[i].deleteAll();
+ }
+ for (int base = 0; base < DiffResource::kStatusCount; ++base) {
+ for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
+ fStatusOfType[base][comparison].deleteAll();
+ }
+ }
+ }
+
+ uint32_t fNumMatches;
+ uint32_t fNumMismatches;
+ uint32_t fMaxMismatchV;
+ float fMaxMismatchPercent;
+
+ FileArray fResultsOfType[DiffRecord::kResultCount];
+ FileArray fStatusOfType[DiffResource::kStatusCount][DiffResource::kStatusCount];
+
+ StringArray fFailedBaseNames[DiffRecord::kResultCount];
+
+ void printContents(const FileArray& fileArray,
+ const char* baseStatus, const char* comparisonStatus,
+ bool listFilenames) {
+ int n = fileArray.count();
+ printf("%d file pairs %s in baseDir and %s in comparisonDir",
+ n, baseStatus, comparisonStatus);
+ if (listFilenames) {
+ printf(": ");
+ for (int i = 0; i < n; ++i) {
+ printf("%s ", fileArray[i]->c_str());
+ }
+ }
+ printf("\n");
+ }
+
+ void printStatus(bool listFilenames,
+ bool failOnStatusType[DiffResource::kStatusCount]
+ [DiffResource::kStatusCount]) {
+ typedef DiffResource::Status Status;
+
+ for (int base = 0; base < DiffResource::kStatusCount; ++base) {
+ Status baseStatus = static_cast<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) {
+ int n = fileArray.count();
+ printf("%d file pairs %s", n, headerText);
+ if (listFilenames) {
+ printf(": ");
+ for (int i = 0; i < n; ++i) {
+ printf("%s ", fileArray[i]->c_str());
+ }
+ }
+ printf("\n");
+ }
+
+ void print(bool listFilenames, bool failOnResultType[DiffRecord::kResultCount],
+ bool failOnStatusType[DiffResource::kStatusCount]
+ [DiffResource::kStatusCount]) {
+ printf("\ncompared %d file pairs:\n", fNumMatches + fNumMismatches);
+ for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) {
+ DiffRecord::Result result = static_cast<DiffRecord::Result>(resultInt);
+ if (failOnResultType[result]) {
+ printf("[*] ");
+ } else {
+ printf("[_] ");
+ }
+ printContents(fResultsOfType[result], DiffRecord::getResultDescription(result),
+ listFilenames);
+ if (DiffRecord::kCouldNotCompare_Result == result) {
+ printStatus(listFilenames, failOnStatusType);
+ }
+ }
+ printf("(results marked with [*] will cause nonzero return value)\n");
+ printf("\nnumber of mismatching file pairs: %d\n", fNumMismatches);
+ if (fNumMismatches > 0) {
+ printf("Maximum pixel intensity mismatch %d\n", fMaxMismatchV);
+ printf("Largest area mismatch was %.2f%% of pixels\n",fMaxMismatchPercent);
+ }
+ }
+
+ void printfFailingBaseNames(const char separator[]) {
+ for (int resultInt = 0; resultInt < DiffRecord::kResultCount; ++resultInt) {
+ const StringArray& array = fFailedBaseNames[resultInt];
+ if (array.count()) {
+ printf("%s [%d]%s", DiffRecord::ResultNames[resultInt], array.count(), separator);
+ for (int j = 0; j < array.count(); ++j) {
+ printf("%s%s", array[j]->c_str(), separator);
+ }
+ printf("\n");
+ }
+ }
+ }
+
+ void add (DiffRecord* drp) {
+ uint32_t mismatchValue;
+
+ if (drp->fBase.fFilename.equals(drp->fComparison.fFilename)) {
+ fResultsOfType[drp->fResult].push(new SkString(drp->fBase.fFilename));
+ } else {
+ SkString* blame = new SkString("(");
+ blame->append(drp->fBase.fFilename);
+ blame->append(", ");
+ blame->append(drp->fComparison.fFilename);
+ blame->append(")");
+ fResultsOfType[drp->fResult].push(blame);
+ }
+ switch (drp->fResult) {
+ case DiffRecord::kEqualBits_Result:
+ fNumMatches++;
+ break;
+ case DiffRecord::kEqualPixels_Result:
+ fNumMatches++;
+ break;
+ case DiffRecord::kDifferentSizes_Result:
+ fNumMismatches++;
+ break;
+ case DiffRecord::kDifferentPixels_Result:
+ fNumMismatches++;
+ if (drp->fFractionDifference * 100 > fMaxMismatchPercent) {
+ fMaxMismatchPercent = drp->fFractionDifference * 100;
+ }
+ mismatchValue = MAX3(drp->fMaxMismatchR, drp->fMaxMismatchG,
+ drp->fMaxMismatchB);
+ if (mismatchValue > fMaxMismatchV) {
+ fMaxMismatchV = mismatchValue;
+ }
+ break;
+ case DiffRecord::kCouldNotCompare_Result:
+ fNumMismatches++;
+ fStatusOfType[drp->fBase.fStatus][drp->fComparison.fStatus].push(
+ new SkString(drp->fBase.fFilename));
+ break;
+ case DiffRecord::kUnknown_Result:
+ SkDEBUGFAIL("adding uncategorized DiffRecord");
+ break;
+ default:
+ SkDEBUGFAIL("adding DiffRecord with unhandled fResult value");
+ break;
+ }
+
+ switch (drp->fResult) {
+ case DiffRecord::kEqualBits_Result:
+ case DiffRecord::kEqualPixels_Result:
+ break;
+ default:
+ add_unique_basename(&fFailedBaseNames[drp->fResult], drp->fBase.fFilename);
+ break;
+ }
+ }
+};
+
+/// Returns true if string contains any of these substrings.
+static bool string_contains_any_of(const SkString& string,
+ const StringArray& substrings) {
+ for (int i = 0; i < substrings.count(); i++) {
+ if (string.contains(substrings[i]->c_str())) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/// Internal (potentially recursive) implementation of get_file_list.
+static void get_file_list_subdir(const SkString& rootDir, const SkString& subDir,
+ const StringArray& matchSubstrings,
+ const StringArray& nomatchSubstrings,
+ bool recurseIntoSubdirs, FileArray *files) {
+ bool isSubDirEmpty = subDir.isEmpty();
+ SkString dir(rootDir);
+ if (!isSubDirEmpty) {
+ dir.append(PATH_DIV_STR);
+ dir.append(subDir);
+ }
+
+ // Iterate over files (not directories) within dir.
+ SkOSFile::Iter fileIterator(dir.c_str());
+ SkString fileName;
+ while (fileIterator.next(&fileName, false)) {
+ if (fileName.startsWith(".")) {
+ continue;
+ }
+ SkString pathRelativeToRootDir(subDir);
+ if (!isSubDirEmpty) {
+ pathRelativeToRootDir.append(PATH_DIV_STR);
+ }
+ pathRelativeToRootDir.append(fileName);
+ if (string_contains_any_of(pathRelativeToRootDir, matchSubstrings) &&
+ !string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
+ files->push(new SkString(pathRelativeToRootDir));
+ }
+ }
+
+ // Recurse into any non-ignored subdirectories.
+ if (recurseIntoSubdirs) {
+ SkOSFile::Iter dirIterator(dir.c_str());
+ SkString dirName;
+ while (dirIterator.next(&dirName, true)) {
+ if (dirName.startsWith(".")) {
+ continue;
+ }
+ SkString pathRelativeToRootDir(subDir);
+ if (!isSubDirEmpty) {
+ pathRelativeToRootDir.append(PATH_DIV_STR);
+ }
+ pathRelativeToRootDir.append(dirName);
+ if (!string_contains_any_of(pathRelativeToRootDir, nomatchSubstrings)) {
+ get_file_list_subdir(rootDir, pathRelativeToRootDir,
+ matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
+ files);
+ }
+ }
+ }
+}
+
+/// Iterate over dir and get all files whose filename:
+/// - matches any of the substrings in matchSubstrings, but...
+/// - DOES NOT match any of the substrings in nomatchSubstrings
+/// - DOES NOT start with a dot (.)
+/// Adds the matching files to the list in *files.
+static void get_file_list(const SkString& dir,
+ const StringArray& matchSubstrings,
+ const StringArray& nomatchSubstrings,
+ bool recurseIntoSubdirs, FileArray *files) {
+ get_file_list_subdir(dir, SkString(""),
+ matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
+ files);
+}
+
+static void release_file_list(FileArray *files) {
+ files->deleteAll();
+}
+
+/// Comparison routines for qsort, sort by file names.
+static int compare_file_name_metrics(SkString **lhs, SkString **rhs) {
+ return strcmp((*lhs)->c_str(), (*rhs)->c_str());
+}
+
+class AutoReleasePixels {
+public:
+ AutoReleasePixels(DiffRecord* drp)
+ : fDrp(drp) {
+ SkASSERT(drp != nullptr);
+ }
+ ~AutoReleasePixels() {
+ fDrp->fBase.fBitmap.setPixelRef(nullptr);
+ fDrp->fComparison.fBitmap.setPixelRef(nullptr);
+ fDrp->fDifference.fBitmap.setPixelRef(nullptr);
+ fDrp->fWhite.fBitmap.setPixelRef(nullptr);
+ }
+
+private:
+ DiffRecord* fDrp;
+};
+
+static void get_bounds(DiffResource& resource, const char* name) {
+ if (resource.fBitmap.empty() && !DiffResource::isStatusFailed(resource.fStatus)) {
+ sk_sp<SkData> fileBits(read_file(resource.fFullPath.c_str()));
+ if (fileBits) {
+ get_bitmap(fileBits, resource, true);
+ } else {
+ SkDebugf("WARNING: couldn't read %s file <%s>\n", name, resource.fFullPath.c_str());
+ resource.fStatus = DiffResource::kCouldNotRead_Status;
+ }
+ }
+}
+
+static void get_bounds(DiffRecord& drp) {
+ get_bounds(drp.fBase, "base");
+ get_bounds(drp.fComparison, "comparison");
+}
+
+#ifdef SK_OS_WIN
+#define ANSI_COLOR_RED ""
+#define ANSI_COLOR_GREEN ""
+#define ANSI_COLOR_YELLOW ""
+#define ANSI_COLOR_RESET ""
+#else
+#define ANSI_COLOR_RED "\x1b[31m"
+#define ANSI_COLOR_GREEN "\x1b[32m"
+#define ANSI_COLOR_YELLOW "\x1b[33m"
+#define ANSI_COLOR_RESET "\x1b[0m"
+#endif
+
+#define VERBOSE_STATUS(status,color,filename) if (verbose) printf( "[ " color " %10s " ANSI_COLOR_RESET " ] %s\n", status, filename->c_str())
+
+/// Creates difference images, returns the number that have a 0 metric.
+/// If outputDir.isEmpty(), don't write out diff files.
+static void create_diff_images (DiffMetricProc dmp,
+ const int colorThreshold,
+ RecordArray* differences,
+ const SkString& baseDir,
+ const SkString& comparisonDir,
+ const SkString& outputDir,
+ const StringArray& matchSubstrings,
+ const StringArray& nomatchSubstrings,
+ bool recurseIntoSubdirs,
+ bool getBounds,
+ bool verbose,
+ DiffSummary* summary) {
+ SkASSERT(!baseDir.isEmpty());
+ SkASSERT(!comparisonDir.isEmpty());
+
+ FileArray baseFiles;
+ FileArray comparisonFiles;
+
+ get_file_list(baseDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, &baseFiles);
+ get_file_list(comparisonDir, matchSubstrings, nomatchSubstrings, recurseIntoSubdirs,
+ &comparisonFiles);
+
+ if (!baseFiles.isEmpty()) {
+ qsort(baseFiles.begin(), baseFiles.count(), sizeof(SkString*),
+ SkCastForQSort(compare_file_name_metrics));
+ }
+ if (!comparisonFiles.isEmpty()) {
+ qsort(comparisonFiles.begin(), comparisonFiles.count(),
+ sizeof(SkString*), SkCastForQSort(compare_file_name_metrics));
+ }
+
+ if (!outputDir.isEmpty()) {
+ sk_mkdir(outputDir.c_str());
+ }
+
+ int i = 0;
+ int j = 0;
+
+ while (i < baseFiles.count() &&
+ j < comparisonFiles.count()) {
+
+ SkString basePath(baseDir);
+ SkString comparisonPath(comparisonDir);
+
+ DiffRecord *drp = new DiffRecord;
+ int v = strcmp(baseFiles[i]->c_str(), comparisonFiles[j]->c_str());
+
+ if (v < 0) {
+ // in baseDir, but not in comparisonDir
+ drp->fResult = DiffRecord::kCouldNotCompare_Result;
+
+ basePath.append(*baseFiles[i]);
+ comparisonPath.append(*baseFiles[i]);
+
+ drp->fBase.fFilename = *baseFiles[i];
+ drp->fBase.fFullPath = basePath;
+ drp->fBase.fStatus = DiffResource::kExists_Status;
+
+ drp->fComparison.fFilename = *baseFiles[i];
+ drp->fComparison.fFullPath = comparisonPath;
+ drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
+
+ VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, baseFiles[i]);
+
+ ++i;
+ } else if (v > 0) {
+ // in comparisonDir, but not in baseDir
+ drp->fResult = DiffRecord::kCouldNotCompare_Result;
+
+ basePath.append(*comparisonFiles[j]);
+ comparisonPath.append(*comparisonFiles[j]);
+
+ drp->fBase.fFilename = *comparisonFiles[j];
+ drp->fBase.fFullPath = basePath;
+ drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
+
+ drp->fComparison.fFilename = *comparisonFiles[j];
+ drp->fComparison.fFullPath = comparisonPath;
+ drp->fComparison.fStatus = DiffResource::kExists_Status;
+
+ VERBOSE_STATUS("MISSING", ANSI_COLOR_YELLOW, comparisonFiles[j]);
+
+ ++j;
+ } else {
+ // Found the same filename in both baseDir and comparisonDir.
+ SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
+
+ basePath.append(*baseFiles[i]);
+ comparisonPath.append(*comparisonFiles[j]);
+
+ drp->fBase.fFilename = *baseFiles[i];
+ drp->fBase.fFullPath = basePath;
+ drp->fBase.fStatus = DiffResource::kExists_Status;
+
+ drp->fComparison.fFilename = *comparisonFiles[j];
+ drp->fComparison.fFullPath = comparisonPath;
+ drp->fComparison.fStatus = DiffResource::kExists_Status;
+
+ sk_sp<SkData> baseFileBits(read_file(drp->fBase.fFullPath.c_str()));
+ if (baseFileBits) {
+ drp->fBase.fStatus = DiffResource::kRead_Status;
+ }
+ sk_sp<SkData> comparisonFileBits(read_file(drp->fComparison.fFullPath.c_str()));
+ if (comparisonFileBits) {
+ drp->fComparison.fStatus = DiffResource::kRead_Status;
+ }
+ if (nullptr == baseFileBits || nullptr == comparisonFileBits) {
+ if (nullptr == baseFileBits) {
+ drp->fBase.fStatus = DiffResource::kCouldNotRead_Status;
+ VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, baseFiles[i]);
+ }
+ if (nullptr == comparisonFileBits) {
+ drp->fComparison.fStatus = DiffResource::kCouldNotRead_Status;
+ VERBOSE_STATUS("READ FAIL", ANSI_COLOR_RED, comparisonFiles[j]);
+ }
+ drp->fResult = DiffRecord::kCouldNotCompare_Result;
+
+ } else if (are_buffers_equal(baseFileBits.get(), comparisonFileBits.get())) {
+ drp->fResult = DiffRecord::kEqualBits_Result;
+ VERBOSE_STATUS("MATCH", ANSI_COLOR_GREEN, baseFiles[i]);
+ } else {
+ AutoReleasePixels arp(drp);
+ get_bitmap(baseFileBits, drp->fBase, false);
+ get_bitmap(comparisonFileBits, drp->fComparison, false);
+ VERBOSE_STATUS("DIFFERENT", ANSI_COLOR_RED, baseFiles[i]);
+ if (DiffResource::kDecoded_Status == drp->fBase.fStatus &&
+ DiffResource::kDecoded_Status == drp->fComparison.fStatus) {
+ create_and_write_diff_image(drp, dmp, colorThreshold,
+ outputDir, drp->fBase.fFilename);
+ } else {
+ drp->fResult = DiffRecord::kCouldNotCompare_Result;
+ }
+ }
+
+ ++i;
+ ++j;
+ }
+
+ if (getBounds) {
+ get_bounds(*drp);
+ }
+ SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
+ differences->push(drp);
+ summary->add(drp);
+ }
+
+ for (; i < baseFiles.count(); ++i) {
+ // files only in baseDir
+ DiffRecord *drp = new DiffRecord();
+ drp->fBase.fFilename = *baseFiles[i];
+ drp->fBase.fFullPath = baseDir;
+ drp->fBase.fFullPath.append(drp->fBase.fFilename);
+ drp->fBase.fStatus = DiffResource::kExists_Status;
+
+ drp->fComparison.fFilename = *baseFiles[i];
+ drp->fComparison.fFullPath = comparisonDir;
+ drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
+ drp->fComparison.fStatus = DiffResource::kDoesNotExist_Status;
+
+ drp->fResult = DiffRecord::kCouldNotCompare_Result;
+ if (getBounds) {
+ get_bounds(*drp);
+ }
+ differences->push(drp);
+ summary->add(drp);
+ }
+
+ for (; j < comparisonFiles.count(); ++j) {
+ // files only in comparisonDir
+ DiffRecord *drp = new DiffRecord();
+ drp->fBase.fFilename = *comparisonFiles[j];
+ drp->fBase.fFullPath = baseDir;
+ drp->fBase.fFullPath.append(drp->fBase.fFilename);
+ drp->fBase.fStatus = DiffResource::kDoesNotExist_Status;
+
+ drp->fComparison.fFilename = *comparisonFiles[j];
+ drp->fComparison.fFullPath = comparisonDir;
+ drp->fComparison.fFullPath.append(drp->fComparison.fFilename);
+ drp->fComparison.fStatus = DiffResource::kExists_Status;
+
+ drp->fResult = DiffRecord::kCouldNotCompare_Result;
+ if (getBounds) {
+ get_bounds(*drp);
+ }
+ differences->push(drp);
+ summary->add(drp);
+ }
+
+ release_file_list(&baseFiles);
+ release_file_list(&comparisonFiles);
+}
+
+static void usage (char * argv0) {
+ SkDebugf("Skia baseline image diff tool\n");
+ SkDebugf("\n"
+"Usage: \n"
+" %s <baseDir> <comparisonDir> [outputDir] \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 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;"
+"\n if unspecified, compare ALL files."
+"\n this flag may be repeated."
+"\n --nodiffs: don't write out image diffs or index.html, just generate"
+"\n report on stdout"
+"\n --nomatch <substring>: regardless of --match, DO NOT compare files whose"
+"\n filenames contain this substring."
+"\n this flag may be repeated."
+"\n --noprintdirs: do not print the directories used."
+"\n --norecurse: do not recurse into subdirectories."
+"\n --sortbymaxmismatch: sort by worst color channel mismatch;"
+"\n break ties with -sortbymismatch"
+"\n --sortbymismatch: sort by average color channel mismatch"
+"\n --threshold <n>: only report differences > n (per color channel) [default 0]"
+"\n --weighted: sort by # pixels different weighted by color difference"
+"\n"
+"\n baseDir: directory to read baseline images from."
+"\n comparisonDir: directory to read comparison images from"
+"\n outputDir: directory to write difference images and index.html to;"
+"\n defaults to comparisonDir"
+"\n"
+"\nIf no sort is specified, it will sort by fraction of pixels mismatching."
+"\n");
+}
+
+const int kNoError = 0;
+const int kGenericError = -1;
+
+int tool_main(int argc, char** argv);
+int tool_main(int argc, char** argv) {
+ DiffMetricProc diffProc = compute_diff_pmcolor;
+ int (*sortProc)(const void*, const void*) = compare<CompareDiffMetrics>;
+
+ // Maximum error tolerated in any one color channel in any one pixel before
+ // a difference is reported.
+ int colorThreshold = 0;
+ SkString baseDir;
+ SkString comparisonDir;
+ SkString outputDir;
+
+ StringArray matchSubstrings;
+ StringArray nomatchSubstrings;
+
+ bool generateDiffs = true;
+ bool listFilenames = false;
+ bool printDirNames = true;
+ bool recurseIntoSubdirs = true;
+ bool verbose = false;
+ bool listFailingBase = false;
+
+ RecordArray differences;
+ DiffSummary summary;
+
+ bool failOnResultType[DiffRecord::kResultCount];
+ for (int i = 0; i < DiffRecord::kResultCount; i++) {
+ failOnResultType[i] = false;
+ }
+
+ bool failOnStatusType[DiffResource::kStatusCount][DiffResource::kStatusCount];
+ for (int base = 0; base < DiffResource::kStatusCount; ++base) {
+ for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
+ failOnStatusType[base][comparison] = false;
+ }
+ }
+
+ int i;
+ int numUnflaggedArguments = 0;
+ for (i = 1; i < argc; i++) {
+ if (!strcmp(argv[i], "--failonresult")) {
+ if (argc == ++i) {
+ SkDebugf("failonresult expects one argument.\n");
+ continue;
+ }
+ DiffRecord::Result type = DiffRecord::getResultByName(argv[i]);
+ if (type != DiffRecord::kResultCount) {
+ failOnResultType[type] = true;
+ } else {
+ SkDebugf("ignoring unrecognized result <%s>\n", argv[i]);
+ }
+ continue;
+ }
+ if (!strcmp(argv[i], "--failonstatus")) {
+ if (argc == ++i) {
+ SkDebugf("failonstatus missing base status.\n");
+ continue;
+ }
+ bool baseStatuses[DiffResource::kStatusCount];
+ if (!DiffResource::getMatchingStatuses(argv[i], baseStatuses)) {
+ SkDebugf("unrecognized base status <%s>\n", argv[i]);
+ }
+
+ if (argc == ++i) {
+ SkDebugf("failonstatus missing comparison status.\n");
+ continue;
+ }
+ bool comparisonStatuses[DiffResource::kStatusCount];
+ if (!DiffResource::getMatchingStatuses(argv[i], comparisonStatuses)) {
+ SkDebugf("unrecognized comarison status <%s>\n", argv[i]);
+ }
+
+ for (int base = 0; base < DiffResource::kStatusCount; ++base) {
+ for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
+ failOnStatusType[base][comparison] |=
+ baseStatuses[base] && comparisonStatuses[comparison];
+ }
+ }
+ continue;
+ }
+ if (!strcmp(argv[i], "--help")) {
+ usage(argv[0]);
+ return kNoError;
+ }
+ if (!strcmp(argv[i], "--listfilenames")) {
+ listFilenames = true;
+ continue;
+ }
+ if (!strcmp(argv[i], "--verbose")) {
+ verbose = true;
+ continue;
+ }
+ if (!strcmp(argv[i], "--match")) {
+ matchSubstrings.push(new SkString(argv[++i]));
+ continue;
+ }
+ if (!strcmp(argv[i], "--nodiffs")) {
+ generateDiffs = false;
+ continue;
+ }
+ if (!strcmp(argv[i], "--nomatch")) {
+ nomatchSubstrings.push(new SkString(argv[++i]));
+ continue;
+ }
+ if (!strcmp(argv[i], "--noprintdirs")) {
+ printDirNames = false;
+ continue;
+ }
+ if (!strcmp(argv[i], "--norecurse")) {
+ recurseIntoSubdirs = false;
+ continue;
+ }
+ if (!strcmp(argv[i], "--sortbymaxmismatch")) {
+ sortProc = compare<CompareDiffMaxMismatches>;
+ continue;
+ }
+ if (!strcmp(argv[i], "--sortbymismatch")) {
+ sortProc = compare<CompareDiffMeanMismatches>;
+ continue;
+ }
+ if (!strcmp(argv[i], "--threshold")) {
+ colorThreshold = atoi(argv[++i]);
+ continue;
+ }
+ if (!strcmp(argv[i], "--weighted")) {
+ sortProc = compare<CompareDiffWeighted>;
+ continue;
+ }
+ if (argv[i][0] != '-') {
+ switch (numUnflaggedArguments++) {
+ case 0:
+ baseDir.set(argv[i]);
+ continue;
+ case 1:
+ comparisonDir.set(argv[i]);
+ continue;
+ case 2:
+ outputDir.set(argv[i]);
+ continue;
+ default:
+ SkDebugf("extra unflagged argument <%s>\n", argv[i]);
+ usage(argv[0]);
+ return kGenericError;
+ }
+ }
+ if (!strcmp(argv[i], "--listFailingBase")) {
+ listFailingBase = true;
+ continue;
+ }
+
+ SkDebugf("Unrecognized argument <%s>\n", argv[i]);
+ usage(argv[0]);
+ return kGenericError;
+ }
+
+ if (numUnflaggedArguments == 2) {
+ outputDir = comparisonDir;
+ } else if (numUnflaggedArguments != 3) {
+ usage(argv[0]);
+ return kGenericError;
+ }
+
+ if (!baseDir.endsWith(PATH_DIV_STR)) {
+ baseDir.append(PATH_DIV_STR);
+ }
+ if (printDirNames) {
+ printf("baseDir is [%s]\n", baseDir.c_str());
+ }
+
+ if (!comparisonDir.endsWith(PATH_DIV_STR)) {
+ comparisonDir.append(PATH_DIV_STR);
+ }
+ if (printDirNames) {
+ printf("comparisonDir is [%s]\n", comparisonDir.c_str());
+ }
+
+ if (!outputDir.endsWith(PATH_DIV_STR)) {
+ outputDir.append(PATH_DIV_STR);
+ }
+ if (generateDiffs) {
+ if (printDirNames) {
+ printf("writing diffs to outputDir is [%s]\n", outputDir.c_str());
+ }
+ } else {
+ if (printDirNames) {
+ printf("not writing any diffs to outputDir [%s]\n", outputDir.c_str());
+ }
+ outputDir.set("");
+ }
+
+ // If no matchSubstrings were specified, match ALL strings
+ // (except for whatever nomatchSubstrings were specified, if any).
+ if (matchSubstrings.isEmpty()) {
+ matchSubstrings.push(new SkString(""));
+ }
+
+ create_diff_images(diffProc, colorThreshold, &differences,
+ baseDir, comparisonDir, outputDir,
+ matchSubstrings, nomatchSubstrings, recurseIntoSubdirs, generateDiffs,
+ verbose, &summary);
+ summary.print(listFilenames, failOnResultType, failOnStatusType);
+
+ if (listFailingBase) {
+ summary.printfFailingBaseNames("\n");
+ }
+
+ if (differences.count()) {
+ qsort(differences.begin(), differences.count(),
+ sizeof(DiffRecord*), sortProc);
+ }
+
+ if (generateDiffs) {
+ print_diff_page(summary.fNumMatches, colorThreshold, differences,
+ baseDir, comparisonDir, outputDir);
+ }
+
+ for (i = 0; i < differences.count(); i++) {
+ delete differences[i];
+ }
+ matchSubstrings.deleteAll();
+ nomatchSubstrings.deleteAll();
+
+ int num_failing_results = 0;
+ for (int i = 0; i < DiffRecord::kResultCount; i++) {
+ if (failOnResultType[i]) {
+ num_failing_results += summary.fResultsOfType[i].count();
+ }
+ }
+ if (!failOnResultType[DiffRecord::kCouldNotCompare_Result]) {
+ for (int base = 0; base < DiffResource::kStatusCount; ++base) {
+ for (int comparison = 0; comparison < DiffResource::kStatusCount; ++comparison) {
+ if (failOnStatusType[base][comparison]) {
+ num_failing_results += summary.fStatusOfType[base][comparison].count();
+ }
+ }
+ }
+ }
+
+ // On Linux (and maybe other platforms too), any results outside of the
+ // range [0...255] are wrapped (mod 256). Do the conversion ourselves, to
+ // make sure that we only return 0 when there were no failures.
+ return (num_failing_results > 255) ? 255 : num_failing_results;
+}
+
+#if !defined SK_BUILD_FOR_IOS
+int main(int argc, char * const argv[]) {
+ return tool_main(argc, (char**) argv);
+}
+#endif
diff --git a/tools/skdiff/skdiff_utils.cpp b/tools/skdiff/skdiff_utils.cpp
new file mode 100644
index 0000000000..609d75d265
--- /dev/null
+++ b/tools/skdiff/skdiff_utils.cpp
@@ -0,0 +1,179 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+#include "skdiff.h"
+#include "skdiff_utils.h"
+#include "SkBitmap.h"
+#include "SkCodec.h"
+#include "SkData.h"
+#include "SkImageEncoder.h"
+#include "SkStream.h"
+#include "SkTypes.h"
+
+#include <memory>
+
+bool are_buffers_equal(SkData* skdata1, SkData* skdata2) {
+ if ((nullptr == skdata1) || (nullptr == skdata2)) {
+ return false;
+ }
+ if (skdata1->size() != skdata2->size()) {
+ return false;
+ }
+ return (0 == memcmp(skdata1->data(), skdata2->data(), skdata1->size()));
+}
+
+sk_sp<SkData> read_file(const char* file_path) {
+ sk_sp<SkData> data(SkData::MakeFromFileName(file_path));
+ if (!data) {
+ SkDebugf("WARNING: could not open file <%s> for reading\n", file_path);
+ }
+ return data;
+}
+
+bool get_bitmap(sk_sp<SkData> fileBits, DiffResource& resource, bool sizeOnly) {
+ SkAutoTDelete<SkCodec> codec(SkCodec::NewFromData(fileBits));
+ if (!codec) {
+ SkDebugf("ERROR: could not create codec for <%s>\n", resource.fFullPath.c_str());
+ resource.fStatus = DiffResource::kCouldNotDecode_Status;
+ return false;
+ }
+
+ if (!resource.fBitmap.setInfo(codec->getInfo().makeColorType(kN32_SkColorType))) {
+ SkDebugf("ERROR: could not set bitmap info for <%s>\n", resource.fFullPath.c_str());
+ resource.fStatus = DiffResource::kCouldNotDecode_Status;
+ return false;
+ }
+
+ if (sizeOnly) {
+ return true;
+ }
+
+ if (!resource.fBitmap.tryAllocPixels()) {
+ SkDebugf("ERROR: could not allocate pixels for <%s>\n", resource.fFullPath.c_str());
+ resource.fStatus = DiffResource::kCouldNotDecode_Status;
+ return false;
+ }
+
+ if (SkCodec::kSuccess != codec->getPixels(resource.fBitmap.info(),
+ resource.fBitmap.getPixels(), resource.fBitmap.rowBytes())) {
+ SkDebugf("ERROR: codec failed for basePath <%s>\n", resource.fFullPath.c_str());
+ resource.fStatus = DiffResource::kCouldNotDecode_Status;
+ return false;
+ }
+
+ resource.fStatus = DiffResource::kDecoded_Status;
+ return true;
+}
+
+/** Thanks to PNG, we need to force all pixels 100% opaque. */
+static void force_all_opaque(const SkBitmap& bitmap) {
+ SkAutoLockPixels lock(bitmap);
+ for (int y = 0; y < bitmap.height(); y++) {
+ for (int x = 0; x < bitmap.width(); x++) {
+ *bitmap.getAddr32(x, y) |= (SK_A32_MASK << SK_A32_SHIFT);
+ }
+ }
+}
+
+bool write_bitmap(const SkString& path, const SkBitmap& bitmap) {
+ SkBitmap copy;
+ bitmap.copyTo(&copy, kN32_SkColorType);
+ force_all_opaque(copy);
+ return SkImageEncoder::EncodeFile(path.c_str(), copy,
+ SkImageEncoder::kPNG_Type, 100);
+}
+
+/// Return a copy of the "input" string, within which we have replaced all instances
+/// of oldSubstring with newSubstring.
+///
+/// TODO: If we like this, we should move it into the core SkString implementation,
+/// adding more checks and ample test cases, and paying more attention to efficiency.
+static SkString replace_all(const SkString &input,
+ const char oldSubstring[], const char newSubstring[]) {
+ SkString output;
+ const char *input_cstr = input.c_str();
+ const char *first_char = input_cstr;
+ const char *match_char;
+ size_t oldSubstringLen = strlen(oldSubstring);
+ while ((match_char = strstr(first_char, oldSubstring))) {
+ output.append(first_char, (match_char - first_char));
+ output.append(newSubstring);
+ first_char = match_char + oldSubstringLen;
+ }
+ output.append(first_char);
+ return output;
+}
+
+static SkString filename_to_derived_filename(const SkString& filename, const char *suffix) {
+ SkString diffName (filename);
+ const char* cstring = diffName.c_str();
+ size_t dotOffset = strrchr(cstring, '.') - cstring;
+ diffName.remove(dotOffset, diffName.size() - dotOffset);
+ diffName.append(suffix);
+
+ // In case we recursed into subdirectories, replace slashes with something else
+ // so the diffs will all be written into a single flat directory.
+ diffName = replace_all(diffName, PATH_DIV_STR, "_");
+ return diffName;
+}
+
+SkString filename_to_diff_filename(const SkString& filename) {
+ return filename_to_derived_filename(filename, "-diff.png");
+}
+
+SkString filename_to_white_filename(const SkString& filename) {
+ return filename_to_derived_filename(filename, "-white.png");
+}
+
+void create_and_write_diff_image(DiffRecord* drp,
+ DiffMetricProc dmp,
+ const int colorThreshold,
+ const SkString& outputDir,
+ const SkString& filename) {
+ const int w = drp->fBase.fBitmap.width();
+ const int h = drp->fBase.fBitmap.height();
+
+ if (w != drp->fComparison.fBitmap.width() || h != drp->fComparison.fBitmap.height()) {
+ drp->fResult = DiffRecord::kDifferentSizes_Result;
+ } else {
+ drp->fDifference.fBitmap.allocN32Pixels(w, h);
+
+ drp->fWhite.fBitmap.allocN32Pixels(w, h);
+
+ SkASSERT(DiffRecord::kUnknown_Result == drp->fResult);
+ compute_diff(drp, dmp, colorThreshold);
+ SkASSERT(DiffRecord::kUnknown_Result != drp->fResult);
+ }
+
+ if (outputDir.isEmpty()) {
+ drp->fDifference.fStatus = DiffResource::kUnspecified_Status;
+ drp->fWhite.fStatus = DiffResource::kUnspecified_Status;
+
+ } else {
+ drp->fDifference.fFilename = filename_to_diff_filename(filename);
+ drp->fDifference.fFullPath = outputDir;
+ drp->fDifference.fFullPath.append(drp->fDifference.fFilename);
+ drp->fDifference.fStatus = DiffResource::kSpecified_Status;
+
+ drp->fWhite.fFilename = filename_to_white_filename(filename);
+ drp->fWhite.fFullPath = outputDir;
+ drp->fWhite.fFullPath.append(drp->fWhite.fFilename);
+ drp->fWhite.fStatus = DiffResource::kSpecified_Status;
+
+ if (DiffRecord::kDifferentPixels_Result == drp->fResult) {
+ if (write_bitmap(drp->fDifference.fFullPath, drp->fDifference.fBitmap)) {
+ drp->fDifference.fStatus = DiffResource::kExists_Status;
+ } else {
+ drp->fDifference.fStatus = DiffResource::kDoesNotExist_Status;
+ }
+ if (write_bitmap(drp->fWhite.fFullPath, drp->fWhite.fBitmap)) {
+ drp->fWhite.fStatus = DiffResource::kExists_Status;
+ } else {
+ drp->fWhite.fStatus = DiffResource::kDoesNotExist_Status;
+ }
+ }
+ }
+}
diff --git a/tools/skdiff/skdiff_utils.h b/tools/skdiff/skdiff_utils.h
new file mode 100644
index 0000000000..c799325e36
--- /dev/null
+++ b/tools/skdiff/skdiff_utils.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2012 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef skdiff_utils_DEFINED
+#define skdiff_utils_DEFINED
+
+#include "skdiff.h"
+
+class SkBitmap;
+class SkData;
+class SkString;
+
+/** Returns true if the two buffers passed in are both non-nullptr,
+ * have the same length, and contain exactly the same byte values.
+ */
+bool are_buffers_equal(SkData* skdata1, SkData* skdata2);
+
+/** Reads the file at the given path and returns its complete contents as an
+ * SkData object (or returns nullptr on error).
+ */
+sk_sp<SkData> read_file(const char* file_path);
+
+/** Decodes the fileBits into the resource.fBitmap. Returns false on failure. */
+bool get_bitmap(sk_sp<SkData> fileBits, DiffResource& resource, bool sizeOnly);
+
+/** Writes the bitmap as a PNG to the path specified. */
+bool write_bitmap(const SkString& path, const SkBitmap& bitmap);
+
+/** Given an image filename, returns the name of the file containing
+ * the associated difference image.
+ */
+SkString filename_to_diff_filename(const SkString& filename);
+
+/** Given an image filename, returns the name of the file containing
+ * the "white" difference image.
+ */
+SkString filename_to_white_filename(const SkString& filename);
+
+/** Calls compute_diff and handles the difference and white diff resources.
+ * If !outputDir.isEmpty(), writes out difference and white images.
+ */
+void create_and_write_diff_image(DiffRecord* drp,
+ DiffMetricProc dmp,
+ const int colorThreshold,
+ const SkString& outputDir,
+ const SkString& filename);
+
+#endif