/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

// Test that basic GifReader operations succeed or fail as expected.
// Note that read-in file contents are tested against golden RGBA
// files in png_optimizer_test.cc, not here.


#include "pagespeed/kernel/image/gif_reader.h"

#include <cstddef>
#include <vector>

#include "pagespeed/kernel/base/gtest.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/mock_message_handler.h"
#include "pagespeed/kernel/base/null_mutex.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/image/gif_square.h"
#include "pagespeed/kernel/image/image_util.h"
#include "pagespeed/kernel/image/png_optimizer.h"
#include "pagespeed/kernel/image/scanline_interface_frame_adapter.h"
#include "pagespeed/kernel/image/scanline_utils.h"
#include "pagespeed/kernel/image/test_utils.h"

namespace pagespeed {

namespace image_compression {

// Friend adapter so we can instantiate GifFrameReader.
class TestGifFrameReader : public GifFrameReader {
 public:
  explicit TestGifFrameReader(MessageHandler* handler) :
      GifFrameReader(handler) {}
};

}  // namespace image_compression

}  // namespace pagespeed

namespace {

extern "C" {
#ifdef USE_SYSTEM_LIBPNG
#include "png.h"  // NOLINT
#else
#include "third_party/libpng/src/png.h"
#endif
}  // extern "C"

using net_instaweb::MockMessageHandler;
using net_instaweb::NullMutex;
using pagespeed::image_compression::kAlphaOpaque;
using pagespeed::image_compression::kAlphaTransparent;
using pagespeed::image_compression::kGifTestDir;
using pagespeed::image_compression::kPngSuiteGifTestDir;
using pagespeed::image_compression::kPngSuiteTestDir;
using pagespeed::image_compression::kPngTestDir;
using pagespeed::image_compression::kValidGifImageCount;
using pagespeed::image_compression::kValidGifImages;
using pagespeed::image_compression::size_px;
using pagespeed::image_compression::FrameSpec;
using pagespeed::image_compression::FrameToScanlineReaderAdapter;
using pagespeed::image_compression::GifDisposalToFrameSpecDisposal;
using pagespeed::image_compression::GifFrameReader;
using pagespeed::image_compression::GifReader;
using pagespeed::image_compression::GifSquare;
using pagespeed::image_compression::ImageFormat;
using pagespeed::image_compression::ImageSpec;
using pagespeed::image_compression::IMAGE_GIF;
using pagespeed::image_compression::IMAGE_PNG;
using pagespeed::image_compression::MultipleFrameReader;
using pagespeed::image_compression::PackAsArgb;
using pagespeed::image_compression::PixelFormat;
using pagespeed::image_compression::PixelFormat;
using pagespeed::image_compression::PixelRgbaChannels;
using pagespeed::image_compression::PngReaderInterface;
using pagespeed::image_compression::ReadTestFile;
using pagespeed::image_compression::ReadFile;
using pagespeed::image_compression::RgbaChannels;
using pagespeed::image_compression::QUIRKS_CHROME;
using pagespeed::image_compression::QUIRKS_FIREFOX;
using pagespeed::image_compression::QUIRKS_NONE;
using pagespeed::image_compression::RGB_888;
using pagespeed::image_compression::RGBA_8888;
using pagespeed::image_compression::RGBA_ALPHA;
using pagespeed::image_compression::RGBA_BLUE;
using pagespeed::image_compression::RGBA_GREEN;
using pagespeed::image_compression::RGBA_NUM_CHANNELS;
using pagespeed::image_compression::RGBA_RED;
using pagespeed::image_compression::ScanlineStatus;
using pagespeed::image_compression::ScopedPngStruct;
using pagespeed::image_compression::TestGifFrameReader;
using pagespeed::image_compression::kMessagePatternFailedToOpen;
using pagespeed::image_compression::kMessagePatternFailedToRead;
using pagespeed::image_compression::kMessagePatternLibpngError;
using pagespeed::image_compression::kMessagePatternLibpngWarning;
using pagespeed::image_compression::kMessagePatternUnexpectedEOF;

const char *kValidOpaqueGifImages[] = {
  "basi0g01",
  "basi0g02",
  "basi0g04",
  "basi0g08",
  "basi3p01",
  "basi3p02",
  "basi3p04",
  "basi3p08",
  "basn0g01",
  "basn0g02",
  "basn0g04",
  "basn0g08",
  "basn3p01",
  "basn3p02",
  "basn3p04",
  "basn3p08",
};

const char *kValidTransparentGifImages[] = {
  "tr-basi4a08",
  "tr-basn4a08"
};

const size_t kValidOpaqueGifImageCount = arraysize(kValidOpaqueGifImages);
const size_t kValidTransparentGifImageCount =
    arraysize(kValidTransparentGifImages);

const char kAnimatedGif[] = "animated";
const char kBadGif[] = "bad";
const char kCompletelyTransparentImage[] = "completely_transparent";
const char kFrameSmallerThanScreen[] = "frame_smaller_than_screen";
const char kInterlacedImage[] = "interlaced";
const char kRedConforming[] = "red_conforming";
const char kRedEmptyScreen[] = "red_empty_screen";
const char kRedUnusedBackground[] = "red_unused_invalid_background";
const char kTransparentGif[] = "transparent";
const char kZeroSizeAnimatedGif[] = "zero_size_animation";
const char kInvalidPixelLocalPaletteGif[] = "bad_pixel_local_palette";
const char kInvalidPixelGlobalPaletteGif[] = "bad_pixel_global_palette";

// Message to ignore.
const char kMessagePatternMultipleFrameGif[] =
    "Multiple frame GIF is not supported.";

class GifReaderTest : public testing::Test {
 public:
  GifReaderTest()
    : message_handler_(new NullMutex),
      gif_reader_(new GifReader(&message_handler_)),
      read_(ScopedPngStruct::READ, &message_handler_) {
  }

 protected:
  virtual void SetUp() {
    message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngError);
    message_handler_.AddPatternToSkipPrinting(kMessagePatternLibpngWarning);
  }

 protected:
  MockMessageHandler message_handler_;
  net_instaweb::scoped_ptr<PngReaderInterface> gif_reader_;
  ScopedPngStruct read_;

 private:
  DISALLOW_COPY_AND_ASSIGN(GifReaderTest);
};

TEST_F(GifReaderTest, LoadValidGifsWithoutTransforms) {
  GoogleString in, out;
  for (size_t i = 0; i < kValidOpaqueGifImageCount; i++) {
    ReadTestFile(
        kPngSuiteGifTestDir, kValidOpaqueGifImages[i], "gif", &in);
    ASSERT_NE(static_cast<size_t>(0), in.length());
    ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                     PNG_TRANSFORM_IDENTITY))
        << kValidOpaqueGifImages[i];
    ASSERT_TRUE(read_.reset());
  }

  for (size_t i = 0; i < kValidTransparentGifImageCount; i++) {
    ReadTestFile(
        kPngSuiteGifTestDir, kValidTransparentGifImages[i], "gif", &in);
    ASSERT_NE(static_cast<size_t>(0), in.length());
    ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                     PNG_TRANSFORM_IDENTITY))
        << kValidTransparentGifImages[i];
    ASSERT_TRUE(read_.reset());
  }

  ReadTestFile(kGifTestDir, "transparent", "gif", &in);
  ASSERT_NE(static_cast<size_t>(0), in.length());
  ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                   PNG_TRANSFORM_IDENTITY));
}

