/*
 * 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.
 */


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

#include <setjmp.h>
#include <cstddef>

#include "base/logging.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/image/image_frame_interface.h"
#include "pagespeed/kernel/image/image_util.h"

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

#include "third_party/giflib/src/lib/gif_lib.h"
}

#if GIFLIB_MAJOR < 5 || (GIFLIB_MAJOR == 5 && GIFLIB_MINOR == 0)
#define DGifCloseFile(a, b) DGifCloseFile(a)
#endif

using net_instaweb::MessageHandler;
using pagespeed::image_compression::ScopedPngStruct;

namespace {

// GIF interlace tables.
static const int kInterlaceOffsets[] = { 0, 4, 2, 1 };
static const int kInterlaceJumps[] = { 8, 8, 4, 2 };
const int kInterlaceNumPass = arraysize(kInterlaceOffsets);
const int kNumColorForUint8 = 256;
const int kGifPaletteSize = 256;

// Flag used to indicate that a gif extension contains transparency
// information.
static const unsigned char kTransparentFlag = 0x01;

// Some older versions of gcc (4.2.x) have trouble with certain setjmp
// declarations. To address this gcc issue, we factor a few setjmp()
// invocations into their own narrowly scoped methods, so gcc can
// process them properly.
bool ProtectedPngSetIhdr(
    png_structp png_ptr, png_infop info_ptr,
    png_uint_32 width, png_uint_32 height, int bit_depth, int color_type,
    int interlace_method, int compression_method, int filter_method) {
  if (setjmp(png_jmpbuf(png_ptr))) {
    return false;
  }
  png_set_IHDR(png_ptr, info_ptr, width, height, bit_depth, color_type,
               interlace_method, compression_method, filter_method);
  return true;
}

bool ProtectedPngSetPlte(png_structp png_ptr, png_infop info_ptr,
                         png_colorp palette, int num_palette) {
  if (setjmp(png_jmpbuf(png_ptr))) {
    return false;
  }
  png_set_PLTE(png_ptr, info_ptr, palette, num_palette);
  return true;
}

int ReadGifFromStream(GifFileType* gif_file, GifByteType* data, int length) {
  pagespeed::image_compression::ScanlineStreamInput* input =
      static_cast<pagespeed::image_compression::ScanlineStreamInput*>(
          gif_file->UserData);
  if (input->offset() + length <= input->length()) {
    memcpy(data, input->data() + input->offset(), length);
    input->set_offset(input->offset() + length);
    return length;
  } else {
    PS_LOG_INFO(input->message_handler(), "Unexpected EOF.");
    return 0;
  }
}

bool AddTransparencyChunk(png_structp png_ptr,
                          png_infop info_ptr,
                          int transparent_palette_index,
                          MessageHandler* handler) {
  const int num_trans = transparent_palette_index + 1;
  png_colorp palette;
  int num_palette = 0;
  png_get_PLTE(png_ptr, info_ptr, &palette, &num_palette);
  if (num_trans <= 0 || num_trans > num_palette) {
    PS_LOG_INFO(handler, "Transparent palette index out of bounds.");
    return false;
  }

  if (setjmp(png_jmpbuf(png_ptr))) {
    return false;
  }
  // TODO(huibao): to optimize tRNS size, could move transparent index to
  // the head of the palette.
  png_byte trans[kNumColorForUint8];
  // First, set all palette indices to fully opaque.
  memset(trans, 0xff, num_trans);
  // Set the one transparent index to fully transparent.
  trans[transparent_palette_index] =
      pagespeed::image_compression::kAlphaTransparent;
  png_set_tRNS(png_ptr, info_ptr, trans, num_trans, NULL);
  return true;
}

bool ReadImageDescriptor(GifFileType* gif_file,
                         png_structp png_ptr,
                         png_infop info_ptr,
                         png_color* palette,
                         MessageHandler* handler) {
  if (DGifGetImageDesc(gif_file) == GIF_ERROR) {
    PS_DLOG_INFO(handler, "Failed to get image descriptor.");
    return false;
  }
  if (gif_file->ImageCount != 1) {
    PS_DLOG_INFO(handler, "Unable to optimize image with %d frames.", \
                 gif_file->ImageCount);
    return false;
  }
  const GifWord row = gif_file->Image.Top;
  const GifWord pixel = gif_file->Image.Left;
  const GifWord width = gif_file->Image.Width;
  const GifWord height = gif_file->Image.Height;

  // Validate coordinates.
  if (pixel + width > gif_file->SWidth ||
      row + height > gif_file->SHeight) {
    PS_DLOG_INFO(handler, "Image coordinates outside of resolution.");
    return false;
  }

  // Populate the color map.
  ColorMapObject* color_map =
      gif_file->Image.ColorMap != NULL ?
      gif_file->Image.ColorMap : gif_file->SColorMap;

  if (color_map == NULL) {
    PS_DLOG_INFO(handler, "Failed to find color map.");
    return false;
  }

  if (color_map->ColorCount < 0 || color_map->ColorCount > kNumColorForUint8) {
    PS_DLOG_INFO(handler, "Invalid color count %d", color_map->ColorCount);
    return false;
  }
  for (int i = 0; i < color_map->ColorCount; ++i) {
    palette[i].red = color_map->Colors[i].Red;
    palette[i].green = color_map->Colors[i].Green;
    palette[i].blue = color_map->Colors[i].Blue;
  }
  if (!ProtectedPngSetPlte(png_ptr, info_ptr, palette, color_map->ColorCount)) {
    return false;
  }

  png_bytepp rows = png_get_rows(png_ptr, info_ptr);
  if (gif_file->Image.Interlace == 0) {
    // Not interlaced. Read each line into the PNG buffer.
    for (GifWord i = 0; i < height; ++i) {
      if (DGifGetLine(gif_file,
                      static_cast<GifPixelType*>(&rows[row + i][pixel]),
                      width) == GIF_ERROR) {
        PS_DLOG_INFO(handler, "Failed to DGifGetLine");
        return false;
      }
    }
  } else {
    // Need to deinterlace. The deinterlace code is based on algorithm
    // in giflib.
    for (int i = 0; i < kInterlaceNumPass; ++i) {
      for (int j = row + kInterlaceOffsets[i];
           j < row + height;
           j += kInterlaceJumps[i]) {
        if (DGifGetLine(gif_file,
                        static_cast<GifPixelType*>(&rows[j][pixel]),
                        width) == GIF_ERROR) {
          PS_DLOG_INFO(handler, "Failed to DGifGetLine");
          return false;
        }
      }
    }
  }

  png_set_rows(png_ptr, info_ptr, rows);  // Marks iDAT as valid.
  return true;
}

// Read a GIF extension. There are various extensions. The only one we
// care about is the transparency extension, so we ignore all other
// extensions.
bool ReadExtension(GifFileType* gif_file,
                   png_structp png_ptr,
                   png_infop info_ptr,
                   int* out_transparent_index,
                   MessageHandler* handler) {
  GifByteType* extension = NULL;
  int ext_code = 0;
  if (DGifGetExtension(gif_file, &ext_code, &extension) == GIF_ERROR) {
    PS_DLOG_INFO(handler, "Failed to read extension.");
    return false;
  }

  // We only care about one extension type, the graphics extension,
  // which can contain transparency information.
  if (ext_code == GRAPHICS_EXT_FUNC_CODE) {
    // Make sure that the extension has the expected length.
    if (extension[0] < 4) {
      PS_DLOG_INFO(handler, \
                   "Received graphics extension with unexpected length.");
      return false;
    }
    // The first payload byte contains the flags. Check to see whether the
    // transparency flag is set.
    if ((extension[1] & kTransparentFlag) != 0) {
      if (*out_transparent_index >= 0) {
        // The transparent index has already been set. Ignore new
        // values.
        PS_DLOG_INFO(handler, \
                     "Found multiple transparency entries. Using first entry.");
      } else {
        // We found a transparency entry. The transparent index is in
        // the 4th payload byte.
        *out_transparent_index = extension[4];
      }
    }
  }

  // Some extensions (i.e. the comment extension, the text extension)
  // allow multiple sub-blocks. However, the graphics extension can
  // contain only one sub-block (handled above). Since we only care
  // about the graphics extension, we can safely ignore all subsequent
  // blocks.
  while (extension != NULL) {
    if (DGifGetExtensionNext(gif_file, &extension) == GIF_ERROR) {
      PS_DLOG_INFO(handler, "Failed to read next extension.");
      return false;
    }
  }

  return true;
}

png_uint_32 AllocatePngPixels(png_structp png_ptr,
                              png_infop info_ptr) {
  // Like libpng's png_read_png, we free the row pointers unless they
  // weren't allocated by libpng, in which case we reuse them.
  png_uint_32 row_size = png_get_rowbytes(png_ptr, info_ptr);
  if (row_size == 0) {
    return 0;
  }

  png_free_data(png_ptr, info_ptr, PNG_FREE_ROWS, 0);
  if (png_get_rows(png_ptr, info_ptr) == NULL) {
    png_uint_32 height = png_get_image_height(png_ptr, info_ptr);

    // Allocate the array of pointers to each row.
    const png_size_t row_pointers_size = height * sizeof(png_bytep);
    png_bytepp row_pointers = static_cast<png_bytepp>(
        png_malloc(png_ptr, row_pointers_size));
    memset(row_pointers, 0, row_pointers_size);
    png_set_rows(png_ptr, info_ptr, row_pointers);
    png_data_freer(png_ptr, info_ptr,
                   PNG_DESTROY_WILL_FREE_DATA, PNG_FREE_ROWS);

    // Allocate memory for each row.
    for (png_uint_32 row = 0; row < height; ++row) {
      row_pointers[row] =
          static_cast<png_bytep>(png_malloc(png_ptr, row_size));
    }
  }
  return row_size;
}

bool ExpandColorMap(png_structp paletted_png_ptr,
                    png_infop paletted_info_ptr,
                    png_color* palette,
                    int transparent_palette_index,
                    png_structp rgb_png_ptr,
                    png_infop rgb_info_ptr) {
  png_uint_32 height = png_get_image_height(paletted_png_ptr,
                                            paletted_info_ptr);
  png_uint_32 width = png_get_image_width(paletted_png_ptr,
                                          paletted_info_ptr);
  bool have_alpha = (transparent_palette_index >= 0);
  if (setjmp(png_jmpbuf(rgb_png_ptr))) {
    return false;
  }
  png_set_IHDR(rgb_png_ptr,
               rgb_info_ptr,
               width,
               height,
               8,  // bit depth
               have_alpha ? PNG_COLOR_TYPE_RGB_ALPHA : PNG_COLOR_TYPE_RGB,
               PNG_INTERLACE_NONE,
               PNG_COMPRESSION_TYPE_BASE,
               PNG_FILTER_TYPE_BASE);
  png_uint_32 row_size = AllocatePngPixels(rgb_png_ptr, rgb_info_ptr);
  png_byte bytes_per_pixel = have_alpha ? 4 : 3;

  if (row_size == 0) {
    return false;
  }

  png_bytepp rgb_row_pointers = png_get_rows(rgb_png_ptr, rgb_info_ptr);
  png_bytepp pal_row_pointers = png_get_rows(paletted_png_ptr,
                                             paletted_info_ptr);
  for (png_uint_32 row = 0; row < height; ++row) {
    png_bytep rgb_next_byte = rgb_row_pointers[row];
    if (have_alpha) {
      // Make the row opaque initially.
      memset(rgb_next_byte, 0xff, row_size);
    }
    for (png_uint_32 column = 0; column < width; ++column) {
      png_byte palette_entry = pal_row_pointers[row][column];
      if (have_alpha &&
          (palette_entry == static_cast<png_byte>(transparent_palette_index))) {
        // Transparent: clear RGBA bytes.
        memset(rgb_next_byte, 0x00, bytes_per_pixel);
      } else {
        // Opaque: Copy RGB, keeping A opaque from above
        memcpy(rgb_next_byte, &(palette[palette_entry]), 3);
      }
      rgb_next_byte += bytes_per_pixel;
    }
  }

  // Marks iDAT as valid.
  png_set_rows(rgb_png_ptr, rgb_info_ptr, rgb_row_pointers);
  return true;
}

bool ReadGifToPng(GifFileType* gif_file,
                  png_structp png_ptr,
                  png_infop info_ptr,
                  bool expand_colormap,
                  bool strip_alpha,
                  bool require_opaque,
                  MessageHandler* handler) {
  if (static_cast<png_size_t>(gif_file->SHeight) >
      PNG_UINT_32_MAX/sizeof(png_bytep)) {
    PS_DLOG_INFO(handler, "GIF image is too big to process.");
    return false;
  }

  // If expand_colormap is true, the color-indexed GIF_file is read
  // into paletted_png, and then transformed into a bona fide RGB(A)
  // PNG in png_ptr and info_ptr. If expand_colormap is false, we just
  // read the color-indexed GIF file directly into png_ptr and
  // info_ptr.
  net_instaweb::scoped_ptr<ScopedPngStruct> paletted_png;
  png_structp paletted_png_ptr = NULL;
  png_infop paletted_info_ptr = NULL;

  if (expand_colormap) {
    // We read the image into a separate struct before expanding the
    // colormap.
    paletted_png.reset(new ScopedPngStruct(ScopedPngStruct::READ, handler));
    if (!paletted_png->valid()) {
      PS_LOG_DFATAL(handler, "Invalid ScopedPngStruct r: %d", \
                    paletted_png->valid());
      return false;
    }
    paletted_png_ptr = paletted_png->png_ptr();
    paletted_info_ptr = paletted_png->info_ptr();
  } else {
    // We read the image directly into the pointers that was passed in.
    paletted_png_ptr = png_ptr;
    paletted_info_ptr = info_ptr;
  }

  if (!ProtectedPngSetIhdr(paletted_png_ptr,
                           paletted_info_ptr,
                           gif_file->SWidth,
                           gif_file->SHeight,
                           8,  // bit depth
                           PNG_COLOR_TYPE_PALETTE,
                           PNG_INTERLACE_NONE,
                           PNG_COMPRESSION_TYPE_BASE,
                           PNG_FILTER_TYPE_BASE)) {
    return false;
  }

  png_uint_32 row_size = AllocatePngPixels(paletted_png_ptr, paletted_info_ptr);
  if (row_size == 0) {
    return false;
  }

  // Fill the rows with the background color.
  png_bytepp row_pointers = png_get_rows(paletted_png_ptr, paletted_info_ptr);
  memset(row_pointers[0], gif_file->SBackGroundColor, row_size);
  png_uint_32 height = png_get_image_height(paletted_png_ptr,
                                            paletted_info_ptr);
  for (png_uint_32 row = 1; row < height; ++row) {
    memcpy(row_pointers[row],
           row_pointers[0],
           row_size);
  }

  int transparent_palette_index = -1;
  bool found_terminator = false;
  png_color palette[kNumColorForUint8];
  while (!found_terminator) {
    GifRecordType record_type = UNDEFINED_RECORD_TYPE;
    if (DGifGetRecordType(gif_file, &record_type) == GIF_ERROR) {
      PS_DLOG_INFO(handler, "Failed to read GifRecordType");
      return false;
    }
    switch (record_type) {
      case IMAGE_DESC_RECORD_TYPE:
        if (!ReadImageDescriptor(gif_file, paletted_png_ptr,
                                 paletted_info_ptr, palette, handler)) {
          return false;
        }
        break;

      case EXTENSION_RECORD_TYPE:
        if (!ReadExtension(gif_file,
                           paletted_png_ptr,
                           paletted_info_ptr,
                           &transparent_palette_index,
                           handler)) {
          return false;
        }
        break;

      case TERMINATE_RECORD_TYPE:
        found_terminator = true;
        break;

      default:
        PS_DLOG_INFO(handler, "Found unexpected record type %d", record_type);
        return false;
    }
  }

  // Process transparency.
  if (transparent_palette_index >= 0) {
    if (require_opaque) {
      return false;
    }
    // If the GIF contained a transparency index and we're not
    // stripping alpha, then add it to the PNG now if we're returning
    // a paletted image. If we're not, don't bother since we have all
    // the information we need for ExpandColorMap below.
    if (!strip_alpha && !expand_colormap) {
      if (!AddTransparencyChunk(paletted_png_ptr, paletted_info_ptr,
                                transparent_palette_index, handler)) {
        return false;
      }
    }
  }

  if (expand_colormap) {
    // Generate the non-paletted PNG data into the pointers that were
    // passed in.
    if (!ExpandColorMap(paletted_png_ptr, paletted_info_ptr,
                        palette,
                        strip_alpha ? -1 : transparent_palette_index,
                        png_ptr, info_ptr)) {
      return false;
    }
  }
  return true;
}

}  // namespace

