| /* |
| * Copyright (C) 2010 Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| // This file input format is based loosely on |
| // WebKitTools/DumpRenderTree/ImageDiff.m |
| |
| // The exact format of this tool's output to stdout is important, to match |
| // what the run-webkit-tests script expects. |
| |
| #include "config.h" |
| |
| #include "webkit/support/webkit_support_gfx.h" |
| #include <algorithm> |
| #include <stdio.h> |
| #include <string.h> |
| #include <vector> |
| #include <wtf/OwnArrayPtr.h> |
| #include <wtf/Vector.h> |
| |
| #if OS(WINDOWS) |
| #include <windows.h> |
| #define PATH_MAX MAX_PATH |
| #endif |
| |
| using namespace std; |
| |
| // Causes the app to remain open, waiting for pairs of filenames on stdin. |
| // The caller is then responsible for terminating this app. |
| static const char optionPollStdin[] = "--use-stdin"; |
| static const char optionGenerateDiff[] = "--diff"; |
| |
| // Return codes used by this utility. |
| static const int statusSame = 0; |
| static const int statusDifferent = 1; |
| static const int statusError = 2; |
| |
| // Color codes. |
| static const uint32_t rgbaRed = 0x000000ff; |
| static const uint32_t rgbaAlpha = 0xff000000; |
| |
| class Image { |
| public: |
| Image() |
| : m_width(0) |
| , m_height(0) {} |
| |
| Image(const Image& image) |
| : m_width(image.m_width) |
| , m_height(image.m_height) |
| , m_data(image.m_data) {} |
| |
| bool hasImage() const { return m_width > 0 && m_height > 0; } |
| int width() const { return m_width; } |
| int height() const { return m_height; } |
| const unsigned char* data() const { return &m_data.front(); } |
| |
| // Creates the image from stdin with the given data length. On success, it |
| // will return true. On failure, no other methods should be accessed. |
| bool craeteFromStdin(size_t byteLength) |
| { |
| if (!byteLength) |
| return false; |
| |
| OwnArrayPtr<unsigned char> source = adoptArrayPtr(new unsigned char[byteLength]); |
| if (fread(source.get(), 1, byteLength, stdin) != byteLength) |
| return false; |
| |
| if (!webkit_support::DecodePNG(source.get(), byteLength, &m_data, &m_width, &m_height)) { |
| clear(); |
| return false; |
| } |
| return true; |
| } |
| |
| // Creates the image from the given filename on disk, and returns true on |
| // success. |
| bool createFromFilename(const char* filename) |
| { |
| FILE* f = fopen(filename, "rb"); |
| if (!f) |
| return false; |
| |
| vector<unsigned char> compressed; |
| const int bufSize = 1024; |
| unsigned char buf[bufSize]; |
| size_t numRead = 0; |
| while ((numRead = fread(buf, 1, bufSize, f)) > 0) |
| std::copy(buf, &buf[numRead], std::back_inserter(compressed)); |
| |
| fclose(f); |
| |
| if (!webkit_support::DecodePNG(&compressed[0], compressed.size(), &m_data, &m_width, &m_height)) { |
| clear(); |
| return false; |
| } |
| return true; |
| } |
| |
| void clear() |
| { |
| m_width = m_height = 0; |
| m_data.clear(); |
| } |
| |
| // Returns the RGBA value of the pixel at the given location |
| const uint32_t pixelAt(int x, int y) const |
| { |
| ASSERT(x >= 0 && x < m_width); |
| ASSERT(y >= 0 && y < m_height); |
| return *reinterpret_cast<const uint32_t*>(&(m_data[(y * m_width + x) * 4])); |
| } |
| |
| void setPixelAt(int x, int y, uint32_t color) const |
| { |
| ASSERT(x >= 0 && x < m_width); |
| ASSERT(y >= 0 && y < m_height); |
| void* addr = &const_cast<unsigned char*>(&m_data.front())[(y * m_width + x) * 4]; |
| *reinterpret_cast<uint32_t*>(addr) = color; |
| } |
| |
| private: |
| // pixel dimensions of the image |
| int m_width, m_height; |
| |
| vector<unsigned char> m_data; |
| }; |
| |
| float percentageDifferent(const Image& baseline, const Image& actual) |
| { |
| int w = min(baseline.width(), actual.width()); |
| int h = min(baseline.height(), actual.height()); |
| |
| // Compute pixels different in the overlap |
| int pixelsDifferent = 0; |
| for (int y = 0; y < h; y++) { |
| for (int x = 0; x < w; x++) { |
| if (baseline.pixelAt(x, y) != actual.pixelAt(x, y)) |
| pixelsDifferent++; |
| } |
| } |
| |
| // Count pixels that are a difference in size as also being different |
| int maxWidth = max(baseline.width(), actual.width()); |
| int maxHeight = max(baseline.height(), actual.height()); |
| |
| // ...pixels off the right side, but not including the lower right corner |
| pixelsDifferent += (maxWidth - w) * h; |
| |
| // ...pixels along the bottom, including the lower right corner |
| pixelsDifferent += (maxHeight - h) * maxWidth; |
| |
| // Like the WebKit ImageDiff tool, we define percentage different in terms |
| // of the size of the 'actual' bitmap. |
| float totalPixels = static_cast<float>(actual.width()) * static_cast<float>(actual.height()); |
| if (!totalPixels) |
| return 100.0f; // When the bitmap is empty, they are 100% different. |
| return static_cast<float>(pixelsDifferent) / totalPixels * 100; |
| } |
| |
| void printHelp() |
| { |
| fprintf(stderr, |
| "Usage:\n" |
| " ImageDiff <compare file> <reference file>\n" |
| " Compares two files on disk, returning 0 when they are the same\n" |
| " ImageDiff --use-stdin\n" |
| " Stays open reading pairs of filenames from stdin, comparing them,\n" |
| " and sending 0 to stdout when they are the same\n" |
| " ImageDiff --diff <compare file> <reference file> <output file>\n" |
| " Compares two files on disk, outputs an image that visualizes the" |
| " difference to <output file>\n"); |
| /* For unfinished webkit-like-mode (see below) |
| "\n" |
| " ImageDiff -s\n" |
| " Reads stream input from stdin, should be EXACTLY of the format\n" |
| " \"Content-length: <byte length> <data>Content-length: ...\n" |
| " it will take as many file pairs as given, and will compare them as\n" |
| " (cmp_file, reference_file) pairs\n"); |
| */ |
| } |
| |
| int compareImages(const char* file1, const char* file2) |
| { |
| Image actualImage; |
| Image baselineImage; |
| |
| if (!actualImage.createFromFilename(file1)) { |
| fprintf(stderr, "ImageDiff: Unable to open file \"%s\"\n", file1); |
| return statusError; |
| } |
| if (!baselineImage.createFromFilename(file2)) { |
| fprintf(stderr, "ImageDiff: Unable to open file \"%s\"\n", file2); |
| return statusError; |
| } |
| |
| float percent = percentageDifferent(actualImage, baselineImage); |
| if (percent > 0.0) { |
| // failure: The WebKit version also writes the difference image to |
| // stdout, which seems excessive for our needs. |
| printf("diff: %01.2f%% failed\n", percent); |
| return statusDifferent; |
| } |
| |
| // success |
| printf("diff: %01.2f%% passed\n", percent); |
| return statusSame; |
| |
| } |
| |
| // Untested mode that acts like WebKit's image comparator. I wrote this but |
| // decided it's too complicated. We may use it in the future if it looks useful. |
| int untestedCompareImages() |
| { |
| Image actualImage; |
| Image baselineImage; |
| char buffer[2048]; |
| while (fgets(buffer, sizeof(buffer), stdin)) { |
| if (!strncmp("Content-length: ", buffer, 16)) { |
| char* context; |
| #if OS(WINDOWS) |
| strtok_s(buffer, " ", &context); |
| int imageSize = strtol(strtok_s(0, " ", &context), 0, 10); |
| #else |
| strtok_r(buffer, " ", &context); |
| int imageSize = strtol(strtok_r(0, " ", &context), 0, 10); |
| #endif |
| |
| bool success = false; |
| if (imageSize > 0 && !actualImage.hasImage()) { |
| if (!actualImage.craeteFromStdin(imageSize)) { |
| fputs("Error, input image can't be decoded.\n", stderr); |
| return 1; |
| } |
| } else if (imageSize > 0 && !baselineImage.hasImage()) { |
| if (!baselineImage.craeteFromStdin(imageSize)) { |
| fputs("Error, baseline image can't be decoded.\n", stderr); |
| return 1; |
| } |
| } else { |
| fputs("Error, image size must be specified.\n", stderr); |
| return 1; |
| } |
| } |
| |
| if (actualImage.hasImage() && baselineImage.hasImage()) { |
| float percent = percentageDifferent(actualImage, baselineImage); |
| if (percent > 0.0) { |
| // failure: The WebKit version also writes the difference image to |
| // stdout, which seems excessive for our needs. |
| printf("diff: %01.2f%% failed\n", percent); |
| } else { |
| // success |
| printf("diff: %01.2f%% passed\n", percent); |
| } |
| actualImage.clear(); |
| baselineImage.clear(); |
| } |
| fflush(stdout); |
| } |
| return 0; |
| } |
| |
| bool createImageDiff(const Image& image1, const Image& image2, Image* out) |
| { |
| int w = min(image1.width(), image2.width()); |
| int h = min(image1.height(), image2.height()); |
| *out = Image(image1); |
| bool same = (image1.width() == image2.width()) && (image1.height() == image2.height()); |
| |
| // FIXME: do something with the extra pixels if the image sizes are different. |
| for (int y = 0; y < h; y++) { |
| for (int x = 0; x < w; x++) { |
| uint32_t basePixel = image1.pixelAt(x, y); |
| if (basePixel != image2.pixelAt(x, y)) { |
| // Set differing pixels red. |
| out->setPixelAt(x, y, rgbaRed | rgbaAlpha); |
| same = false; |
| } else { |
| // Set same pixels as faded. |
| uint32_t alpha = basePixel & rgbaAlpha; |
| uint32_t newPixel = basePixel - ((alpha / 2) & rgbaAlpha); |
| out->setPixelAt(x, y, newPixel); |
| } |
| } |
| } |
| |
| return same; |
| } |
| |
| static bool writeFile(const char* outFile, const unsigned char* data, size_t dataSize) |
| { |
| FILE* file = fopen(outFile, "wb"); |
| if (!file) { |
| fprintf(stderr, "ImageDiff: Unable to create file \"%s\"\n", outFile); |
| return false; |
| } |
| if (dataSize != fwrite(data, 1, dataSize, file)) { |
| fclose(file); |
| fprintf(stderr, "ImageDiff: Unable to write data to file \"%s\"\n", outFile); |
| return false; |
| } |
| fclose(file); |
| return true; |
| } |
| |
| int diffImages(const char* file1, const char* file2, const char* outFile) |
| { |
| Image actualImage; |
| Image baselineImage; |
| |
| if (!actualImage.createFromFilename(file1)) { |
| fprintf(stderr, "ImageDiff: Unable to open file \"%s\"\n", file1); |
| return statusError; |
| } |
| if (!baselineImage.createFromFilename(file2)) { |
| fprintf(stderr, "ImageDiff: Unable to open file \"%s\"\n", file2); |
| return statusError; |
| } |
| |
| Image diffImage; |
| bool same = createImageDiff(baselineImage, actualImage, &diffImage); |
| if (same) |
| return statusSame; |
| |
| vector<unsigned char> pngData; |
| webkit_support::EncodeRGBAPNG(diffImage.data(), diffImage.width(), diffImage.height(), |
| diffImage.width() * 4, &pngData); |
| if (!writeFile(outFile, &pngData.front(), pngData.size())) |
| return statusError; |
| return statusDifferent; |
| } |
| |
| int main(int argc, const char* argv[]) |
| { |
| Vector<const char*> values; |
| bool pollStdin = false; |
| bool generateDiff = false; |
| for (int i = 1; i < argc; ++i) { |
| if (!strcmp(argv[i], optionPollStdin)) |
| pollStdin = true; |
| else if (!strcmp(argv[i], optionGenerateDiff)) |
| generateDiff = true; |
| else |
| values.append(argv[i]); |
| } |
| |
| if (pollStdin) { |
| // Watch stdin for filenames. |
| const size_t bufferSize = PATH_MAX; |
| char stdinBuffer[bufferSize]; |
| char firstName[bufferSize]; |
| bool haveFirstName = false; |
| while (fgets(stdinBuffer, bufferSize, stdin)) { |
| if (!stdinBuffer[0]) |
| continue; |
| |
| if (haveFirstName) { |
| // compareImages writes results to stdout unless an error occurred. |
| if (compareImages(firstName, stdinBuffer) == statusError) |
| printf("error\n"); |
| fflush(stdout); |
| haveFirstName = false; |
| } else { |
| // Save the first filename in another buffer and wait for the second |
| // filename to arrive via stdin. |
| strcpy(firstName, stdinBuffer); |
| haveFirstName = true; |
| } |
| } |
| return 0; |
| } |
| |
| if (generateDiff) { |
| if (values.size() == 3) |
| return diffImages(values[0], values[1], values[2]); |
| } else if (values.size() == 2) |
| return compareImages(argv[1], argv[2]); |
| |
| printHelp(); |
| return statusError; |
| } |