TEST_F(GifReaderTest, ExpandColorMapForValidGifs) {
  GoogleString in, out;
  for (size_t i = 0; i < kValidOpaqueGifImageCount; i++) {
    ReadTestFile(
        kPngSuiteGifTestDir, kValidOpaqueGifImages[i], "gif", &in);
    ASSERT_NE(static_cast<size_t>(0), in.length());
    ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                     PNG_TRANSFORM_EXPAND))
        << kValidOpaqueGifImages[i];
    ASSERT_TRUE(read_.reset());
  }

  for (size_t i = 0; i < kValidTransparentGifImageCount; i++) {
    ReadTestFile(
        kPngSuiteGifTestDir, kValidTransparentGifImages[i], "gif", &in);
    ASSERT_NE(static_cast<size_t>(0), in.length());
    ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                     PNG_TRANSFORM_EXPAND))
        << kValidTransparentGifImages[i];
    ASSERT_TRUE(read_.reset());
  }

  ReadTestFile(kGifTestDir, "transparent", "gif", &in);
  ASSERT_NE(static_cast<size_t>(0), in.length());
  ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                   PNG_TRANSFORM_EXPAND));
}

TEST_F(GifReaderTest, RequireOpaqueForValidGifs) {
  GoogleString in;
  for (size_t i = 0; i < kValidOpaqueGifImageCount; i++) {
    ReadTestFile(
        kPngSuiteGifTestDir, kValidOpaqueGifImages[i], "gif", &in);
    ASSERT_NE(static_cast<size_t>(0), in.length());
    ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                     PNG_TRANSFORM_IDENTITY, true))
        << kValidOpaqueGifImages[i];
    ASSERT_TRUE(read_.reset());
  }

  for (size_t i = 0; i < kValidTransparentGifImageCount; i++) {
    ReadTestFile(
        kPngSuiteGifTestDir, kValidTransparentGifImages[i], "gif", &in);
    ASSERT_NE(static_cast<size_t>(0), in.length());
    ASSERT_FALSE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                      PNG_TRANSFORM_IDENTITY, true))
        << kValidTransparentGifImages[i];
    ASSERT_TRUE(read_.reset());
  }

  ReadTestFile(kGifTestDir, "transparent", "gif", &in);
  ASSERT_NE(static_cast<size_t>(0), in.length());
  ASSERT_FALSE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                    PNG_TRANSFORM_IDENTITY, true));
}

TEST_F(GifReaderTest, ExpandColormapAndRequireOpaqueForValidGifs) {
  GoogleString in;
  for (size_t i = 0; i < kValidOpaqueGifImageCount; i++) {
    ReadTestFile(
        kPngSuiteGifTestDir, kValidOpaqueGifImages[i], "gif", &in);
    ASSERT_NE(static_cast<size_t>(0), in.length());
    ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                     PNG_TRANSFORM_EXPAND, true))
        << kValidOpaqueGifImages[i];
    ASSERT_TRUE(read_.reset());
  }

  for (size_t i = 0; i < kValidTransparentGifImageCount; i++) {
    ReadTestFile(
        kPngSuiteGifTestDir, kValidTransparentGifImages[i], "gif", &in);
    ASSERT_NE(static_cast<size_t>(0), in.length());
    ASSERT_FALSE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                      PNG_TRANSFORM_EXPAND, true))
        << kValidTransparentGifImages[i];
    ASSERT_TRUE(read_.reset());
  }

  ReadTestFile(kGifTestDir, "transparent", "gif", &in);
  ASSERT_NE(static_cast<size_t>(0), in.length());
  ASSERT_FALSE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                    PNG_TRANSFORM_EXPAND, true));
}

TEST_F(GifReaderTest, StripAlpha) {
  GoogleString in;
  png_uint_32 height;
  png_uint_32 width;
  int bit_depth;
  int color_type;
  png_bytep trans;
  int num_trans;
  png_color_16p trans_values;

  ReadTestFile(kGifTestDir, "transparent", "gif", &in);
  ASSERT_NE(static_cast<size_t>(0), in.length());
  ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                   PNG_TRANSFORM_STRIP_ALPHA, false));
  png_get_IHDR(read_.png_ptr(), read_.info_ptr(),
               &width, &height, &bit_depth, &color_type,
               NULL, NULL, NULL);
  ASSERT_TRUE((color_type & PNG_COLOR_MASK_ALPHA) == 0);  // NOLINT
  ASSERT_EQ(static_cast<unsigned int>(0),
            png_get_tRNS(read_.png_ptr(),
                         read_.info_ptr(),
                         &trans,
                         &num_trans,
                         &trans_values));

  read_.reset();

  ASSERT_TRUE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                   PNG_TRANSFORM_STRIP_ALPHA |
                                   PNG_TRANSFORM_EXPAND, false));
  png_get_IHDR(read_.png_ptr(), read_.info_ptr(),
               &width, &height, &bit_depth, &color_type,
               NULL, NULL, NULL);
  ASSERT_TRUE((color_type & PNG_COLOR_MASK_ALPHA) == 0);  // NOLINT

  ASSERT_EQ(static_cast<unsigned int>(0),
            png_get_tRNS(read_.png_ptr(),
                         read_.info_ptr(),
                         &trans,
                         &num_trans,
                         &trans_values));
}

TEST_F(GifReaderTest, ExpandColormapOnZeroSizeCanvasAndCatchLibPngError) {
  GoogleString in;
  // This is a free image from
  // <http://www.gifs.net/subcategory/40/0/20/Email>, with the canvas
  // size information manually set to zero in order to trigger a
  // libpng error.
  ReadTestFile(kGifTestDir, "zero_size_animation", "gif", &in);
  ASSERT_NE(static_cast<size_t>(0), in.length());
  ASSERT_FALSE(gif_reader_->ReadPng(in, read_.png_ptr(), read_.info_ptr(),
                                    PNG_TRANSFORM_EXPAND, true));
}

class GifScanlineReaderRawTest : public testing::Test {
 public:
  GifScanlineReaderRawTest()
    : scanline_(NULL),
      message_handler_(new NullMutex),
      reader_(new TestGifFrameReader(&message_handler_)) {
  }

  bool Initialize(const char* file_name) {
    if (!ReadTestFile(kGifTestDir, file_name, "gif", &input_image_)) {
      PS_LOG_FATAL((&message_handler_), "Failed to read file: %s", file_name);
      return false;
    }
    return reader_.Initialize(input_image_.c_str(), input_image_.length());
  }

 protected:
  virtual void SetUp() {
    message_handler_.AddPatternToSkipPrinting(kMessagePatternFailedToOpen);
    message_handler_.AddPatternToSkipPrinting(kMessagePatternFailedToRead);
    message_handler_.AddPatternToSkipPrinting(kMessagePatternMultipleFrameGif);
    message_handler_.AddPatternToSkipPrinting(kMessagePatternUnexpectedEOF);
  }

 protected:
  void* scanline_;
  MockMessageHandler message_handler_;
  FrameToScanlineReaderAdapter reader_;
  GoogleString input_image_;

 private:
  DISALLOW_COPY_AND_ASSIGN(GifScanlineReaderRawTest);
};

TEST_F(GifScanlineReaderRawTest, CorruptHeader) {
  ReadTestFile(kGifTestDir, kTransparentGif, "gif", &input_image_);
  // Make GifRecordType invalid.
  input_image_[781] = 0;
  ASSERT_FALSE(reader_.Initialize(input_image_.c_str(),
      input_image_.length()));
}

TEST_F(GifScanlineReaderRawTest, InitializeWithoutRead) {
  ASSERT_TRUE(Initialize(kTransparentGif));
}

TEST_F(GifScanlineReaderRawTest, ReadOneRow) {
  ASSERT_TRUE(Initialize(kTransparentGif));
  EXPECT_TRUE(reader_.ReadNextScanline(&scanline_));
}

TEST_F(GifScanlineReaderRawTest, ReinitializeAfterOneRow) {
  ASSERT_TRUE(Initialize(kTransparentGif));
  EXPECT_TRUE(reader_.ReadNextScanline(&scanline_));
  ASSERT_TRUE(Initialize(kInterlacedImage));
  EXPECT_TRUE(reader_.ReadNextScanline(&scanline_));
}

