// Copyright 2015 Google Inc. All Rights Reserved. // // Use of this source code is governed by a BSD-style license // that can be found in the COPYING file in the root of the source // tree. An additional intellectual property rights grant can be found // in the file PATENTS. All contributing project authors may // be found in the AUTHORS file in the root of the source tree. // ----------------------------------------------------------------------------- // // Utilities for animated images #include "./anim_util.h" #include #include #include #include #include #include // for 'ostringstream'. #ifdef WEBP_HAVE_GIF #include #endif #include "webp/format_constants.h" #include "webp/decode.h" #include "webp/demux.h" using std::ifstream; using std::ios; using std::ofstream; using std::ostringstream; static const int kNumChannels = 4; // ----------------------------------------------------------------------------- // Common utilities. // Returns true if the frame covers the full canvas. static bool IsFullFrame(int width, int height, int canvas_width, int canvas_height) { return (width == canvas_width && height == canvas_height); } static void AllocateFrames(AnimatedImage* const image, uint32_t frame_count) { image->frames.resize(frame_count); for (size_t i = 0; i < image->frames.size(); ++i) { const size_t rgba_size = image->canvas_width * kNumChannels * image->canvas_height; image->frames[i].rgba.resize(rgba_size); } } // Clear the canvas to transparent. static void ZeroFillCanvas(uint8_t* rgba, uint32_t canvas_width, uint32_t canvas_height) { memset(rgba, 0, canvas_width * kNumChannels * canvas_height); } // Clear given frame rectangle to transparent. static void ZeroFillFrameRect(uint8_t* rgba, int rgba_stride, int x_offset, int y_offset, int width, int height) { assert(width * kNumChannels <= rgba_stride); rgba += y_offset * rgba_stride + x_offset * kNumChannels; for (int j = 0; j < height; ++j) { memset(rgba, 0, width * kNumChannels); rgba += rgba_stride; } } // Copy width * height pixels from 'src' to 'dst'. static void CopyCanvas(const uint8_t* src, uint8_t* dst, uint32_t width, uint32_t height) { assert(src != NULL && dst != NULL); memcpy(dst, src, width * kNumChannels * height); } // Copy pixels in the given rectangle from 'src' to 'dst' honoring the 'stride'. static void CopyFrameRectangle(const uint8_t* src, uint8_t* dst, int stride, int x_offset, int y_offset, int width, int height) { const int width_in_bytes = width * kNumChannels; assert(width_in_bytes <= stride); const size_t offset = y_offset * stride + x_offset * kNumChannels; src += offset; dst += offset; for (int j = 0; j < height; ++j) { memcpy(dst, src, width_in_bytes); src += stride; dst += stride; } } // Canonicalize all transparent pixels to transparent black to aid comparison. static void CleanupTransparentPixels(uint32_t* rgba, uint32_t width, uint32_t height) { const uint32_t* const rgba_end = rgba + width * height; while (rgba < rgba_end) { const uint8_t alpha = (*rgba >> 24) & 0xff; if (alpha == 0) { *rgba = 0; } ++rgba; } } // Dump frame to a PAM file. // Returns true on success. static bool DumpFrame(const char filename[], const char dump_folder[], uint32_t frame_num, const uint8_t rgba[], int canvas_width, int canvas_height) { const std::string filename_str = filename; const size_t slash_idx = filename_str.find_last_of("/\\"); const std::string base_name = (slash_idx != std::string::npos) ? filename_str.substr(slash_idx + 1) : filename_str; ostringstream dump_file; dump_file << dump_folder << "/" << base_name << "_frame_" << frame_num << ".pam"; ofstream fout(dump_file.str().c_str(), ios::binary | ios::out); if (!fout.good()) { fprintf(stderr, "Error opening file for writing: %s\n", dump_file.str().c_str()); return false; } fout << "P7\nWIDTH " << canvas_width << "\nHEIGHT " << canvas_height << "\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n"; for (int y = 0; y < canvas_height; ++y) { fout.write( reinterpret_cast(rgba) + y * canvas_width * kNumChannels, canvas_width * kNumChannels); if (!fout.good()) { fprintf(stderr, "Error writing to file: %s\n", dump_file.str().c_str()); return 0; } } fout.close(); return true; } // ----------------------------------------------------------------------------- // WebP Decoding. // Returns true if this is a valid WebP bitstream. static bool IsWebP(const std::string& file_str) { return WebPGetInfo(reinterpret_cast(file_str.c_str()), file_str.length(), NULL, NULL) != 0; } // Returns true if the current frame is a key-frame. static bool IsKeyFrameWebP(const WebPIterator& curr, const WebPIterator& prev, const DecodedFrame* const prev_frame, int canvas_width, int canvas_height) { if (prev_frame == NULL) { return true; } else if ((!curr.has_alpha || curr.blend_method == WEBP_MUX_NO_BLEND) && IsFullFrame(curr.width, curr.height, canvas_width, canvas_height)) { return true; } else { return (prev.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) && (IsFullFrame(prev.width, prev.height, canvas_width, canvas_height) || prev_frame->is_key_frame); } } // Blend a single channel of 'src' over 'dst', given their alpha channel values. static uint8_t BlendChannelWebP(uint32_t src, uint8_t src_a, uint32_t dst, uint8_t dst_a, uint32_t scale, int shift) { const uint8_t src_channel = (src >> shift) & 0xff; const uint8_t dst_channel = (dst >> shift) & 0xff; const uint32_t blend_unscaled = src_channel * src_a + dst_channel * dst_a; assert(blend_unscaled < (1ULL << 32) / scale); return (blend_unscaled * scale) >> 24; } // Blend 'src' over 'dst' assuming they are NOT pre-multiplied by alpha. static uint32_t BlendPixelWebP(uint32_t src, uint32_t dst) { const uint8_t src_a = (src >> 24) & 0xff; if (src_a == 0) { return dst; } else { const uint8_t dst_a = (dst >> 24) & 0xff; // This is the approximate integer arithmetic for the actual formula: // dst_factor_a = (dst_a * (255 - src_a)) / 255. const uint8_t dst_factor_a = (dst_a * (256 - src_a)) >> 8; assert(src_a + dst_factor_a < 256); const uint8_t blend_a = src_a + dst_factor_a; const uint32_t scale = (1UL << 24) / blend_a; const uint8_t blend_r = BlendChannelWebP(src, src_a, dst, dst_factor_a, scale, 0); const uint8_t blend_g = BlendChannelWebP(src, src_a, dst, dst_factor_a, scale, 8); const uint8_t blend_b = BlendChannelWebP(src, src_a, dst, dst_factor_a, scale, 16); return (blend_r << 0) | (blend_g << 8) | (blend_b << 16) | (blend_a << 24); } } // Returns two ranges ( pairs) at row 'canvas_y', that belong to // 'src' but not 'dst'. A point range is empty if the corresponding width is 0. static void FindBlendRangeAtRowWebP(const WebPIterator* const src, const WebPIterator* const dst, int canvas_y, int* const left1, int* const width1, int* const left2, int* const width2) { const int src_max_x = src->x_offset + src->width; const int dst_max_x = dst->x_offset + dst->width; const int dst_max_y = dst->y_offset + dst->height; assert(canvas_y >= src->y_offset && canvas_y < (src->y_offset + src->height)); *left1 = -1; *width1 = 0; *left2 = -1; *width2 = 0; if (canvas_y < dst->y_offset || canvas_y >= dst_max_y || src->x_offset >= dst_max_x || src_max_x <= dst->x_offset) { *left1 = src->x_offset; *width1 = src->width; return; } if (src->x_offset < dst->x_offset) { *left1 = src->x_offset; *width1 = dst->x_offset - src->x_offset; } if (src_max_x > dst_max_x) { *left2 = dst_max_x; *width2 = src_max_x - dst_max_x; } } // Blend 'num_pixels' in 'src' over 'dst'. static void BlendPixelRowWebP(uint32_t* const src, const uint32_t* const dst, int num_pixels) { for (int i = 0; i < num_pixels; ++i) { uint32_t* const src_pixel_ptr = &src[i]; const uint8_t src_alpha = (*src_pixel_ptr >> 24) & 0xff; if (src_alpha != 0xff) { const uint32_t dst_pixel = dst[i]; *src_pixel_ptr = BlendPixelWebP(*src_pixel_ptr, dst_pixel); } } } // Read animated WebP bitstream 'file_str' into 'AnimatedImage' struct. static bool ReadAnimatedWebP(const char filename[], const std::string& file_str, AnimatedImage* const image, bool dump_frames, const char dump_folder[]) { bool ok = true; const WebPData webp_data = { reinterpret_cast(file_str.data()), file_str.size() }; WebPDemuxer* const demux = WebPDemux(&webp_data); if (demux == NULL) return false; // Animation properties. image->canvas_width = WebPDemuxGetI(demux, WEBP_FF_CANVAS_WIDTH); image->canvas_height = WebPDemuxGetI(demux, WEBP_FF_CANVAS_HEIGHT); image->loop_count = WebPDemuxGetI(demux, WEBP_FF_LOOP_COUNT); image->bgcolor = WebPDemuxGetI(demux, WEBP_FF_BACKGROUND_COLOR); const uint32_t frame_count = WebPDemuxGetI(demux, WEBP_FF_FRAME_COUNT); const uint32_t canvas_width = image->canvas_width; const uint32_t canvas_height = image->canvas_height; // Allocate frames. AllocateFrames(image, frame_count); // Decode and reconstruct frames. WebPIterator prev_iter = WebPIterator(); WebPIterator curr_iter = WebPIterator(); for (uint32_t i = 0; i < frame_count; ++i) { prev_iter = curr_iter; // Get frame. if (!WebPDemuxGetFrame(demux, i + 1, &curr_iter)) { fprintf(stderr, "Error retrieving frame #%u\n", i); return false; } DecodedFrame* const prev_frame = (i > 0) ? &image->frames[i - 1] : NULL; uint8_t* const prev_rgba = (prev_frame != NULL) ? prev_frame->rgba.data() : NULL; DecodedFrame* const curr_frame = &image->frames[i]; uint8_t* const curr_rgba = curr_frame->rgba.data(); curr_frame->duration = curr_iter.duration; curr_frame->is_key_frame = IsKeyFrameWebP(curr_iter, prev_iter, prev_frame, canvas_width, canvas_height); // TODO(urvang): The logic of decoding and reconstructing the next animated // frame given the previous one should be a single library call (ideally a // user-facing API), which takes care of frame disposal, blending etc. // Initialize. if (curr_frame->is_key_frame) { ZeroFillCanvas(curr_rgba, canvas_width, canvas_height); } else { CopyCanvas(prev_rgba, curr_rgba, canvas_width, canvas_height); if (prev_iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { ZeroFillFrameRect(curr_rgba, canvas_width * kNumChannels, prev_iter.x_offset, prev_iter.y_offset, prev_iter.width, prev_iter.height); } } // Decode. const uint8_t* input = curr_iter.fragment.bytes; const size_t input_size = curr_iter.fragment.size; const size_t output_offset = (curr_iter.y_offset * canvas_width + curr_iter.x_offset) * kNumChannels; uint8_t* output = curr_rgba + output_offset; const int output_stride = kNumChannels * canvas_width; const size_t output_size = output_stride * curr_iter.height; if (WebPDecodeRGBAInto(input, input_size, output, output_size, output_stride) == NULL) { ok = false; break; } // During the decoding of current frame, we may have set some pixels to be // transparent (i.e. alpha < 255). However, the value of each of these // pixels should have been determined by blending it against the value of // that pixel in the previous frame if blending method of is WEBP_MUX_BLEND. if (i > 0 && curr_iter.blend_method == WEBP_MUX_BLEND && !curr_frame->is_key_frame) { if (prev_iter.dispose_method == WEBP_MUX_DISPOSE_NONE) { // Blend transparent pixels with pixels in previous canvas. for (int y = 0; y < curr_iter.height; ++y) { const size_t offset = (curr_iter.y_offset + y) * canvas_width + curr_iter.x_offset; BlendPixelRowWebP(reinterpret_cast(curr_rgba) + offset, reinterpret_cast(prev_rgba) + offset, curr_iter.width); } } else { assert(prev_iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND); // We need to blend a transparent pixel with its value just after // initialization. That is, blend it with: // * Fully transparent pixel if it belongs to prevRect <-- No-op. // * The pixel in the previous canvas otherwise <-- Need alpha-blending. for (int y = 0; y < curr_iter.height; ++y) { const int canvas_y = curr_iter.y_offset + y; int left1, width1, left2, width2; FindBlendRangeAtRowWebP(&curr_iter, &prev_iter, canvas_y, &left1, &width1, &left2, &width2); if (width1 > 0) { const size_t offset1 = canvas_y * canvas_width + left1; BlendPixelRowWebP(reinterpret_cast(curr_rgba) + offset1, reinterpret_cast(prev_rgba) + offset1, width1); } if (width2 > 0) { const size_t offset2 = canvas_y * canvas_width + left2; BlendPixelRowWebP(reinterpret_cast(curr_rgba) + offset2, reinterpret_cast(prev_rgba) + offset2, width2); } } } } // Needed only because we may want to compare with GIF later. CleanupTransparentPixels(reinterpret_cast(curr_rgba), canvas_width, canvas_height); if (dump_frames) { ok = ok && DumpFrame(filename, dump_folder, i, curr_rgba, canvas_width, canvas_height); } } WebPDemuxReleaseIterator(&prev_iter); WebPDemuxReleaseIterator(&curr_iter); WebPDemuxDelete(demux); return ok; } // ----------------------------------------------------------------------------- // GIF Decoding. // Returns true if this is a valid GIF bitstream. static bool IsGIF(const std::string& file_str) { const char* const cstr = file_str.c_str(); return file_str.length() > GIF_STAMP_LEN && (!memcmp(GIF_STAMP, cstr, GIF_STAMP_LEN) || !memcmp(GIF87_STAMP, cstr, GIF_STAMP_LEN) || !memcmp(GIF89_STAMP, cstr, GIF_STAMP_LEN)); } #ifdef WEBP_HAVE_GIF // 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 #if !LOCAL_GIF_PREREQ(5, 0) // Added in v5.0 typedef struct GraphicsControlBlock { int DisposalMode; #define DISPOSAL_UNSPECIFIED 0 // No disposal specified #define DISPOSE_DO_NOT 1 // Leave image in place #define DISPOSE_BACKGROUND 2 // Set area to background color #define DISPOSE_PREVIOUS 3 // Restore to previous content bool UserInputFlag; // User confirmation required before disposal int DelayTime; // Pre-display delay in 0.01sec units int TransparentColor; // Palette index for transparency, -1 if none #define NO_TRANSPARENT_COLOR -1 } GraphicsControlBlock; static int DGifExtensionToGCB(const size_t GifExtensionLength, const GifByteType* GifExtension, GraphicsControlBlock* gcb) { if (GifExtensionLength != 4) { return GIF_ERROR; } gcb->DisposalMode = (GifExtension[0] >> 2) & 0x07; gcb->UserInputFlag = (GifExtension[0] & 0x02) != 0; gcb->DelayTime = GifExtension[1] | (GifExtension[2] << 8); if (GifExtension[0] & 0x01) { gcb->TransparentColor = static_cast(GifExtension[3]); } else { gcb->TransparentColor = NO_TRANSPARENT_COLOR; } return GIF_OK; } static int DGifSavedExtensionToGCB(GifFileType* GifFile, int ImageIndex, GraphicsControlBlock* gcb) { int i; if (ImageIndex < 0 || ImageIndex > GifFile->ImageCount - 1) { return GIF_ERROR; } gcb->DisposalMode = DISPOSAL_UNSPECIFIED; gcb->UserInputFlag = false; gcb->DelayTime = 0; gcb->TransparentColor = NO_TRANSPARENT_COLOR; for (i = 0; i < GifFile->SavedImages[ImageIndex].ExtensionBlockCount; i++) { ExtensionBlock* ep = &GifFile->SavedImages[ImageIndex].ExtensionBlocks[i]; if (ep->Function == GRAPHICS_EXT_FUNC_CODE) { return DGifExtensionToGCB( ep->ByteCount, reinterpret_cast(ep->Bytes), gcb); } } return GIF_ERROR; } #define CONTINUE_EXT_FUNC_CODE 0x00 // Signature was changed in v5.0 #define DGifOpenFileName(a, b) DGifOpenFileName(a) #endif // !LOCAL_GIF_PREREQ(5, 0) // Signature changed in v5.1 #if !LOCAL_GIF_PREREQ(5, 1) #define DGifCloseFile(a, b) DGifCloseFile(a) #endif static void GIFDisplayError(const GifFileType* const gif, int gif_error) { // 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 == NULL) ? gif_error : gif->Error); #else const char* error_str = (const char*)GifErrorString(); (void)gif; #endif if (error_str == NULL) error_str = "Unknown error"; fprintf(stderr, "GIFLib Error %d: %s\n", gif_error, error_str); #else (void)gif; fprintf(stderr, "GIFLib Error %d: ", gif_error); PrintGifError(); fprintf(stderr, "\n"); #endif } static bool IsKeyFrameGIF(const GifImageDesc& prev_desc, int prev_dispose, const DecodedFrame* const prev_frame, int canvas_width, int canvas_height) { if (prev_frame == NULL) return true; if (prev_dispose == DISPOSE_BACKGROUND) { if (IsFullFrame(prev_desc.Width, prev_desc.Height, canvas_width, canvas_height)) { return true; } if (prev_frame->is_key_frame) return true; } return false; } static int GetTransparentIndexGIF(GifFileType* gif) { GraphicsControlBlock first_gcb = GraphicsControlBlock(); DGifSavedExtensionToGCB(gif, 0, &first_gcb); return first_gcb.TransparentColor; } static uint32_t GetBackgroundColorGIF(GifFileType* gif) { const int transparent_index = GetTransparentIndexGIF(gif); const ColorMapObject* const color_map = gif->SColorMap; if (transparent_index != NO_TRANSPARENT_COLOR && gif->SBackGroundColor == transparent_index) { return 0x00ffffff; // Special case: transparent white. } else if (color_map == NULL || color_map->Colors == NULL || gif->SBackGroundColor >= color_map->ColorCount) { return 0xffffffff; // Invalid: assume white. } else { const GifColorType color = color_map->Colors[gif->SBackGroundColor]; return (0xff << 24) | (color.Red << 16) | (color.Green << 8) | (color.Blue << 0); } } // Find appropriate app extension and get loop count from the next extension. static uint32_t GetLoopCountGIF(const GifFileType* const gif) { for (int i = 0; i < gif->ImageCount; ++i) { const SavedImage* const image = &gif->SavedImages[i]; for (int j = 0; (j + 1) < image->ExtensionBlockCount; ++j) { const ExtensionBlock* const eb1 = image->ExtensionBlocks + j; const ExtensionBlock* const eb2 = image->ExtensionBlocks + j + 1; const char* const signature = reinterpret_cast(eb1->Bytes); const bool signature_is_ok = (eb1->Function == APPLICATION_EXT_FUNC_CODE) && (eb1->ByteCount == 11) && (!memcmp(signature, "NETSCAPE2.0", 11) || !memcmp(signature, "ANIMEXTS1.0", 11)); if (signature_is_ok && eb2->Function == CONTINUE_EXT_FUNC_CODE && eb2->ByteCount >= 3 && eb2->Bytes[0] == 1) { return (static_cast(eb2->Bytes[2]) << 8) + (static_cast(eb2->Bytes[1]) << 0); } } } return 0; // Default. } // Get duration of 'n'th frame in milliseconds. static int GetFrameDurationGIF(GifFileType* gif, int n) { GraphicsControlBlock gcb = GraphicsControlBlock(); DGifSavedExtensionToGCB(gif, n, &gcb); return gcb.DelayTime * 10; } // Returns true if frame 'target' completely covers 'covered'. static bool CoversFrameGIF(const GifImageDesc& target, const GifImageDesc& covered) { return target.Left <= covered.Left && covered.Left + covered.Width <= target.Left + target.Width && target.Top <= covered.Top && covered.Top + covered.Height <= target.Top + target.Height; } static void RemapPixelsGIF(const uint8_t* const src, const ColorMapObject* const cmap, int transparent_color, int len, uint8_t* dst) { int i; for (i = 0; i < len; ++i) { if (src[i] != transparent_color) { // If a pixel in the current frame is transparent, we don't modify it, so // that we can see-through the corresponding pixel from an earlier frame. const GifColorType c = cmap->Colors[src[i]]; dst[4 * i + 0] = c.Red; dst[4 * i + 1] = c.Green; dst[4 * i + 2] = c.Blue; dst[4 * i + 3] = 0xff; } } } static bool ReadFrameGIF(const SavedImage* const gif_image, const ColorMapObject* cmap, int transparent_color, int out_stride, uint8_t* const dst) { const GifImageDesc& image_desc = gif_image->ImageDesc; if (image_desc.ColorMap) { cmap = image_desc.ColorMap; } if (cmap == NULL || cmap->ColorCount != (1 << cmap->BitsPerPixel)) { fprintf(stderr, "Potentially corrupt color map.\n"); return false; } const uint8_t* in = reinterpret_cast(gif_image->RasterBits); uint8_t* out = dst + image_desc.Top * out_stride + image_desc.Left * kNumChannels; for (int j = 0; j < image_desc.Height; ++j) { RemapPixelsGIF(in, cmap, transparent_color, image_desc.Width, out); in += image_desc.Width; out += out_stride; } return true; } // Read animated GIF bitstream from 'filename' into 'AnimatedImage' struct. static bool ReadAnimatedGIF(const char filename[], AnimatedImage* const image, bool dump_frames, const char dump_folder[]) { GifFileType* gif = DGifOpenFileName(filename, NULL); if (gif == NULL) { fprintf(stderr, "Could not read file: %s.\n", filename); return false; } const int gif_error = DGifSlurp(gif); if (gif_error != GIF_OK) { fprintf(stderr, "Could not parse image: %s.\n", filename); GIFDisplayError(gif, gif_error); DGifCloseFile(gif, NULL); return false; } // Animation properties. image->canvas_width = static_cast(gif->SWidth); image->canvas_height = static_cast(gif->SHeight); if (image->canvas_width > MAX_CANVAS_SIZE || image->canvas_height > MAX_CANVAS_SIZE) { fprintf(stderr, "Invalid canvas dimension: %d x %d\n", image->canvas_width, image->canvas_height); DGifCloseFile(gif, NULL); return false; } image->loop_count = GetLoopCountGIF(gif); image->bgcolor = GetBackgroundColorGIF(gif); const uint32_t frame_count = static_cast(gif->ImageCount); if (frame_count == 0) { DGifCloseFile(gif, NULL); return false; } if (image->canvas_width == 0 || image->canvas_height == 0) { image->canvas_width = gif->SavedImages[0].ImageDesc.Width; image->canvas_height = gif->SavedImages[0].ImageDesc.Height; gif->SavedImages[0].ImageDesc.Left = 0; gif->SavedImages[0].ImageDesc.Top = 0; if (image->canvas_width == 0 || image->canvas_height == 0) { fprintf(stderr, "Invalid canvas size in GIF.\n"); DGifCloseFile(gif, NULL); return false; } } // Allocate frames. AllocateFrames(image, frame_count); const uint32_t canvas_width = image->canvas_width; const uint32_t canvas_height = image->canvas_height; // Decode and reconstruct frames. for (uint32_t i = 0; i < frame_count; ++i) { const int canvas_width_in_bytes = canvas_width * kNumChannels; const SavedImage* const curr_gif_image = &gif->SavedImages[i]; GraphicsControlBlock curr_gcb = GraphicsControlBlock(); DGifSavedExtensionToGCB(gif, i, &curr_gcb); DecodedFrame* const curr_frame = &image->frames[i]; uint8_t* const curr_rgba = curr_frame->rgba.data(); curr_frame->duration = GetFrameDurationGIF(gif, i); if (i == 0) { // Initialize as transparent. curr_frame->is_key_frame = true; ZeroFillCanvas(curr_rgba, canvas_width, canvas_height); } else { DecodedFrame* const prev_frame = &image->frames[i - 1]; const GifImageDesc& prev_desc = gif->SavedImages[i - 1].ImageDesc; GraphicsControlBlock prev_gcb = GraphicsControlBlock(); DGifSavedExtensionToGCB(gif, i - 1, &prev_gcb); curr_frame->is_key_frame = IsKeyFrameGIF(prev_desc, prev_gcb.DisposalMode, prev_frame, canvas_width, canvas_height); if (curr_frame->is_key_frame) { // Initialize as transparent. ZeroFillCanvas(curr_rgba, canvas_width, canvas_height); } else { // Initialize with previous canvas. uint8_t* const prev_rgba = image->frames[i - 1].rgba.data(); CopyCanvas(prev_rgba, curr_rgba, canvas_width, canvas_height); // Dispose previous frame rectangle. bool prev_frame_disposed = (prev_gcb.DisposalMode == DISPOSE_BACKGROUND || prev_gcb.DisposalMode == DISPOSE_PREVIOUS); bool curr_frame_opaque = (curr_gcb.TransparentColor == NO_TRANSPARENT_COLOR); bool prev_frame_completely_covered = curr_frame_opaque && CoversFrameGIF(curr_gif_image->ImageDesc, prev_desc); if (prev_frame_disposed && !prev_frame_completely_covered) { switch (prev_gcb.DisposalMode) { case DISPOSE_BACKGROUND: { ZeroFillFrameRect(curr_rgba, canvas_width_in_bytes, prev_desc.Left, prev_desc.Top, prev_desc.Width, prev_desc.Height); break; } case DISPOSE_PREVIOUS: { int src_frame_num = i - 2; while (src_frame_num >= 0) { GraphicsControlBlock src_frame_gcb = GraphicsControlBlock(); DGifSavedExtensionToGCB(gif, src_frame_num, &src_frame_gcb); if (src_frame_gcb.DisposalMode != DISPOSE_PREVIOUS) break; --src_frame_num; } if (src_frame_num >= 0) { // Restore pixels inside previous frame rectangle to // corresponding pixels in source canvas. uint8_t* const src_frame_rgba = image->frames[src_frame_num].rgba.data(); CopyFrameRectangle(src_frame_rgba, curr_rgba, canvas_width_in_bytes, prev_desc.Left, prev_desc.Top, prev_desc.Width, prev_desc.Height); } else { // Source canvas doesn't exist. So clear previous frame // rectangle to background. ZeroFillFrameRect(curr_rgba, canvas_width_in_bytes, prev_desc.Left, prev_desc.Top, prev_desc.Width, prev_desc.Height); } break; } default: break; // Nothing to do. } } } } // Decode current frame. if (!ReadFrameGIF(curr_gif_image, gif->SColorMap, curr_gcb.TransparentColor, canvas_width_in_bytes, curr_rgba)) { DGifCloseFile(gif, NULL); return false; } if (dump_frames) { if (!DumpFrame(filename, dump_folder, i, curr_rgba, canvas_width, canvas_height)) { DGifCloseFile(gif, NULL); return false; } } } DGifCloseFile(gif, NULL); return true; } #else static bool ReadAnimatedGIF(const char filename[], AnimatedImage* const image, bool dump_frames, const char dump_folder[]) { (void)filename; (void)image; (void)dump_frames; (void)dump_folder; fprintf(stderr, "GIF support not compiled. Please install the libgif-dev " "package before building.\n"); return false; } #endif // WEBP_HAVE_GIF // ----------------------------------------------------------------------------- static bool ReadFile(const char filename[], std::string* filestr) { ifstream fin(filename, ios::binary); if (!fin.good()) return false; ostringstream strout; strout << fin.rdbuf(); *filestr = strout.str(); fin.close(); return true; } bool ReadAnimatedImage(const char filename[], AnimatedImage* const image, bool dump_frames, const char dump_folder[]) { std::string file_str; if (!ReadFile(filename, &file_str)) { fprintf(stderr, "Error reading file: %s\n", filename); return false; } if (IsWebP(file_str)) { return ReadAnimatedWebP(filename, file_str, image, dump_frames, dump_folder); } else if (IsGIF(file_str)) { return ReadAnimatedGIF(filename, image, dump_frames, dump_folder); } else { fprintf(stderr, "Unknown file type: %s. Supported file types are WebP and GIF\n", filename); return false; } } static void Accumulate(double v1, double v2, double* const max_diff, double* const sse) { const double diff = fabs(v1 - v2); if (diff > *max_diff) *max_diff = diff; *sse += diff * diff; } void GetDiffAndPSNR(const uint8_t rgba1[], const uint8_t rgba2[], uint32_t width, uint32_t height, bool premultiply, int* const max_diff, double* const psnr) { const uint32_t stride = width * kNumChannels; const int kAlphaChannel = kNumChannels - 1; double f_max_diff = 0.; double sse = 0.; for (uint32_t y = 0; y < height; ++y) { for (uint32_t x = 0; x < stride; x += kNumChannels) { const size_t offset = y * stride + x; const int alpha1 = rgba1[offset + kAlphaChannel]; const int alpha2 = rgba2[offset + kAlphaChannel]; Accumulate(alpha1, alpha2, &f_max_diff, &sse); if (!premultiply) { for (int k = 0; k < kAlphaChannel; ++k) { Accumulate(rgba1[offset + k], rgba2[offset + k], &f_max_diff, &sse); } } else { // premultiply R/G/B channels with alpha value for (int k = 0; k < kAlphaChannel; ++k) { Accumulate(rgba1[offset + k] * alpha1 / 255., rgba2[offset + k] * alpha2 / 255., &f_max_diff, &sse); } } } } *max_diff = static_cast(f_max_diff); if (*max_diff == 0) { *psnr = 99.; // PSNR when images are identical. } else { sse /= stride * height; *psnr = 10. * log10(255. * 255. / sse); } }