WebKitTools/DumpRenderTree/chromium/ImageDiff.cpp
author Dremov Kirill (Nokia-D-MSW/Tampere) <kirill.dremov@nokia.com>
Fri, 17 Sep 2010 09:02:29 +0300
changeset 0 4f2f89ce4247
permissions -rw-r--r--
Revision: 201037

/*
 * 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 "gfx/codec/png_codec.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 gfx;
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(new unsigned char[byteLength]);
        if (fread(source.get(), 1, byteLength, stdin) != byteLength)
            return false;

        if (!PNGCodec::Decode(source.get(), byteLength, PNGCodec::FORMAT_RGBA,
                              &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 (!PNGCodec::Decode(&compressed[0], compressed.size(),
                              PNGCodec::FORMAT_RGBA, &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", file);
        return false;
    }
    if (dataSize != fwrite(data, 1, dataSize, file)) {
        fclose(file);
        fprintf(stderr, "ImageDiff: Unable to write data to file \"%s\"\n", file);
        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;
    PNGCodec::Encode(diffImage.data(), PNGCodec::FORMAT_RGBA, diffImage.width(),
                     diffImage.height(), diffImage.width() * 4, false, &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;
}