TEST_F(GifScanlineReaderRawTest, ReInitializeAfterLastRow) {
  ASSERT_TRUE(Initialize(kTransparentGif));
  while (reader_.HasMoreScanLines()) {
    EXPECT_TRUE(reader_.ReadNextScanline(&scanline_));
  }

  // After depleting the scanlines, any further call to
  // ReadNextScanline leads to death in debugging mode, or a
  // false in release mode.
#ifdef NDEBUG
  EXPECT_FALSE(reader_.ReadNextScanline(&scanline_));
#else
  EXPECT_DEATH(reader_.ReadNextScanline(&scanline_),
               "The GIF image was not initialized or does not "
               "have more scanlines.");
#endif

  ASSERT_TRUE(Initialize(kInterlacedImage));
  EXPECT_TRUE(reader_.ReadNextScanline(&scanline_));
}

// Animated GIF is not supported. Make sure GIF reader exits gracefully.
TEST_F(GifScanlineReaderRawTest, AnimatedGif) {
  ASSERT_FALSE(Initialize(kAnimatedGif));
}

TEST_F(GifScanlineReaderRawTest, BadGif) {
  ASSERT_FALSE(Initialize(kBadGif));
}

TEST_F(GifScanlineReaderRawTest, ZeroSizeGif) {
  ASSERT_FALSE(Initialize(kZeroSizeAnimatedGif));
}

// Check the accuracy of the reader. Compare the decoded results with the gold
// data (".gif.rgba"). The test images include transparent and opaque ones.
TEST_F(GifScanlineReaderRawTest, ValidGifs) {
  for (size_t i = 0; i < kValidGifImageCount; i++) {
    GoogleString rgba_image, gif_image;
    const char* file_name = kValidGifImages[i].filename;
    ReadTestFile(kPngSuiteGifTestDir, file_name, "gif.rgba", &rgba_image);
    ReadTestFile(kPngSuiteGifTestDir, file_name, "gif", &gif_image);

    const uint8_t* reference_rgba =
        reinterpret_cast<const uint8*>(rgba_image.data());
    uint8* decoded_pixels = NULL;

    ASSERT_TRUE(reader_.Initialize(gif_image.data(), gif_image.length()));

    PixelFormat pixel_format = reader_.GetPixelFormat();
    int width = reader_.GetImageWidth();
    int height = reader_.GetImageHeight();
    int bytes_per_row = reader_.GetBytesPerScanline();
    int num_channels = GetNumChannelsFromPixelFormat(pixel_format,
                                                     &message_handler_);

    EXPECT_EQ(kValidGifImages[i].width, width);
    EXPECT_EQ(kValidGifImages[i].height, height);
    if (kValidGifImages[i].transparency) {
      EXPECT_EQ(pagespeed::image_compression::RGBA_8888, pixel_format);
      EXPECT_EQ(4, num_channels);
    } else {
      EXPECT_EQ(pagespeed::image_compression::RGB_888, pixel_format);
      EXPECT_EQ(3, num_channels);
    }
    EXPECT_EQ(width*num_channels, bytes_per_row);

    // Decode and check the image a row at a time.
    int row = 0;
    while (reader_.HasMoreScanLines()) {
      EXPECT_TRUE(reader_.ReadNextScanline(
        reinterpret_cast<void**>(&decoded_pixels)));

      for (int x = 0; x < width; ++x) {
        int index_dec = x * num_channels;
        int index_ref = (row * width + x) * 4;
        // ASSERT_EQ is used in stead of EXPECT_EQ. Otherwise, the log will be
        // spammed when an error happens.
        ASSERT_EQ(0, memcmp(reference_rgba+index_ref, decoded_pixels+index_dec,
                            num_channels));
      }
      ++row;
    }

    // Make sure both readers have exhausted all image rows.
    EXPECT_EQ(height, row);
    EXPECT_EQ(rgba_image.length(),
              static_cast<size_t>(4 * height * width));
  }
}

TEST_F(GifScanlineReaderRawTest, Interlaced) {
  GoogleString png_image, gif_image;
  ReadTestFile(kGifTestDir, kInterlacedImage, "png", &png_image);
  ReadTestFile(kGifTestDir, kInterlacedImage, "gif", &gif_image);
  DecodeAndCompareImages(IMAGE_PNG, png_image.c_str(), png_image.length(),
                         IMAGE_GIF, gif_image.c_str(), gif_image.length(),
                         false,  // ignore_transparent_rgb
                         &message_handler_);
}

TEST_F(GifScanlineReaderRawTest, EmptyScreen) {
  GoogleString png_image, gif_image;
  ReadTestFile(kGifTestDir, kRedConforming, "png", &png_image);
  ReadTestFile(kGifTestDir, kRedEmptyScreen, "gif", &gif_image);
  DecodeAndCompareImages(IMAGE_PNG, png_image.c_str(), png_image.length(),
                         IMAGE_GIF, gif_image.c_str(), gif_image.length(),
                         false,  // ignore_transparent_rgb
                         &message_handler_);
}

TEST_F(GifScanlineReaderRawTest, UnusedBackground) {
  GoogleString png_image, gif_image;
  ReadTestFile(kGifTestDir, kRedConforming, "png", &png_image);
  ReadTestFile(kGifTestDir, kRedUnusedBackground, "gif", &gif_image);
  DecodeAndCompareImages(IMAGE_PNG, png_image.c_str(), png_image.length(),
                         IMAGE_GIF, gif_image.c_str(), gif_image.length(),
                         false,  // ignore_transparent_rgb
                         &message_handler_);
}

TEST_F(GifScanlineReaderRawTest, FrameSmallerThanImage) {
  ASSERT_FALSE(Initialize(kFrameSmallerThanScreen));
  EXPECT_EQ(1, message_handler_.MessagesOfType(net_instaweb::kInfo));
  EXPECT_EQ(0, message_handler_.MessagesOfType(net_instaweb::kWarning));
  EXPECT_EQ(0, message_handler_.MessagesOfType(net_instaweb::kError));
  EXPECT_EQ(0, message_handler_.MessagesOfType(net_instaweb::kFatal));
}

TEST(GifReaderUtil, DisposalMethod) {
  for (int i = -1; i < 4; ++i) {
    FrameSpec::DisposalMethod actual_disposal =
        GifDisposalToFrameSpecDisposal(i);
    FrameSpec::DisposalMethod expected_disposal = FrameSpec::DISPOSAL_NONE;
    switch (i) {
      case 0:
        // intentional fall-through
      case 1:
        expected_disposal = FrameSpec::DISPOSAL_NONE;
        break;
      case 2:
        expected_disposal = FrameSpec::DISPOSAL_BACKGROUND;
        break;
      case 3:
        expected_disposal = FrameSpec::DISPOSAL_RESTORE;
        break;
    }
    EXPECT_EQ(expected_disposal, actual_disposal);
  }
}

void CheckQuirksModeChangesToImageSpec(const FrameSpec& frame_spec,
                                       const ImageSpec& original_spec,
                                       const bool has_loop_count,
                                       const ImageSpec& expected_noquirks_spec,
                                       const ImageSpec& expected_firefox_spec,
                                       const ImageSpec& expected_chrome_spec) {
  ImageSpec noquirks_spec = original_spec;
  ImageSpec firefox_spec = original_spec;
  ImageSpec chrome_spec = original_spec;

  GifFrameReader::ApplyQuirksModeToImage(QUIRKS_NONE, has_loop_count,
                                         frame_spec, &noquirks_spec);
  EXPECT_TRUE(noquirks_spec.Equals(expected_noquirks_spec));

  GifFrameReader::ApplyQuirksModeToImage(QUIRKS_FIREFOX, has_loop_count,
                                         frame_spec, &firefox_spec);
  EXPECT_TRUE(firefox_spec.Equals(expected_firefox_spec))
      << "\nActual:\n" << firefox_spec.ToString()
      << "\nExpected:\n" << expected_firefox_spec.ToString();

  GifFrameReader::ApplyQuirksModeToImage(QUIRKS_CHROME, has_loop_count,
                                         frame_spec, &chrome_spec);
  EXPECT_TRUE(chrome_spec.Equals(expected_chrome_spec))
      << "\nActual:\n" << chrome_spec.ToString()
      << "\nExpected:\n" << expected_chrome_spec.ToString();
}