namespace pagespeed {

namespace image_compression {

const int GifFrameReader::kNoTransparentIndex = -1;

// Utility to skip over the extra subblocks in a GIF file extension so
// we leave the file in a state where we've finished processing this
// extension and are ready to begin parsing another.
ScanlineStatus SkipOverGifExtensionSubblocks(GifFileType* gif_file,
                                             GifByteType* extension,
                                             MessageHandler* message_handler) {
  while (extension != NULL) {
    if (DGifGetExtensionNext(gif_file, &extension) == GIF_ERROR) {
      return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler,
                              SCANLINE_STATUS_PARSE_ERROR,
                              FRAME_GIFREADER,
                              "Failed to read next extension.");
    }
  }
  return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}

// Utility to expand a color index from a palette.
void ExpandColorIndex(const ColorMapObject* colormap,
                      const int color_index,
                      PixelRgbaChannels rgba) {
  if ((colormap != NULL) &&
      (colormap->Colors != NULL) &&
      (color_index < colormap->ColorCount)) {
    const GifColorType color = colormap->Colors[color_index];
    rgba[RGBA_RED] = color.Red;
    rgba[RGBA_GREEN] = color.Green;
    rgba[RGBA_BLUE] = color.Blue;
    rgba[RGBA_ALPHA] = kAlphaOpaque;
  }
}

GifReader::GifReader(MessageHandler* handler)
    : message_handler_(handler) {
}

GifReader::~GifReader() {
}

bool GifReader::ReadPng(const GoogleString& body,
                        png_structp png_ptr,
                        png_infop info_ptr,
                        int transforms,
                        bool require_opaque) const {
  int allowed_transforms =
      // These transforms are no-ops when reading a .gif file.
      PNG_TRANSFORM_STRIP_16 |
      PNG_TRANSFORM_GRAY_TO_RGB |
      // We implement this transform explicitly.
      PNG_TRANSFORM_EXPAND |
      // We implement this transform explicitly, regardless of require_opaque.
      PNG_TRANSFORM_STRIP_ALPHA;

  if ((transforms & ~allowed_transforms) != 0) {
    PS_LOG_DFATAL(message_handler_, "Unsupported transform %d", transforms);
    return false;
  }

  bool expand_colormap = ((transforms & PNG_TRANSFORM_EXPAND) != 0);
  bool strip_alpha = ((transforms & PNG_TRANSFORM_STRIP_ALPHA) != 0);

  // Wrap the resource's response body in a structure that keeps a
  // pointer to the body and a read offset, and pass a pointer to this
  // object as the user data to be received by the GIF read function.
  ScanlineStreamInput input(message_handler_);
  input.Initialize(body);

#if GIFLIB_MAJOR < 5
  GifFileType* gif_file = DGifOpen(&input, ReadGifFromStream);
#else
  GifFileType* gif_file = DGifOpen(&input, ReadGifFromStream, NULL);
#endif
  if (gif_file == NULL) {
    return false;
  }

  bool result = ReadGifToPng(gif_file, png_ptr, info_ptr,
                             expand_colormap, strip_alpha,
                             require_opaque, message_handler_);
  if (DGifCloseFile(gif_file, NULL) == GIF_ERROR) {
    PS_DLOG_INFO(message_handler_, "Failed to close GIF.");
  }

  return result;
}

bool GifReader::GetAttributes(const GoogleString& body,
                              int* out_width,
                              int* out_height,
                              int* out_bit_depth,
                              int* out_color_type) const {
  // We need the length of the magic bytes (GIF_STAMP_LEN), plus 2
  // bytes for width, plus 2 bytes for height.
  const size_t kGifMinHeaderSize = GIF_STAMP_LEN + 2 + 2;
  if (body.size() < kGifMinHeaderSize) {
    return false;
  }

  // Make sure this looks like a GIF. Either GIF87a or GIF89a.
  if (strncmp(GIF_STAMP, body.data(), GIF_VERSION_POS) != 0) {
    return false;
  }
  const unsigned char* body_data =
      reinterpret_cast<const unsigned char*>(body.data());
  const unsigned char* width_data = body_data + GIF_STAMP_LEN;
  const unsigned char* height_data = width_data + 2;

  *out_width =
      (static_cast<unsigned int>(width_data[1]) << 8) + width_data[0];
  *out_height =
      (static_cast<unsigned int>(height_data[1]) << 8) + height_data[0];

  // GIFs are always 8 bits per channel, paletted images.
  *out_bit_depth = 8;
  *out_color_type = PNG_COLOR_TYPE_PALETTE;
  return true;
}

class ScopedGifStruct {
 public:
  explicit ScopedGifStruct(MessageHandler* handler) :
      gif_file_(NULL),
      message_handler_(handler),
      gif_input_(ScanlineStreamInput(handler)) {
  }

