blob: 050ceeaee3d2c874bcfd407e8f987fe4e74d0701 [file] [log] [blame]
// Copyright 2019 Google LLC
//
// Licensed 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
//
// https://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.
// -----------------------------------------------------------------------------
//
// GIF decoding.
// Note: Legacy sequential functions are used, not DGifSlurp(). From giflib:
// "If you are handling large images on an extremely memory-limited machine,
// you may need to use the functions for sequential read and write."
//
// Author: Yannis Guyon (yguyon@google.com)
#include <cstdio>
#include <cstdlib>
#ifdef HAVE_CONFIG_H
#include "wp2/config.h"
#endif
#include "imageio/anim_image_dec.h"
#include "imageio/imageio_util.h"
#include "src/wp2/base.h"
#ifdef WP2_HAVE_GIF
#include <gif_lib.h>
#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include "src/dsp/dsp.h"
#include "src/utils/utils.h"
// GIFLIB_MAJOR is only defined in libgif >= 4.2.0.
#if defined(GIFLIB_MAJOR) && defined(GIFLIB_MINOR)
#define LOCAL_GIF_VERSION ((GIFLIB_MAJOR << 8) | GIFLIB_MINOR)
#define LOCAL_GIF_PREREQ(maj, min) (LOCAL_GIF_VERSION >= (((maj) << 8) | (min)))
#else
#define LOCAL_GIF_VERSION 0
#define LOCAL_GIF_PREREQ(maj, min) 0
#endif
// D_GIF_SUCCEEDED was added in 5.1.0.
#ifndef D_GIF_SUCCEEDED
#define D_GIF_SUCCEEDED 0
#endif
//------------------------------------------------------------------------------
namespace WP2 {
namespace {
// Returns the WP2Status corresponding to the D_GIF_ERR and logs it if not OK.
WP2Status ErrorToStatus(const GifFileType* const gif, int gif_error,
LogLevel log_level) {
#if LOCAL_GIF_PREREQ(5, 0)
if (gif != NULL) gif_error = gif->Error;
#else
(void)gif;
#endif
if (gif_error == D_GIF_SUCCEEDED) return WP2_STATUS_OK;
if (log_level >= LogLevel::DEFAULT) {
// libgif 4.2.0 has retired PrintGifError() and added GifErrorString().
#if LOCAL_GIF_PREREQ(4, 2)
#if LOCAL_GIF_PREREQ(5, 0)
// Static string actually, hence the const char* cast.
const char* error_str = (const char*)GifErrorString(gif_error);
#else
const char* error_str = (const char*)GifErrorString();
#endif
if (error_str == NULL) error_str = "Unknown error";
fprintf(stderr, "GIFLib Error %d: %s\n", gif_error, error_str);
#else
fprintf(stderr, "GIFLib Error %d: ", gif_error);
PrintGifError();
fprintf(stderr, "\n");
#endif
}
switch (gif_error) {
case D_GIF_ERR_OPEN_FAILED:
case D_GIF_ERR_READ_FAILED:
case D_GIF_ERR_CLOSE_FAILED:
case D_GIF_ERR_NOT_READABLE:
return WP2_STATUS_BAD_READ;
case D_GIF_ERR_EOF_TOO_SOON:
return WP2_STATUS_NOT_ENOUGH_DATA;
case D_GIF_ERR_DATA_TOO_BIG:
return WP2_STATUS_FILE_TOO_BIG;
case D_GIF_ERR_NOT_ENOUGH_MEM:
return WP2_STATUS_OUT_OF_MEMORY;
default:
return WP2_STATUS_BITSTREAM_ERROR;
}
}
constexpr int kInvalidIndex = -1;
constexpr int kDefaultDurationMs = 100;
constexpr Argb32b kTransparentColor{0x00u, 0x00u, 0x00u, 0x00u};
constexpr Argb32b kWhiteColor{0xFFu, 0xFFu, 0xFFu, 0xFFu};
enum class DisposeMethod { NONE, BACKGROUND, RESTORE_PREVIOUS };
bool IsConversionNeeded(WP2SampleFormat format) {
return (format != WP2_Argb_32 && format != WP2_ARGB_32);
}
//------------------------------------------------------------------------------
// Extracts background color (to dispose frames with).
Argb32b GetBackgroundColor(const ColorMapObject* const color_map,
int bgcolor_index, int transparent_index) {
if (transparent_index != kInvalidIndex &&
bgcolor_index == transparent_index) {
return kTransparentColor; // Special case.
} else if (color_map == NULL || color_map->Colors == NULL ||
bgcolor_index >= color_map->ColorCount) {
// Invalid background color index. Assuming white background.
return kWhiteColor;
} else {
const GifColorType c = color_map->Colors[bgcolor_index];
return {0xFFu, c.Red, c.Green, c.Blue};
}
}
// Converts 'indexed_color_pixels' and copies to 'pixels' in 'pixel_format'.
WP2Status ConvertIndexedColors(const GifFileType* const decoder,
const uint8_t* const indexed_color_pixels,
uint32_t* const temp_argb_pixels,
uint32_t num_pixels, int transparent_index,
uint8_t* const pixels,
WP2SampleFormat pixel_format) {
const ColorMapObject* const color_map = (decoder->Image.ColorMap != NULL)
? decoder->Image.ColorMap
: decoder->SColorMap;
if (color_map == NULL) return WP2_STATUS_OK; // Nothing to do.
WP2_CHECK_OK(color_map->Colors != NULL && color_map->ColorCount > 0,
WP2_STATUS_BITSTREAM_ERROR);
assert(IsConversionNeeded(pixel_format) == (temp_argb_pixels != nullptr));
if (IsConversionNeeded(pixel_format)) {
WP2ArgbConverterInit();
// For transparent pixels that reuse the previous value.
WP2ArgbConvertFrom[pixel_format]((const uint8_t*)pixels, num_pixels,
(uint8_t*)temp_argb_pixels);
}
uint32_t* const dst_pixels =
(IsConversionNeeded(pixel_format) ? temp_argb_pixels : (uint32_t*)pixels);
for (uint32_t i = 0; i < num_pixels; ++i) {
if (indexed_color_pixels[i] == transparent_index) {
// Do nothing. A transparent pixel implies keeping previous color.
} else if (indexed_color_pixels[i] < color_map->ColorCount) {
const GifColorType color = color_map->Colors[indexed_color_pixels[i]];
dst_pixels[i] = (uint32_t)0xffu | (color.Red << 8) | (color.Green << 16) |
(color.Blue << 24);
} else {
return WP2_STATUS_BITSTREAM_ERROR;
}
}
if (IsConversionNeeded(pixel_format)) {
WP2ArgbConvertTo[pixel_format]((const uint8_t*)temp_argb_pixels, num_pixels,
pixels);
}
return WP2_STATUS_OK;
}
// Decodes pixels into 'frame'.
WP2Status DecodeFrame(GifFileType* const decoder, int transparent_index,
ArgbBuffer* const frame, uint8_t* const tmp_indexed_row,
uint32_t* const tmp_argb_row, LogLevel log_level) {
if (decoder->Image.Interlace) {
static constexpr uint32_t kNumPasses = 4;
static constexpr uint32_t kOffsets[kNumPasses] = {0, 4, 2, 1};
static constexpr uint32_t kJumps[kNumPasses] = {8, 8, 4, 2};
for (uint32_t pass = 0; pass < kNumPasses; ++pass) {
uint32_t y = kOffsets[pass];
// It is not written in GIF specs if an interlaced image can be <5px tall.
if (y < frame->height()) {
uint8_t* row = frame->GetRow8(y);
const uint32_t jump = kJumps[pass] * frame->stride();
for (; y < frame->height(); y += kJumps[pass], row += jump) {
WP2_CHECK_STATUS(ErrorToStatus(
decoder, DGifGetLine(decoder, tmp_indexed_row, frame->width()),
log_level));
WP2_CHECK_STATUS(ConvertIndexedColors(
decoder, tmp_indexed_row, tmp_argb_row, frame->width(),
transparent_index, row, frame->format()));
}
}
}
} else {
uint8_t* row = frame->GetRow8(0);
for (uint32_t y = 0; y < frame->height(); ++y, row += frame->stride()) {
WP2_CHECK_STATUS(ErrorToStatus(
decoder, DGifGetLine(decoder, tmp_indexed_row, frame->width()),
log_level));
WP2_CHECK_STATUS(ConvertIndexedColors(
decoder, tmp_indexed_row, tmp_argb_row, frame->width(),
transparent_index, row, frame->format()));
}
}
return WP2_STATUS_OK;
}
//------------------------------------------------------------------------------
// Decodes frame duration, dispose method, transparent color index.
WP2Status ReadGraphicsExtension(const GifByteType* const data,
uint32_t* const frame_duration_ms,
DisposeMethod* const frame_dispose_method_,
int* const transparent_index) {
static constexpr int kDisposeShift = 2;
static constexpr int kDisposeMask = 0x07;
static constexpr int kTransparentMask = 0x01;
const int flags = data[1];
const int dispose_raw = (flags >> kDisposeShift) & kDisposeMask;
const int duration_raw = data[2] | (data[3] << 8); // In 10 ms units.
WP2_CHECK_OK(data[0] == 4, WP2_STATUS_BITSTREAM_ERROR);
*frame_duration_ms = (uint32_t)duration_raw * 10;
if (dispose_raw == 2) {
*frame_dispose_method_ = DisposeMethod::BACKGROUND;
} else if (dispose_raw == 3) {
*frame_dispose_method_ = DisposeMethod::RESTORE_PREVIOUS;
} else {
*frame_dispose_method_ = DisposeMethod::NONE;
}
*transparent_index = (flags & kTransparentMask) ? data[4] : kInvalidIndex;
return WP2_STATUS_OK;
}
// Decodes loop count, metadata.
WP2Status ReadApplicationExtension(GifFileType* const decoder,
const GifByteType* const data,
uint32_t* const loop_count,
LogLevel log_level) {
if (data[0] != 11) return WP2_STATUS_OK; // Chunk is too short.
if (!memcmp(data + 1, "NETSCAPE2.0", 11) ||
!memcmp(data + 1, "ANIMEXTS1.0", 11)) {
GifByteType* extension_data = NULL;
WP2_CHECK_STATUS(ErrorToStatus(
decoder, DGifGetExtensionNext(decoder, &extension_data), log_level));
WP2_CHECK_OK(extension_data != NULL, WP2_STATUS_BITSTREAM_ERROR);
WP2_CHECK_OK(extension_data[0] >= 3 && extension_data[1] == 1,
WP2_STATUS_BITSTREAM_ERROR);
assert(loop_count != nullptr);
*loop_count = (uint32_t)extension_data[2] | (extension_data[3] << 8);
if (*loop_count == 0) *loop_count = ImageReader::kInfiniteLoopCount;
}
return WP2_STATUS_OK;
}
} // namespace
//------------------------------------------------------------------------------
// Will be used by giflib.
static int InputFunction(GifFileType* decoder, GifByteType* data_source,
int num_bytes_to_read);
class ImageReaderGIF : public ImageReader::Impl {
public:
ImageReaderGIF(const uint8_t* data, size_t data_size,
ArgbBuffer* const buffer, LogLevel log_level,
size_t max_num_pixels)
: ImageReader::Impl(buffer, data, data_size, log_level, max_num_pixels),
previous_canvas_((buffer_ != nullptr) ? buffer_->format()
: WP2_Argb_32),
frame_(previous_canvas_.format()) {}
~ImageReaderGIF() override {
if (decoder_ != NULL) {
#if LOCAL_GIF_PREREQ(5, 1)
int error = D_GIF_SUCCEEDED;
DGifCloseFile(decoder_, &error);
(void)ErrorToStatus(nullptr, error, log_level_); // For error logging.
#else
DGifCloseFile(decoder_);
#endif
}
WP2Free(tmp_indexed_row_);
WP2Free(tmp_argb_row_);
}
// Returns the number of bytes successfully copied (at most 'num_bytes').
size_t TryRead(size_t num_bytes, GifByteType* const into) {
assert(num_read_bytes_ <= data_size_);
num_bytes = std::min(num_bytes, data_size_ - num_read_bytes_);
if (num_bytes > 0) {
memcpy((void*)into, (const void*)(data_ + num_read_bytes_), num_bytes);
num_read_bytes_ += num_bytes;
}
return num_bytes;
}
protected:
// Allocates 'buffer_', 'previous_canvas_' and temporary arrays.
WP2Status InitCanvas() {
// Fix some broken GIF global headers that report 0 x 0 dimension.
if (decoder_->SWidth == 0 || decoder_->SHeight == 0) {
decoder_->Image.Left = 0;
decoder_->Image.Top = 0;
decoder_->SWidth = decoder_->Image.Width;
decoder_->SHeight = decoder_->Image.Height;
WP2_CHECK_OK(decoder_->SWidth > 0 && decoder_->SHeight > 0,
WP2_STATUS_BITSTREAM_ERROR);
}
WP2_CHECK_STATUS(CheckDimensions((uint32_t)decoder_->SWidth,
(uint32_t)decoder_->SHeight));
// Background color. Some decoders might always use transparent instead,
// or might update it if the palette changes.
background_color_ = GetBackgroundColor(
decoder_->SColorMap, decoder_->SBackGroundColor, transparent_index_);
// Do as gif2webp from libwebp: ignore the signaled background color.
background_color_ = kTransparentColor;
// Allocate canvas and fill it with transparent pixels.
WP2_CHECK_STATUS(buffer_->Resize((uint32_t)decoder_->SWidth,
(uint32_t)decoder_->SHeight));
buffer_->Fill(background_color_);
WP2_CHECK_STATUS(previous_canvas_.CopyFrom(*buffer_));
// Allocate temp rows (used for DGifGetLine()).
tmp_indexed_row_ =
(uint8_t*)WP2Malloc(buffer_->width(), sizeof(*tmp_indexed_row_));
WP2_CHECK_ALLOC_OK(tmp_indexed_row_ != nullptr);
if (IsConversionNeeded(buffer_->format())) {
tmp_argb_row_ =
(uint32_t*)WP2Malloc(buffer_->width(), sizeof(*tmp_argb_row_));
WP2_CHECK_ALLOC_OK(tmp_argb_row_ != nullptr);
}
return WP2_STATUS_OK;
}
// Clears the canvas depending on 'previous_frame_dispose_method_'.
WP2Status DisposeCanvas() {
if (previous_frame_dispose_method_ == DisposeMethod::BACKGROUND) {
frame_.Fill(background_color_);
} else if (previous_frame_dispose_method_ ==
DisposeMethod::RESTORE_PREVIOUS) {
const uint8_t* const previous_pixels_inside_frame_rect_ =
previous_canvas_.GetRow8(frame_rect_.y) +
frame_rect_.x * WP2FormatBpp(previous_canvas_.format());
WP2_CHECK_STATUS(frame_.Import(
previous_canvas_.format(), frame_rect_.width, frame_rect_.height,
previous_pixels_inside_frame_rect_, previous_canvas_.stride()));
}
WP2_CHECK_STATUS(previous_canvas_.CopyFrom(*buffer_));
return WP2_STATUS_OK;
}
public:
WP2Status ReadFrame(bool* const is_last,
uint32_t* const duration_ms) override {
WP2_CHECK_STATUS(CheckData());
GifRecordType record_type = TERMINATE_RECORD_TYPE;
if (decoder_ == NULL) {
// First frame.
// Metadata must be decoded now but Giflib DGifGetRecordType() doesn't
// work without DGifGetLine(), hence the use of ReadGIFMetadata().
WP2_CHECK_STATUS(ReadGIFMetadata(data_, data_size_, &buffer_->metadata_));
int error = D_GIF_SUCCEEDED;
decoder_ = DGifOpen((void*)this, InputFunction, &error);
WP2_CHECK_STATUS(ErrorToStatus(decoder_, error, log_level_));
WP2_CHECK_ALLOC_OK(decoder_ != NULL);
WP2_CHECK_STATUS(ErrorToStatus(
decoder_, DGifGetRecordType(decoder_, &record_type), log_level_));
} else {
// Not the first frame, we already know the next 'record_type'.
record_type = IMAGE_DESC_RECORD_TYPE;
++frame_index_;
}
bool just_decoded_frame = false;
while (record_type != TERMINATE_RECORD_TYPE) {
if (record_type == IMAGE_DESC_RECORD_TYPE) {
if (just_decoded_frame) {
*is_last = false;
return WP2_STATUS_OK;
}
WP2_CHECK_STATUS(
ErrorToStatus(decoder_, DGifGetImageDesc(decoder_), log_level_));
if (frame_index_ == 0) {
WP2_CHECK_STATUS(InitCanvas());
} else {
WP2_CHECK_STATUS(DisposeCanvas());
}
// Some broken GIF can have sub-rect with zero width/height.
if (decoder_->Image.Width == 0 || decoder_->Image.Height == 0) {
decoder_->Image.Width = decoder_->SWidth;
decoder_->Image.Height = decoder_->SHeight;
}
frame_rect_ = {
(uint32_t)decoder_->Image.Left, (uint32_t)decoder_->Image.Top,
(uint32_t)decoder_->Image.Width, (uint32_t)decoder_->Image.Height};
WP2_CHECK_STATUS(frame_.SetView(*buffer_, frame_rect_));
WP2_CHECK_STATUS(DecodeFrame(decoder_, transparent_index_, &frame_,
tmp_indexed_row_, tmp_argb_row_,
log_level_));
just_decoded_frame = true;
// In GIF, graphic control extensions are optional, so we may not get
// one before reading the next frame. To handle this case, we reset
// frame properties to reasonable defaults.
previous_frame_dispose_method_ = frame_dispose_method_;
frame_dispose_method_ = DisposeMethod::NONE;
*duration_ms = frame_duration_ms_;
frame_duration_ms_ = kDefaultDurationMs;
transparent_index_ = kInvalidIndex;
} else if (record_type == EXTENSION_RECORD_TYPE) {
int extension;
GifByteType* data = NULL;
WP2_CHECK_STATUS(ErrorToStatus(
decoder_, DGifGetExtension(decoder_, &extension, &data),
log_level_));
if (data != NULL) {
if (extension == GRAPHICS_EXT_FUNC_CODE) {
uint32_t frame_duration_ms = 0;
WP2_CHECK_STATUS(ReadGraphicsExtension(data, &frame_duration_ms,
&frame_dispose_method_,
&transparent_index_));
// Avoid instant frames from inconsistent GIF images.
frame_duration_ms_ = (frame_duration_ms > 0) ? frame_duration_ms
: kDefaultDurationMs;
} else if (extension == APPLICATION_EXT_FUNC_CODE) {
WP2_CHECK_STATUS(ReadApplicationExtension(
decoder_, data, &loop_count_, log_level_));
}
do {
WP2_CHECK_STATUS(ErrorToStatus(
decoder_, DGifGetExtensionNext(decoder_, &data), log_level_));
} while (data != NULL);
}
} else {
// Skipping over unknown record type.
}
WP2_CHECK_STATUS(ErrorToStatus(
decoder_, DGifGetRecordType(decoder_, &record_type), log_level_));
}
WP2_CHECK_OK(just_decoded_frame, WP2_STATUS_BITSTREAM_ERROR);
*is_last = true;
return WP2_STATUS_OK;
}
protected:
GifFileType* decoder_ = NULL;
size_t num_read_bytes_ = 0;
ArgbBuffer previous_canvas_;
ArgbBuffer frame_; // Is a subview of buffer_, which represents the canvas.
Rectangle frame_rect_;
uint32_t frame_index_ = 0;
uint32_t frame_duration_ms_ = kDefaultDurationMs;
DisposeMethod frame_dispose_method_ = DisposeMethod::NONE;
DisposeMethod previous_frame_dispose_method_ = DisposeMethod::NONE;
int transparent_index_ = kInvalidIndex;
Argb32b background_color_ = kTransparentColor;
uint8_t* tmp_indexed_row_ = nullptr;
uint32_t* tmp_argb_row_ = nullptr;
};
// -----------------------------------------------------------------------------
static int InputFunction(GifFileType* decoder, GifByteType* data_source,
int num_bytes_to_read) {
ImageReaderGIF* const user_data = (ImageReaderGIF*)decoder->UserData;
assert(user_data != nullptr);
return (int)user_data->TryRead((size_t)num_bytes_to_read, data_source);
}
// -----------------------------------------------------------------------------
void ImageReader::SetImplGIF(ArgbBuffer* const buffer, LogLevel log_level,
size_t max_num_pixels) {
impl_.reset(new (WP2Allocable::nothrow) ImageReaderGIF(
data_.bytes, data_.size, buffer, log_level, max_num_pixels));
if (impl_ == nullptr) status_ = WP2_STATUS_OUT_OF_MEMORY;
}
} // namespace WP2
#else // !WP2_HAVE_GIF
void WP2::ImageReader::SetImplGIF(WP2::ArgbBuffer* const buffer,
WP2::LogLevel log_level,
size_t max_num_pixels) {
(void)buffer;
(void)max_num_pixels;
if (log_level >= WP2::LogLevel::DEFAULT) {
fprintf(stderr,
"GIF support not compiled. Please install the libgif "
"development package before building.\n");
}
status_ = WP2_STATUS_UNSUPPORTED_FEATURE;
}
#endif // WP2_HAVE_GIF