void SetOpaqueBackground(PixelRgbaChannels rgba) {
  static const PixelRgbaChannels kOpaqueBackground =
      {0x80, 0x80, 0x80, kAlphaOpaque};
  for (int channel = 0;
       channel < static_cast<RgbaChannels>(channel);
       ++channel) {
    rgba[channel] = kOpaqueBackground[channel];
  }
}

TEST(ApplyQuirksModeToImage, TestFrameWidthLargerThanImageWidth) {
  ImageSpec image_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  image_spec.num_frames = 2;
  SetOpaqueBackground(image_spec.bg_color);

  FrameSpec frame_spec;
  frame_spec.width = 200;
  frame_spec.height = 100;
  frame_spec.top = 10;
  frame_spec.left = 2;

  ImageSpec expected_noquirks_spec = image_spec;
  ImageSpec expected_firefox_spec = image_spec;
  ImageSpec expected_chrome_spec = image_spec;

  expected_chrome_spec.width = frame_spec.width;
  expected_chrome_spec.height = frame_spec.height;
  expected_chrome_spec.image_size_adjusted = true;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);

  image_spec.num_frames =
      expected_noquirks_spec.num_frames =
      expected_firefox_spec.num_frames =
      expected_chrome_spec.num_frames = 1;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);
}


TEST(ApplyQuirksModeToImage, TestFrameHeightLargerThanImageHeight) {
  ImageSpec image_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  image_spec.num_frames = 2;
  SetOpaqueBackground(image_spec.bg_color);

  FrameSpec frame_spec;
  frame_spec.width = 100;
  frame_spec.height = 200;
  frame_spec.top = 10;
  frame_spec.left = 2;

  ImageSpec expected_noquirks_spec = image_spec;
  ImageSpec expected_firefox_spec = image_spec;
  ImageSpec expected_chrome_spec = image_spec;

  expected_chrome_spec.width = frame_spec.width;
  expected_chrome_spec.height = frame_spec.height;
  expected_chrome_spec.image_size_adjusted = true;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);

  image_spec.num_frames =
      expected_noquirks_spec.num_frames =
      expected_firefox_spec.num_frames =
      expected_chrome_spec.num_frames = 1;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);
}

TEST(ApplyQuirksModeToImage, TestFrameWidthSmallerThanImageWidth) {
  ImageSpec image_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  image_spec.num_frames = 2;
  SetOpaqueBackground(image_spec.bg_color);

  FrameSpec frame_spec;
  frame_spec.width = 50;
  frame_spec.height = 100;
  frame_spec.top = 10;
  frame_spec.left = 2;

  ImageSpec expected_noquirks_spec = image_spec;
  ImageSpec expected_firefox_spec = image_spec;
  ImageSpec expected_chrome_spec = image_spec;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);

  // With only one frame that is smaller than the image, the
  // background color becomes fully transparent.
  image_spec.num_frames =
      expected_noquirks_spec.num_frames =
      expected_firefox_spec.num_frames =
      expected_chrome_spec.num_frames = 1;

  expected_chrome_spec.bg_color[RGBA_ALPHA] = kAlphaTransparent;
  expected_firefox_spec.bg_color[RGBA_ALPHA] = kAlphaTransparent;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);
}

TEST(ApplyQuirksModeToImage, TestFrameHeightSmallerThanImageHeight) {
  ImageSpec image_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  image_spec.num_frames = 2;
  SetOpaqueBackground(image_spec.bg_color);

  FrameSpec frame_spec;
  frame_spec.width = 100;
  frame_spec.height = 50;
  frame_spec.top = 10;
  frame_spec.left = 2;

  ImageSpec expected_noquirks_spec = image_spec;
  ImageSpec expected_firefox_spec = image_spec;
  ImageSpec expected_chrome_spec = image_spec;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);

  // With only one frame that is smaller than the image, the
  // background color becomes fully transparent.
  image_spec.num_frames =
      expected_noquirks_spec.num_frames =
      expected_firefox_spec.num_frames =
      expected_chrome_spec.num_frames = 1;

  expected_chrome_spec.bg_color[RGBA_ALPHA] = kAlphaTransparent;
  expected_firefox_spec.bg_color[RGBA_ALPHA] = kAlphaTransparent;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);
}

TEST(ApplyQuirksModeToImage, TestFrameHeightSmallerWidthLargerThanImage) {
  ImageSpec image_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  image_spec.num_frames = 2;
  SetOpaqueBackground(image_spec.bg_color);

  FrameSpec frame_spec;
  frame_spec.width = 150;
  frame_spec.height = 50;
  frame_spec.top = 10;
  frame_spec.left = 2;

  ImageSpec expected_noquirks_spec = image_spec;
  ImageSpec expected_firefox_spec = image_spec;
  ImageSpec expected_chrome_spec = image_spec;

  expected_chrome_spec.width = frame_spec.width;
  expected_chrome_spec.height = frame_spec.height;
  expected_chrome_spec.image_size_adjusted = true;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);

  // With only one frame that is smaller than the image, the
  // background color becomes fully transparent.
  image_spec.num_frames =
      expected_noquirks_spec.num_frames =
      expected_firefox_spec.num_frames =
      expected_chrome_spec.num_frames = 1;

  expected_chrome_spec.bg_color[RGBA_ALPHA] = kAlphaTransparent;
  expected_firefox_spec.bg_color[RGBA_ALPHA] = kAlphaTransparent;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);
}

TEST(ApplyQuirksModeToImage, TestLoopCount) {
  ImageSpec image_spec;
  FrameSpec frame_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  image_spec.loop_count = 3;
  frame_spec.width = 100;
  frame_spec.height = 100;
  frame_spec.top = 0;
  frame_spec.left = 0;

  ImageSpec expected_noquirks_spec = image_spec;
  ImageSpec expected_firefox_spec = image_spec;
  ImageSpec expected_chrome_spec = image_spec;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);

  expected_chrome_spec.loop_count = image_spec.loop_count + 1;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, true,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);
}

TEST(ApplyQuirksModeToImage, TestNoop) {
  ImageSpec image_spec;
  FrameSpec frame_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  frame_spec.width = 50;
  frame_spec.height = 50;
  frame_spec.top = 10;
  frame_spec.left = 2;

  ImageSpec expected_noquirks_spec = image_spec;
  ImageSpec expected_firefox_spec = image_spec;
  ImageSpec expected_chrome_spec = image_spec;

  CheckQuirksModeChangesToImageSpec(frame_spec, image_spec, false,
                                    expected_noquirks_spec,
                                    expected_firefox_spec,
                                    expected_chrome_spec);
}