  ~ScopedGifStruct() {
    ScanlineStatus status;
    Reset(&status);
    LOG_IF(ERROR, !status.Success());
  }

  bool Initialize(const void* image_buffer,
                  size_t buffer_length,
                  ScanlineStatus* status) {
    if (Reset(status)) {
      gif_input_.Initialize(image_buffer, buffer_length);
      return InitializeGifFile(status);
    }
    return false;
  }

  bool Initialize(const std::string& image_string, ScanlineStatus* status) {
    if (Reset(status)) {
      gif_input_.Initialize(image_string);
      return InitializeGifFile(status);
    }
    return false;
  }

  bool Reset(ScanlineStatus* status) {
    if (gif_file_ != NULL) {
      if (DGifCloseFile(gif_file_, NULL) == GIF_ERROR) {
        *status = PS_LOGGED_STATUS(PS_LOG_INFO,
                                   message_handler_,
                                   SCANLINE_STATUS_INTERNAL_ERROR,
                                   FRAME_GIFREADER,
                                   "Failed to close GIF file.");
        return false;
      }
      gif_file_ = NULL;
    }
    gif_input_.Reset();
    *status = ScanlineStatus(SCANLINE_STATUS_SUCCESS);
    return true;
  }

  GifFileType* gif_file() { return gif_file_; }
  size_t offset() { return gif_input_.offset(); }
  void set_offset(size_t val) { gif_input_.set_offset(val); }