void CheckQuirksModeChangesToFirstFrameSpec(
    const ImageSpec& image_spec,
    const FrameSpec& original_spec,
    const FrameSpec& expected_noquirks_spec,
    const FrameSpec& expected_firefox_spec,
    const FrameSpec& expected_chrome_spec) {
  FrameSpec noquirks_spec = original_spec;
  FrameSpec firefox_spec = original_spec;
  FrameSpec chrome_spec = original_spec;

  ImageSpec image = image_spec;
  GifFrameReader::ApplyQuirksModeToImage(QUIRKS_NONE, false, noquirks_spec,
                                         &image);
  GifFrameReader::ApplyQuirksModeToFirstFrame(QUIRKS_NONE,
                                              image, &noquirks_spec);
  EXPECT_TRUE(noquirks_spec.Equals(expected_noquirks_spec))
      << " Got: " << noquirks_spec.ToString()
      << "\n Expected: " << expected_noquirks_spec.ToString();

  image = image_spec;
  GifFrameReader::ApplyQuirksModeToImage(QUIRKS_FIREFOX, false, firefox_spec,
                                         &image);
  GifFrameReader::ApplyQuirksModeToFirstFrame(QUIRKS_FIREFOX,
                                              image, &firefox_spec);
  EXPECT_TRUE(firefox_spec.Equals(expected_firefox_spec))
      << "      Got: " << firefox_spec.ToString()
      << "\n Expected: " << expected_firefox_spec.ToString();

  image = image_spec;
  GifFrameReader::ApplyQuirksModeToImage(QUIRKS_CHROME, false, chrome_spec,
                                         &image);
  GifFrameReader::ApplyQuirksModeToFirstFrame(QUIRKS_CHROME,
                                              image, &chrome_spec);
  EXPECT_TRUE(chrome_spec.Equals(expected_chrome_spec))
      << "      Got: " << chrome_spec.ToString()
      << "\n Expected: " << expected_chrome_spec.ToString();
}

TEST(ApplyQuirksModeToFirstFrame, TestWidth) {
  ImageSpec image_spec;
  FrameSpec frame_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  frame_spec.width = 200;
  frame_spec.height = 50;
  frame_spec.top = 10;
  frame_spec.left = 2;

  FrameSpec expected_noquirks_spec = frame_spec;
  FrameSpec expected_firefox_spec = frame_spec;
  FrameSpec expected_chrome_spec = frame_spec;

  expected_firefox_spec.top = 0;
  expected_firefox_spec.left = 0;
  expected_firefox_spec.width = 0;
  expected_firefox_spec.height = 0;

  expected_chrome_spec.top = 0;
  expected_chrome_spec.left = 0;

  CheckQuirksModeChangesToFirstFrameSpec(image_spec, frame_spec,
                                         expected_noquirks_spec,
                                         expected_firefox_spec,
                                         expected_chrome_spec);
}

TEST(ApplyQuirksModeToFirstFrame, TestHeight) {
  ImageSpec image_spec;
  FrameSpec frame_spec;
  image_spec.width = 100;
  image_spec.height = 100;
  frame_spec.width = 50;
  frame_spec.height = 200;
  frame_spec.top = 10;
  frame_spec.left = 2;

  FrameSpec expected_noquirks_spec = frame_spec;
  FrameSpec expected_firefox_spec = frame_spec;
  FrameSpec expected_chrome_spec = frame_spec;

  expected_firefox_spec.top = 0;
  expected_firefox_spec.left = 0;
  expected_firefox_spec.width = 0;
  expected_firefox_spec.height = 0;

  expected_chrome_spec.top = 0;
  expected_chrome_spec.left = 0;

  CheckQuirksModeChangesToFirstFrameSpec(image_spec, frame_spec,
                                         expected_noquirks_spec,
                                         expected_firefox_spec,
                                         expected_chrome_spec);
}

TEST(ApplyQuirksModeToFirstFrame, TestZeroWidthFrame) {
  ImageSpec image_spec;
  FrameSpec frame_spec;
  image_spec.width = 100;
  image_spec.height = 120;
  frame_spec.width = 0;
  frame_spec.height = 50;
  frame_spec.top = 10;
  frame_spec.left = 2;

  FrameSpec expected_noquirks_spec = frame_spec;
  FrameSpec expected_chrome_spec = frame_spec;
  FrameSpec expected_firefox_spec = frame_spec;

  expected_chrome_spec.height = image_spec.height;
  expected_chrome_spec.width = image_spec.width;
  expected_chrome_spec.top = 0;
  expected_chrome_spec.left = 0;

  expected_firefox_spec.top = 0;
  expected_firefox_spec.left = 0;
  expected_firefox_spec.width = 0;
  expected_firefox_spec.height = 0;

  CheckQuirksModeChangesToFirstFrameSpec(image_spec, frame_spec,
                                         expected_noquirks_spec,
                                         expected_firefox_spec,
                                         expected_chrome_spec);
}

TEST(ApplyQuirksModeToFirstFrame, TestZeroHeightFrame) {
  ImageSpec image_spec;
  FrameSpec frame_spec;
  image_spec.width = 100;
  image_spec.height = 120;
  frame_spec.width = 50;
  frame_spec.height = 0;
  frame_spec.top = 10;
  frame_spec.left = 2;

  FrameSpec expected_noquirks_spec = frame_spec;
  FrameSpec expected_chrome_spec = frame_spec;
  FrameSpec expected_firefox_spec = frame_spec;

  expected_chrome_spec.height = image_spec.height;
  expected_chrome_spec.width = image_spec.width;
  expected_chrome_spec.top = 0;
  expected_chrome_spec.left = 0;

  expected_firefox_spec.top = 0;
  expected_firefox_spec.left = 0;
  expected_firefox_spec.width = 0;
  expected_firefox_spec.height = 0;

  CheckQuirksModeChangesToFirstFrameSpec(image_spec, frame_spec,
                                         expected_noquirks_spec,
                                         expected_firefox_spec,
                                         expected_chrome_spec);
}

TEST(ApplyQuirksModeToFirstFrame, TestNoopChrome) {
  ImageSpec image_spec;
  FrameSpec frame_spec;
  image_spec.width = 100;
  image_spec.height = 120;
  frame_spec.width = 60;
  frame_spec.height = 50;
  frame_spec.top = 10;
  frame_spec.left = 2;

  FrameSpec expected_noquirks_spec = frame_spec;
  FrameSpec expected_firefox_spec = frame_spec;
  FrameSpec expected_chrome_spec = frame_spec;

  expected_firefox_spec.top = 0;
  expected_firefox_spec.left = 0;
  expected_firefox_spec.width = 0;
  expected_firefox_spec.height = 0;

  CheckQuirksModeChangesToFirstFrameSpec(image_spec, frame_spec,
                                         expected_noquirks_spec,
                                         expected_firefox_spec,
                                         expected_chrome_spec);
}

TEST(ApplyQuirksModeToFirstFrame, TestNoop) {
  ImageSpec image_spec;
  FrameSpec frame_spec;
  image_spec.width = 100;
  image_spec.height = 120;
  frame_spec.width = 100;
  frame_spec.height = 120;
  frame_spec.top = 0;
  frame_spec.left = 0;

  FrameSpec expected_noquirks_spec = frame_spec;
  FrameSpec expected_firefox_spec = frame_spec;
  FrameSpec expected_chrome_spec = frame_spec;

  CheckQuirksModeChangesToFirstFrameSpec(image_spec, frame_spec,
                                         expected_noquirks_spec,
                                         expected_firefox_spec,
                                         expected_chrome_spec);
}

class GifAnimationTest : public testing::Test {
 public:
  struct Image {
    size_px width;
    size_px height;
    int bg_color_idx;
    size_t loop_count;
  };

  struct Frame {
    size_px width;
    size_px height;
    bool interlace;
    int delay_cs;
    int disposal;
    const GifColorType* colormap;
    int num_colors;
    int transparent_idx;
    int square_color_idx;
    size_px top;
    size_px left;
  };

  GifAnimationTest()
      : scanline_(NULL),
        message_handler_(new NullMutex),
        gif_(true, &message_handler_),
        reader_(new TestGifFrameReader(&message_handler_)),
        read_all_scanlines_(true) {
  }

  void SynthesizeImage(const char* filename, const Image& image) {
    // Note that these images are synthesized with QUIRKS_NONE.
    filename_ = net_instaweb::StrCat(net_instaweb::GTestTempDir(),
                                     "/", filename, ".gif");
    EXPECT_TRUE(gif_.Open(filename_));
    PS_LOG_INFO((&message_handler_), "Generating image: %s", filename_.c_str());
    gif_.PrepareScreen(true, image.width, image.height, kColorMap, kNumColors,
                       image.bg_color_idx, image.loop_count);
    for (std::vector<Frame>::iterator frame = synth_frames_.begin();
         frame != synth_frames_.end();
         ++frame) {
      EXPECT_TRUE(gif_.PutImage(
          frame->left, frame->top, frame->width, frame->height,
          frame->colormap, frame->num_colors,
          frame->square_color_idx, frame->transparent_idx,
          frame->interlace, frame->delay_cs, frame->disposal));
    }
    EXPECT_TRUE(gif_.Close());
  }

  void ReadImage(const Image& image) {
    if (!ReadFile(filename_, &input_image_)) {
      PS_LOG_FATAL((&message_handler_),
                    "Failed to read file: %s", filename_.c_str());
      return;
    }

    EXPECT_TRUE(reader_->Initialize(input_image_.c_str(),
                                    input_image_.length()).Success());
    ScanlineStatus status;
    ImageSpec image_spec;
    EXPECT_TRUE(reader_->GetImageSpec(&image_spec, &status));
    EXPECT_EQ(image.width, image_spec.width);
    EXPECT_EQ(image.height, image_spec.height);
    EXPECT_EQ((image.loop_count == GifSquare::kNoLoopCountSpecified ?
               1 : image.loop_count),
              image_spec.loop_count);
    EXPECT_EQ(synth_frames_.size(), image_spec.num_frames);
    EXPECT_FALSE(image_spec.use_bg_color);
    EXPECT_EQ(kColorMap[image.bg_color_idx].Red,
              image_spec.bg_color[RGBA_RED]);
    EXPECT_EQ(kColorMap[image.bg_color_idx].Green,
              image_spec.bg_color[RGBA_GREEN]);
    EXPECT_EQ(kColorMap[image.bg_color_idx].Blue,
              image_spec.bg_color[RGBA_BLUE]);
    EXPECT_EQ(kAlphaOpaque, image_spec.bg_color[RGBA_ALPHA]);
    for (std::vector<Frame>::iterator set_frame = synth_frames_.begin();
         set_frame != synth_frames_.end();
         ++set_frame) {
      FrameSpec frame_spec;
      EXPECT_TRUE(reader_->HasMoreFrames());
      EXPECT_TRUE(reader_->PrepareNextFrame(&status));
      EXPECT_TRUE(reader_->GetFrameSpec(&frame_spec, &status));

      EXPECT_EQ(set_frame->delay_cs < 0 ?
                0 : set_frame->delay_cs * 10, frame_spec.duration_ms);
      EXPECT_EQ(set_frame->width, frame_spec.width);
      EXPECT_EQ(set_frame->height, frame_spec.height);
      EXPECT_EQ(set_frame->top, frame_spec.top);
      EXPECT_EQ(set_frame->left, frame_spec.left);
      EXPECT_EQ(set_frame->interlace, frame_spec.hint_progressive);
      EXPECT_EQ(set_frame->transparent_idx >= 0 ?
                RGBA_8888 : RGB_888, frame_spec.pixel_format);
      FrameSpec::DisposalMethod frame_disposal =
          GifDisposalToFrameSpecDisposal(set_frame->disposal);
      EXPECT_EQ((frame_disposal == FrameSpec::DISPOSAL_UNKNOWN ?
                 FrameSpec::DISPOSAL_NONE : frame_disposal),
                frame_spec.disposal);

      const GifColorType* cmap = (set_frame->colormap == NULL ?
                                  kColorMap : set_frame->colormap);
      const int bytes_per_pixel = GetBytesPerPixel(frame_spec.pixel_format);
      static const int kRgbBytes = 3;

      if (read_all_scanlines_) {
        for (int row = 0; row < frame_spec.height; ++row) {
          EXPECT_TRUE(reader_->HasMoreScanlines());
          const uint8_t* scanline;
          ScanlineStatus status =
              reader_->ReadNextScanline(
                  reinterpret_cast<const void **>(&scanline));
          EXPECT_TRUE(status.Success());

          for (int col = 0; col < frame_spec.width; ++col) {
            // Since we're comparing pixel by pixel, correlated errors
            // would generate huge output if we use EXPECT rather than
            // ASSERT below.
            if (frame_spec.pixel_format == RGBA_8888 &&
                (set_frame->transparent_idx == set_frame->square_color_idx)) {
              // This pixel is fully transparent so other channels do not
              // matter.
              ASSERT_EQ(kAlphaTransparent,
                        *(scanline + col * bytes_per_pixel + kRgbBytes));
              const uint8 black_pixel[3] = {0, 0, 0};
              ASSERT_EQ(0, memcmp(scanline + col * bytes_per_pixel, black_pixel,
                                  kRgbBytes));
            } else {
              // This pixel must be opaque. Check all color channels.
              ASSERT_EQ(0, memcmp(scanline + col * bytes_per_pixel,
                                  cmap + set_frame->square_color_idx,
                                  kRgbBytes));
              if (frame_spec.pixel_format == RGBA_8888) {
                ASSERT_EQ(kAlphaOpaque,
                          *(scanline + col * bytes_per_pixel + kRgbBytes));
              }
            }
          }
        }
        EXPECT_FALSE(reader_->HasMoreScanlines());
      }
    }
    EXPECT_FALSE(reader_->HasMoreFrames());
  }

  Image DefineImage() {
    Image image;
    image.width = 100;
    image.height = 100;
    image.bg_color_idx = 3;
    image.loop_count = GifSquare::kNoLoopCountSpecified;
    return image;
  }

  void SynthesizeAndRead(const char* name, const Image& image) {
    SynthesizeImage(name, image);
    ReadImage(image);
  }

 protected:
  virtual void SetUp() {
    message_handler_.AddPatternToSkipPrinting(kMessagePatternFailedToOpen);
    message_handler_.AddPatternToSkipPrinting(kMessagePatternFailedToRead);
    message_handler_.AddPatternToSkipPrinting(kMessagePatternMultipleFrameGif);
    message_handler_.AddPatternToSkipPrinting(kMessagePatternUnexpectedEOF);
  }

 protected:
  void* scanline_;
  MockMessageHandler message_handler_;
  GifSquare gif_;
  net_instaweb::scoped_ptr<MultipleFrameReader> reader_;
  bool read_all_scanlines_;
  GoogleString filename_;
  GoogleString input_image_;
  std::vector<Frame> synth_frames_;

  static const int kNumColors;
  static const GifColorType kColorMap[];
  static const GifColorType kAlternateColorMap[];

 private:
  DISALLOW_COPY_AND_ASSIGN(GifAnimationTest);
};

const int GifAnimationTest::kNumColors = 8;
const GifColorType GifAnimationTest::kColorMap[kNumColors] = {
  GifSquare::kGifWhite,
  GifSquare::kGifBlack,
  GifSquare::kGifRed,
  GifSquare::kGifGreen,
  GifSquare::kGifBlue,
  GifSquare::kGifYellow,
  GifSquare::kGifGray,
  GifSquare::kGifGray,
};

const GifColorType GifAnimationTest::kAlternateColorMap[kNumColors] = {
  GifSquare::kGifBlue,
  GifSquare::kGifRed,
  GifSquare::kGifYellow,
  GifSquare::kGifGreen,
  GifSquare::kGifWhite,
  GifSquare::kGifBlack,
  GifSquare::kGifGray,
  GifSquare::kGifGray,
};