 private:
  bool InitializeGifFile(ScanlineStatus* status) {
#if GIFLIB_MAJOR < 5
    gif_file_ = DGifOpen(&gif_input_, ReadGifFromStream);
#else
    gif_file_ = DGifOpen(&gif_input_, ReadGifFromStream, NULL);
#endif

    if (gif_file_ == NULL) {
      *status = PS_LOGGED_STATUS(PS_LOG_INFO,
                                 message_handler_,
                                 SCANLINE_STATUS_INTERNAL_ERROR,
                                 FRAME_GIFREADER,
                                 "Failed to open GIF file.");
      return false;
    }
    *status = ScanlineStatus(SCANLINE_STATUS_SUCCESS);
    return true;
  }

 private:
  GifFileType* gif_file_;
  MessageHandler* message_handler_;
  ScanlineStreamInput gif_input_;
};

FrameSpec::DisposalMethod GifDisposalToFrameSpecDisposal(int gif_disposal) {
  if ((gif_disposal > FrameSpec::DISPOSAL_UNKNOWN) &&
      (gif_disposal <= FrameSpec::DISPOSAL_RESTORE)) {
    return static_cast<FrameSpec::DisposalMethod>(gif_disposal);
  }
  return FrameSpec::DISPOSAL_NONE;
}

GifFrameReader::GifFrameReader(MessageHandler* handler)
    : MultipleFrameReader(handler) {
  Reset();
}

GifFrameReader::~GifFrameReader() {
}

ScanlineStatus GifFrameReader::Reset() {
  image_initialized_ = false;
  frame_initialized_ = false;

  image_spec_.Reset();
  frame_spec_.Reset();

  has_loop_count_ = false;
  next_frame_ = 0;

  next_row_ = 0;
  frame_transparent_index_ = kNoTransparentIndex;

  // Note that gif_struct and gif_palette_ are allocated once in the
  // first call to Initialize(), and re-used across Reset()s

  // Note that frame_buffer_ and frame_index_ are allocated each time
  // PrepareNextFrame() is called.

  ScanlineStatus status(SCANLINE_STATUS_SUCCESS);
  if (gif_struct_.get() != NULL) {
    gif_struct_->Reset(&status);
  }

  frame_palette_size_ = 0;
  frame_eagerly_read_ = false;

  return status;
}

ScanlineStatus GifFrameReader::ProcessExtensionAffectingFrame() {
  //
  // For more information on the format of the Graphics Control
  // Extension (GCE) refer to
  // http://www.w3.org/Graphics/GIF/spec-gif89a.txt
  // http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp#graphics_control_extension_block

  // Expected values, masks, shifts for the various fields in the
  // Graphics Control Extension.
  static const int kGifGceExpectedSize = 4;
  static const int kGifGceDisposeMask = 0x07;
  static const int kGifGceDisposeShift = 2;
  static const int kGifGceTransparentMask = 0x01;

  // Indices of fields in the Graphics Control Extension.
  static const int kGifGceSizeIndex = 0;
  static const int kGifGceFlagsIndex = 1;
  static const int kGifGceDelayLoIndex = 2;
  static const int kGifGceDelayHiIndex = 3;
  static const int kGifGceTransparentIndexIndex = 4;

  GifFileType* gif_file = gif_struct_->gif_file();

  GifByteType* extension = NULL;
  int ext_code = 0;
  if (DGifGetExtension(gif_file, &ext_code, &extension) == GIF_ERROR) {
    return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                            SCANLINE_STATUS_PARSE_ERROR,
                            FRAME_GIFREADER,
                            "Failed to read extension.");
  }

  if (ext_code == GRAPHICS_EXT_FUNC_CODE) {
    if (extension[kGifGceSizeIndex] != kGifGceExpectedSize) {
      return PS_LOGGED_STATUS(
          PS_LOG_INFO, message_handler(),
          SCANLINE_STATUS_PARSE_ERROR,
          FRAME_GIFREADER,
          "Received graphics extension with unexpected length.");
    }
    const int flags = extension[kGifGceFlagsIndex];
    const int dispose = (flags >> kGifGceDisposeShift) & kGifGceDisposeMask;
    const int delay =
        extension[kGifGceDelayLoIndex] |
        (extension[kGifGceDelayHiIndex] << 8);  // In 10 ms units.
    frame_spec_.duration_ms = delay * 10;

    FrameSpec::DisposalMethod frame_dispose =
        GifDisposalToFrameSpecDisposal(dispose);
    if (frame_dispose != FrameSpec::DISPOSAL_UNKNOWN) {
      frame_spec_.disposal = frame_dispose;
    } else {
      // For non-animated images, the disposal method does not
      // matter, so just note errors and move on. For animated
      // images, an unrecognized disposal methods constitutes an
      // error.
      if (image_spec_.num_frames == 1) {
        PS_LOG_INFO(message_handler(),
                    "Unrecognized disposal method %d.", dispose);
        frame_spec_.disposal = FrameSpec::DISPOSAL_NONE;
      } else {
        return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                SCANLINE_STATUS_PARSE_ERROR,
                                FRAME_GIFREADER,
                                "Unrecognized disposal method %d.", dispose);
      }
    }