// Note that the various test cases with "FallingOffImage" generate
// images that, when viewed on Chrome and Firefox, exhibit the quirky
// behavior that is captured in ImageSpec and FrameSpec when the
// appropriate QuirksMode is used. The test cases below use
// QUIRKS_NONE to test behavior to the spec.

// Non-animated, non-interlaced, only global colormap, varying disposals.

TEST_F(GifAnimationTest, ReadSingleFrameOpaque) {
  const Frame frame = {10, 10, false, 0, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_opaque", DefineImage());
}

TEST_F(GifAnimationTest, ReadSingleFrameTransparency) {
  const Frame frame = {10, 10, false, 0, 1, NULL, 0, 4, 2, 10, 10};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_transparency", DefineImage());
}

TEST_F(GifAnimationTest, ReadSingleFrameOpaqueFallingOffImage) {
  const Frame frame = {10, 10, false, 0, 2, NULL, 0, -1, 2, 95, 95};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_opaque_falling_off_image", DefineImage());
}

TEST_F(GifAnimationTest, ReadSingleFrameOpaqueLargeFallingOffImage) {
  const Frame frame = {250, 250, false, 0, 2, NULL, 0, -1, 2, 95, 95};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_opaque_large_falling_off_image",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadSingleFrameTransparencyFallingOffImage) {
  const Frame frame = {10, 10, false, 0, 3, NULL, 0, 4, 2, 95, 95};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_transparent_falling_off_image",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadSingleFrameTransparencyFallingOffImageAtOrigin) {
  const Frame frame = {250, 250, false, 0, 3, NULL, 0, 4, 2, 0, 0};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_transparent_falling_off_image_at_origin",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadSingleFrameOpaqueInZeroSizeImage) {
  Image image = DefineImage();
  image.width = 0;
  image.height = 0;

  const Frame frame = {10, 10, false, 0, 1, NULL, 0, 4, 2, 10, 10};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_opaque_in_zero_size_image", image);
}

TEST_F(GifAnimationTest, ReadSingleFrameOpaqueInZeroSizeImageAtOrigin) {
  Image image = DefineImage();
  image.width = 0;
  image.height = 0;

  const Frame frame = {10, 10, false, 0, 1, NULL, 0, 4, 2, 0, 0};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_opaque_in_zero_size_image_at_origin", image);
}

// Non-animated, interlaced, only global colormap, varying disposals.

TEST_F(GifAnimationTest, ReadSingleFrameInterlacedOpaque) {
  const Frame frame = {10, 10, true, 0, 4, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_interlaced_opaque", DefineImage());
}

TEST_F(GifAnimationTest, ReadSingleFrameInterlacedTransparency) {
  const Frame frame = {10, 10, true, 0, 0, NULL, 0, 4, 2, 10, 10};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_interlaced_transparency", DefineImage());
}

TEST_F(GifAnimationTest,
       ReadSingleFrameInterlacedOpaqueFallingOffImage) {
  const Frame frame = {10, 10, true, 0, 1, NULL, 0, -1, 2, 95, 95};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_interlaced_opaque_falling_off_image",
                    DefineImage());
}

TEST_F(GifAnimationTest,
       ReadSingleFrameInterlacedTransparencyFallingOffImage) {
  const Frame frame = {10, 10, true, 0, 2, NULL, 0, 4, 2, 95, 95};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_interlaced_transparent_falling_off_image",
                    DefineImage());
}

// Non-animated, non-interlaced, both global and per-frame colormap,
// varying disposals.

TEST_F(GifAnimationTest, ReadSingleFrameDualColormapsOpaque) {
  const Frame frame =
      {10, 10, false, 0, 3, kAlternateColorMap, kNumColors, -1, 2, 10, 10};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_colormaps_opaque", DefineImage());
}

TEST_F(GifAnimationTest, ReadSingleFrameDualColormapsTransparency) {
  const Frame frame =
      {10, 10, false, 0, 4, kAlternateColorMap, kNumColors, 4, 2, 10, 10};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_colormaps_transparency", DefineImage());
}

TEST_F(GifAnimationTest,
       ReadSingleFrameDualColormapsOpaqueFallingOffImage) {
  const Frame frame =
      {10, 10, false, 0, 0, kAlternateColorMap, kNumColors, -1, 2, 95, 95};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_colormaps_opaque_falling_off_image",
                    DefineImage());
}

TEST_F(GifAnimationTest,
       ReadSingleFrameDualColormapsTransparencyFallingOffImage) {
  const Frame frame =
      {10, 10, false, 0, 1, kAlternateColorMap, kNumColors, 4, 2, 95, 95};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_colormaps_transparent_falling_off_image",
                    DefineImage());
}

// Non-animated, non-interlaced, only global colormap, varying
// disposals, varying delays.
TEST_F(GifAnimationTest, ReadSingleFrameDelayOpaque) {
  const Frame frame = {10, 10, true, 10, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame);

  SynthesizeAndRead("single_frame_opaque", DefineImage());
}

// Animated images.