    // Gif89a standard permits multiple Graphic Control Extensions
    // (corresponding to EXTENSION_RECORD_TYPE) in a GIF file. Each
    // Graphic Control Extension only affects the Image Extension or
    // Text Extension which immediately follows it. There may be
    // Text Extensions in the GIF file, too. Thus, we want to get
    // the Control Extensions preceding the next Image Extension.
    //
    // Reference: http://www.w3.org/Graphics/GIF/spec-gif89a.txt

    // In case the next record is an Image Extension (i.e. the
    // next frame), get the transparent index.
    frame_transparent_index_ =
        ((flags & kGifGceTransparentMask) ?
         extension[kGifGceTransparentIndexIndex] : kNoTransparentIndex);
  }

  // We skip all other extension block types, such as
  // APPLICATION_EXT_FUNC_CODE, COMMENT_EXT_FUNC_CODE,
  // PLAINTEXT_EXT_FUNC_CODE.

  return SkipOverGifExtensionSubblocks(gif_file, extension, message_handler());
}

ScanlineStatus GifFrameReader::ProcessExtensionAffectingImage(
    bool past_first_frame) {
  // For more information on the format of the Application Extension
  // (AE) block refer to
  // http://shortn/_19L2jvGJc9

  // Expected values for the various fields in the Application
  // Extension block,
  static const char* kGifAeIdentifier = "NETSCAPE2.0";
  static const int kGifAeIdentifierLength = strlen(kGifAeIdentifier);
  static const int kGifAeLoopCountExpectedLength = 3;
  static const int kGifAeLoopCountExpectedFixedConst = 1;

  // Indices of fields in the Application Extension block,
  static const int kGifAeIdentifierLengthIndex = 0;
  static const int kGifAeLoopCountLengthIndex = 0;
  static const int kGifAeLoopCountFixedConstIndex = 1;
  static const int kGifAeLoopCountLoIndex = 2;
  static const int kGifAeLoopCountHiIndex = 3;

  GifFileType* gif_file = gif_struct_->gif_file();
  GifByteType* extension = NULL;
  int ext_code = 0;

  if (DGifGetExtension(gif_file, &ext_code, &extension) == GIF_ERROR) {
    return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                            SCANLINE_STATUS_PARSE_ERROR,
                            FRAME_GIFREADER,
                            "Failed to read extension.");
  }

  if  (ext_code == APPLICATION_EXT_FUNC_CODE) {
    if (extension == NULL) {
      return PS_LOGGED_STATUS(
          PS_LOG_INFO, message_handler(),
          SCANLINE_STATUS_PARSE_ERROR,
          FRAME_GIFREADER,
          "NULL Application Extension Block.");
    }
    if (extension[kGifAeIdentifierLengthIndex] != kGifAeIdentifierLength) {
      return PS_LOGGED_STATUS(
          PS_LOG_INFO, message_handler(),
          SCANLINE_STATUS_PARSE_ERROR,
          FRAME_GIFREADER,
          "Application extension block size has unexpected size.");
    }
    if (!memcmp(extension + 1, kGifAeIdentifier, kGifAeIdentifierLength)) {
      // Recognize and parse Netscape2.0 NAB extension for loop count.
      if (DGifGetExtensionNext(gif_file, &extension) == GIF_ERROR) {
        return PS_LOGGED_STATUS(
            PS_LOG_INFO, message_handler(),
            SCANLINE_STATUS_PARSE_ERROR,
            FRAME_GIFREADER,
            "DGifGetExtensionNext failed while trying to get loop count");
      }
      if (((extension[kGifAeLoopCountLengthIndex] !=
            kGifAeLoopCountExpectedLength) &&
           (extension[kGifAeLoopCountFixedConstIndex] !=
            kGifAeLoopCountExpectedFixedConst))) {
        return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                SCANLINE_STATUS_PARSE_ERROR,
                                FRAME_GIFREADER,
                                "animation loop count: wrong size/marker");
      }
      if (past_first_frame) {
        // This is allowed by the spec but appears to be rare in practice.
        PS_LOG_INFO(message_handler(),
                    "Animation loop count in unexpected location.");
      }
      if (has_loop_count_) {
        PS_LOG_INFO(
            message_handler(),
            "Multiple loop counts encountered. Using the last one.");
      }
      has_loop_count_ = true;
      image_spec_.loop_count = (extension[kGifAeLoopCountLoIndex] |
                                (extension[kGifAeLoopCountHiIndex] << 8));
    } else {
      // An extension containing metadata.
      // Do nothing for now.
    }
  }

  return SkipOverGifExtensionSubblocks(gif_file, extension, message_handler());
}

ScanlineStatus GifFrameReader::Initialize() {
  if (image_buffer_ == NULL) {
    return PS_LOGGED_STATUS(PS_LOG_DFATAL, message_handler(),
                            SCANLINE_STATUS_INVOCATION_ERROR,
                            FRAME_GIFREADER,
                            "null or empty image buffer.");
  }
  if (image_initialized_) {
    // Reset the reader if it has been initialized before.
    Reset();
  } else {
    // Allocate and initialize gif_struct_, if that has not been done.
    if (gif_struct_ == NULL) {
      gif_struct_.reset(new ScopedGifStruct(message_handler()));
      if (gif_struct_ == NULL) {
        return PS_LOGGED_STATUS(PS_LOG_ERROR, message_handler(),
                                SCANLINE_STATUS_MEMORY_ERROR,
                                FRAME_GIFREADER,
                                "Failed to allocate ScopedGifStruct.");
      }
    }
    // Allocate and initialize gif_palette_, if that has not been done.
    if (gif_palette_ == NULL) {
      gif_palette_.reset(new PaletteRGBA[kGifPaletteSize]);
      if (gif_palette_ == NULL) {
        return PS_LOGGED_STATUS(PS_LOG_ERROR, message_handler(),
                                SCANLINE_STATUS_MEMORY_ERROR,
                                FRAME_GIFREADER,
                                "Failed to allocate PaletteRGBA.");
      }
    }
  }

  ScanlineStatus status(SCANLINE_STATUS_SUCCESS);
  if (gif_struct_->Initialize(image_buffer_, buffer_length_, &status)) {
    status = GetImageData();
  }
  if (!status.Success()) {
    Reset();
    return status;
  }

  next_frame_ = 0;
  image_initialized_ = true;
  return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}

// Helper function for GetImageData
inline void ClearGifColorMap(GifFileType* gif_file) {
#if GIFLIB_MAJOR < 5
  // For each frame, we call DGifGetImageDesc, and that always
  // allocates a new Image.ColorMap without freeing any previous color
  // map stored there; in fact, the only free of that data field
  // occurs in DGifCloseFile. Thus, to prevent a memory leak, we need
  // to clear it manually before each call to DGifGetImageDesc.
  if (gif_file->Image.ColorMap != NULL) {
    FreeMapObject(gif_file->Image.ColorMap);
    gif_file->Image.ColorMap = NULL;
  }
#endif
}

// TODO(vchudnov): Consider simplifying this function so that it only
// determines whether there's more than one frame, rather than the
// number of frames. This would avoid having to scan the whole file
// here and again when we read pixels. Doing this would involve moving
// the logic to detect more frames in the stream to HasMoreFrames(),
// and updating image_spec dynamically. Cons: if, in the wild, the
// Applications Extension Blocks are found anywhere in the file (as
// seems to be allowed by the spec), then we need to scan through the
// whole file anyway.
ScanlineStatus GifFrameReader::GetImageData() {
  GifFileType* gif_file = gif_struct_->gif_file();
  size_t offset = gif_struct_->offset();
  image_spec_.width =  gif_file->SWidth;
  image_spec_.height = gif_file->SHeight;

  GifRecordType record_type = UNDEFINED_RECORD_TYPE;
  while (record_type != TERMINATE_RECORD_TYPE) {
    DVLOG(1) << "GetImageData: offset: ", gif_struct_->offset();

    if (DGifGetRecordType(gif_file, &record_type) == GIF_ERROR) {
      return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                              SCANLINE_STATUS_PARSE_ERROR,
                              FRAME_GIFREADER,
                              "DGifGetRecordType()");
    }
    switch (record_type) {
      case IMAGE_DESC_RECORD_TYPE:
        DVLOG(1) << "GetImageData: IMAGE_DESC";

        ClearGifColorMap(gif_file);
        if (DGifGetImageDesc(gif_file) == GIF_ERROR) {
          return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                  SCANLINE_STATUS_PARSE_ERROR,
                                  FRAME_GIFREADER,
                                  "DGifGetImageDesc()");
        }

        ++image_spec_.num_frames;
        if (image_spec_.num_frames == 1) {
          // We need the first frame info for the quirks mode.
          frame_spec_.top = gif_file->Image.Top;
          frame_spec_.left = gif_file->Image.Left;
          frame_spec_.height = gif_file->Image.Height;
          frame_spec_.width = gif_file->Image.Width;
        }

        // Bypass the pixel data without decoding it.
        int code_size;
        GifByteType* code_block;
        if (DGifGetCode(gif_file, &code_size, &code_block) == GIF_ERROR) {
          return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                  SCANLINE_STATUS_PARSE_ERROR,
                                  FRAME_GIFREADER,
                                  "DGifGetCode()");
        }
        while (code_block != NULL) {
          if (DGifGetCodeNext(gif_file, &code_block) == GIF_ERROR) {
            return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                    SCANLINE_STATUS_PARSE_ERROR,
                                    FRAME_GIFREADER,
                                    "DGifGetCodeNext()");
          }
        }

        break;

      case EXTENSION_RECORD_TYPE: {
        DVLOG(1) << "GetImageData: EXTENSION";
        ScanlineStatus status =
            ProcessExtensionAffectingImage(image_spec_.num_frames > 0);
        if (!status.Success()) {
          return status;
        }
        break;
      }

      case TERMINATE_RECORD_TYPE:
        DVLOG(1) << "GetImageData: TERMINATE";
        break;

      default:
        return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                SCANLINE_STATUS_PARSE_ERROR,
                                FRAME_GIFREADER,
                                "unexpected record %d",
                                record_type);
        break;
    }
  }

  ApplyQuirksModeToImage(quirks_mode(), has_loop_count_, frame_spec_,
                         &image_spec_);

  // Get the global color map, and extract and store the background
  // color from it.
  //
  // TODO(vchudnov): The GIF spec implies there could be more than one
  // global color map in a file. Each global color map replaces any
  // preceding one and applies to the image descriptors following
  // (unless they have a local color map, of course). Find out how
  // giflib deals with this, how often it occurs in practice, and, if
  // necessary, accommodate this situation in code.
  ExpandColorIndex(gif_file->SColorMap,
                   gif_file->SBackGroundColor,
                   image_spec_.bg_color);
  // The GIF background color seems to not be interpreted by Chrome,
  // Firefox, and other image viewers, so regardless of whether we
  // found a background color or not, we don't use it.
  image_spec_.use_bg_color = false;

  gif_struct_->set_offset(offset);
  return ScanlineStatus(SCANLINE_STATUS_SUCCESS);;
}

ScanlineStatus GifFrameReader::set_quirks_mode(QuirksMode quirks_mode) {
  if (image_initialized_) {
    return PS_LOGGED_STATUS(PS_LOG_DFATAL, message_handler(),
                            SCANLINE_STATUS_INVOCATION_ERROR,
                            FRAME_GIFREADER,
                            "Can't change quirks mode for initialized image.");
  }
  return MultipleFrameReader::set_quirks_mode(quirks_mode);
}

void GifFrameReader::ApplyQuirksModeToImage(QuirksMode quirks_mode,
                                            const bool got_loop_count,
                                            const FrameSpec& frame_spec,
                                            ImageSpec* image_spec) {
  bool clear_bg_color = false;

  switch (quirks_mode) {
    case QUIRKS_CHROME: {
      // Based on Chrome 38 behavior on Linux.
      bool image_smaller_than_first_frame = (
          (frame_spec.width > image_spec->width) ||
          (frame_spec.height > image_spec->height));

      if (image_smaller_than_first_frame) {
        image_spec->image_size_adjusted = true;

        image_spec->width = frame_spec.width;
        image_spec->height = frame_spec.height;

        clear_bg_color = true;
      }

      if (got_loop_count) {
        image_spec->loop_count++;
      }
      break;
    }
    case QUIRKS_FIREFOX: {
      // Based on Firefox 32 behavior on Linux.
      bool image_and_frame_match = (
          (frame_spec.width == image_spec->width) &&
          (frame_spec.height == image_spec->height) &&
          (frame_spec.left == 0) &&
          (frame_spec.top == 0));
      if (!image_and_frame_match) {
        clear_bg_color = true;
      }
      break;
    }
    case QUIRKS_NONE:
      // We enumerate all cases and purposefully exclude the default
      // label so compiler will complain about unhandled values.
      break;
  }

  if (clear_bg_color) {
    image_spec->bg_color[RGBA_ALPHA] = kAlphaTransparent;
    image_spec->use_bg_color = true;

    // To maximize compression, make the other channels all be zero.
    image_spec->bg_color[RGBA_RED] = 0;
    image_spec->bg_color[RGBA_GREEN] = 0;
    image_spec->bg_color[RGBA_BLUE] = 0;
  }
}