TEST_F(GifAnimationTest, ReadMultipleFrameOpaque) {
  Frame frame1 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, true, 100, 0, NULL, 0, -1, 3, 20, 20};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque", DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueFirstFallingOffImage) {
  Frame frame1 = {250, 250, true, 100, 0, NULL, 0, -1, 3, 90, 90};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 79, 79};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque_1st_falling_off_image",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueSecondFallingOffImage) {
  Frame frame1 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 79, 79};
  synth_frames_.push_back(frame1);

  Frame frame2 = {250, 250, true, 100, 0, NULL, 0, -1, 3, 90, 90};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque_2nd_falling_off_image",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueFirstFallingOffImageAtOrigin) {
  Frame frame1 = {250, 250, true, 100, 0, NULL, 0, -1, 3, 0, 0};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 79, 79};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque_1st_falling_off_image_at_origin",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueFirstFallingOffXImage) {
  Frame frame1 = {250, 20, false, 100, 0, NULL, 0, -1, 3, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 79, 79};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque_1st_falling_off_x_image",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueFirstFallingOffYImage) {
  Frame frame1 = {20, 250, false, 100, 0, NULL, 0, -1, 3, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 79, 79};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque_1st_falling_off_y_image",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueSecondFallingOffImageAtOrigin) {
  Frame frame1 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 79, 79};
  synth_frames_.push_back(frame1);

  Frame frame2 = {250, 250, false, 100, 0, NULL, 0, -1, 3, 0, 0};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque_2nd_falling_off_image_at_origin",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueNoDelay) {
  Frame frame1 = {20, 20, false, 0, 1, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 0, 2, NULL, 0, -1, 3, 20, 20};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque_nodelay", DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameTransparency) {
  Frame frame1 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 100, 0, NULL, 0, 3, 3, 20, 20};
  synth_frames_.push_back(frame2);

  Frame frame3 = {20, 20, false, 100, 0, NULL, 0, 2, 3, 25, 25};
  synth_frames_.push_back(frame3);

  SynthesizeAndRead("multiple_frame_transparency", DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameNoDelay2FrameOpaque) {
  // Tests that the transparency information for one frame is not
  // carried to the next. By setting the transparent index, delay, and
  // disposal to -1, the synthesized image will not have a Graphic
  // Control Extension for the given frame, and we can test that
  // transparent index was not carried over.
  const int frame1_transparent_idx = 3;
  const int frame2_color_idx = frame1_transparent_idx;

  Frame frame1 =
      {20, 20, false, 0, 0, NULL, 0, frame1_transparent_idx, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 =
      {20, 20, false, -1, -1, NULL, 0, -1, frame2_color_idx, 20, 20};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_transparency_no_delay_2frame_opaque",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameTransparencySkipScanlines) {
  read_all_scanlines_ = false;

  Frame frame1 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 100, 0, NULL, 0, 3, 3, 20, 20};
  synth_frames_.push_back(frame2);

  Frame frame3 = {20, 20, false, 100, 0, NULL, 0, 2, 3, 25, 25};
  synth_frames_.push_back(frame3);

  SynthesizeAndRead("multiple_frame_transparency_skip_scanlines",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameTransparencyMixInterlaced) {
  Frame frame1 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, true, 100, 0, NULL, 0, 3, 3, 20, 20};
  synth_frames_.push_back(frame2);

  Frame frame3 = {20, 20, false, 100, 0, NULL, 0, 2, 3, 25, 25};
  synth_frames_.push_back(frame3);

  SynthesizeAndRead("multiple_frame_transparency_mix_interlaced",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameTransparencyMixColormaps) {
  Frame frame1 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 =
      {20, 20, false, 100, 0, kAlternateColorMap, kNumColors, 3, 3, 20, 20};
  synth_frames_.push_back(frame2);

  Frame frame3 = {20, 20, false, 100, 0, NULL, 0, 2, 3, 25, 25};
  synth_frames_.push_back(frame3);

  SynthesizeAndRead("multiple_frame_transparency_mix_colormaps", DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueFallingOffImage) {
  Frame frame1 = {10, 10, false, 0, 2, NULL, 0, -1, 2, 93, 93};
  synth_frames_.push_back(frame1);

  Frame frame2 = {10, 10, false, 0, 2, NULL, 0, -1, 4, 95, 95};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_opaque_falling_off_image", DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameTransparencyFallingOffImage) {
  Frame frame1 = {10, 10, false, 0, 3, NULL, 0, 4, 2, 93, 93};
  synth_frames_.push_back(frame1);

  Frame frame2 = {10, 10, false, 0, 3, NULL, 0, 4, 4, 95, 95};
  synth_frames_.push_back(frame2);

  SynthesizeAndRead("multiple_frame_transparent_falling_off_image",
                    DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameOpaqueDisposal) {
  Frame frame1 = {20, 20, false, 100, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 100, 0, NULL, 0, -1, 3, 20, 20};
  synth_frames_.push_back(frame2);

  Frame frame3 = {20, 20, false, 100, 2, NULL, 0, -1, 4, 15, 15};
  synth_frames_.push_back(frame3);

  Frame frame4 = {20, 20, false, 100, 3, NULL, 0, -1, 1, 0, 0};
  synth_frames_.push_back(frame4);

  Frame frame5 = {20, 20, false, 100, 1, NULL, 0, -1, 5, 30, 30};
  synth_frames_.push_back(frame5);

  SynthesizeAndRead("multiple_frame_opaque_disposal", DefineImage());
}

TEST_F(GifAnimationTest, ReadMultipleFrameTransparencyLoopInfinite) {
  Image image = DefineImage();
  image.loop_count = 0;

  Frame frame1 = {20, 20, false, 50, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 50, 0, NULL, 0, 3, 3, 20, 20};
  synth_frames_.push_back(frame2);

  Frame frame3 = {20, 20, false, 50, 0, NULL, 0, 2, 3, 25, 25};
  synth_frames_.push_back(frame3);

  SynthesizeAndRead("multiple_frame_transparency_loop_infinite", image);
}

TEST_F(GifAnimationTest, ReadMultipleFrameTransparencyNoDelayLoopInfinite) {
  Image image = DefineImage();
  image.loop_count = 0;

  Frame frame1 = {20, 20, false, 0, 1, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 0, 1, NULL, 0, 3, 3, 20, 20};
  synth_frames_.push_back(frame2);

  Frame frame3 = {20, 20, false, 0, 1, NULL, 0, 2, 3, 25, 25};
  synth_frames_.push_back(frame3);

  SynthesizeAndRead("multiple_frame_transparency_nodelay_loop_infinite", image);
}

TEST_F(GifAnimationTest, ReadMultipleFrameTransparencyLoopThrice) {
  Image image = DefineImage();
  image.loop_count = 3;

  Frame frame1 = {20, 20, false, 50, 0, NULL, 0, -1, 2, 10, 10};
  synth_frames_.push_back(frame1);

  Frame frame2 = {20, 20, false, 50, 0, NULL, 0, 3, 3, 20, 20};
  synth_frames_.push_back(frame2);

  Frame frame3 = {20, 20, false, 50, 0, NULL, 0, 2, 3, 25, 25};
  synth_frames_.push_back(frame3);

  SynthesizeAndRead("multiple_frame_transparency_loop_thrice", image);
}

void CheckImageForOutOfBoundsPixel(const char* filename,
                                   int first_invalid_frame) {
  const PixelRgbaChannels kTransparentPixel = {0, 0, 0, kAlphaTransparent};

  MockMessageHandler message_handler(new NullMutex);
  net_instaweb::scoped_ptr<MultipleFrameReader>
      reader(new TestGifFrameReader(&message_handler));
  GoogleString input_image;

  if (!ReadTestFile(kGifTestDir, filename, "gif", &input_image)) {
    PS_LOG_FATAL((&message_handler),
                 "Failed to read file: %s", filename);
    return;
  }

  EXPECT_TRUE(reader->Initialize(input_image.c_str(),
                                 input_image.length()).Success());
  ScanlineStatus status;
  ImageSpec image_spec;
  FrameSpec frame_spec;

  EXPECT_TRUE(reader->GetImageSpec(&image_spec, &status));
  EXPECT_GT(image_spec.num_frames, first_invalid_frame);

  // Frames up to the first invalid frame should have format RGB_888.
  for (size_px frame = 0; frame < first_invalid_frame; ++frame) {
    EXPECT_TRUE(reader->HasMoreFrames());
    EXPECT_TRUE(reader->PrepareNextFrame(&status));
    EXPECT_TRUE(reader->GetFrameSpec(&frame_spec, &status));
    EXPECT_EQ(RGB_888, frame_spec.pixel_format);
  }

  // The invalid frame should have format RGBA_8888, and all its
  // pixels, which are out of palette bounds, should be reported
  // as transparent.
  EXPECT_TRUE(reader->HasMoreFrames());
  EXPECT_TRUE(reader->PrepareNextFrame(&status));
  EXPECT_TRUE(reader->GetFrameSpec(&frame_spec, &status));
  EXPECT_EQ(RGBA_8888, frame_spec.pixel_format);
  const uint8_t* scanline;
  for (size_px row = 0; row < frame_spec.height; ++row) {
    EXPECT_TRUE(reader->HasMoreScanlines());
    EXPECT_TRUE(
        reader->ReadNextScanline(
            reinterpret_cast<const void **>(&scanline),
            &status));
    for (size_px col = 0; col < frame_spec.width; ++col) {
      int cmp_result = memcmp(scanline + RGBA_NUM_CHANNELS * col,
                              &kTransparentPixel,
                              RGBA_NUM_CHANNELS * sizeof(uint8_t));
      EXPECT_EQ(0, cmp_result);
      if (cmp_result != 0) {
        // Return eagerly to avoid excessive output in case of error.
        return;
      }
    }
  }
  EXPECT_FALSE(reader->HasMoreScanlines());

  // We don't check any additional frames
}

TEST(InvalidPixels, TestOutOfRangePixelValueInLocalPalette) {
  // This image has a global palette as well as local palettes for all
  // frames. For frame #3, the local palette is only four colors long
  // but has pixels referring to color #4, which is out of range.
  CheckImageForOutOfBoundsPixel(kInvalidPixelLocalPaletteGif, 3);
}

TEST(InvalidPixels, TestOutOfRangePixelValueInGlobalPalette) {
  // This image has a four-color global palette as well as eight-color
  // local palettes for frames 0-2. Frame #3 does not have a local
  // palette (so it uses the global one) but has pixels referring to
  // color #4, which is out of range.
  CheckImageForOutOfBoundsPixel(kInvalidPixelGlobalPaletteGif, 3);
}

}  // namespace