void GifFrameReader::ApplyQuirksModeToFirstFrame(const QuirksMode quirks_mode,
                                                 const ImageSpec& image_spec,
                                                 FrameSpec* frame_spec) {
  switch (quirks_mode) {
    case QUIRKS_CHROME: {
      // Based on Chrome 38 behavior on Linux.
      bool first_frame_zero_dimension = (
          (frame_spec->width == 0) || (frame_spec->height == 0));

      if (first_frame_zero_dimension) {
        frame_spec->width = image_spec.width;
        frame_spec->height = image_spec.height;
        frame_spec->left = 0;
        frame_spec->top = 0;
      }

      if (image_spec.image_size_adjusted) {
        frame_spec->left = 0;
        frame_spec->top = 0;
      }

      break;
    }
    case QUIRKS_FIREFOX: {
      // Based on Firefox 32 behavior on Linux.
      bool image_and_frame_match = (
          (frame_spec->width == image_spec.width) &&
          (frame_spec->height == image_spec.height) &&
          (frame_spec->left == 0) &&
          (frame_spec->top == 0));
      if (!image_and_frame_match) {
        frame_spec->height = 0;
        frame_spec->width = 0;
        frame_spec->top = 0;
        frame_spec->left = 0;
      }
      break;
    }

    case QUIRKS_NONE:
      // We enumerate all cases and purposefully exclude the default
      // label so compiler will complain about unhandled values.
      break;
  }
}

ScanlineStatus GifFrameReader::CreateColorMap() {
  GifFileType* gif_file = gif_struct_->gif_file();

  // Populate the color map.
  ColorMapObject* color_map =
      gif_file->Image.ColorMap != NULL ?
      gif_file->Image.ColorMap : gif_file->SColorMap;
  if (color_map == NULL) {
    return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                            SCANLINE_STATUS_INTERNAL_ERROR,
                            FRAME_GIFREADER,
                            "missing colormap in image and screen");
  }

  GifColorType* palette_in = color_map->Colors;
  if (palette_in == NULL) {
    return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                            SCANLINE_STATUS_INTERNAL_ERROR,
                            FRAME_GIFREADER,
                            "Could not find colormap in the GIF image.");
  }
  if (color_map->ColorCount > kGifPaletteSize) {
    return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                            SCANLINE_STATUS_INTERNAL_ERROR,
                            FRAME_GIFREADER,
                            "ColorCount is too large: %d",
                            color_map->ColorCount);
  }

  frame_palette_size_ = color_map->ColorCount;
  for (int i = 0; i < frame_palette_size_; ++i) {
    gif_palette_[i].red_ = palette_in[i].Red;
    gif_palette_[i].green_ = palette_in[i].Green;
    gif_palette_[i].blue_ = palette_in[i].Blue;
    gif_palette_[i].alpha_ = kAlphaOpaque;
  }
  // Set any out-of-range palette entries to be completely transparent
  // in case any pixel references them.
  memset(gif_palette_.get() + frame_palette_size_,
         kAlphaTransparent,
         (kGifPaletteSize - frame_palette_size_) * sizeof(PaletteRGBA));

  // Process the transparency information. The output format will be
  // RGBA if the transparent color has been specified, or RGB
  // otherwise. Note that we allow frame_transparent_index_ to be
  // greater than frame_palette_size_, since the GIF spec does not
  // explicitly say the index has to be in range (and in some images,
  // it's not); this allows us to set RGBA_8888 immediately and thus
  // skip the check for out-of-range pixels in PrepareNextFrame().
  //
  // Note, too, that frame_transparent_index_ either is
  // kNoTransparentIndex or has a value in the range
  // [0, kGifPaletteSize), since it is obtained from a single byte in
  // the GIF file.
  if (frame_transparent_index_ >= 0) {
    frame_spec_.pixel_format = RGBA_8888;
    if (frame_transparent_index_ < frame_palette_size_) {
      // Set transparent index's color to be transparent.
      memset(&(gif_palette_[frame_transparent_index_]),
             kAlphaTransparent,
             sizeof(PaletteRGBA));
    }
  } else {
    frame_spec_.pixel_format = RGB_888;
  }

  return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}

ScanlineStatus GifFrameReader::DecodeProgressiveGif() {
  // The deinterlace code is based on the algorithm in giflib.
  GifFileType* gif_file = gif_struct_->gif_file();
  for (int pass = 0; pass < kInterlaceNumPass; ++pass) {
    for (size_px y = kInterlaceOffsets[pass];
         y < frame_spec_.height;
         y += kInterlaceJumps[pass]) {
      GifPixelType* row_pointer = frame_index_.get() + y * frame_spec_.width;
      if (DGifGetLine(gif_file, row_pointer, frame_spec_.width) == GIF_ERROR) {
        return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                SCANLINE_STATUS_INTERNAL_ERROR, FRAME_GIFREADER,
                                "DGifGetLine()");
      }
    }
  }
  return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}

ScanlineStatus GifFrameReader::DecodeNonProgressiveGif() {
  GifFileType* gif_file = gif_struct_->gif_file();
  GifPixelType* row_pointer = frame_index_.get();
  const GifPixelType* row_pointer_end =
      frame_index_.get() + frame_spec_.height * frame_spec_.width;
  for (; row_pointer < row_pointer_end; row_pointer += frame_spec_.width) {
      if (DGifGetLine(gif_file, row_pointer, frame_spec_.width) == GIF_ERROR) {
        return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                SCANLINE_STATUS_INTERNAL_ERROR, FRAME_GIFREADER,
                                "DGifGetLine()");
      }
  }
  return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}

// Helper function for PrepareNextFrame(). This returns true if any of
// the 'num_pixel' pixel entries starting at 'px' reference a palette
// value greater than 'frame_palette_size'.
inline bool FrameHasOutOfRangePixels(GifPixelType* px,
                                     const size_px num_pixels,
                                     const int frame_palette_size) {
  const GifPixelType* end_px = px + num_pixels;
  for (; px < end_px; ++px) {
    if ((*px) >= frame_palette_size) {
      return true;
    }
  }
  return false;
}

ScanlineStatus GifFrameReader::PrepareNextFrame() {
  // Consume any remaining scanlines
  const void* out_bytes;
  ScanlineStatus status;
  while (HasMoreScanlines()) {
    if (!MultipleFrameReader::ReadNextScanline(&out_bytes, &status)) {
      return status;
    }
  }

  frame_initialized_ = false;
  frame_spec_.Reset();
  frame_transparent_index_ = kNoTransparentIndex;

  if (next_frame_ >= image_spec_.num_frames) {
    return PS_LOGGED_STATUS(PS_LOG_DFATAL, message_handler(),
                            SCANLINE_STATUS_INVOCATION_ERROR,
                            FRAME_GIFREADER,
                            "PrepareNextFrame: no more frames.");
  }

  GifFileType* gif_file = gif_struct_->gif_file();
  bool found_frame = false;

  while (!found_frame) {
    DVLOG(1) << "PrepareNextFrame: offset: ", gif_struct_->offset();

    GifRecordType record_type = UNDEFINED_RECORD_TYPE;
    if (DGifGetRecordType(gif_file, &record_type) == GIF_ERROR) {
      return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                              SCANLINE_STATUS_PARSE_ERROR,
                              FRAME_GIFREADER,
                              "DGifGetRecordType()");
    }

    switch (record_type) {
      case IMAGE_DESC_RECORD_TYPE: {
        DVLOG(1) << "PrepareNextFrame: IMAGE_DESC";
        ClearGifColorMap(gif_file);
        if (DGifGetImageDesc(gif_file) == GIF_ERROR) {
          return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                  SCANLINE_STATUS_PARSE_ERROR,
                                  FRAME_GIFREADER,
                                  "DGifGetImageDesc()");
        }
        frame_spec_.top = gif_file->Image.Top;
        frame_spec_.left = gif_file->Image.Left;
        frame_spec_.height = gif_file->Image.Height;
        frame_spec_.width = gif_file->Image.Width;
        if (next_frame_ == 0) {
          ApplyQuirksModeToFirstFrame(quirks_mode(), image_spec_, &frame_spec_);
        }
        ++next_frame_;
        found_frame = true;
        break;
      }

      case EXTENSION_RECORD_TYPE: {
        DVLOG(1) << "PrepareNextFrame: EXTENSION";
        ScanlineStatus status = ProcessExtensionAffectingFrame();
        if (!status.Success()) {
          return status;
        }
        break;
      }

      case TERMINATE_RECORD_TYPE: {
        DVLOG(1) << "PrepareNextFrame: TERMINATE";
        return PS_LOGGED_STATUS(
            PS_LOG_INFO, message_handler(),
            SCANLINE_STATUS_INTERNAL_ERROR,
            FRAME_GIFREADER,
            "PrepareNextFrame: expected to find the next frame, failed.");
        break;
      }

      default: {
        return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                                SCANLINE_STATUS_PARSE_ERROR,
                                FRAME_GIFREADER,
                                "unexpected record %d",
                                record_type);
        break;
      }
    }
  }

  status = CreateColorMap();

  if (!status.Success()) {
    Reset();
    return status;
  }

  // For a progressive frame, we have to decode the entire frame
  // before rendering any row. Moreover, for each frame in
  // non-RGBA_8888 format, we need to check all the pixels and change
  // frame_spec_.pixel_format to RGBA_8888 if any out-of-range entries
  // are found (since they will be converted to transparent pixels in
  // ReadNextScanline()).
  frame_spec_.hint_progressive = (gif_file->Image.Interlace != 0);
  const bool is_originally_rgba = (frame_spec_.pixel_format == RGBA_8888);
  frame_eagerly_read_ = frame_spec_.hint_progressive || !is_originally_rgba;

  if (!frame_eagerly_read_) {
    // We only read one row at a time.
    frame_index_.reset(new GifPixelType[frame_spec_.width]);
  } else {
    // We need to read all the rows before the first call to
    // ReadNextScanline().
    frame_index_.reset(new GifPixelType[frame_spec_.width *
                                        frame_spec_.height]);
  }
  if (frame_index_ == NULL) {
    Reset();
    return PS_LOGGED_STATUS(PS_LOG_ERROR, message_handler(),
                            SCANLINE_STATUS_MEMORY_ERROR,
                            FRAME_GIFREADER,
                            "new GiPixelType[] for frame_index_");
  }

  next_row_ = 0;
  frame_initialized_ = true;

  if (frame_eagerly_read_) {
    ScanlineStatus status = (frame_spec_.hint_progressive ?
                             DecodeProgressiveGif() :
                             DecodeNonProgressiveGif());
    if (!status.Success()) {
      PS_LOG_INFO(message_handler(), "Failed to decode entire GIF frame.");
      Reset();
      return status;
    }

    if (!is_originally_rgba &&
        FrameHasOutOfRangePixels(frame_index_.get(),
                                 frame_spec_.width * frame_spec_.height,
                                 frame_palette_size_)) {
      frame_spec_.pixel_format = RGBA_8888;
      PS_DLOG_INFO(
          message_handler(),
          "Found out-of-range pixel in frame %i. Switching frame to %s",
          next_frame_ - 1,
          GetPixelFormatString(frame_spec_.pixel_format));
    }
  }

  // Now that we have the correct pixel_format, allocate the memory
  // we'll need to read the image data in ReadNextScanline().
  size_t bytes_per_row =
      GetBytesPerPixel(frame_spec_.pixel_format) * frame_spec_.width;
  frame_buffer_.reset(new GifPixelType[bytes_per_row]);
  if (frame_buffer_ == NULL) {
    Reset();
    return PS_LOGGED_STATUS(PS_LOG_ERROR, message_handler(),
                            SCANLINE_STATUS_MEMORY_ERROR,
                            FRAME_GIFREADER,
                            "new GiPixelType[] for frame_buffer_ ");
  }

  return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}

ScanlineStatus GifFrameReader::ReadNextScanline(
    const void** out_scanline_bytes) {
  if (!frame_initialized_ || !HasMoreScanlines()) {
    return PS_LOGGED_STATUS(PS_LOG_DFATAL, message_handler(),
                            SCANLINE_STATUS_INVOCATION_ERROR,
                            FRAME_GIFREADER,
                            "The GIF image was not initialized or does not "
                            "have more scanlines.");
  }

  // Convert the color index to the actual color.
  GifPixelType* color_buffer = frame_buffer_.get();
  const size_t pixel_size = GetBytesPerPixel(frame_spec_.pixel_format);

  // Find out the color index for the requested row.
  GifPixelType* index_buffer = NULL;
  GifFileType* gif_file = gif_struct_->gif_file();
  if (!frame_eagerly_read_) {
    // Decode the image a row at a time.
    index_buffer = frame_index_.get();
    if (DGifGetLine(gif_file, index_buffer, frame_spec_.width) == GIF_ERROR) {
      Reset();
      return PS_LOGGED_STATUS(PS_LOG_INFO, message_handler(),
                              SCANLINE_STATUS_INTERNAL_ERROR,
                              FRAME_GIFREADER,
                              "DGifGetLine()");
    }
  } else {
    // We simply point the output to the corresponding row because
    // the image has already been decoded.
    index_buffer = frame_index_.get() + next_row_ * frame_spec_.width;
  }

  for (size_px pixel_index = 0;
       pixel_index < frame_spec_.width;
       ++pixel_index) {
    // Convert the color index to the actual color. Note that
    // out-of-range pixel values get automatically converted to
    // transparent pixels because the corresponding palette entries
    // have been cleared in CreateColorMap().
    GifPixelType color_index = *(index_buffer++);
    memcpy(color_buffer, gif_palette_.get() + color_index, pixel_size);
    color_buffer += pixel_size;
  }

  *out_scanline_bytes = static_cast<void*>(frame_buffer_.get());
  ++next_row_;
  return ScanlineStatus(SCANLINE_STATUS_SUCCESS);
}

}  // namespace image_compression

}  // namespace pagespeed
