From 31e225d25fed30b0770c2073f069c98f72c24542 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Fri, 24 Oct 2025 20:56:36 -0300 Subject: [PATCH 1/2] NEW FEATURE - GIF image support with animated bitmap handling EasyRPG is now able to render gif animations. Based on libnsgif used in mkxp-z: https://github.com/netsurf-plan9/libnsgif (MIT license). This PR Introduces GIF image decoding and animation support by adding new source files for GIF and LZW decoding, animated bitmap management, and GIF image handling. Updates build scripts to include new sources, extends Bitmap creation to detect and load GIFs as AnimatedBitmap, and registers '.gif' as a supported image type. Also ensures proper virtual destructor in Bitmap and integrates animated bitmap update logic. I tried to inject the code very early on the image pipeline, to affect every image source. Known hiccups from current approach: Picture effects, such as flip or overlay color mode locks the gif into a single frame, I'm not that aware about the entire pipeline to solve that... --- CMakeLists.txt | 10 +- Makefile.am | 8 + src/animated_bitmap.cpp | 80 ++ src/animated_bitmap.h | 32 + src/bitmap.cpp | 21 + src/bitmap.h | 1 + src/filefinder.h | 2 +- src/gif.c | 2076 +++++++++++++++++++++++++++++++++++++++ src/image_gif.cpp | 103 ++ src/image_gif.h | 34 + src/lzw.c | 613 ++++++++++++ src/lzw.h | 137 +++ src/nsgif.h | 527 ++++++++++ src/player.cpp | 8 +- 14 files changed, 3649 insertions(+), 3 deletions(-) create mode 100644 src/animated_bitmap.cpp create mode 100644 src/animated_bitmap.h create mode 100644 src/gif.c create mode 100644 src/image_gif.cpp create mode 100644 src/image_gif.h create mode 100644 src/lzw.c create mode 100644 src/lzw.h create mode 100644 src/nsgif.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f4214968d..c59ccd3ec5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ cmake_minimum_required(VERSION 3.18...3.31 FATAL_ERROR) project(EasyRPG_Player VERSION 0.8.1 DESCRIPTION "Interpreter for RPG Maker 2000/2003 games" HOMEPAGE_URL "https://easyrpg.org" - LANGUAGES CXX) + LANGUAGES C CXX) # Extra CMake Module files list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/builds/cmake/Modules") @@ -78,6 +78,14 @@ add_library(${PROJECT_NAME} OBJECT src/bitmapfont_glyph.h src/bitmap.h src/bitmap_hslrgb.h + src/gif.c + src/lzw.c + src/lzw.h + src/nsgif.h + src/image_gif.h + src/image_gif.cpp + src/animated_bitmap.h + src/animated_bitmap.cpp src/cache.cpp src/cache.h src/callback.h diff --git a/Makefile.am b/Makefile.am index 5d00e6cdcd..120c4573e8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -47,6 +47,14 @@ libeasyrpg_player_a_SOURCES = \ src/bitmapfont.h \ src/bitmapfont_glyph.h \ src/bitmap_hslrgb.h \ + src/gif.c \ + src/lzw.c \ + src/lzw.h \ + src/nsgif.h \ + src/image_gif.h \ + src/image_gif.cpp \ + src/animated_bitmap.h \ + src/animated_bitmap.cpp \ src/cache.cpp \ src/cache.h \ src/callback.h \ diff --git a/src/animated_bitmap.cpp b/src/animated_bitmap.cpp new file mode 100644 index 0000000000..3f8ce3ca92 --- /dev/null +++ b/src/animated_bitmap.cpp @@ -0,0 +1,80 @@ +/*start of file animated_bitmap.cpp*/ +#include "animated_bitmap.h" +#include "image_gif.h" +#include "output.h" + +static std::vector active_animations; + +void AnimationManager::Register(AnimatedBitmap* bmp) { + active_animations.push_back(bmp); +} + +void AnimationManager::Unregister(AnimatedBitmap* bmp) { + auto it = std::find(active_animations.begin(), active_animations.end(), bmp); + if (it != active_animations.end()) { + active_animations.erase(it); + } +} + +void AnimationManager::UpdateAll(std::chrono::microseconds delta) { + for (AnimatedBitmap* bmp : active_animations) { + bmp->Update(delta); + } +} + +AnimatedBitmap::AnimatedBitmap(const uint8_t* data, unsigned bytes, bool transparent, uint32_t flags) + : Bitmap(0, 0, true) { // Initial dummy construction + GifDecoder decoder(data, bytes); + + if (!decoder.IsValid() || decoder.GetFrames().empty()) { + //Output::Warning("Failed to decode GIF or GIF has no frames."); + // Create a placeholder bitmap + Init(16, 16, nullptr); + Fill(Color(255, 0, 255, 255)); // Pink error color + return; + } + + // Re-initialize the base Bitmap with correct dimensions + Init(decoder.GetWidth(), decoder.GetHeight(), nullptr); + id = "animated_gif"; + + for (const auto& frame : decoder.GetFrames()) { + _frames.push_back(frame.bitmap); + int delay_ms = frame.delay_cs * 10; + if (delay_ms < 20) { // Enforce a minimum delay of 20ms + delay_ms = 100; // Default to 100ms + } + _durations.push_back(std::chrono::milliseconds(delay_ms)); + } + + // Blit the first frame + if (!_frames.empty()) { + BlitFast(0, 0, *_frames[0], _frames[0]->GetRect(), Opacity::Opaque()); + } + + AnimationManager::Register(this); +} + +AnimatedBitmap::~AnimatedBitmap() { + AnimationManager::Unregister(this); +} + +void AnimatedBitmap::Update(std::chrono::microseconds delta) { + if (_frames.size() <= 1) { + return; + } + + _time_accumulator += delta; + + bool frame_changed = false; + while (_time_accumulator >= _durations[_current_frame]) { + _time_accumulator -= _durations[_current_frame]; + _current_frame = (_current_frame + 1) % _frames.size(); + frame_changed = true; + } + + if (frame_changed) { + BlitFast(0, 0, *_frames[_current_frame], _frames[_current_frame]->GetRect(), Opacity::Opaque()); + } +} +/*end of file animated_bitmap.cpp*/ diff --git a/src/animated_bitmap.h b/src/animated_bitmap.h new file mode 100644 index 0000000000..c513d3d2f8 --- /dev/null +++ b/src/animated_bitmap.h @@ -0,0 +1,32 @@ +/*start of file animated_bitmap.h*/ +#pragma once +#include "bitmap.h" +#include +#include + +class AnimatedBitmap; + +class AnimationManager { +public: + static void Register(AnimatedBitmap* bmp); + static void Unregister(AnimatedBitmap* bmp); + static void UpdateAll(std::chrono::microseconds delta); +}; + +class AnimatedBitmap : public Bitmap { +public: + AnimatedBitmap(const uint8_t* data, unsigned bytes, bool transparent, uint32_t flags); + ~AnimatedBitmap() override; + + void Update(std::chrono::microseconds delta); + + // Add this public method + int GetCurrentFrame() const { return _current_frame; } + +private: + std::vector _frames; + std::vector _durations; + int _current_frame = 0; + std::chrono::microseconds _time_accumulator{ 0 }; +}; +/*end of file animated_bitmap.h*/ diff --git a/src/bitmap.cpp b/src/bitmap.cpp index 32aa4b6557..473f3c106a 100644 --- a/src/bitmap.cpp +++ b/src/bitmap.cpp @@ -40,6 +40,8 @@ #include "bitmap_hslrgb.h" #include +#include "animated_bitmap.h" + BitmapRef Bitmap::Create(int width, int height, const Color& color) { BitmapRef surface = Bitmap::Create(width, height, true); surface->Fill(color); @@ -47,6 +49,19 @@ BitmapRef Bitmap::Create(int width, int height, const Color& color) { } BitmapRef Bitmap::Create(Filesystem_Stream::InputStream stream, bool transparent, uint32_t flags) { + + // Proposed change: Add GIF check + uint8_t magic[8] = {}; + (void)stream.read(reinterpret_cast(magic), 8).gcount(); + stream.seekg(0, std::ios::ios_base::beg); + if (strncmp((char*)magic, "GIF87a", 6) == 0 || strncmp((char*)magic, "GIF89a", 6) == 0) { + std::vector buffer = Utils::ReadStream(stream); + BitmapRef bmp = std::make_shared(&buffer.front(), (unsigned)buffer.size(), transparent, flags); + if (!bmp->pixels()) return BitmapRef(); + return bmp; + } + // End of proposed change + BitmapRef bmp = std::make_shared(std::move(stream), transparent, flags); if (!bmp->pixels()) { @@ -57,6 +72,12 @@ BitmapRef Bitmap::Create(Filesystem_Stream::InputStream stream, bool transparent } BitmapRef Bitmap::Create(const uint8_t* data, unsigned bytes, bool transparent, uint32_t flags) { + if (bytes >= 6 && (strncmp((char*)data, "GIF87a", 6) == 0 || strncmp((char*)data, "GIF89a", 6) == 0)) { + BitmapRef bmp = std::make_shared(data, bytes, transparent, flags); + if (!bmp->pixels()) return BitmapRef(); + return bmp; + } + BitmapRef bmp = std::make_shared(data, bytes, transparent, flags); if (!bmp->pixels()) { diff --git a/src/bitmap.h b/src/bitmap.h index 2060b274ab..e482e8d36c 100644 --- a/src/bitmap.h +++ b/src/bitmap.h @@ -107,6 +107,7 @@ class Bitmap { Bitmap(const uint8_t* data, unsigned bytes, bool transparent, uint32_t flags); Bitmap(Bitmap const& source, Rect const& src_rect, bool transparent); Bitmap(void *pixels, int width, int height, int pitch, const DynamicFormat& format); + virtual ~Bitmap() = default; /** * Gets the bitmap width. diff --git a/src/filefinder.h b/src/filefinder.h index d33d1ea4e6..af617dd109 100644 --- a/src/filefinder.h +++ b/src/filefinder.h @@ -38,7 +38,7 @@ * insensitive files paths. */ namespace FileFinder { - constexpr const auto IMG_TYPES = Utils::MakeSvArray(".bmp", ".png", ".xyz"); + constexpr const auto IMG_TYPES = Utils::MakeSvArray(".bmp", ".png", ".xyz",".gif"); constexpr const auto MUSIC_TYPES = Utils::MakeSvArray( ".opus", ".oga", ".ogg", ".wav", ".mid", ".midi", ".mp3", ".wma"); constexpr const auto SOUND_TYPES = Utils::MakeSvArray( diff --git a/src/gif.c b/src/gif.c new file mode 100644 index 0000000000..73814bf352 --- /dev/null +++ b/src/gif.c @@ -0,0 +1,2076 @@ +/* + * Copyright 2004 Richard Wilson + * Copyright 2008 Sean Fox + * Copyright 2013-2022 Michael Drake + * + * This file is part of NetSurf's libnsgif, http://www.netsurf-browser.org/ + * Licenced under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + */ + +#include +#include +#include +#include +#include + +#include "lzw.h" +#include "nsgif.h" + +/** Default minimum allowable frame delay in cs. */ +#define NSGIF_FRAME_DELAY_MIN 2 + +/** + * Default frame delay to apply. + * + * Used when a frame delay lower than the minimum is requested. + */ +#define NSGIF_FRAME_DELAY_DEFAULT 10 + +/** GIF frame data */ +typedef struct nsgif_frame { + struct nsgif_frame_info info; + + /** offset (in bytes) to the GIF frame data */ + size_t frame_offset; + /** whether the frame has previously been decoded. */ + bool decoded; + /** whether the frame is totally opaque */ + bool opaque; + /** whether a full image redraw is required */ + bool redraw_required; + + /** Amount of LZW data found in scan */ + uint32_t lzw_data_length; + + /** the index designating a transparent pixel */ + uint32_t transparency_index; + + /** offset to frame colour table */ + uint32_t colour_table_offset; + + /* Frame flags */ + uint32_t flags; +} nsgif_frame; + +/** Pixel format: colour component order. */ +struct nsgif_colour_layout { + uint8_t r; /**< Byte offset within pixel to red component. */ + uint8_t g; /**< Byte offset within pixel to green component. */ + uint8_t b; /**< Byte offset within pixel to blue component. */ + uint8_t a; /**< Byte offset within pixel to alpha component. */ +}; + +/** GIF animation data */ +struct nsgif { + struct nsgif_info info; + + /** LZW decode context */ + void *lzw_ctx; + /** callbacks for bitmap functions */ + nsgif_bitmap_cb_vt bitmap; + /** decoded frames */ + nsgif_frame *frames; + /** current frame */ + uint32_t frame; + /** current frame decoded to bitmap */ + uint32_t decoded_frame; + + /** currently decoded image; stored as bitmap from bitmap_create callback */ + nsgif_bitmap_t *frame_image; + /** Row span of frame_image in pixels. */ + uint32_t rowspan; + + /** Minimum allowable frame delay. */ + uint16_t delay_min; + + /** Frame delay to apply when delay is less than \ref delay_min. */ + uint16_t delay_default; + + /** number of animation loops so far */ + int loop_count; + + /** number of frames partially decoded */ + uint32_t frame_count_partial; + + /** + * Whether all the GIF data has been supplied, or if there may be + * more to come. + */ + bool data_complete; + + /** pointer to GIF data */ + const uint8_t *buf; + /** current index into GIF data */ + size_t buf_pos; + /** total number of bytes of GIF data available */ + size_t buf_len; + + /** current number of frame holders */ + uint32_t frame_holders; + /** background index */ + uint32_t bg_index; + /** image aspect ratio (ignored) */ + uint32_t aspect_ratio; + /** size of global colour table (in entries) */ + uint32_t colour_table_size; + + /** current colour table */ + uint32_t *colour_table; + /** Client's colour component order. */ + struct nsgif_colour_layout colour_layout; + /** global colour table */ + uint32_t global_colour_table[NSGIF_MAX_COLOURS]; + /** local colour table */ + uint32_t local_colour_table[NSGIF_MAX_COLOURS]; + + /** previous frame for NSGIF_FRAME_RESTORE */ + void *prev_frame; + /** previous frame index */ + uint32_t prev_index; +}; + +/** + * Helper macro to get number of elements in an array. + * + * \param[in] _a Array to count elements of. + * \return NUlber of elements in array. + */ +#define NSGIF_ARRAY_LEN(_a) ((sizeof(_a)) / (sizeof(*_a))) + +/** + * + * \file + * \brief GIF image decoder + * + * The GIF format is thoroughly documented; a full description can be found at + * http://www.w3.org/Graphics/GIF/spec-gif89a.txt + * + * \todo Plain text and comment extensions should be implemented. + */ + +/** Internal flag that the colour table needs to be processed */ +#define NSGIF_PROCESS_COLOURS 0xaa000000 + +/** Internal flag that a frame is invalid/unprocessed */ +#define NSGIF_FRAME_INVALID UINT32_MAX + +/** Transparent colour */ +#define NSGIF_TRANSPARENT_COLOUR 0x00 + +/** No transparency */ +#define NSGIF_NO_TRANSPARENCY (0xFFFFFFFFu) + +/* GIF Flags */ +#define NSGIF_COLOUR_TABLE_MASK 0x80 +#define NSGIF_COLOUR_TABLE_SIZE_MASK 0x07 +#define NSGIF_BLOCK_TERMINATOR 0x00 +#define NSGIF_TRAILER 0x3b + +/** + * Convert an LZW result code to equivalent GIF result code. + * + * \param[in] l_res LZW response code. + * \return GIF result code. + */ +static nsgif_error nsgif__error_from_lzw(lzw_result l_res) +{ + static const nsgif_error g_res[] = { + [LZW_OK] = NSGIF_OK, + [LZW_NO_MEM] = NSGIF_ERR_OOM, + [LZW_OK_EOD] = NSGIF_ERR_END_OF_DATA, + [LZW_NO_DATA] = NSGIF_ERR_END_OF_DATA, + [LZW_EOI_CODE] = NSGIF_ERR_DATA_FRAME, + [LZW_BAD_ICODE] = NSGIF_ERR_DATA_FRAME, + [LZW_BAD_CODE] = NSGIF_ERR_DATA_FRAME, + }; + assert(l_res != LZW_BAD_PARAM); + assert(l_res != LZW_NO_COLOUR); + return g_res[l_res]; +} + +/** + * Updates the sprite memory size + * + * \param gif The animation context + * \param width The width of the sprite + * \param height The height of the sprite + * \return NSGIF_ERR_OOM for a memory error NSGIF_OK for success + */ +static nsgif_error nsgif__initialise_sprite( + struct nsgif *gif, + uint32_t width, + uint32_t height) +{ + /* Already allocated? */ + if (gif->frame_image) { + return NSGIF_OK; + } + + assert(gif->bitmap.create); + gif->frame_image = gif->bitmap.create(width, height); + if (gif->frame_image == NULL) { + return NSGIF_ERR_OOM; + } + + return NSGIF_OK; +} + +/** + * Helper to get the rendering bitmap for a gif. + * + * \param[in] gif The gif object we're decoding. + * \return Client pixel buffer for rendering into. + */ +static inline uint32_t* nsgif__bitmap_get( + struct nsgif *gif) +{ + nsgif_error ret; + + /* Make sure we have a buffer to decode to. */ + ret = nsgif__initialise_sprite(gif, gif->info.width, gif->info.height); + if (ret != NSGIF_OK) { + return NULL; + } + + gif->rowspan = gif->info.width; + if (gif->bitmap.get_rowspan) { + gif->rowspan = gif->bitmap.get_rowspan(gif->frame_image); + } + + /* Get the frame data */ + assert(gif->bitmap.get_buffer); + return (void *)gif->bitmap.get_buffer(gif->frame_image); +} + +/** + * Helper to tell the client that their bitmap was modified. + * + * \param[in] gif The gif object we're decoding. + */ +static inline void nsgif__bitmap_modified( + const struct nsgif *gif) +{ + if (gif->bitmap.modified) { + gif->bitmap.modified(gif->frame_image); + } +} + +/** + * Helper to tell the client that whether the bitmap is opaque. + * + * \param[in] gif The gif object we're decoding. + * \param[in] frame The frame that has been decoded. + */ +static inline void nsgif__bitmap_set_opaque( + const struct nsgif *gif, + const struct nsgif_frame *frame) +{ + if (gif->bitmap.set_opaque) { + gif->bitmap.set_opaque( + gif->frame_image, frame->opaque); + } +} + +/** + * Helper to get the client to determine if the bitmap is opaque. + * + * \todo: We don't really need to get the client to do this for us. + * + * \param[in] gif The gif object we're decoding. + * \return true if the bitmap is opaque, false otherwise. + */ +static inline bool nsgif__bitmap_get_opaque( + const struct nsgif *gif) +{ + if (gif->bitmap.test_opaque) { + return gif->bitmap.test_opaque( + gif->frame_image); + } + + return false; +} + +static void nsgif__record_frame( + struct nsgif *gif, + const uint32_t *bitmap) +{ + size_t pixel_bytes = sizeof(*bitmap); + size_t height = gif->info.height; + size_t width = gif->info.width; + uint32_t *prev_frame; + + if (gif->decoded_frame == NSGIF_FRAME_INVALID || + gif->decoded_frame == gif->prev_index) { + /* No frame to copy, or already have this frame recorded. */ + return; + } + + bitmap = nsgif__bitmap_get(gif); + if (bitmap == NULL) { + return; + } + + if (gif->prev_frame == NULL) { + prev_frame = realloc(gif->prev_frame, + width * height * pixel_bytes); + if (prev_frame == NULL) { + return; + } + } else { + prev_frame = gif->prev_frame; + } + + memcpy(prev_frame, bitmap, width * height * pixel_bytes); + + gif->prev_frame = prev_frame; + gif->prev_index = gif->decoded_frame; +} + +static nsgif_error nsgif__recover_frame( + const struct nsgif *gif, + uint32_t *bitmap) +{ + const uint32_t *prev_frame = gif->prev_frame; + size_t pixel_bytes = sizeof(*bitmap); + size_t height = gif->info.height; + size_t width = gif->info.width; + + memcpy(bitmap, prev_frame, height * width * pixel_bytes); + + return NSGIF_OK; +} + +/** + * Get the next line for GIF decode. + * + * Note that the step size must be initialised to 24 at the start of the frame + * (when y == 0). This is because of the first two passes of the frame have + * the same step size of 8, and the step size is used to determine the current + * pass. + * + * \param[in] height Frame height in pixels. + * \param[in,out] y Current row, starting from 0, updated on exit. + * \param[in,out] step Current step starting with 24, updated on exit. + * \return true if there is a row to process, false at the end of the frame. + */ +static inline bool nsgif__deinterlace(uint32_t height, uint32_t *y, uint8_t *step) +{ + *y += *step & 0xf; + + if (*y < height) return true; + + switch (*step) { + case 24: *y = 4; *step = 8; if (*y < height) return true; + /* Fall through. */ + case 8: *y = 2; *step = 4; if (*y < height) return true; + /* Fall through. */ + case 4: *y = 1; *step = 2; if (*y < height) return true; + /* Fall through. */ + default: + break; + } + + return false; +} + +/** + * Get the next line for GIF decode. + * + * \param[in] interlace Non-zero if the frame is not interlaced. + * \param[in] height Frame height in pixels. + * \param[in,out] y Current row, starting from 0, updated on exit. + * \param[in,out] step Current step starting with 24, updated on exit. + * \return true if there is a row to process, false at the end of the frame. + */ +static inline bool nsgif__next_row(uint32_t interlace, + uint32_t height, uint32_t *y, uint8_t *step) +{ + if (!interlace) { + return (++*y != height); + } else { + return nsgif__deinterlace(height, y, step); + } +} + +/** + * Get any frame clip adjustment for the image extent. + * + * \param[in] frame_off Frame's X or Y offset. + * \param[in] frame_dim Frame width or height. + * \param[in] image_ext Image width or height constraint. + * \return the amount the frame needs to be clipped to fit the image in given + * dimension. + */ +static inline uint32_t gif__clip( + uint32_t frame_off, + uint32_t frame_dim, + uint32_t image_ext) +{ + uint32_t frame_ext = frame_off + frame_dim; + + if (frame_ext <= image_ext) { + return 0; + } + + return frame_ext - image_ext; +} + +/** + * Perform any jump over decoded data, to accommodate clipped portion of frame. + * + * \param[in,out] skip Number of pixels of data to jump. + * \param[in,out] available Number of pixels of data currently available. + * \param[in,out] pos Position in decoded pixel value data. + */ +static inline void gif__jump_data( + uint32_t *skip, + uint32_t *available, + const uint8_t **pos) +{ + uint32_t jump = (*skip < *available) ? *skip : *available; + + *skip -= jump; + *available -= jump; + *pos += jump; +} + +static nsgif_error nsgif__decode_complex( + struct nsgif *gif, + uint32_t width, + uint32_t height, + uint32_t offset_x, + uint32_t offset_y, + uint32_t interlace, + const uint8_t *data, + uint32_t transparency_index, + uint32_t *restrict frame_data, + uint32_t *restrict colour_table) +{ + lzw_result res; + nsgif_error ret = NSGIF_OK; + uint32_t clip_x = gif__clip(offset_x, width, gif->info.width); + uint32_t clip_y = gif__clip(offset_y, height, gif->info.height); + const uint8_t *uncompressed; + uint32_t available = 0; + uint8_t step = 24; + uint32_t skip = 0; + uint32_t y = 0; + + if (offset_x >= gif->info.width || + offset_y >= gif->info.height) { + return NSGIF_OK; + } + + width -= clip_x; + height -= clip_y; + + if (width == 0 || height == 0) { + return NSGIF_OK; + } + + /* Initialise the LZW decoding */ + res = lzw_decode_init(gif->lzw_ctx, data[0], + gif->buf, gif->buf_len, + data + 1 - gif->buf); + if (res != LZW_OK) { + return nsgif__error_from_lzw(res); + } + + do { + uint32_t x; + uint32_t *frame_scanline; + + frame_scanline = frame_data + offset_x + + (y + offset_y) * gif->rowspan; + + x = width; + while (x > 0) { + unsigned row_available; + while (available == 0) { + if (res != LZW_OK) { + /* Unexpected end of frame, try to recover */ + if (res == LZW_OK_EOD || + res == LZW_EOI_CODE) { + ret = NSGIF_OK; + } else { + ret = nsgif__error_from_lzw(res); + } + return ret; + } + res = lzw_decode(gif->lzw_ctx, + &uncompressed, &available); + + if (available == 0) { + return NSGIF_OK; + } + gif__jump_data(&skip, &available, &uncompressed); + } + + row_available = x < available ? x : available; + x -= row_available; + available -= row_available; + if (transparency_index > 0xFF) { + while (row_available-- > 0) { + *frame_scanline++ = + colour_table[*uncompressed++]; + } + } else { + while (row_available-- > 0) { + register uint32_t colour; + colour = *uncompressed++; + if (colour != transparency_index) { + *frame_scanline = + colour_table[colour]; + } + frame_scanline++; + } + } + } + + skip = clip_x; + gif__jump_data(&skip, &available, &uncompressed); + } while (nsgif__next_row(interlace, height, &y, &step)); + + return ret; +} + +static nsgif_error nsgif__decode_simple( + struct nsgif *gif, + uint32_t height, + uint32_t offset_y, + const uint8_t *data, + uint32_t transparency_index, + uint32_t *restrict frame_data, + uint32_t *restrict colour_table) +{ + uint32_t pixels; + uint32_t written = 0; + nsgif_error ret = NSGIF_OK; + lzw_result res; + + if (offset_y >= gif->info.height) { + return NSGIF_OK; + } + + height -= gif__clip(offset_y, height, gif->info.height); + + if (height == 0) { + return NSGIF_OK; + } + + /* Initialise the LZW decoding */ + res = lzw_decode_init_map(gif->lzw_ctx, data[0], + transparency_index, colour_table, + gif->buf, gif->buf_len, + data + 1 - gif->buf); + if (res != LZW_OK) { + return nsgif__error_from_lzw(res); + } + + frame_data += (offset_y * gif->info.width); + pixels = gif->info.width * height; + + while (pixels > 0) { + res = lzw_decode_map(gif->lzw_ctx, + frame_data, pixels, &written); + pixels -= written; + frame_data += written; + if (res != LZW_OK) { + /* Unexpected end of frame, try to recover */ + if (res == LZW_OK_EOD || res == LZW_EOI_CODE) { + ret = NSGIF_OK; + } else { + ret = nsgif__error_from_lzw(res); + } + break; + } + } + + if (pixels == 0) { + ret = NSGIF_OK; + } + + return ret; +} + +static inline nsgif_error nsgif__decode( + struct nsgif *gif, + struct nsgif_frame *frame, + const uint8_t *data, + uint32_t *restrict frame_data) +{ + nsgif_error ret; + uint32_t width = frame->info.rect.x1 - frame->info.rect.x0; + uint32_t height = frame->info.rect.y1 - frame->info.rect.y0; + uint32_t offset_x = frame->info.rect.x0; + uint32_t offset_y = frame->info.rect.y0; + uint32_t transparency_index = frame->transparency_index; + uint32_t *restrict colour_table = gif->colour_table; + + if (frame->info.interlaced == false && offset_x == 0 && + width == gif->info.width && + width == gif->rowspan) { + ret = nsgif__decode_simple(gif, height, offset_y, + data, transparency_index, + frame_data, colour_table); + } else { + ret = nsgif__decode_complex(gif, width, height, + offset_x, offset_y, frame->info.interlaced, + data, transparency_index, + frame_data, colour_table); + } + + if (gif->data_complete && ret == NSGIF_ERR_END_OF_DATA) { + /* This is all the data there is, so make do. */ + ret = NSGIF_OK; + } + + return ret; +} + +/** + * Restore a GIF to the background colour. + * + * \param[in] gif The gif object we're decoding. + * \param[in] frame The frame to clear, or NULL. + * \param[in] bitmap The bitmap to clear the frame in. + */ +static void nsgif__restore_bg( + struct nsgif *gif, + struct nsgif_frame *frame, + uint32_t *bitmap) +{ + size_t pixel_bytes = sizeof(*bitmap); + + if (frame == NULL) { + size_t width = gif->info.width; + size_t height = gif->info.height; + + memset(bitmap, NSGIF_TRANSPARENT_COLOUR, + width * height * pixel_bytes); + } else { + uint32_t width = frame->info.rect.x1 - frame->info.rect.x0; + uint32_t height = frame->info.rect.y1 - frame->info.rect.y0; + uint32_t offset_x = frame->info.rect.x0; + uint32_t offset_y = frame->info.rect.y0; + + if (frame->info.display == false || + frame->info.rect.x0 >= gif->info.width || + frame->info.rect.y0 >= gif->info.height) { + return; + } + + width -= gif__clip(offset_x, width, gif->info.width); + height -= gif__clip(offset_y, height, gif->info.height); + + if (frame->info.transparency) { + for (uint32_t y = 0; y < height; y++) { + uint32_t *scanline = bitmap + offset_x + + (offset_y + y) * gif->info.width; + memset(scanline, NSGIF_TRANSPARENT_COLOUR, + width * pixel_bytes); + } + } else { + for (uint32_t y = 0; y < height; y++) { + uint32_t *scanline = bitmap + offset_x + + (offset_y + y) * gif->info.width; + for (uint32_t x = 0; x < width; x++) { + scanline[x] = gif->info.background; + } + } + } + } +} + +static nsgif_error nsgif__update_bitmap( + struct nsgif *gif, + struct nsgif_frame *frame, + const uint8_t *data, + uint32_t frame_idx) +{ + nsgif_error ret; + uint32_t *bitmap; + + gif->decoded_frame = frame_idx; + + bitmap = nsgif__bitmap_get(gif); + if (bitmap == NULL) { + return NSGIF_ERR_OOM; + } + + /* Handle any bitmap clearing/restoration required before decoding this + * frame. */ + if (frame_idx == 0 || gif->decoded_frame == NSGIF_FRAME_INVALID) { + nsgif__restore_bg(gif, NULL, bitmap); + + } else { + struct nsgif_frame *prev = &gif->frames[frame_idx - 1]; + + if (prev->info.disposal == NSGIF_DISPOSAL_RESTORE_BG) { + nsgif__restore_bg(gif, prev, bitmap); + + } else if (prev->info.disposal == NSGIF_DISPOSAL_RESTORE_PREV) { + ret = nsgif__recover_frame(gif, bitmap); + if (ret != NSGIF_OK) { + nsgif__restore_bg(gif, prev, bitmap); + } + } + } + + if (frame->info.disposal == NSGIF_DISPOSAL_RESTORE_PREV) { + /* Store the previous frame for later restoration */ + nsgif__record_frame(gif, bitmap); + } + + ret = nsgif__decode(gif, frame, data, bitmap); + + nsgif__bitmap_modified(gif); + + if (!frame->decoded) { + frame->opaque = nsgif__bitmap_get_opaque(gif); + frame->decoded = true; + } + nsgif__bitmap_set_opaque(gif, frame); + + return ret; +} + +/** + * Parse the graphic control extension + * + * \param[in] frame The gif frame object we're decoding. + * \param[in] data The data to decode. + * \param[in] len Byte length of data. + * \return NSGIF_ERR_END_OF_DATA if more data is needed, + * NSGIF_OK for success. + */ +static nsgif_error nsgif__parse_extension_graphic_control( + struct nsgif_frame *frame, + const uint8_t *data, + size_t len) +{ + enum { + GIF_MASK_TRANSPARENCY = 0x01, + GIF_MASK_DISPOSAL = 0x1c, + }; + + /* 6-byte Graphic Control Extension is: + * + * +0 CHAR Graphic Control Label + * +1 CHAR Block Size + * +2 CHAR __Packed Fields__ + * 3BITS Reserved + * 3BITS Disposal Method + * 1BIT User Input Flag + * 1BIT Transparent Color Flag + * +3 SHORT Delay Time + * +5 CHAR Transparent Color Index + */ + if (len < 6) { + return NSGIF_ERR_END_OF_DATA; + } + + frame->info.delay = data[3] | (data[4] << 8); + + if (data[2] & GIF_MASK_TRANSPARENCY) { + frame->info.transparency = true; + frame->transparency_index = data[5]; + } + + frame->info.disposal = ((data[2] & GIF_MASK_DISPOSAL) >> 2); + /* I have encountered documentation and GIFs in the + * wild that use 0x04 to restore the previous frame, + * rather than the officially documented 0x03. I + * believe some (older?) software may even actually + * export this way. We handle this as a type of + * "quirks" mode. */ + if (frame->info.disposal == NSGIF_DISPOSAL_RESTORE_QUIRK) { + frame->info.disposal = NSGIF_DISPOSAL_RESTORE_PREV; + } + + /* if we are clearing the background then we need to + * redraw enough to cover the previous frame too. */ + frame->redraw_required = + frame->info.disposal == NSGIF_DISPOSAL_RESTORE_BG || + frame->info.disposal == NSGIF_DISPOSAL_RESTORE_PREV; + + return NSGIF_OK; +} + +/** + * Check an app ext identifier and authentication code for loop count extension. + * + * \param[in] data The data to decode. + * \param[in] len Byte length of data. + * \return true if extension is a loop count extension. + */ +static bool nsgif__app_ext_is_loop_count( + const uint8_t *data, + size_t len) +{ + enum { + EXT_LOOP_COUNT_BLOCK_SIZE = 0x0b, + }; + + assert(len > 13); + (void)(len); + + if (data[1] == EXT_LOOP_COUNT_BLOCK_SIZE) { + if (strncmp((const char *)data + 2, "NETSCAPE2.0", 11) == 0 || + strncmp((const char *)data + 2, "ANIMEXTS1.0", 11) == 0) { + return true; + } + } + + return false; +} + +/** + * Parse the application extension + * + * \param[in] gif The gif object we're decoding. + * \param[in] data The data to decode. + * \param[in] len Byte length of data. + * \return NSGIF_ERR_END_OF_DATA if more data is needed, + * NSGIF_OK for success. + */ +static nsgif_error nsgif__parse_extension_application( + struct nsgif *gif, + const uint8_t *data, + size_t len) +{ + /* 14-byte+ Application Extension is: + * + * +0 CHAR Application Extension Label + * +1 CHAR Block Size + * +2 8CHARS Application Identifier + * +10 3CHARS Appl. Authentication Code + * +13 1-256 Application Data (Data sub-blocks) + */ + if (len < 17) { + return NSGIF_ERR_END_OF_DATA; + } + + if (nsgif__app_ext_is_loop_count(data, len)) { + enum { + EXT_LOOP_COUNT_SUB_BLOCK_SIZE = 0x03, + EXT_LOOP_COUNT_SUB_BLOCK_ID = 0x01, + }; + if ((data[13] == EXT_LOOP_COUNT_SUB_BLOCK_SIZE) && + (data[14] == EXT_LOOP_COUNT_SUB_BLOCK_ID)) { + gif->info.loop_max = data[15] | (data[16] << 8); + + /* The value in the source data means repeat N times + * after the first implied play. A value of zero has + * the special meaning of loop forever. (The only way + * to play the animation once is not to have this + * extension at all. */ + if (gif->info.loop_max > 0) { + gif->info.loop_max++; + } + } + } + + return NSGIF_OK; +} + +/** + * Parse the frame's extensions + * + * \param[in] gif The gif object we're decoding. + * \param[in] frame The frame to parse extensions for. + * \param[in] pos Current position in data, updated on exit. + * \param[in] decode Whether to decode or skip over the extension. + * \return NSGIF_ERR_END_OF_DATA if more data is needed, + * NSGIF_OK for success. + */ +static nsgif_error nsgif__parse_frame_extensions( + struct nsgif *gif, + struct nsgif_frame *frame, + const uint8_t **pos, + bool decode) +{ + enum { + GIF_EXT_INTRODUCER = 0x21, + GIF_EXT_GRAPHIC_CONTROL = 0xf9, + GIF_EXT_COMMENT = 0xfe, + GIF_EXT_PLAIN_TEXT = 0x01, + GIF_EXT_APPLICATION = 0xff, + }; + const uint8_t *nsgif_data = *pos; + const uint8_t *nsgif_end = gif->buf + gif->buf_len; + int nsgif_bytes = nsgif_end - nsgif_data; + + /* Initialise the extensions */ + while (nsgif_bytes > 0 && nsgif_data[0] == GIF_EXT_INTRODUCER) { + bool block_step = true; + nsgif_error ret; + + nsgif_data++; + nsgif_bytes--; + + if (nsgif_bytes == 0) { + return NSGIF_ERR_END_OF_DATA; + } + + /* Switch on extension label */ + switch (nsgif_data[0]) { + case GIF_EXT_GRAPHIC_CONTROL: + if (decode) { + ret = nsgif__parse_extension_graphic_control( + frame, + nsgif_data, + nsgif_bytes); + if (ret != NSGIF_OK) { + return ret; + } + } + break; + + case GIF_EXT_APPLICATION: + if (decode) { + ret = nsgif__parse_extension_application( + gif, nsgif_data, nsgif_bytes); + if (ret != NSGIF_OK) { + return ret; + } + } + break; + + case GIF_EXT_COMMENT: + /* Move the pointer to the first data sub-block Skip 1 + * byte for the extension label. */ + ++nsgif_data; + block_step = false; + break; + + default: + break; + } + + if (block_step) { + /* Move the pointer to the first data sub-block Skip 2 + * bytes for the extension label and size fields Skip + * the extension size itself + */ + if (nsgif_bytes < 2) { + return NSGIF_ERR_END_OF_DATA; + } + nsgif_data += 2 + nsgif_data[1]; + } + + /* Repeatedly skip blocks until we get a zero block or run out + * of data. This data is ignored by this gif decoder. */ + while (nsgif_data < nsgif_end && nsgif_data[0] != NSGIF_BLOCK_TERMINATOR) { + nsgif_data += nsgif_data[0] + 1; + if (nsgif_data >= nsgif_end) { + return NSGIF_ERR_END_OF_DATA; + } + } + nsgif_data++; + nsgif_bytes = nsgif_end - nsgif_data; + } + + if (nsgif_data > nsgif_end) { + nsgif_data = nsgif_end; + } + + /* Set buffer position and return */ + *pos = nsgif_data; + return NSGIF_OK; +} + +/** + * Parse a GIF Image Descriptor. + * + * The format is: + * + * +0 CHAR Image Separator (0x2c) + * +1 SHORT Image Left Position + * +3 SHORT Image Top Position + * +5 SHORT Width + * +7 SHORT Height + * +9 CHAR __Packed Fields__ + * 1BIT Local Colour Table Flag + * 1BIT Interlace Flag + * 1BIT Sort Flag + * 2BITS Reserved + * 3BITS Size of Local Colour Table + * + * \param[in] gif The gif object we're decoding. + * \param[in] frame The frame to parse an image descriptor for. + * \param[in] pos Current position in data, updated on exit. + * \param[in] decode Whether to decode the image descriptor. + * \return NSGIF_OK on success, appropriate error otherwise. + */ +static nsgif_error nsgif__parse_image_descriptor( + struct nsgif *gif, + struct nsgif_frame *frame, + const uint8_t **pos, + bool decode) +{ + const uint8_t *data = *pos; + size_t len = gif->buf + gif->buf_len - data; + enum { + NSGIF_IMAGE_DESCRIPTOR_LEN = 10u, + NSGIF_IMAGE_SEPARATOR = 0x2Cu, + NSGIF_MASK_INTERLACE = 0x40u, + }; + + assert(gif != NULL); + assert(frame != NULL); + + if (len < NSGIF_IMAGE_DESCRIPTOR_LEN) { + return NSGIF_ERR_END_OF_DATA; + } + + if (decode) { + uint32_t x, y, w, h; + + if (data[0] != NSGIF_IMAGE_SEPARATOR) { + return NSGIF_ERR_DATA_FRAME; + } + + x = data[1] | (data[2] << 8); + y = data[3] | (data[4] << 8); + w = data[5] | (data[6] << 8); + h = data[7] | (data[8] << 8); + frame->flags = data[9]; + + frame->info.rect.x0 = x; + frame->info.rect.y0 = y; + frame->info.rect.x1 = x + w; + frame->info.rect.y1 = y + h; + + frame->info.interlaced = frame->flags & NSGIF_MASK_INTERLACE; + + /* Allow first frame to grow image dimensions. */ + if (gif->info.frame_count == 0) { + if (x + w > gif->info.width) { + gif->info.width = x + w; + } + if (y + h > gif->info.height) { + gif->info.height = y + h; + } + } + } + + *pos += NSGIF_IMAGE_DESCRIPTOR_LEN; + return NSGIF_OK; +} + +/** + * Extract a GIF colour table into a LibNSGIF colour table buffer. + * + * \param[in] colour_table The colour table to populate. + * \param[in] layout la. + * \param[in] colour_table_entries The number of colour table entries. + * \param[in] data Raw colour table data. + */ +static void nsgif__colour_table_decode( + uint32_t colour_table[NSGIF_MAX_COLOURS], + const struct nsgif_colour_layout *layout, + size_t colour_table_entries, + const uint8_t *data) +{ + uint8_t *entry = (uint8_t *)colour_table; + + while (colour_table_entries--) { + /* Gif colour map contents are r,g,b. + * + * We want to pack them bytewise into the colour table, + * according to the client colour layout. + */ + + entry[layout->r] = *data++; + entry[layout->g] = *data++; + entry[layout->b] = *data++; + entry[layout->a] = 0xff; + + entry += sizeof(uint32_t); + } +} + +/** + * Extract a GIF colour table into a LibNSGIF colour table buffer. + * + * \param[in] colour_table The colour table to populate. + * \param[in] layout The target pixel format to decode to. + * \param[in] colour_table_entries The number of colour table entries. + * \param[in] data Current position in data. + * \param[in] data_len The available length of `data`. + * \param[out] used Number of colour table bytes read. + * \param[in] decode Whether to decode the colour table. + * \return NSGIF_OK on success, appropriate error otherwise. + */ +static inline nsgif_error nsgif__colour_table_extract( + uint32_t colour_table[NSGIF_MAX_COLOURS], + const struct nsgif_colour_layout *layout, + size_t colour_table_entries, + const uint8_t *data, + size_t data_len, + size_t *used, + bool decode) +{ + if (data_len < colour_table_entries * 3) { + return NSGIF_ERR_END_OF_DATA; + } + + if (decode) { + nsgif__colour_table_decode(colour_table, layout, + colour_table_entries, data); + } + + *used = colour_table_entries * 3; + return NSGIF_OK; +} + +/** + * Get a frame's colour table. + * + * Sets up gif->colour_table for the frame. + * + * \param[in] gif The gif object we're decoding. + * \param[in] frame The frame to get the colour table for. + * \param[in] pos Current position in data, updated on exit. + * \param[in] decode Whether to decode the colour table. + * \return NSGIF_OK on success, appropriate error otherwise. + */ +static nsgif_error nsgif__parse_colour_table( + struct nsgif *gif, + struct nsgif_frame *frame, + const uint8_t **pos, + bool decode) +{ + nsgif_error ret; + const uint8_t *data = *pos; + size_t len = gif->buf + gif->buf_len - data; + size_t used_bytes; + + assert(gif != NULL); + assert(frame != NULL); + + if ((frame->flags & NSGIF_COLOUR_TABLE_MASK) == 0) { + gif->colour_table = gif->global_colour_table; + return NSGIF_OK; + } + + if (decode == false) { + frame->colour_table_offset = *pos - gif->buf; + } + + ret = nsgif__colour_table_extract( + gif->local_colour_table, &gif->colour_layout, + 2 << (frame->flags & NSGIF_COLOUR_TABLE_SIZE_MASK), + data, len, &used_bytes, decode); + if (ret != NSGIF_OK) { + return ret; + } + *pos += used_bytes; + + if (decode) { + gif->colour_table = gif->local_colour_table; + } else { + frame->info.local_palette = true; + } + + return NSGIF_OK; +} + +/** + * Parse the image data for a gif frame. + * + * Sets up gif->colour_table for the frame. + * + * \param[in] gif The gif object we're decoding. + * \param[in] frame The frame to parse image data for. + * \param[in] pos Current position in data, updated on exit. + * \param[in] decode Whether to decode the image data. + * \return NSGIF_OK on success, appropriate error otherwise. + */ +static nsgif_error nsgif__parse_image_data( + struct nsgif *gif, + struct nsgif_frame *frame, + const uint8_t **pos, + bool decode) +{ + const uint8_t *data = *pos; + size_t len = gif->buf + gif->buf_len - data; + uint32_t frame_idx = frame - gif->frames; + uint8_t minimum_code_size; + nsgif_error ret; + + assert(gif != NULL); + assert(frame != NULL); + + if (!decode) { + gif->frame_count_partial = frame_idx + 1; + } + + /* Ensure sufficient data remains. A gif trailer or a minimum lzw code + * followed by a gif trailer is treated as OK, although without any + * image data. */ + switch (len) { + default: if (data[0] == NSGIF_TRAILER) return NSGIF_OK; + break; + case 2: if (data[1] == NSGIF_TRAILER) return NSGIF_OK; + /* Fall through. */ + case 1: if (data[0] == NSGIF_TRAILER) return NSGIF_OK; + /* Fall through. */ + case 0: return NSGIF_ERR_END_OF_DATA; + } + + minimum_code_size = data[0]; + if (minimum_code_size >= LZW_CODE_MAX) { + return NSGIF_ERR_DATA_FRAME; + } + + if (decode) { + ret = nsgif__update_bitmap(gif, frame, data, frame_idx); + } else { + uint32_t block_size = 0; + + /* Skip the minimum code size. */ + data++; + len--; + + while (block_size != 1) { + if (len < 1) { + return NSGIF_ERR_END_OF_DATA; + } + block_size = data[0] + 1; + /* Check if the frame data runs off the end of the file */ + if (block_size > len) { + frame->lzw_data_length += len; + return NSGIF_ERR_END_OF_DATA; + } + + len -= block_size; + data += block_size; + frame->lzw_data_length += block_size; + } + + *pos = data; + + gif->info.frame_count = frame_idx + 1; + gif->frames[frame_idx].info.display = true; + + return NSGIF_OK; + } + + return ret; +} + +static struct nsgif_frame *nsgif__get_frame( + struct nsgif *gif, + uint32_t frame_idx) +{ + struct nsgif_frame *frame; + + if (gif->frame_holders > frame_idx) { + frame = &gif->frames[frame_idx]; + } else { + /* Allocate more memory */ + size_t count = frame_idx + 1; + struct nsgif_frame *temp; + + temp = realloc(gif->frames, count * sizeof(*frame)); + if (temp == NULL) { + return NULL; + } + gif->frames = temp; + gif->frame_holders = count; + + frame = &gif->frames[frame_idx]; + + frame->info.local_palette = false; + frame->info.transparency = false; + frame->info.display = false; + frame->info.disposal = 0; + frame->info.delay = 10; + + frame->transparency_index = NSGIF_NO_TRANSPARENCY; + frame->frame_offset = gif->buf_pos; + frame->redraw_required = false; + frame->lzw_data_length = 0; + frame->decoded = false; + } + + return frame; +} + +/** + * Attempts to initialise the next frame + * + * \param[in] gif The animation context + * \param[in] frame_idx The frame number to decode. + * \param[in] decode Whether to decode the graphical image data. + * \return NSGIF_OK on success, appropriate error otherwise. +*/ +static nsgif_error nsgif__process_frame( + struct nsgif *gif, + uint32_t frame_idx, + bool decode) +{ + nsgif_error ret; + const uint8_t *pos; + const uint8_t *end; + struct nsgif_frame *frame; + + frame = nsgif__get_frame(gif, frame_idx); + if (frame == NULL) { + return NSGIF_ERR_OOM; + } + + end = gif->buf + gif->buf_len; + + if (decode) { + pos = gif->buf + frame->frame_offset; + + /* Ensure this frame is supposed to be decoded */ + if (frame->info.display == false) { + return NSGIF_OK; + } + + /* Ensure the frame is in range to decode */ + if (frame_idx > gif->frame_count_partial) { + return NSGIF_ERR_END_OF_DATA; + } + + /* Done if frame is already decoded */ + if (frame_idx == gif->decoded_frame) { + return NSGIF_OK; + } + } else { + pos = gif->buf + gif->buf_pos; + + /* Check if we've finished */ + if (pos < end && pos[0] == NSGIF_TRAILER) { + return NSGIF_OK; + } + } + + ret = nsgif__parse_frame_extensions(gif, frame, &pos, !decode); + if (ret != NSGIF_OK) { + goto cleanup; + } + + ret = nsgif__parse_image_descriptor(gif, frame, &pos, !decode); + if (ret != NSGIF_OK) { + goto cleanup; + } + + ret = nsgif__parse_colour_table(gif, frame, &pos, decode); + if (ret != NSGIF_OK) { + goto cleanup; + } + + ret = nsgif__parse_image_data(gif, frame, &pos, decode); + if (ret != NSGIF_OK) { + goto cleanup; + } + +cleanup: + if (!decode) { + gif->buf_pos = pos - gif->buf; + } + + return ret; +} + +/* exported function documented in nsgif.h */ +void nsgif_destroy(nsgif_t *gif) +{ + if (gif == NULL) { + return; + } + + /* Release all our memory blocks */ + if (gif->frame_image) { + assert(gif->bitmap.destroy); + gif->bitmap.destroy(gif->frame_image); + gif->frame_image = NULL; + } + + free(gif->frames); + gif->frames = NULL; + + free(gif->prev_frame); + gif->prev_frame = NULL; + + lzw_context_destroy(gif->lzw_ctx); + gif->lzw_ctx = NULL; + + free(gif); +} + +/** + * Check whether the host is little endian. + * + * Checks whether least significant bit is in the first byte of a `uint16_t`. + * + * \return true if host is little endian. + */ +static inline bool nsgif__host_is_little_endian(void) +{ + const uint16_t test = 1; + + return ((const uint8_t *) &test)[0]; +} + +static struct nsgif_colour_layout nsgif__bitmap_fmt_to_colour_layout( + nsgif_bitmap_fmt_t bitmap_fmt) +{ + bool le = nsgif__host_is_little_endian(); + + /* Map endian-dependant formats to byte-wise format for the host. */ + switch (bitmap_fmt) { + case NSGIF_BITMAP_FMT_RGBA8888: + bitmap_fmt = (le) ? NSGIF_BITMAP_FMT_A8B8G8R8 + : NSGIF_BITMAP_FMT_R8G8B8A8; + break; + case NSGIF_BITMAP_FMT_BGRA8888: + bitmap_fmt = (le) ? NSGIF_BITMAP_FMT_A8R8G8B8 + : NSGIF_BITMAP_FMT_B8G8R8A8; + break; + case NSGIF_BITMAP_FMT_ARGB8888: + bitmap_fmt = (le) ? NSGIF_BITMAP_FMT_B8G8R8A8 + : NSGIF_BITMAP_FMT_A8R8G8B8; + break; + case NSGIF_BITMAP_FMT_ABGR8888: + bitmap_fmt = (le) ? NSGIF_BITMAP_FMT_R8G8B8A8 + : NSGIF_BITMAP_FMT_A8B8G8R8; + break; + default: + break; + } + + /* Set up colour component order for bitmap format. */ + switch (bitmap_fmt) { + default: + /* Fall through. */ + case NSGIF_BITMAP_FMT_R8G8B8A8: + return (struct nsgif_colour_layout) { + .r = 0, + .g = 1, + .b = 2, + .a = 3, + }; + + case NSGIF_BITMAP_FMT_B8G8R8A8: + return (struct nsgif_colour_layout) { + .b = 0, + .g = 1, + .r = 2, + .a = 3, + }; + + case NSGIF_BITMAP_FMT_A8R8G8B8: + return (struct nsgif_colour_layout) { + .a = 0, + .r = 1, + .g = 2, + .b = 3, + }; + + case NSGIF_BITMAP_FMT_A8B8G8R8: + return (struct nsgif_colour_layout) { + .a = 0, + .b = 1, + .g = 2, + .r = 3, + }; + } +} + +/* exported function documented in nsgif.h */ +nsgif_error nsgif_create( + const nsgif_bitmap_cb_vt *bitmap_vt, + nsgif_bitmap_fmt_t bitmap_fmt, + nsgif_t **gif_out) +{ + nsgif_t *gif; + + gif = calloc(1, sizeof(*gif)); + if (gif == NULL) { + return NSGIF_ERR_OOM; + } + + gif->bitmap = *bitmap_vt; + gif->decoded_frame = NSGIF_FRAME_INVALID; + gif->prev_index = NSGIF_FRAME_INVALID; + + gif->delay_min = NSGIF_FRAME_DELAY_MIN; + gif->delay_default = NSGIF_FRAME_DELAY_DEFAULT; + + gif->colour_layout = nsgif__bitmap_fmt_to_colour_layout(bitmap_fmt); + + *gif_out = gif; + return NSGIF_OK; +} + +/* exported function documented in nsgif.h */ +void nsgif_set_frame_delay_behaviour( + nsgif_t *gif, + uint16_t delay_min, + uint16_t delay_default) +{ + gif->delay_min = delay_min; + gif->delay_default = delay_default; +} + +/** + * Read GIF header. + * + * 6-byte GIF file header is: + * + * +0 3CHARS Signature ('GIF') + * +3 3CHARS Version ('87a' or '89a') + * + * \param[in] gif The GIF object we're decoding. + * \param[in,out] pos The current buffer position, updated on success. + * \param[in] strict Whether to require a known GIF version. + * \return NSGIF_OK on success, appropriate error otherwise. + */ +static nsgif_error nsgif__parse_header( + struct nsgif *gif, + const uint8_t **pos, + bool strict) +{ + const uint8_t *data = *pos; + size_t len = gif->buf + gif->buf_len - data; + + if (len < 6) { + return NSGIF_ERR_END_OF_DATA; + } + + if (strncmp((const char *) data, "GIF", 3) != 0) { + return NSGIF_ERR_DATA; + } + data += 3; + + if (strict == true) { + if ((strncmp((const char *) data, "87a", 3) != 0) && + (strncmp((const char *) data, "89a", 3) != 0)) { + return NSGIF_ERR_DATA; + } + } + data += 3; + + *pos = data; + return NSGIF_OK; +} + +/** + * Read Logical Screen Descriptor. + * + * 7-byte Logical Screen Descriptor is: + * + * +0 SHORT Logical Screen Width + * +2 SHORT Logical Screen Height + * +4 CHAR __Packed Fields__ + * 1BIT Global Colour Table Flag + * 3BITS Colour Resolution + * 1BIT Sort Flag + * 3BITS Size of Global Colour Table + * +5 CHAR Background Colour Index + * +6 CHAR Pixel Aspect Ratio + * + * \param[in] gif The GIF object we're decoding. + * \param[in,out] pos The current buffer position, updated on success. + * \return NSGIF_OK on success, appropriate error otherwise. + */ +static nsgif_error nsgif__parse_logical_screen_descriptor( + struct nsgif *gif, + const uint8_t **pos) +{ + const uint8_t *data = *pos; + size_t len = gif->buf + gif->buf_len - data; + + if (len < 7) { + return NSGIF_ERR_END_OF_DATA; + } + + gif->info.width = data[0] | (data[1] << 8); + gif->info.height = data[2] | (data[3] << 8); + gif->info.global_palette = data[4] & NSGIF_COLOUR_TABLE_MASK; + gif->colour_table_size = 2 << (data[4] & NSGIF_COLOUR_TABLE_SIZE_MASK); + gif->bg_index = data[5]; + gif->aspect_ratio = data[6]; + gif->info.loop_max = 1; + + *pos += 7; + return NSGIF_OK; +} + +/* exported function documented in nsgif.h */ +nsgif_error nsgif_data_scan( + nsgif_t *gif, + size_t size, + const uint8_t *data) +{ + const uint8_t *nsgif_data; + nsgif_error ret; + uint32_t frames; + + if (gif->data_complete) { + return NSGIF_ERR_DATA_COMPLETE; + } + + /* Initialize values */ + gif->buf_len = size; + gif->buf = data; + + /* Get our current processing position */ + nsgif_data = gif->buf + gif->buf_pos; + + /* See if we should initialise the GIF */ + if (gif->buf_pos == 0) { + /* We want everything to be NULL before we start so we've no + * chance of freeing bad pointers (paranoia) + */ + gif->frame_image = NULL; + gif->frames = NULL; + gif->frame_holders = 0; + + /* The caller may have been lazy and not reset any values */ + gif->info.frame_count = 0; + gif->frame_count_partial = 0; + gif->decoded_frame = NSGIF_FRAME_INVALID; + gif->frame = NSGIF_FRAME_INVALID; + + ret = nsgif__parse_header(gif, &nsgif_data, false); + if (ret != NSGIF_OK) { + return ret; + } + + ret = nsgif__parse_logical_screen_descriptor(gif, &nsgif_data); + if (ret != NSGIF_OK) { + return ret; + } + + /* Remember we've done this now */ + gif->buf_pos = nsgif_data - gif->buf; + + /* Some broken GIFs report the size as the screen size they + * were created in. As such, we detect for the common cases and + * set the sizes as 0 if they are found which results in the + * GIF being the maximum size of the frames. + */ + if (((gif->info.width == 640) && (gif->info.height == 480)) || + ((gif->info.width == 640) && (gif->info.height == 512)) || + ((gif->info.width == 800) && (gif->info.height == 600)) || + ((gif->info.width == 1024) && (gif->info.height == 768)) || + ((gif->info.width == 1280) && (gif->info.height == 1024)) || + ((gif->info.width == 1600) && (gif->info.height == 1200)) || + ((gif->info.width == 0) || (gif->info.height == 0)) || + ((gif->info.width > 2048) || (gif->info.height > 2048))) { + gif->info.width = 1; + gif->info.height = 1; + } + + /* Set the first colour to a value that will never occur in + * reality so we know if we've processed it + */ + gif->global_colour_table[0] = NSGIF_PROCESS_COLOURS; + + /* Check if the GIF has no frame data (13-byte header + 1-byte + * termination block) Although generally useless, the GIF + * specification does not expressly prohibit this + */ + if (gif->buf_len == gif->buf_pos + 1) { + if (nsgif_data[0] == NSGIF_TRAILER) { + return NSGIF_OK; + } + } + } + + /* Do the colour map if we haven't already. As the top byte is always + * 0xff or 0x00 depending on the transparency we know if it's been + * filled in. + */ + if (gif->global_colour_table[0] == NSGIF_PROCESS_COLOURS) { + /* Check for a global colour map signified by bit 7 */ + if (gif->info.global_palette) { + size_t remaining = gif->buf + gif->buf_len - nsgif_data; + size_t used; + + ret = nsgif__colour_table_extract( + gif->global_colour_table, + &gif->colour_layout, + gif->colour_table_size, + nsgif_data, remaining, &used, true); + if (ret != NSGIF_OK) { + return ret; + } + + nsgif_data += used; + gif->buf_pos = (nsgif_data - gif->buf); + } else { + /* Create a default colour table with the first two + * colours as black and white. */ + uint8_t *entry = (uint8_t *)gif->global_colour_table; + + /* Black */ + entry[gif->colour_layout.r] = 0x00; + entry[gif->colour_layout.g] = 0x00; + entry[gif->colour_layout.b] = 0x00; + entry[gif->colour_layout.a] = 0xFF; + + entry += sizeof(uint32_t); + + /* White */ + entry[gif->colour_layout.r] = 0xFF; + entry[gif->colour_layout.g] = 0xFF; + entry[gif->colour_layout.b] = 0xFF; + entry[gif->colour_layout.a] = 0xFF; + + gif->colour_table_size = 2; + } + + if (gif->info.global_palette && + gif->bg_index < gif->colour_table_size) { + size_t bg_idx = gif->bg_index; + gif->info.background = gif->global_colour_table[bg_idx]; + } else { + gif->info.background = gif->global_colour_table[0]; + } + } + + if (gif->lzw_ctx == NULL) { + lzw_result res = lzw_context_create( + (struct lzw_ctx **)&gif->lzw_ctx); + if (res != LZW_OK) { + return nsgif__error_from_lzw(res); + } + } + + /* Try to initialise all frames. */ + do { + frames = gif->info.frame_count; + ret = nsgif__process_frame(gif, frames, false); + } while (gif->info.frame_count > frames); + + if (ret == NSGIF_ERR_END_OF_DATA && gif->info.frame_count > 0) { + ret = NSGIF_OK; + } + + return ret; +} + +/* exported function documented in nsgif.h */ +void nsgif_data_complete( + nsgif_t *gif) +{ + if (gif->data_complete == false) { + uint32_t start = gif->info.frame_count; + uint32_t end = gif->frame_count_partial; + + for (uint32_t f = start; f < end; f++) { + nsgif_frame *frame = &gif->frames[f]; + + if (frame->lzw_data_length > 0) { + frame->info.display = true; + gif->info.frame_count = f + 1; + + if (f == 0) { + frame->info.transparency = true; + } + break; + } + } + } + + gif->data_complete = true; +} + +static void nsgif__redraw_rect_extend( + const nsgif_rect_t *frame, + nsgif_rect_t *redraw) +{ + if (redraw->x1 == 0 || redraw->y1 == 0) { + *redraw = *frame; + } else { + if (redraw->x0 > frame->x0) { + redraw->x0 = frame->x0; + } + if (redraw->x1 < frame->x1) { + redraw->x1 = frame->x1; + } + if (redraw->y0 > frame->y0) { + redraw->y0 = frame->y0; + } + if (redraw->y1 < frame->y1) { + redraw->y1 = frame->y1; + } + } +} + +static uint32_t nsgif__frame_next( + const nsgif_t *gif, + bool partial, + uint32_t frame) +{ + uint32_t frames = partial ? + gif->frame_count_partial : + gif->info.frame_count; + + if (frames == 0) { + return NSGIF_FRAME_INVALID; + } + + frame++; + return (frame >= frames) ? 0 : frame; +} + +static nsgif_error nsgif__next_displayable_frame( + const nsgif_t *gif, + uint32_t *frame, + uint32_t *delay) +{ + uint32_t next = *frame; + + do { + next = nsgif__frame_next(gif, false, next); + if (next <= *frame && *frame != NSGIF_FRAME_INVALID && + gif->data_complete == false) { + return NSGIF_ERR_END_OF_DATA; + + } else if (next == *frame || next == NSGIF_FRAME_INVALID) { + return NSGIF_ERR_FRAME_DISPLAY; + } + + if (delay != NULL) { + *delay += gif->frames[next].info.delay; + } + + } while (gif->frames[next].info.display == false); + + *frame = next; + return NSGIF_OK; +} + +static inline bool nsgif__animation_complete(int count, int max) +{ + if (max == 0) { + return false; + } + + return (count >= max); +} + +nsgif_error nsgif_reset( + nsgif_t *gif) +{ + gif->loop_count = 0; + gif->frame = NSGIF_FRAME_INVALID; + + return NSGIF_OK; +} + +/* exported function documented in nsgif.h */ +nsgif_error nsgif_frame_prepare( + nsgif_t *gif, + nsgif_rect_t *area, + uint32_t *delay_cs, + uint32_t *frame_new) +{ + nsgif_error ret; + nsgif_rect_t rect = { + .x1 = 0, + .y1 = 0, + }; + uint32_t delay = 0; + uint32_t frame = gif->frame; + + if (gif->frame != NSGIF_FRAME_INVALID && + gif->frame < gif->info.frame_count && + gif->frames[gif->frame].info.display) { + rect = gif->frames[gif->frame].info.rect; + } + + if (nsgif__animation_complete( + gif->loop_count, + gif->info.loop_max)) { + return NSGIF_ERR_ANIMATION_END; + } + + ret = nsgif__next_displayable_frame(gif, &frame, &delay); + if (ret != NSGIF_OK) { + return ret; + } + + if (gif->frame != NSGIF_FRAME_INVALID && frame < gif->frame) { + gif->loop_count++; + } + + if (gif->data_complete) { + /* Check for last frame, which has infinite delay. */ + + if (gif->info.frame_count == 1) { + delay = NSGIF_INFINITE; + } else if (gif->info.loop_max != 0) { + uint32_t frame_next = frame; + + ret = nsgif__next_displayable_frame(gif, + &frame_next, NULL); + if (ret != NSGIF_OK) { + return ret; + } + + if (gif->data_complete && frame_next < frame) { + if (nsgif__animation_complete( + gif->loop_count + 1, + gif->info.loop_max)) { + delay = NSGIF_INFINITE; + } + } + } + } + + gif->frame = frame; + nsgif__redraw_rect_extend(&gif->frames[frame].info.rect, &rect); + + if (delay < gif->delay_min) { + delay = gif->delay_default; + } + + *frame_new = gif->frame; + *delay_cs = delay; + *area = rect; + + return NSGIF_OK; +} + +/* exported function documented in nsgif.h */ +nsgif_error nsgif_frame_decode( + nsgif_t *gif, + uint32_t frame, + nsgif_bitmap_t **bitmap) +{ + uint32_t start_frame; + nsgif_error ret = NSGIF_OK; + + if (frame >= gif->info.frame_count) { + return NSGIF_ERR_BAD_FRAME; + } + + if (gif->decoded_frame == frame) { + *bitmap = gif->frame_image; + return NSGIF_OK; + + } else if (gif->decoded_frame >= frame || + gif->decoded_frame == NSGIF_FRAME_INVALID) { + /* Can skip to first frame or restart. */ + start_frame = 0; + } else { + start_frame = nsgif__frame_next( + gif, false, gif->decoded_frame); + } + + for (uint32_t f = start_frame; f <= frame; f++) { + ret = nsgif__process_frame(gif, f, true); + if (ret != NSGIF_OK) { + return ret; + } + } + + *bitmap = gif->frame_image; + return ret; +} + +/* exported function documented in nsgif.h */ +const nsgif_info_t *nsgif_get_info(const nsgif_t *gif) +{ + return &gif->info; +} + +/* exported function documented in nsgif.h */ +const nsgif_frame_info_t *nsgif_get_frame_info( + const nsgif_t *gif, + uint32_t frame) +{ + if (frame >= gif->info.frame_count) { + return NULL; + } + + return &gif->frames[frame].info; +} + +/* exported function documented in nsgif.h */ +void nsgif_global_palette( + const nsgif_t *gif, + uint32_t table[NSGIF_MAX_COLOURS], + size_t *entries) +{ + size_t len = sizeof(*table) * NSGIF_MAX_COLOURS; + + memcpy(table, gif->global_colour_table, len); + *entries = gif->colour_table_size; +} + +/* exported function documented in nsgif.h */ +bool nsgif_local_palette( + const nsgif_t *gif, + uint32_t frame, + uint32_t table[NSGIF_MAX_COLOURS], + size_t *entries) +{ + const nsgif_frame *f; + + if (frame >= gif->frame_count_partial) { + return false; + } + + f = &gif->frames[frame]; + if (f->info.local_palette == false) { + return false; + } + + *entries = 2 << (f->flags & NSGIF_COLOUR_TABLE_SIZE_MASK); + nsgif__colour_table_decode(table, &gif->colour_layout, + *entries, gif->buf + f->colour_table_offset); + + return true; +} + +/* exported function documented in nsgif.h */ +const char *nsgif_strerror(nsgif_error err) +{ + static const char *const str[] = { + [NSGIF_OK] = "Success", + [NSGIF_ERR_OOM] = "Out of memory", + [NSGIF_ERR_DATA] = "Invalid source data", + [NSGIF_ERR_BAD_FRAME] = "Requested frame does not exist", + [NSGIF_ERR_DATA_FRAME] = "Invalid frame data", + [NSGIF_ERR_END_OF_DATA] = "Unexpected end of GIF source data", + [NSGIF_ERR_DATA_COMPLETE] = "Can't add data to completed GIF", + [NSGIF_ERR_FRAME_DISPLAY] = "Frame can't be displayed", + [NSGIF_ERR_ANIMATION_END] = "Animation complete", + }; + + if (err >= NSGIF_ARRAY_LEN(str) || str[err] == NULL) { + return "Unknown error"; + } + + return str[err]; +} + +/* exported function documented in nsgif.h */ +const char *nsgif_str_disposal(enum nsgif_disposal disposal) +{ + static const char *const str[] = { + [NSGIF_DISPOSAL_UNSPECIFIED] = "Unspecified", + [NSGIF_DISPOSAL_NONE] = "None", + [NSGIF_DISPOSAL_RESTORE_BG] = "Restore background", + [NSGIF_DISPOSAL_RESTORE_PREV] = "Restore previous", + [NSGIF_DISPOSAL_RESTORE_QUIRK] = "Restore quirk", + }; + + if (disposal >= NSGIF_ARRAY_LEN(str) || str[disposal] == NULL) { + return "Unspecified"; + } + + return str[disposal]; +} diff --git a/src/image_gif.cpp b/src/image_gif.cpp new file mode 100644 index 0000000000..9e0dc08a02 --- /dev/null +++ b/src/image_gif.cpp @@ -0,0 +1,103 @@ +/*start of file image_gif.cpp*/ +#include "image_gif.h" +#include "output.h" + +// This struct is used temporarily by the nsgif callbacks. +struct gif_bitmap { + BitmapRef bmp; +}; + +nsgif_bitmap_t* GifDecoder::cb_bitmap_create(int width, int height) { + auto* bitmap = new gif_bitmap(); + bitmap->bmp = Bitmap::Create(width, height, true); + return reinterpret_cast(bitmap); +} + +void GifDecoder::cb_bitmap_destroy(nsgif_bitmap_t* bitmap) { + delete reinterpret_cast(bitmap); +} + +uint8_t* GifDecoder::cb_bitmap_get_buffer(nsgif_bitmap_t* bitmap) { + return reinterpret_cast(reinterpret_cast(bitmap)->bmp->pixels()); +} + +GifDecoder::GifDecoder(const uint8_t* data, size_t len) { + nsgif_bitmap_cb_vt callbacks; + callbacks.create = cb_bitmap_create; + callbacks.destroy = cb_bitmap_destroy; + callbacks.get_buffer = cb_bitmap_get_buffer; + // Set other optional callbacks to NULL if not used + callbacks.set_opaque = nullptr; + callbacks.test_opaque = nullptr; + callbacks.modified = nullptr; + callbacks.get_rowspan = nullptr; + + if (nsgif_create(&callbacks, NSGIF_BITMAP_FMT_ARGB8888, &_gif) != NSGIF_OK) { + Output::Warning("Could not create GIF decoder context."); + return; + } + + if (nsgif_data_scan(_gif, len, data) != NSGIF_OK) { + Output::Warning("Error scanning GIF data."); + nsgif_destroy(_gif); + _gif = nullptr; + return; + } + nsgif_data_complete(_gif); + + const nsgif_info_t* info = nsgif_get_info(_gif); + _width = info->width; + _height = info->height; + + // Decode all frames + for (uint32_t i = 0; i < info->frame_count; ++i) { + nsgif_bitmap_t* decoded_bmp_ptr = nullptr; + const nsgif_frame_info_t* frame_info = nsgif_get_frame_info(_gif, i); + + if (nsgif_frame_decode(_gif, i, &decoded_bmp_ptr) != NSGIF_OK) { + Output::Warning("Error decoding GIF frame %d.", i); + continue; + } + + // nsgif reuses its internal buffer. We must copy the frame data. + BitmapRef source_frame_bitmap = reinterpret_cast(decoded_bmp_ptr)->bmp; + BitmapRef frame_copy = Bitmap::Create(*source_frame_bitmap, source_frame_bitmap->GetRect(), true); + + // If the frame has no explicit transparency, check if we should apply it manually. + if (!frame_info->transparency) { + uint32_t palette[NSGIF_MAX_COLOURS]; + size_t entries; + + // Get the appropriate palette (local or global) + if (!nsgif_local_palette(_gif, i, palette, &entries)) { + nsgif_global_palette(_gif, palette, &entries); + } + + if (entries > 0) { + // The color at index 0 is our target for transparency. + // nsgif outputs fully opaque pixels, so we need to match the opaque version of the color. + uint32_t transparent_color = palette[0] | 0xFF000000; // Force alpha to full for comparison + + uint32_t* pixels = reinterpret_cast(frame_copy->pixels()); + int pixel_count = frame_copy->GetWidth() * frame_copy->GetHeight(); + + for (int p = 0; p < pixel_count; ++p) { + if (pixels[p] == transparent_color) { + pixels[p] = 0; // Set to transparent black + } + } + } + } + + _frames.push_back({ frame_copy, (int)frame_info->delay }); + } + + _valid = true; +} + +GifDecoder::~GifDecoder() { + if (_gif) { + nsgif_destroy(_gif); + } +} +/*end of file image_gif.cpp*/ diff --git a/src/image_gif.h b/src/image_gif.h new file mode 100644 index 0000000000..8b93dbeeb6 --- /dev/null +++ b/src/image_gif.h @@ -0,0 +1,34 @@ +/*start of file image_gif.h*/ +#pragma once +#include +#include +#include "bitmap.h" +#include "nsgif.h" // No more extern "C" wrapper here + +struct GifFrame { + BitmapRef bitmap; + int delay_cs; // Delay in centiseconds +}; + +class GifDecoder { +public: + GifDecoder(const uint8_t* data, size_t len); + ~GifDecoder(); + + bool IsValid() const { return _valid; } + int GetWidth() const { return _width; } + int GetHeight() const { return _height; } + const std::vector& GetFrames() const { return _frames; } + +private: + static nsgif_bitmap_t* cb_bitmap_create(int width, int height); + static void cb_bitmap_destroy(nsgif_bitmap_t* bitmap); + static uint8_t* cb_bitmap_get_buffer(nsgif_bitmap_t* bitmap); + + bool _valid = false; + int _width = 0; + int _height = 0; + std::vector _frames; + nsgif_t* _gif = nullptr; +}; +/*end of file image_gif.h*/ diff --git a/src/lzw.c b/src/lzw.c new file mode 100644 index 0000000000..3098ef5052 --- /dev/null +++ b/src/lzw.c @@ -0,0 +1,613 @@ +/* + * This file is part of NetSurf's LibNSGIF, http://www.netsurf-browser.org/ + * Licensed under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + * + * Copyright 2017 Michael Drake + * Copyright 2021 Michael Drake + */ + +#include +#include +#include +#include + +#include "lzw.h" + +/** + * \file + * \brief LZW decompression (implementation) + * + * Decoder for GIF LZW data. + */ + +/** Maximum number of lzw table entries. */ +#define LZW_TABLE_ENTRY_MAX (1u << LZW_CODE_MAX) + +/** + * Context for reading LZW data. + * + * LZW data is split over multiple sub-blocks. Each sub-block has a + * byte at the start, which says the sub-block size, and then the data. + * Zero-size sub-blocks have no data, and the biggest sub-block size is + * 255, which means there are 255 bytes of data following the sub-block + * size entry. + * + * Note that an individual LZW code can be split over up to three sub-blocks. + */ +struct lzw_read_ctx { + const uint8_t *restrict data; /**< Pointer to start of input data */ + size_t data_len; /**< Input data length */ + size_t data_sb_next; /**< Offset to sub-block size */ + + const uint8_t *sb_data; /**< Pointer to current sub-block in data */ + size_t sb_bit; /**< Current bit offset in sub-block */ + uint32_t sb_bit_count; /**< Bit count in sub-block */ +}; + +/** + * LZW table entry. + * + * Records in the table are composed of 1 or more entries. + * Entries refer to the entry they extend which can be followed to compose + * the complete record. To compose the record in reverse order, take + * the `value` from each entry, and move to the entry it extends. + * If the extended entries index is < the current clear_code, then it + * is the last entry in the record. + */ +struct lzw_table_entry { + uint8_t value; /**< Last value for record ending at entry. */ + uint8_t first; /**< First value in entry's entire record. */ + uint16_t count; /**< Count of values in this entry's record. */ + uint16_t extends; /**< Offset in table to previous entry. */ +}; + +/** + * LZW decompression context. + */ +struct lzw_ctx { + struct lzw_read_ctx input; /**< Input reading context */ + + uint16_t prev_code; /**< Code read from input previously. */ + uint16_t prev_code_first; /**< First value of previous code. */ + uint16_t prev_code_count; /**< Total values for previous code. */ + + uint8_t initial_code_size; /**< Starting LZW code size. */ + + uint8_t code_size; /**< Current LZW code size. */ + uint16_t code_max; /**< Max code value for current code size. */ + + uint16_t clear_code; /**< Special Clear code value */ + uint16_t eoi_code; /**< Special End of Information code value */ + + uint16_t table_size; /**< Next position in table to fill. */ + + uint16_t output_code; /**< Code that has been partially output. */ + uint16_t output_left; /**< Number of values left for output_code. */ + + bool has_transparency; /**< Whether the image is opaque. */ + uint8_t transparency_idx; /**< Index representing transparency. */ + const uint32_t *restrict colour_map; /**< Index to colour mapping. */ + + /** LZW code table. Generated during decode. */ + struct lzw_table_entry table[LZW_TABLE_ENTRY_MAX]; + + /** Output value stack. */ + uint8_t stack_base[LZW_TABLE_ENTRY_MAX]; +}; + +/* Exported function, documented in lzw.h */ +lzw_result lzw_context_create(struct lzw_ctx **ctx) +{ + struct lzw_ctx *c = malloc(sizeof(*c)); + if (c == NULL) { + return LZW_NO_MEM; + } + + *ctx = c; + return LZW_OK; +} + +/* Exported function, documented in lzw.h */ +void lzw_context_destroy(struct lzw_ctx *ctx) +{ + free(ctx); +} + +/** + * Advance the context to the next sub-block in the input data. + * + * \param[in] ctx LZW reading context, updated on success. + * \return LZW_OK or LZW_OK_EOD on success, appropriate error otherwise. + */ +static lzw_result lzw__block_advance(struct lzw_read_ctx *restrict ctx) +{ + size_t block_size; + size_t next_block_pos = ctx->data_sb_next; + const uint8_t *data_next = ctx->data + next_block_pos; + + if (next_block_pos >= ctx->data_len) { + return LZW_NO_DATA; + } + + block_size = *data_next; + + if ((next_block_pos + block_size) >= ctx->data_len) { + return LZW_NO_DATA; + } + + ctx->sb_bit = 0; + ctx->sb_bit_count = block_size * 8; + + if (block_size == 0) { + ctx->data_sb_next += 1; + return LZW_OK_EOD; + } + + ctx->sb_data = data_next + 1; + ctx->data_sb_next += block_size + 1; + + return LZW_OK; +} + +/** + * Get the next LZW code of given size from the raw input data. + * + * Reads codes from the input data stream coping with GIF data sub-blocks. + * + * \param[in] ctx LZW reading context, updated. + * \param[in] code_size Size of LZW code to get from data. + * \param[out] code_out Returns an LZW code on success. + * \return LZW_OK or LZW_OK_EOD on success, appropriate error otherwise. + */ +static inline lzw_result lzw__read_code( + struct lzw_read_ctx *restrict ctx, + uint16_t code_size, + uint16_t *restrict code_out) +{ + uint32_t code = 0; + uint32_t current_bit = ctx->sb_bit & 0x7; + + if (ctx->sb_bit + 24 <= ctx->sb_bit_count) { + /* Fast path: read three bytes from this sub-block */ + const uint8_t *data = ctx->sb_data + (ctx->sb_bit >> 3); + code |= *data++ << 0; + code |= *data++ << 8; + code |= *data << 16; + ctx->sb_bit += code_size; + } else { + /* Slow path: code spans sub-blocks */ + uint8_t byte_advance = (current_bit + code_size) >> 3; + uint8_t byte = 0; + uint8_t bits_remaining_0 = (code_size < (8u - current_bit)) ? + code_size : (8u - current_bit); + uint8_t bits_remaining_1 = code_size - bits_remaining_0; + uint8_t bits_used[3] = { + [0] = bits_remaining_0, + [1] = bits_remaining_1 < 8 ? bits_remaining_1 : 8, + [2] = bits_remaining_1 - 8, + }; + + assert(byte_advance <= 2); + + while (true) { + const uint8_t *data = ctx->sb_data; + lzw_result res; + + /* Get any data from end of this sub-block */ + while (byte <= byte_advance && + ctx->sb_bit < ctx->sb_bit_count) { + code |= data[ctx->sb_bit >> 3] << (byte << 3); + ctx->sb_bit += bits_used[byte]; + byte++; + } + + /* Check if we have all we need */ + if (byte > byte_advance) { + break; + } + + /* Move to next sub-block */ + res = lzw__block_advance(ctx); + if (res != LZW_OK) { + return res; + } + } + } + + *code_out = (code >> current_bit) & ((1 << code_size) - 1); + return LZW_OK; +} + +/** + * Handle clear code. + * + * \param[in] ctx LZW reading context, updated. + * \param[out] code_out Returns next code after a clear code. + * \return LZW_OK or error code. + */ +static inline lzw_result lzw__handle_clear( + struct lzw_ctx *ctx, + uint16_t *code_out) +{ + uint16_t code; + + /* Reset table building context */ + ctx->code_size = ctx->initial_code_size; + ctx->code_max = (1 << ctx->initial_code_size) - 1; + ctx->table_size = ctx->eoi_code + 1; + + /* There might be a sequence of clear codes, so process them all */ + do { + lzw_result res = lzw__read_code(&ctx->input, + ctx->code_size, &code); + if (res != LZW_OK) { + return res; + } + } while (code == ctx->clear_code); + + /* The initial code must be from the initial table. */ + if (code > ctx->clear_code) { + return LZW_BAD_ICODE; + } + + *code_out = code; + return LZW_OK; +} + +/* Exported function, documented in lzw.h */ +lzw_result lzw_decode_init( + struct lzw_ctx *ctx, + uint8_t minimum_code_size, + const uint8_t *input_data, + size_t input_length, + size_t input_pos) +{ + struct lzw_table_entry *table = ctx->table; + lzw_result res; + uint16_t code; + + if (minimum_code_size >= LZW_CODE_MAX) { + return LZW_BAD_ICODE; + } + + /* Initialise the input reading context */ + ctx->input.data = input_data; + ctx->input.data_len = input_length; + ctx->input.data_sb_next = input_pos; + + ctx->input.sb_bit = 0; + ctx->input.sb_bit_count = 0; + + /* Initialise the table building context */ + ctx->initial_code_size = minimum_code_size + 1; + + ctx->clear_code = (1 << minimum_code_size) + 0; + ctx->eoi_code = (1 << minimum_code_size) + 1; + + ctx->output_left = 0; + + /* Initialise the standard table entries */ + for (uint16_t i = 0; i < ctx->clear_code; i++) { + table[i].first = i; + table[i].value = i; + table[i].count = 1; + } + + res = lzw__handle_clear(ctx, &code); + if (res != LZW_OK) { + return res; + } + + /* Store details of this code as "previous code" to the context. */ + ctx->prev_code_first = ctx->table[code].first; + ctx->prev_code_count = ctx->table[code].count; + ctx->prev_code = code; + + /* Add code to context for immediate output. */ + ctx->output_code = code; + ctx->output_left = 1; + + ctx->has_transparency = false; + ctx->transparency_idx = 0; + ctx->colour_map = NULL; + + return LZW_OK; +} + +/* Exported function, documented in lzw.h */ +lzw_result lzw_decode_init_map( + struct lzw_ctx *ctx, + uint8_t minimum_code_size, + uint32_t transparency_idx, + const uint32_t *colour_table, + const uint8_t *input_data, + size_t input_length, + size_t input_pos) +{ + lzw_result res; + + if (colour_table == NULL) { + return LZW_BAD_PARAM; + } + + res = lzw_decode_init(ctx, minimum_code_size, + input_data, input_length, input_pos); + if (res != LZW_OK) { + return res; + } + + ctx->has_transparency = (transparency_idx <= 0xFF); + ctx->transparency_idx = transparency_idx; + ctx->colour_map = colour_table; + + return LZW_OK; +} + +/** + * Create new table entry. + * + * \param[in] ctx LZW reading context, updated. + * \param[in] code Last value code for new table entry. + */ +static inline void lzw__table_add_entry( + struct lzw_ctx *ctx, + uint16_t code) +{ + struct lzw_table_entry *entry = &ctx->table[ctx->table_size]; + + entry->value = code; + entry->first = ctx->prev_code_first; + entry->count = ctx->prev_code_count + 1; + entry->extends = ctx->prev_code; + + ctx->table_size++; +} + +typedef uint32_t (*lzw_writer_fn)( + struct lzw_ctx *ctx, + void *restrict output_data, + uint32_t output_length, + uint32_t output_pos, + uint16_t code, + uint16_t left); + +/** + * Get the next LZW code and write its value(s) to output buffer. + * + * \param[in] ctx LZW reading context, updated. + * \param[in] write_fn Function for writing pixels to output. + * \param[in] output_data Array to write output values into. + * \param[in] output_length Size of output array. + * \param[in,out] output_written Number of values written. Updated on exit. + * \return LZW_OK on success, or appropriate error code otherwise. + */ +static inline lzw_result lzw__decode( + struct lzw_ctx *ctx, + lzw_writer_fn write_fn, + void *restrict output_data, + uint32_t output_length, + uint32_t *restrict output_written) +{ + lzw_result res; + uint16_t code; + + /* Get a new code from the input */ + res = lzw__read_code(&ctx->input, ctx->code_size, &code); + if (res != LZW_OK) { + return res; + } + + /* Handle the new code */ + if (code == ctx->eoi_code) { + /* Got End of Information code */ + return LZW_EOI_CODE; + + } else if (code > ctx->table_size) { + /* Code is invalid */ + return LZW_BAD_CODE; + + } else if (code == ctx->clear_code) { + res = lzw__handle_clear(ctx, &code); + if (res != LZW_OK) { + return res; + } + + } else if (ctx->table_size < LZW_TABLE_ENTRY_MAX) { + uint16_t size = ctx->table_size; + lzw__table_add_entry(ctx, (code < size) ? + ctx->table[code].first : + ctx->prev_code_first); + + /* Ensure code size is increased, if needed. */ + if (size == ctx->code_max && ctx->code_size < LZW_CODE_MAX) { + ctx->code_size++; + ctx->code_max = (1 << ctx->code_size) - 1; + } + } + + *output_written += write_fn(ctx, + output_data, output_length, *output_written, + code, ctx->table[code].count); + + /* Store details of this code as "previous code" to the context. */ + ctx->prev_code_first = ctx->table[code].first; + ctx->prev_code_count = ctx->table[code].count; + ctx->prev_code = code; + + return LZW_OK; +} + +/** + * Write values for this code to the output stack. + * + * If there isn't enough space in the output stack, this function will write + * the as many as it can into the output. If `ctx->output_left > 0` after + * this call, then there is more data for this code left to output. The code + * is stored to the context as `ctx->output_code`. + * + * \param[in] ctx LZW reading context, updated. + * \param[in] output_data Array to write output values into. + * \param[in] output_length length Size of output array. + * \param[in] output_used Current position in output array. + * \param[in] code LZW code to output values for. + * \param[in] left Number of values remaining to output for this code. + * \return Number of pixel values written. + */ +static inline uint32_t lzw__write_fn(struct lzw_ctx *ctx, + void *restrict output_data, + uint32_t output_length, + uint32_t output_used, + uint16_t code, + uint16_t left) +{ + uint8_t *restrict output_pos = (uint8_t *)output_data + output_used; + const struct lzw_table_entry * const table = ctx->table; + uint32_t space = output_length - output_used; + uint16_t count = left; + + if (count > space) { + left = count - space; + count = space; + } else { + left = 0; + } + + ctx->output_code = code; + ctx->output_left = left; + + /* Skip over any values we don't have space for. */ + for (unsigned i = left; i != 0; i--) { + const struct lzw_table_entry *entry = table + code; + code = entry->extends; + } + + output_pos += count; + for (unsigned i = count; i != 0; i--) { + const struct lzw_table_entry *entry = table + code; + *--output_pos = entry->value; + code = entry->extends; + } + + return count; +} + +/* Exported function, documented in lzw.h */ +lzw_result lzw_decode(struct lzw_ctx *ctx, + const uint8_t *restrict *const restrict output_data, + uint32_t *restrict output_written) +{ + const uint32_t output_length = sizeof(ctx->stack_base); + + *output_written = 0; + *output_data = ctx->stack_base; + + if (ctx->output_left != 0) { + *output_written += lzw__write_fn(ctx, + ctx->stack_base, output_length, *output_written, + ctx->output_code, ctx->output_left); + } + + while (*output_written != output_length) { + lzw_result res = lzw__decode(ctx, lzw__write_fn, + ctx->stack_base, output_length, output_written); + if (res != LZW_OK) { + return res; + } + } + + return LZW_OK; +} + +/** + * Write colour mapped values for this code to the output. + * + * If there isn't enough space in the output stack, this function will write + * the as many as it can into the output. If `ctx->output_left > 0` after + * this call, then there is more data for this code left to output. The code + * is stored to the context as `ctx->output_code`. + * + * \param[in] ctx LZW reading context, updated. + * \param[in] output_data Array to write output values into. + * \param[in] output_length Size of output array. + * \param[in] output_used Current position in output array. + * \param[in] code LZW code to output values for. + * \param[in] left Number of values remaining to output for code. + * \return Number of pixel values written. + */ +static inline uint32_t lzw__map_write_fn(struct lzw_ctx *ctx, + void *restrict output_data, + uint32_t output_length, + uint32_t output_used, + uint16_t code, + uint16_t left) +{ + uint32_t *restrict output_pos = (uint32_t *)output_data + output_used; + const struct lzw_table_entry * const table = ctx->table; + uint32_t space = output_length - output_used; + uint16_t count = left; + + if (count > space) { + left = count - space; + count = space; + } else { + left = 0; + } + + ctx->output_code = code; + ctx->output_left = left; + + for (unsigned i = left; i != 0; i--) { + const struct lzw_table_entry *entry = table + code; + code = entry->extends; + } + + output_pos += count; + if (ctx->has_transparency) { + for (unsigned i = count; i != 0; i--) { + const struct lzw_table_entry *entry = table + code; + --output_pos; + if (entry->value != ctx->transparency_idx) { + *output_pos = ctx->colour_map[entry->value]; + } + code = entry->extends; + } + } else { + for (unsigned i = count; i != 0; i--) { + const struct lzw_table_entry *entry = table + code; + *--output_pos = ctx->colour_map[entry->value]; + code = entry->extends; + } + } + + return count; +} + +/* Exported function, documented in lzw.h */ +lzw_result lzw_decode_map(struct lzw_ctx *ctx, + uint32_t *restrict output_data, + uint32_t output_length, + uint32_t *restrict output_written) +{ + *output_written = 0; + + if (ctx->colour_map == NULL) { + return LZW_NO_COLOUR; + } + + if (ctx->output_left != 0) { + *output_written += lzw__map_write_fn(ctx, + output_data, output_length, *output_written, + ctx->output_code, ctx->output_left); + } + + while (*output_written != output_length) { + lzw_result res = lzw__decode(ctx, lzw__map_write_fn, + output_data, output_length, output_written); + if (res != LZW_OK) { + return res; + } + } + + return LZW_OK; +} diff --git a/src/lzw.h b/src/lzw.h new file mode 100644 index 0000000000..f19432f1c8 --- /dev/null +++ b/src/lzw.h @@ -0,0 +1,137 @@ +/* + * This file is part of NetSurf's LibNSGIF, http://www.netsurf-browser.org/ + * Licensed under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + * + * Copyright 2017 Michael Drake + * Copyright 2021 Michael Drake + */ + +#ifndef LZW_H_ +#define LZW_H_ + +/** + * \file + * \brief LZW decompression (interface) + * + * Decoder for GIF LZW data. + */ + +/** Maximum LZW code size in bits */ +#define LZW_CODE_MAX 12 + +/* Declare lzw internal context structure */ +struct lzw_ctx; + +/** LZW decoding response codes */ +typedef enum lzw_result { + LZW_OK, /**< Success */ + LZW_OK_EOD, /**< Success; reached zero-length sub-block */ + LZW_NO_MEM, /**< Error: Out of memory */ + LZW_NO_DATA, /**< Error: Out of data */ + LZW_EOI_CODE, /**< Error: End of Information code */ + LZW_NO_COLOUR, /**< Error: No colour map provided. */ + LZW_BAD_ICODE, /**< Error: Bad initial LZW code */ + LZW_BAD_PARAM, /**< Error: Bad function parameter. */ + LZW_BAD_CODE, /**< Error: Bad LZW code */ +} lzw_result; + +/** + * Create an LZW decompression context. + * + * \param[out] ctx Returns an LZW decompression context. Caller owned, + * free with lzw_context_destroy(). + * \return LZW_OK on success, or appropriate error code otherwise. + */ +lzw_result lzw_context_create( + struct lzw_ctx **ctx); + +/** + * Destroy an LZW decompression context. + * + * \param[in] ctx The LZW decompression context to destroy. + */ +void lzw_context_destroy( + struct lzw_ctx *ctx); + +/** + * Initialise an LZW decompression context for decoding. + * + * \param[in] ctx The LZW decompression context to initialise. + * \param[in] minimum_code_size The LZW Minimum Code Size. + * \param[in] input_data The compressed data. + * \param[in] input_length Byte length of compressed data. + * \param[in] input_pos Start position in data. Must be position + * of a size byte at sub-block start. + * \return LZW_OK on success, or appropriate error code otherwise. + */ +lzw_result lzw_decode_init( + struct lzw_ctx *ctx, + uint8_t minimum_code_size, + const uint8_t *input_data, + size_t input_length, + size_t input_pos); + +/** + * Read input codes until end of LZW context owned output buffer. + * + * Ensure anything in output is used before calling this, as anything + * there before this call will be trampled. + * + * \param[in] ctx LZW reading context, updated. + * \param[out] output_data Returns pointer to array of output values. + * \param[out] output_written Returns the number of values written to data. + * \return LZW_OK on success, or appropriate error code otherwise. + */ +lzw_result lzw_decode(struct lzw_ctx *ctx, + const uint8_t *restrict *const restrict output_data, + uint32_t *restrict output_written); + +/** + * Initialise an LZW decompression context for decoding to colour map values. + * + * For transparency to work correctly, the given client buffer must have + * the values from the previous frame. The transparency_idx should be a value + * of 256 or above, if the frame does not have transparency. + * + * \param[in] ctx The LZW decompression context to initialise. + * \param[in] minimum_code_size The LZW Minimum Code Size. + * \param[in] transparency_idx Index representing transparency. + * \param[in] colour_table Index to pixel colour mapping. + * \param[in] input_data The compressed data. + * \param[in] input_length Byte length of compressed data. + * \param[in] input_pos Start position in data. Must be position + * of a size byte at sub-block start. + * \return LZW_OK on success, or appropriate error code otherwise. + */ +lzw_result lzw_decode_init_map( + struct lzw_ctx *ctx, + uint8_t minimum_code_size, + uint32_t transparency_idx, + const uint32_t *colour_table, + const uint8_t *input_data, + size_t input_length, + size_t input_pos); + +/** + * Read LZW codes into client buffer, mapping output to colours. + * + * The context must have been initialised using \ref lzw_decode_init_map + * before calling this function, in order to provide the colour mapping table + * and any transparency index. + * + * Ensure anything in output is used before calling this, as anything + * there before this call will be trampled. + * + * \param[in] ctx LZW reading context, updated. + * \param[in] output_data Client buffer to fill with colour mapped values. + * \param[in] output_length Size of output array. + * \param[out] output_written Returns the number of values written to data. + * \return LZW_OK on success, or appropriate error code otherwise. + */ +lzw_result lzw_decode_map(struct lzw_ctx *ctx, + uint32_t *restrict output_data, + uint32_t output_length, + uint32_t *restrict output_written); + +#endif diff --git a/src/nsgif.h b/src/nsgif.h new file mode 100644 index 0000000000..e6873b68d3 --- /dev/null +++ b/src/nsgif.h @@ -0,0 +1,527 @@ +/* + * Copyright 2004 Richard Wilson + * Copyright 2008 Sean Fox + * Copyright 2013-2022 Michael Drake + * + * This file is part of NetSurf's libnsgif, http://www.netsurf-browser.org/ + * Licenced under the MIT License, + * http://www.opensource.org/licenses/mit-license.php + */ + +/** + * \file + * Interface to progressive animated GIF file decoding. + */ + +#ifndef NSNSGIF_H +#define NSNSGIF_H + +#include +#include +#include + +/** Representation of infinity. */ +#define NSGIF_INFINITE (UINT32_MAX) + +/** Maximum colour table size */ +#define NSGIF_MAX_COLOURS 256 + +/** + * Opaque type used by LibNSGIF to represent a GIF object in memory. + */ +typedef struct nsgif nsgif_t; + +/** + * LibNSGIF rectangle structure. + * + * * Top left coordinate is `(x0, y0)`. + * * Width is `x1 - x0`. + * * Height is `y1 - y0`. + * * Units are pixels. + */ +typedef struct nsgif_rect { + /** x co-ordinate of redraw rectangle, left */ + uint32_t x0; + /** y co-ordinate of redraw rectangle, top */ + uint32_t y0; + /** x co-ordinate of redraw rectangle, right */ + uint32_t x1; + /** y co-ordinate of redraw rectangle, bottom */ + uint32_t y1; +} nsgif_rect_t; + +/** + * LibNSGIF return codes. + */ +typedef enum { + /** + * Success. + */ + NSGIF_OK, + + /** + * Out of memory error. + */ + NSGIF_ERR_OOM, + + /** + * GIF source data is invalid, and no frames are recoverable. + */ + NSGIF_ERR_DATA, + + /** + * Frame number is not valid. + */ + NSGIF_ERR_BAD_FRAME, + + /** + * GIF source data contained an error in a frame. + */ + NSGIF_ERR_DATA_FRAME, + + /** + * Unexpected end of GIF source data. + */ + NSGIF_ERR_END_OF_DATA, + + /** + * Can't supply more data after calling \ref nsgif_data_complete. + */ + NSGIF_ERR_DATA_COMPLETE, + + /** + * The current frame cannot be displayed. + */ + NSGIF_ERR_FRAME_DISPLAY, + + /** + * Indicates an animation is complete, and \ref nsgif_reset must be + * called to restart the animation from the beginning. + */ + NSGIF_ERR_ANIMATION_END, +} nsgif_error; + +/** + * NSGIF \ref nsgif_bitmap_t pixel format. + * + * All pixel formats are 32 bits per pixel (bpp). The different formats + * allow control over the ordering of the colour channels. All colour + * channels are 8 bits wide. + * + * Note that the GIF file format only supports an on/off mask, so the + * alpha (A) component (opacity) will always have a value of `0` (fully + * transparent) or `255` (fully opaque). + */ +typedef enum nsgif_bitmap_fmt { + /** Bite-wise RGBA: Byte order: 0xRR, 0xGG, 0xBB, 0xAA. */ + NSGIF_BITMAP_FMT_R8G8B8A8, + + /** Bite-wise BGRA: Byte order: 0xBB, 0xGG, 0xRR, 0xAA. */ + NSGIF_BITMAP_FMT_B8G8R8A8, + + /** Bite-wise ARGB: Byte order: 0xAA, 0xRR, 0xGG, 0xBB. */ + NSGIF_BITMAP_FMT_A8R8G8B8, + + /** Bite-wise ABGR: Byte order: 0xAA, 0xBB, 0xGG, 0xRR. */ + NSGIF_BITMAP_FMT_A8B8G8R8, + + /** + * 32-bit RGBA (0xRRGGBBAA). + * + * * On little endian host, same as \ref NSGIF_BITMAP_FMT_A8B8G8R8. + * * On big endian host, same as \ref NSGIF_BITMAP_FMT_R8G8B8A8. + */ + NSGIF_BITMAP_FMT_RGBA8888, + + /** + * 32-bit BGRA (0xBBGGRRAA). + * + * * On little endian host, same as \ref NSGIF_BITMAP_FMT_A8R8G8B8. + * * On big endian host, same as \ref NSGIF_BITMAP_FMT_B8G8R8A8. + */ + NSGIF_BITMAP_FMT_BGRA8888, + + /** + * 32-bit ARGB (0xAARRGGBB). + * + * * On little endian host, same as \ref NSGIF_BITMAP_FMT_B8G8R8A8. + * * On big endian host, same as \ref NSGIF_BITMAP_FMT_A8R8G8B8. + */ + NSGIF_BITMAP_FMT_ARGB8888, + + /** + * 32-bit BGRA (0xAABBGGRR). + * + * * On little endian host, same as \ref NSGIF_BITMAP_FMT_R8G8B8A8. + * * On big endian host, same as \ref NSGIF_BITMAP_FMT_A8B8G8R8. + */ + NSGIF_BITMAP_FMT_ABGR8888, +} nsgif_bitmap_fmt_t; + +/** + * Client bitmap type. + * + * These are client-created and destroyed, via the \ref nsgif_bitmap_cb_vt + * callbacks, but they are owned by a \ref nsgif_t. + * + * See \ref nsgif_bitmap_fmt for pixel format information. + * + * The bitmap may have a row_span greater than the bitmap width, but the + * difference between row span and width must be a whole number of pixels + * (a multiple of four bytes). + */ +typedef void nsgif_bitmap_t; + +/** Bitmap callbacks function table */ +typedef struct nsgif_bitmap_cb_vt { + /** + * Callback to create a bitmap with the given dimensions. + * + * \param[in] width Required bitmap width in pixels. + * \param[in] height Required bitmap height in pixels. + * \return pointer to client's bitmap structure or NULL on error. + */ + nsgif_bitmap_t* (*create)(int width, int height); + + /** + * Callback to free a bitmap. + * + * \param[in] bitmap The bitmap to destroy. + */ + void (*destroy)(nsgif_bitmap_t *bitmap); + + /** + * Get pointer to pixel buffer in a bitmap. + * + * The pixel buffer must be `(width + N) * height * sizeof(uint32_t)`. + * Where `N` is any number greater than or equal to 0. + * Note that the returned pointer to uint8_t must be 4-byte aligned. + * + * \param[in] bitmap The bitmap. + * \return pointer to bitmap's pixel buffer. + */ + uint8_t* (*get_buffer)(nsgif_bitmap_t *bitmap); + + /* The following functions are optional. */ + + /** + * Set whether a bitmap can be plotted opaque. + * + * \param[in] bitmap The bitmap. + * \param[in] opaque Whether the current frame is opaque. + */ + void (*set_opaque)(nsgif_bitmap_t *bitmap, bool opaque); + + /** + * Tests whether a bitmap has an opaque alpha channel. + * + * \param[in] bitmap The bitmap. + * \return true if the bitmap is opaque, false otherwise. + */ + bool (*test_opaque)(nsgif_bitmap_t *bitmap); + + /** + * Bitmap modified notification. + * + * \param[in] bitmap The bitmap. + */ + void (*modified)(nsgif_bitmap_t *bitmap); + + /** + * Get row span in pixels. + * + * If this callback is not provided, LibNSGIF will use the width. + * + * If row span is greater than width, this callback must be provided. + * + * \param[in] bitmap The bitmap. + */ + uint32_t (*get_rowspan)(nsgif_bitmap_t *bitmap); +} nsgif_bitmap_cb_vt; + +/** + * Convert an error code to a string. + * + * \param[in] err The error code to convert. + * \return String representation of given error code. + */ +const char *nsgif_strerror(nsgif_error err); + +/** + * Create the NSGIF object. + * + * \param[in] bitmap_vt Bitmap operation functions v-table. + * \param[in] bitmap_fmt Bitmap pixel format specification. + * \param[out] gif_out Return \ref nsgif_t object on success. + * + * \return NSGIF_OK on success, or appropriate error otherwise. + */ +nsgif_error nsgif_create( + const nsgif_bitmap_cb_vt *bitmap_vt, + nsgif_bitmap_fmt_t bitmap_fmt, + nsgif_t **gif_out); + +/** + * Free a NSGIF object. + * + * \param[in] gif The NSGIF to free. + */ +void nsgif_destroy(nsgif_t *gif); + +/** + * Scan the source image data. + * + * This is used to feed the source data into LibNSGIF. This must be called + * before calling \ref nsgif_frame_decode. + * + * It can be called multiple times with, with increasing sizes. If it is called + * several times, as more data is available (e.g. slow network fetch) the data + * already given to \ref nsgif_data_scan must be provided each time. + * + * Once all the data has been provided, call \ref nsgif_data_complete. + * + * For example, if you call \ref nsgif_data_scan with 25 bytes of data, and then + * fetch another 10 bytes, you would need to call \ref nsgif_data_scan with a + * size of 35 bytes, and the whole 35 bytes must be contiguous memory. It is + * safe to `realloc` the source buffer between calls to \ref nsgif_data_scan. + * (The actual data pointer is allowed to be different.) + * + * If an error occurs, all previously scanned frames are retained. + * + * Note that an error returned from this function is purely informational. + * So long as at least one frame is available, you can display frames. + * + * \param[in] gif The \ref nsgif_t object. + * \param[in] size Number of bytes in data. + * \param[in] data Raw source GIF data. + * + * \return NSGIF_OK on success, or appropriate error otherwise. + */ +nsgif_error nsgif_data_scan( + nsgif_t *gif, + size_t size, + const uint8_t *data); + +/** + * Tell libnsgif that all the gif data has been provided. + * + * Call this after calling \ref nsgif_data_scan with the the entire GIF + * source data. You can call \ref nsgif_data_scan multiple times up until + * this is called, and after this is called, \ref nsgif_data_scan will + * return an error. + * + * You can decode a GIF before this is called, however, it will fail to + * decode any truncated final frame data and will not perform loops when + * driven via \ref nsgif_frame_prepare (because it doesn't know if there + * will be more frames supplied in future data). + * + * \param[in] gif The \ref nsgif_t object. + */ +void nsgif_data_complete( + nsgif_t *gif); + +/** + * Prepare to show a frame. + * + * If this is the last frame of an animation with a finite loop count, the + * returned `delay_cs` will be \ref NSGIF_INFINITE, indicating that the frame + * should be shown forever. + * + * Note that if \ref nsgif_data_complete has not been called on this gif, + * animated GIFs will not loop back to the start. Instead it will return + * \ref NSGIF_ERR_END_OF_DATA. + * + * \param[in] gif The \ref nsgif_t object. + * \param[out] area The area in pixels that must be redrawn. + * \param[out] delay_cs Time to wait after frame_new before next frame in cs. + * \param[out] frame_new The frame to decode. + * + * \return NSGIF_OK on success, or appropriate error otherwise. + */ +nsgif_error nsgif_frame_prepare( + nsgif_t *gif, + nsgif_rect_t *area, + uint32_t *delay_cs, + uint32_t *frame_new); + +/** + * Decodes a GIF frame. + * + * \param[in] gif The \ref nsgif_t object. + * \param[in] frame The frame number to decode. + * \param[out] bitmap On success, returns pointer to the client-allocated, + * nsgif-owned client bitmap structure. + * + * \return NSGIF_OK on success, or appropriate error otherwise. + */ +nsgif_error nsgif_frame_decode( + nsgif_t *gif, + uint32_t frame, + nsgif_bitmap_t **bitmap); + +/** + * Reset a GIF animation. + * + * Some animations are only meant to loop N times, and then show the + * final frame forever. This function resets the loop and frame counters, + * so that the animation can be replayed without the overhead of recreating + * the \ref nsgif_t object and rescanning the raw data. + * + * \param[in] gif A \ref nsgif_t object. + * + * \return NSGIF_OK on success, or appropriate error otherwise. + */ +nsgif_error nsgif_reset( + nsgif_t *gif); + +/** + * Information about a GIF. + */ +typedef struct nsgif_info { + /** width of GIF (may increase during decoding) */ + uint32_t width; + /** height of GIF (may increase during decoding) */ + uint32_t height; + /** number of frames decoded */ + uint32_t frame_count; + /** number of times to play animation (zero means loop forever) */ + int loop_max; + /** background colour in same pixel format as \ref nsgif_bitmap_t. */ + uint32_t background; + /** whether the GIF has a global colour table */ + bool global_palette; +} nsgif_info_t; + +/** + * Frame disposal method. + * + * Clients do not need to know about this, it is provided purely for dumping + * raw information about GIF frames. + */ +enum nsgif_disposal { + NSGIF_DISPOSAL_UNSPECIFIED, /**< No disposal method specified. */ + NSGIF_DISPOSAL_NONE, /**< Frame remains. */ + NSGIF_DISPOSAL_RESTORE_BG, /**< Clear frame to background colour. */ + NSGIF_DISPOSAL_RESTORE_PREV, /**< Restore previous frame. */ + NSGIF_DISPOSAL_RESTORE_QUIRK, /**< Alias for NSGIF_DISPOSAL_RESTORE_PREV. */ +}; + +/** + * Convert a disposal method to a string. + * + * \param[in] disposal The disposal method to convert. + * \return String representation of given disposal method. + */ +const char *nsgif_str_disposal(enum nsgif_disposal disposal); + +/** + * Information about a GIF frame. + */ +typedef struct nsgif_frame_info { + /** whether the frame should be displayed/animated */ + bool display; + /** whether the frame may have transparency */ + bool transparency; + /** whether the frame has a local colour table */ + bool local_palette; + /** whether the frame is interlaced */ + bool interlaced; + /** Disposal method for previous frame; affects plotting */ + uint8_t disposal; + /** delay (in cs) before animating the frame */ + uint32_t delay; + + /** Frame's redraw rectangle. */ + nsgif_rect_t rect; +} nsgif_frame_info_t; + +/** + * Get information about a GIF from an \ref nsgif_t object. + * + * \param[in] gif The \ref nsgif_t object to get info for. + * + * \return The gif info, or NULL on error. + */ +const nsgif_info_t *nsgif_get_info(const nsgif_t *gif); + +/** + * Get information about a GIF from an \ref nsgif_t object. + * + * \param[in] gif The \ref nsgif_t object to get frame info for. + * \param[in] frame The frame number to get info for. + * + * \return The gif frame info, or NULL on error. + */ +const nsgif_frame_info_t *nsgif_get_frame_info( + const nsgif_t *gif, + uint32_t frame); + +/** + * Get the global colour palette. + * + * If the GIF has no global colour table, this will return the default + * colour palette. + * + * Colours in same pixel format as \ref nsgif_bitmap_t. + * + * \param[in] gif The \ref nsgif_t object. + * \param[out] table Client buffer to hold the colour table. + * \param[out] entries The number of used entries in the colour table. + */ +void nsgif_global_palette( + const nsgif_t *gif, + uint32_t table[NSGIF_MAX_COLOURS], + size_t *entries); + +/** + * Get the local colour palette for a frame. + * + * Frames may have no local palette. In this case they use the global palette. + * This function returns false if the frame has no local palette. + * + * Colours in same pixel format as \ref nsgif_bitmap_t. + * + * \param[in] gif The \ref nsgif_t object. + * \param[in] frame The frame to get the palette for. + * \param[out] table Client buffer to hold the colour table. + * \param[out] entries The number of used entries in the colour table. + * \return true if a palette is returned, false otherwise. + */ +bool nsgif_local_palette( + const nsgif_t *gif, + uint32_t frame, + uint32_t table[NSGIF_MAX_COLOURS], + size_t *entries); + +/** + * Configure handling of small frame delays. + * + * Historically people created GIFs with a tiny frame delay, however the slow + * hardware of the time meant they actually played much slower. As computers + * sped up, to prevent animations playing faster than intended, decoders came + * to ignore overly small frame delays. + * + * By default a \ref nsgif_frame_prepare() managed animation will override + * frame delays of less than 2 centiseconds with a default frame delay of + * 10 centiseconds. This matches the behaviour of web browsers and other + * renderers. + * + * Both the minimum and the default values can be overridden for a given GIF + * by the client. To get frame delays exactly as specified by the GIF file, set + * `delay_min` to zero. + * + * Note that this does not affect the frame delay in the frame info + * (\ref nsgif_frame_info_t) structure, which will always contain values + * specified by the GIF. + * + * \param[in] gif The \ref nsgif_t object to configure. + * \param[in] delay_min The minimum frame delay in centiseconds. + * \param[in] delay_default The delay to use if a frame delay is less than + * `delay_min`. + */ +void nsgif_set_frame_delay_behaviour( + nsgif_t *gif, + uint16_t delay_min, + uint16_t delay_default); + +#endif diff --git a/src/player.cpp b/src/player.cpp index bb4799c4d7..31b5eb8e9d 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -84,6 +84,8 @@ #include "audio_midi.h" #include "maniac_patch.h" +#include "animated_bitmap.h" + #if defined(__ANDROID__) && !defined(USE_LIBRETRO) #include "platform/android/android.h" #endif @@ -229,7 +231,11 @@ void Player::MainLoop() { Instrumentation::FrameScope iframe; const auto frame_time = Game_Clock::now(); - Game_Clock::OnNextFrame(frame_time); + const auto delta = Game_Clock::OnNextFrame(frame_time); + + // Proposed change: Update all animated bitmaps + AnimationManager::UpdateAll(std::chrono::duration_cast(delta)); + // End of proposed change Player::UpdateInput(); From f54011b00223f4ada98211e29368f2ad1ce822a7 Mon Sep 17 00:00:00 2001 From: Mauro Junior <45118493+jetrotal@users.noreply.github.com> Date: Fri, 24 Oct 2025 21:17:36 -0300 Subject: [PATCH 2/2] Add GPL license headers and update C headers for C++ Added GNU GPL license headers to animated_bitmap and image_gif source/header files. Updated lzw.h and nsgif.h to add extern "C" guards for C++ compatibility and improved formatting and documentation. Minor cleanup in bitmap.cpp. Update README.md --- README.md | 3 + src/animated_bitmap.cpp | 21 +- src/animated_bitmap.h | 19 +- src/bitmap.cpp | 2 - src/image_gif.cpp | 19 +- src/image_gif.h | 21 +- src/lzw.h | 216 +++++----- src/nsgif.h | 854 ++++++++++++++++++++-------------------- 8 files changed, 617 insertions(+), 538 deletions(-) diff --git a/README.md b/README.md index 52c414e9ce..ccf4061c9d 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ EasyRPG Player makes use of the following 3rd party software: (Yoshio Uno), provided under the (3-clause) BSD license * [dr_wav] WAV audio loader and writer - Copyright (c) David Reid, provided under public domain or MIT-0 +* [libnsgif] for progressive animated GIF decoding - Copyright (c) 2004-2022 The + NetSurf Browser Project, provided under the MIT license ### 3rd party resources @@ -121,6 +123,7 @@ EasyRPG Player makes use of the following 3rd party software: [Logo2]: resources/logo2.png [FMMidi]: http://unhaut.epizy.com/fmmidi [dr_wav]: https://github.com/mackron/dr_libs +[libnsgif]: http://www.netsurf-browser.org/projects/libnsgif/ [baekmuk]: https://kldp.net/baekmuk [Shinonome]: http://openlab.ring.gr.jp/efont/shinonome [ttyp0]: https://people.mpi-inf.mpg.de/~uwe/misc/uw-ttyp0 diff --git a/src/animated_bitmap.cpp b/src/animated_bitmap.cpp index 3f8ce3ca92..8bd77cb012 100644 --- a/src/animated_bitmap.cpp +++ b/src/animated_bitmap.cpp @@ -1,4 +1,21 @@ -/*start of file animated_bitmap.cpp*/ + +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + #include "animated_bitmap.h" #include "image_gif.h" #include "output.h" @@ -77,4 +94,4 @@ void AnimatedBitmap::Update(std::chrono::microseconds delta) { BlitFast(0, 0, *_frames[_current_frame], _frames[_current_frame]->GetRect(), Opacity::Opaque()); } } -/*end of file animated_bitmap.cpp*/ + diff --git a/src/animated_bitmap.h b/src/animated_bitmap.h index c513d3d2f8..b2de72d82e 100644 --- a/src/animated_bitmap.h +++ b/src/animated_bitmap.h @@ -1,4 +1,20 @@ -/*start of file animated_bitmap.h*/ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + #pragma once #include "bitmap.h" #include @@ -29,4 +45,3 @@ class AnimatedBitmap : public Bitmap { int _current_frame = 0; std::chrono::microseconds _time_accumulator{ 0 }; }; -/*end of file animated_bitmap.h*/ diff --git a/src/bitmap.cpp b/src/bitmap.cpp index 473f3c106a..2881db74e2 100644 --- a/src/bitmap.cpp +++ b/src/bitmap.cpp @@ -50,7 +50,6 @@ BitmapRef Bitmap::Create(int width, int height, const Color& color) { BitmapRef Bitmap::Create(Filesystem_Stream::InputStream stream, bool transparent, uint32_t flags) { - // Proposed change: Add GIF check uint8_t magic[8] = {}; (void)stream.read(reinterpret_cast(magic), 8).gcount(); stream.seekg(0, std::ios::ios_base::beg); @@ -60,7 +59,6 @@ BitmapRef Bitmap::Create(Filesystem_Stream::InputStream stream, bool transparent if (!bmp->pixels()) return BitmapRef(); return bmp; } - // End of proposed change BitmapRef bmp = std::make_shared(std::move(stream), transparent, flags); diff --git a/src/image_gif.cpp b/src/image_gif.cpp index 9e0dc08a02..9ef6eb775f 100644 --- a/src/image_gif.cpp +++ b/src/image_gif.cpp @@ -1,4 +1,20 @@ -/*start of file image_gif.cpp*/ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + #include "image_gif.h" #include "output.h" @@ -100,4 +116,3 @@ GifDecoder::~GifDecoder() { nsgif_destroy(_gif); } } -/*end of file image_gif.cpp*/ diff --git a/src/image_gif.h b/src/image_gif.h index 8b93dbeeb6..582a398f7f 100644 --- a/src/image_gif.h +++ b/src/image_gif.h @@ -1,9 +1,25 @@ -/*start of file image_gif.h*/ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + #pragma once #include #include #include "bitmap.h" -#include "nsgif.h" // No more extern "C" wrapper here +#include "nsgif.h" struct GifFrame { BitmapRef bitmap; @@ -31,4 +47,3 @@ class GifDecoder { std::vector _frames; nsgif_t* _gif = nullptr; }; -/*end of file image_gif.h*/ diff --git a/src/lzw.h b/src/lzw.h index f19432f1c8..f73d1f21e0 100644 --- a/src/lzw.h +++ b/src/lzw.h @@ -10,128 +10,136 @@ #ifndef LZW_H_ #define LZW_H_ -/** - * \file - * \brief LZW decompression (interface) - * - * Decoder for GIF LZW data. - */ +#ifdef __cplusplus +extern "C" { +#endif -/** Maximum LZW code size in bits */ + /** + * \file + * \brief LZW decompression (interface) + * + * Decoder for GIF LZW data. + */ + + /** Maximum LZW code size in bits */ #define LZW_CODE_MAX 12 /* Declare lzw internal context structure */ -struct lzw_ctx; + struct lzw_ctx; -/** LZW decoding response codes */ -typedef enum lzw_result { - LZW_OK, /**< Success */ - LZW_OK_EOD, /**< Success; reached zero-length sub-block */ - LZW_NO_MEM, /**< Error: Out of memory */ - LZW_NO_DATA, /**< Error: Out of data */ - LZW_EOI_CODE, /**< Error: End of Information code */ - LZW_NO_COLOUR, /**< Error: No colour map provided. */ - LZW_BAD_ICODE, /**< Error: Bad initial LZW code */ - LZW_BAD_PARAM, /**< Error: Bad function parameter. */ - LZW_BAD_CODE, /**< Error: Bad LZW code */ -} lzw_result; + /** LZW decoding response codes */ + typedef enum lzw_result { + LZW_OK, /**< Success */ + LZW_OK_EOD, /**< Success; reached zero-length sub-block */ + LZW_NO_MEM, /**< Error: Out of memory */ + LZW_NO_DATA, /**< Error: Out of data */ + LZW_EOI_CODE, /**< Error: End of Information code */ + LZW_NO_COLOUR, /**< Error: No colour map provided. */ + LZW_BAD_ICODE, /**< Error: Bad initial LZW code */ + LZW_BAD_PARAM, /**< Error: Bad function parameter. */ + LZW_BAD_CODE, /**< Error: Bad LZW code */ + } lzw_result; -/** - * Create an LZW decompression context. - * - * \param[out] ctx Returns an LZW decompression context. Caller owned, - * free with lzw_context_destroy(). - * \return LZW_OK on success, or appropriate error code otherwise. - */ -lzw_result lzw_context_create( - struct lzw_ctx **ctx); + /** + * Create an LZW decompression context. + * + * \param[out] ctx Returns an LZW decompression context. Caller owned, + * free with lzw_context_destroy(). + * \return LZW_OK on success, or appropriate error code otherwise. + */ + lzw_result lzw_context_create( + struct lzw_ctx** ctx); -/** - * Destroy an LZW decompression context. - * - * \param[in] ctx The LZW decompression context to destroy. - */ -void lzw_context_destroy( - struct lzw_ctx *ctx); + /** + * Destroy an LZW decompression context. + * + * \param[in] ctx The LZW decompression context to destroy. + */ + void lzw_context_destroy( + struct lzw_ctx* ctx); -/** - * Initialise an LZW decompression context for decoding. - * - * \param[in] ctx The LZW decompression context to initialise. - * \param[in] minimum_code_size The LZW Minimum Code Size. - * \param[in] input_data The compressed data. - * \param[in] input_length Byte length of compressed data. - * \param[in] input_pos Start position in data. Must be position - * of a size byte at sub-block start. - * \return LZW_OK on success, or appropriate error code otherwise. - */ -lzw_result lzw_decode_init( - struct lzw_ctx *ctx, + /** + * Initialise an LZW decompression context for decoding. + * + * \param[in] ctx The LZW decompression context to initialise. + * \param[in] minimum_code_size The LZW Minimum Code Size. + * \param[in] input_data The compressed data. + * \param[in] input_length Byte length of compressed data. + * \param[in] input_pos Start position in data. Must be position + * of a size byte at sub-block start. + * \return LZW_OK on success, or appropriate error code otherwise. + */ + lzw_result lzw_decode_init( + struct lzw_ctx* ctx, uint8_t minimum_code_size, - const uint8_t *input_data, + const uint8_t* input_data, size_t input_length, size_t input_pos); -/** - * Read input codes until end of LZW context owned output buffer. - * - * Ensure anything in output is used before calling this, as anything - * there before this call will be trampled. - * - * \param[in] ctx LZW reading context, updated. - * \param[out] output_data Returns pointer to array of output values. - * \param[out] output_written Returns the number of values written to data. - * \return LZW_OK on success, or appropriate error code otherwise. - */ -lzw_result lzw_decode(struct lzw_ctx *ctx, - const uint8_t *restrict *const restrict output_data, - uint32_t *restrict output_written); + /** + * Read input codes until end of LZW context owned output buffer. + * + * Ensure anything in output is used before calling this, as anything + * there before this call will be trampled. + * + * \param[in] ctx LZW reading context, updated. + * \param[out] output_data Returns pointer to array of output values. + * \param[out] output_written Returns the number of values written to data. + * \return LZW_OK on success, or appropriate error code otherwise. + */ + lzw_result lzw_decode(struct lzw_ctx* ctx, + const uint8_t* restrict* const restrict output_data, + uint32_t* restrict output_written); -/** - * Initialise an LZW decompression context for decoding to colour map values. - * - * For transparency to work correctly, the given client buffer must have - * the values from the previous frame. The transparency_idx should be a value - * of 256 or above, if the frame does not have transparency. - * - * \param[in] ctx The LZW decompression context to initialise. - * \param[in] minimum_code_size The LZW Minimum Code Size. - * \param[in] transparency_idx Index representing transparency. - * \param[in] colour_table Index to pixel colour mapping. - * \param[in] input_data The compressed data. - * \param[in] input_length Byte length of compressed data. - * \param[in] input_pos Start position in data. Must be position - * of a size byte at sub-block start. - * \return LZW_OK on success, or appropriate error code otherwise. - */ -lzw_result lzw_decode_init_map( - struct lzw_ctx *ctx, + /** + * Initialise an LZW decompression context for decoding to colour map values. + * + * For transparency to work correctly, the given client buffer must have + * the values from the previous frame. The transparency_idx should be a value + * of 256 or above, if the frame does not have transparency. + * + * \param[in] ctx The LZW decompression context to initialise. + * \param[in] minimum_code_size The LZW Minimum Code Size. + * \param[in] transparency_idx Index representing transparency. + * \param[in] colour_table Index to pixel colour mapping. + * \param[in] input_data The compressed data. + * \param[in] input_length Byte length of compressed data. + * \param[in] input_pos Start position in data. Must be position + * of a size byte at sub-block start. + * \return LZW_OK on success, or appropriate error code otherwise. + */ + lzw_result lzw_decode_init_map( + struct lzw_ctx* ctx, uint8_t minimum_code_size, uint32_t transparency_idx, - const uint32_t *colour_table, - const uint8_t *input_data, + const uint32_t* colour_table, + const uint8_t* input_data, size_t input_length, size_t input_pos); -/** - * Read LZW codes into client buffer, mapping output to colours. - * - * The context must have been initialised using \ref lzw_decode_init_map - * before calling this function, in order to provide the colour mapping table - * and any transparency index. - * - * Ensure anything in output is used before calling this, as anything - * there before this call will be trampled. - * - * \param[in] ctx LZW reading context, updated. - * \param[in] output_data Client buffer to fill with colour mapped values. - * \param[in] output_length Size of output array. - * \param[out] output_written Returns the number of values written to data. - * \return LZW_OK on success, or appropriate error code otherwise. - */ -lzw_result lzw_decode_map(struct lzw_ctx *ctx, - uint32_t *restrict output_data, + /** + * Read LZW codes into client buffer, mapping output to colours. + * + * The context must have been initialised using \ref lzw_decode_init_map + * before calling this function, in order to provide the colour mapping table + * and any transparency index. + * + * Ensure anything in output is used before calling this, as anything + * there before this call will be trampled. + * + * \param[in] ctx LZW reading context, updated. + * \param[in] output_data Client buffer to fill with colour mapped values. + * \param[in] output_length Size of output array. + * \param[out] output_written Returns the number of values written to data. + * \return LZW_OK on success, or appropriate error code otherwise. + */ + lzw_result lzw_decode_map(struct lzw_ctx* ctx, + uint32_t* restrict output_data, uint32_t output_length, - uint32_t *restrict output_written); + uint32_t* restrict output_written); + +#ifdef __cplusplus +} +#endif #endif diff --git a/src/nsgif.h b/src/nsgif.h index e6873b68d3..e68ce254ef 100644 --- a/src/nsgif.h +++ b/src/nsgif.h @@ -8,10 +8,10 @@ * http://www.opensource.org/licenses/mit-license.php */ -/** - * \file - * Interface to progressive animated GIF file decoding. - */ + /** + * \file + * Interface to progressive animated GIF file decoding. + */ #ifndef NSNSGIF_H #define NSNSGIF_H @@ -20,7 +20,11 @@ #include #include -/** Representation of infinity. */ +#ifdef __cplusplus +extern "C" { +#endif + + /** Representation of infinity. */ #define NSGIF_INFINITE (UINT32_MAX) /** Maximum colour table size */ @@ -29,499 +33,503 @@ /** * Opaque type used by LibNSGIF to represent a GIF object in memory. */ -typedef struct nsgif nsgif_t; + typedef struct nsgif nsgif_t; -/** - * LibNSGIF rectangle structure. - * - * * Top left coordinate is `(x0, y0)`. - * * Width is `x1 - x0`. - * * Height is `y1 - y0`. - * * Units are pixels. - */ -typedef struct nsgif_rect { - /** x co-ordinate of redraw rectangle, left */ - uint32_t x0; - /** y co-ordinate of redraw rectangle, top */ - uint32_t y0; - /** x co-ordinate of redraw rectangle, right */ - uint32_t x1; - /** y co-ordinate of redraw rectangle, bottom */ - uint32_t y1; -} nsgif_rect_t; - -/** - * LibNSGIF return codes. - */ -typedef enum { /** - * Success. + * LibNSGIF rectangle structure. + * + * * Top left coordinate is `(x0, y0)`. + * * Width is `x1 - x0`. + * * Height is `y1 - y0`. + * * Units are pixels. */ - NSGIF_OK, + typedef struct nsgif_rect { + /** x co-ordinate of redraw rectangle, left */ + uint32_t x0; + /** y co-ordinate of redraw rectangle, top */ + uint32_t y0; + /** x co-ordinate of redraw rectangle, right */ + uint32_t x1; + /** y co-ordinate of redraw rectangle, bottom */ + uint32_t y1; + } nsgif_rect_t; /** - * Out of memory error. + * LibNSGIF return codes. */ - NSGIF_ERR_OOM, + typedef enum { + /** + * Success. + */ + NSGIF_OK, + + /** + * Out of memory error. + */ + NSGIF_ERR_OOM, + + /** + * GIF source data is invalid, and no frames are recoverable. + */ + NSGIF_ERR_DATA, + + /** + * Frame number is not valid. + */ + NSGIF_ERR_BAD_FRAME, + + /** + * GIF source data contained an error in a frame. + */ + NSGIF_ERR_DATA_FRAME, + + /** + * Unexpected end of GIF source data. + */ + NSGIF_ERR_END_OF_DATA, + + /** + * Can't supply more data after calling \ref nsgif_data_complete. + */ + NSGIF_ERR_DATA_COMPLETE, + + /** + * The current frame cannot be displayed. + */ + NSGIF_ERR_FRAME_DISPLAY, + + /** + * Indicates an animation is complete, and \ref nsgif_reset must be + * called to restart the animation from the beginning. + */ + NSGIF_ERR_ANIMATION_END, + } nsgif_error; /** - * GIF source data is invalid, and no frames are recoverable. + * NSGIF \ref nsgif_bitmap_t pixel format. + * + * All pixel formats are 32 bits per pixel (bpp). The different formats + * allow control over the ordering of the colour channels. All colour + * channels are 8 bits wide. + * + * Note that the GIF file format only supports an on/off mask, so the + * alpha (A) component (opacity) will always have a value of `0` (fully + * transparent) or `255` (fully opaque). */ - NSGIF_ERR_DATA, + typedef enum nsgif_bitmap_fmt { + /** Bite-wise RGBA: Byte order: 0xRR, 0xGG, 0xBB, 0xAA. */ + NSGIF_BITMAP_FMT_R8G8B8A8, + + /** Bite-wise BGRA: Byte order: 0xBB, 0xGG, 0xRR, 0xAA. */ + NSGIF_BITMAP_FMT_B8G8R8A8, + + /** Bite-wise ARGB: Byte order: 0xAA, 0xRR, 0xGG, 0xBB. */ + NSGIF_BITMAP_FMT_A8R8G8B8, + + /** Bite-wise ABGR: Byte order: 0xAA, 0xBB, 0xGG, 0xRR. */ + NSGIF_BITMAP_FMT_A8B8G8R8, + + /** + * 32-bit RGBA (0xRRGGBBAA). + * + * * On little endian host, same as \ref NSGIF_BITMAP_FMT_A8B8G8R8. + * * On big endian host, same as \ref NSGIF_BITMAP_FMT_R8G8B8A8. + */ + NSGIF_BITMAP_FMT_RGBA8888, + + /** + * 32-bit BGRA (0xBBGGRRAA). + * + * * On little endian host, same as \ref NSGIF_BITMAP_FMT_A8R8G8B8. + * * On big endian host, same as \ref NSGIF_BITMAP_FMT_B8G8R8A8. + */ + NSGIF_BITMAP_FMT_BGRA8888, + + /** + * 32-bit ARGB (0xAARRGGBB). + * + * * On little endian host, same as \ref NSGIF_BITMAP_FMT_B8G8R8A8. + * * On big endian host, same as \ref NSGIF_BITMAP_FMT_A8R8G8B8. + */ + NSGIF_BITMAP_FMT_ARGB8888, + + /** + * 32-bit BGRA (0xAABBGGRR). + * + * * On little endian host, same as \ref NSGIF_BITMAP_FMT_R8G8B8A8. + * * On big endian host, same as \ref NSGIF_BITMAP_FMT_A8B8G8R8. + */ + NSGIF_BITMAP_FMT_ABGR8888, + } nsgif_bitmap_fmt_t; /** - * Frame number is not valid. + * Client bitmap type. + * + * These are client-created and destroyed, via the \ref nsgif_bitmap_cb_vt + * callbacks, but they are owned by a \ref nsgif_t. + * + * See \ref nsgif_bitmap_fmt for pixel format information. + * + * The bitmap may have a row_span greater than the bitmap width, but the + * difference between row span and width must be a whole number of pixels + * (a multiple of four bytes). */ - NSGIF_ERR_BAD_FRAME, + typedef void nsgif_bitmap_t; + + /** Bitmap callbacks function table */ + typedef struct nsgif_bitmap_cb_vt { + /** + * Callback to create a bitmap with the given dimensions. + * + * \param[in] width Required bitmap width in pixels. + * \param[in] height Required bitmap height in pixels. + * \return pointer to client's bitmap structure or NULL on error. + */ + nsgif_bitmap_t* (*create)(int width, int height); + + /** + * Callback to free a bitmap. + * + * \param[in] bitmap The bitmap to destroy. + */ + void (*destroy)(nsgif_bitmap_t* bitmap); + + /** + * Get pointer to pixel buffer in a bitmap. + * + * The pixel buffer must be `(width + N) * height * sizeof(uint32_t)`. + * Where `N` is any number greater than or equal to 0. + * Note that the returned pointer to uint8_t must be 4-byte aligned. + * + * \param[in] bitmap The bitmap. + * \return pointer to bitmap's pixel buffer. + */ + uint8_t* (*get_buffer)(nsgif_bitmap_t* bitmap); + + /* The following functions are optional. */ + + /** + * Set whether a bitmap can be plotted opaque. + * + * \param[in] bitmap The bitmap. + * \param[in] opaque Whether the current frame is opaque. + */ + void (*set_opaque)(nsgif_bitmap_t* bitmap, bool opaque); + + /** + * Tests whether a bitmap has an opaque alpha channel. + * + * \param[in] bitmap The bitmap. + * \return true if the bitmap is opaque, false otherwise. + */ + bool (*test_opaque)(nsgif_bitmap_t* bitmap); + + /** + * Bitmap modified notification. + * + * \param[in] bitmap The bitmap. + */ + void (*modified)(nsgif_bitmap_t* bitmap); + + /** + * Get row span in pixels. + * + * If this callback is not provided, LibNSGIF will use the width. + * + * If row span is greater than width, this callback must be provided. + * + * \param[in] bitmap The bitmap. + */ + uint32_t(*get_rowspan)(nsgif_bitmap_t* bitmap); + } nsgif_bitmap_cb_vt; /** - * GIF source data contained an error in a frame. + * Convert an error code to a string. + * + * \param[in] err The error code to convert. + * \return String representation of given error code. */ - NSGIF_ERR_DATA_FRAME, + const char* nsgif_strerror(nsgif_error err); /** - * Unexpected end of GIF source data. + * Create the NSGIF object. + * + * \param[in] bitmap_vt Bitmap operation functions v-table. + * \param[in] bitmap_fmt Bitmap pixel format specification. + * \param[out] gif_out Return \ref nsgif_t object on success. + * + * \return NSGIF_OK on success, or appropriate error otherwise. */ - NSGIF_ERR_END_OF_DATA, + nsgif_error nsgif_create( + const nsgif_bitmap_cb_vt* bitmap_vt, + nsgif_bitmap_fmt_t bitmap_fmt, + nsgif_t** gif_out); /** - * Can't supply more data after calling \ref nsgif_data_complete. + * Free a NSGIF object. + * + * \param[in] gif The NSGIF to free. */ - NSGIF_ERR_DATA_COMPLETE, + void nsgif_destroy(nsgif_t* gif); /** - * The current frame cannot be displayed. + * Scan the source image data. + * + * This is used to feed the source data into LibNSGIF. This must be called + * before calling \ref nsgif_frame_decode. + * + * It can be called multiple times with, with increasing sizes. If it is called + * several times, as more data is available (e.g. slow network fetch) the data + * already given to \ref nsgif_data_scan must be provided each time. + * + * Once all the data has been provided, call \ref nsgif_data_complete. + * + * For example, if you call \ref nsgif_data_scan with 25 bytes of data, and then + * fetch another 10 bytes, you would need to call \ref nsgif_data_scan with a + * size of 35 bytes, and the whole 35 bytes must be contiguous memory. It is + * safe to `realloc` the source buffer between calls to \ref nsgif_data_scan. + * (The actual data pointer is allowed to be different.) + * + * If an error occurs, all previously scanned frames are retained. + * + * Note that an error returned from this function is purely informational. + * So long as at least one frame is available, you can display frames. + * + * \param[in] gif The \ref nsgif_t object. + * \param[in] size Number of bytes in data. + * \param[in] data Raw source GIF data. + * + * \return NSGIF_OK on success, or appropriate error otherwise. */ - NSGIF_ERR_FRAME_DISPLAY, + nsgif_error nsgif_data_scan( + nsgif_t* gif, + size_t size, + const uint8_t* data); /** - * Indicates an animation is complete, and \ref nsgif_reset must be - * called to restart the animation from the beginning. + * Tell libnsgif that all the gif data has been provided. + * + * Call this after calling \ref nsgif_data_scan with the the entire GIF + * source data. You can call \ref nsgif_data_scan multiple times up until + * this is called, and after this is called, \ref nsgif_data_scan will + * return an error. + * + * You can decode a GIF before this is called, however, it will fail to + * decode any truncated final frame data and will not perform loops when + * driven via \ref nsgif_frame_prepare (because it doesn't know if there + * will be more frames supplied in future data). + * + * \param[in] gif The \ref nsgif_t object. */ - NSGIF_ERR_ANIMATION_END, -} nsgif_error; - -/** - * NSGIF \ref nsgif_bitmap_t pixel format. - * - * All pixel formats are 32 bits per pixel (bpp). The different formats - * allow control over the ordering of the colour channels. All colour - * channels are 8 bits wide. - * - * Note that the GIF file format only supports an on/off mask, so the - * alpha (A) component (opacity) will always have a value of `0` (fully - * transparent) or `255` (fully opaque). - */ -typedef enum nsgif_bitmap_fmt { - /** Bite-wise RGBA: Byte order: 0xRR, 0xGG, 0xBB, 0xAA. */ - NSGIF_BITMAP_FMT_R8G8B8A8, - - /** Bite-wise BGRA: Byte order: 0xBB, 0xGG, 0xRR, 0xAA. */ - NSGIF_BITMAP_FMT_B8G8R8A8, - - /** Bite-wise ARGB: Byte order: 0xAA, 0xRR, 0xGG, 0xBB. */ - NSGIF_BITMAP_FMT_A8R8G8B8, - - /** Bite-wise ABGR: Byte order: 0xAA, 0xBB, 0xGG, 0xRR. */ - NSGIF_BITMAP_FMT_A8B8G8R8, + void nsgif_data_complete( + nsgif_t* gif); /** - * 32-bit RGBA (0xRRGGBBAA). + * Prepare to show a frame. + * + * If this is the last frame of an animation with a finite loop count, the + * returned `delay_cs` will be \ref NSGIF_INFINITE, indicating that the frame + * should be shown forever. + * + * Note that if \ref nsgif_data_complete has not been called on this gif, + * animated GIFs will not loop back to the start. Instead it will return + * \ref NSGIF_ERR_END_OF_DATA. * - * * On little endian host, same as \ref NSGIF_BITMAP_FMT_A8B8G8R8. - * * On big endian host, same as \ref NSGIF_BITMAP_FMT_R8G8B8A8. + * \param[in] gif The \ref nsgif_t object. + * \param[out] area The area in pixels that must be redrawn. + * \param[out] delay_cs Time to wait after frame_new before next frame in cs. + * \param[out] frame_new The frame to decode. + * + * \return NSGIF_OK on success, or appropriate error otherwise. */ - NSGIF_BITMAP_FMT_RGBA8888, + nsgif_error nsgif_frame_prepare( + nsgif_t* gif, + nsgif_rect_t* area, + uint32_t* delay_cs, + uint32_t* frame_new); /** - * 32-bit BGRA (0xBBGGRRAA). + * Decodes a GIF frame. + * + * \param[in] gif The \ref nsgif_t object. + * \param[in] frame The frame number to decode. + * \param[out] bitmap On success, returns pointer to the client-allocated, + * nsgif-owned client bitmap structure. * - * * On little endian host, same as \ref NSGIF_BITMAP_FMT_A8R8G8B8. - * * On big endian host, same as \ref NSGIF_BITMAP_FMT_B8G8R8A8. + * \return NSGIF_OK on success, or appropriate error otherwise. */ - NSGIF_BITMAP_FMT_BGRA8888, + nsgif_error nsgif_frame_decode( + nsgif_t* gif, + uint32_t frame, + nsgif_bitmap_t** bitmap); /** - * 32-bit ARGB (0xAARRGGBB). + * Reset a GIF animation. * - * * On little endian host, same as \ref NSGIF_BITMAP_FMT_B8G8R8A8. - * * On big endian host, same as \ref NSGIF_BITMAP_FMT_A8R8G8B8. + * Some animations are only meant to loop N times, and then show the + * final frame forever. This function resets the loop and frame counters, + * so that the animation can be replayed without the overhead of recreating + * the \ref nsgif_t object and rescanning the raw data. + * + * \param[in] gif A \ref nsgif_t object. + * + * \return NSGIF_OK on success, or appropriate error otherwise. */ - NSGIF_BITMAP_FMT_ARGB8888, + nsgif_error nsgif_reset( + nsgif_t* gif); /** - * 32-bit BGRA (0xAABBGGRR). - * - * * On little endian host, same as \ref NSGIF_BITMAP_FMT_R8G8B8A8. - * * On big endian host, same as \ref NSGIF_BITMAP_FMT_A8B8G8R8. + * Information about a GIF. */ - NSGIF_BITMAP_FMT_ABGR8888, -} nsgif_bitmap_fmt_t; + typedef struct nsgif_info { + /** width of GIF (may increase during decoding) */ + uint32_t width; + /** height of GIF (may increase during decoding) */ + uint32_t height; + /** number of frames decoded */ + uint32_t frame_count; + /** number of times to play animation (zero means loop forever) */ + int loop_max; + /** background colour in same pixel format as \ref nsgif_bitmap_t. */ + uint32_t background; + /** whether the GIF has a global colour table */ + bool global_palette; + } nsgif_info_t; -/** - * Client bitmap type. - * - * These are client-created and destroyed, via the \ref nsgif_bitmap_cb_vt - * callbacks, but they are owned by a \ref nsgif_t. - * - * See \ref nsgif_bitmap_fmt for pixel format information. - * - * The bitmap may have a row_span greater than the bitmap width, but the - * difference between row span and width must be a whole number of pixels - * (a multiple of four bytes). - */ -typedef void nsgif_bitmap_t; - -/** Bitmap callbacks function table */ -typedef struct nsgif_bitmap_cb_vt { /** - * Callback to create a bitmap with the given dimensions. + * Frame disposal method. * - * \param[in] width Required bitmap width in pixels. - * \param[in] height Required bitmap height in pixels. - * \return pointer to client's bitmap structure or NULL on error. + * Clients do not need to know about this, it is provided purely for dumping + * raw information about GIF frames. */ - nsgif_bitmap_t* (*create)(int width, int height); + enum nsgif_disposal { + NSGIF_DISPOSAL_UNSPECIFIED, /**< No disposal method specified. */ + NSGIF_DISPOSAL_NONE, /**< Frame remains. */ + NSGIF_DISPOSAL_RESTORE_BG, /**< Clear frame to background colour. */ + NSGIF_DISPOSAL_RESTORE_PREV, /**< Restore previous frame. */ + NSGIF_DISPOSAL_RESTORE_QUIRK, /**< Alias for NSGIF_DISPOSAL_RESTORE_PREV. */ + }; /** - * Callback to free a bitmap. + * Convert a disposal method to a string. * - * \param[in] bitmap The bitmap to destroy. + * \param[in] disposal The disposal method to convert. + * \return String representation of given disposal method. */ - void (*destroy)(nsgif_bitmap_t *bitmap); + const char* nsgif_str_disposal(enum nsgif_disposal disposal); /** - * Get pointer to pixel buffer in a bitmap. - * - * The pixel buffer must be `(width + N) * height * sizeof(uint32_t)`. - * Where `N` is any number greater than or equal to 0. - * Note that the returned pointer to uint8_t must be 4-byte aligned. - * - * \param[in] bitmap The bitmap. - * \return pointer to bitmap's pixel buffer. + * Information about a GIF frame. */ - uint8_t* (*get_buffer)(nsgif_bitmap_t *bitmap); - - /* The following functions are optional. */ + typedef struct nsgif_frame_info { + /** whether the frame should be displayed/animated */ + bool display; + /** whether the frame may have transparency */ + bool transparency; + /** whether the frame has a local colour table */ + bool local_palette; + /** whether the frame is interlaced */ + bool interlaced; + /** Disposal method for previous frame; affects plotting */ + uint8_t disposal; + /** delay (in cs) before animating the frame */ + uint32_t delay; + + /** Frame's redraw rectangle. */ + nsgif_rect_t rect; + } nsgif_frame_info_t; /** - * Set whether a bitmap can be plotted opaque. + * Get information about a GIF from an \ref nsgif_t object. + * + * \param[in] gif The \ref nsgif_t object to get info for. * - * \param[in] bitmap The bitmap. - * \param[in] opaque Whether the current frame is opaque. + * \return The gif info, or NULL on error. */ - void (*set_opaque)(nsgif_bitmap_t *bitmap, bool opaque); + const nsgif_info_t* nsgif_get_info(const nsgif_t* gif); /** - * Tests whether a bitmap has an opaque alpha channel. + * Get information about a GIF from an \ref nsgif_t object. * - * \param[in] bitmap The bitmap. - * \return true if the bitmap is opaque, false otherwise. + * \param[in] gif The \ref nsgif_t object to get frame info for. + * \param[in] frame The frame number to get info for. + * + * \return The gif frame info, or NULL on error. */ - bool (*test_opaque)(nsgif_bitmap_t *bitmap); + const nsgif_frame_info_t* nsgif_get_frame_info( + const nsgif_t* gif, + uint32_t frame); /** - * Bitmap modified notification. + * Get the global colour palette. + * + * If the GIF has no global colour table, this will return the default + * colour palette. * - * \param[in] bitmap The bitmap. + * Colours in same pixel format as \ref nsgif_bitmap_t. + * + * \param[in] gif The \ref nsgif_t object. + * \param[out] table Client buffer to hold the colour table. + * \param[out] entries The number of used entries in the colour table. */ - void (*modified)(nsgif_bitmap_t *bitmap); + void nsgif_global_palette( + const nsgif_t* gif, + uint32_t table[NSGIF_MAX_COLOURS], + size_t* entries); /** - * Get row span in pixels. + * Get the local colour palette for a frame. * - * If this callback is not provided, LibNSGIF will use the width. + * Frames may have no local palette. In this case they use the global palette. + * This function returns false if the frame has no local palette. * - * If row span is greater than width, this callback must be provided. + * Colours in same pixel format as \ref nsgif_bitmap_t. * - * \param[in] bitmap The bitmap. + * \param[in] gif The \ref nsgif_t object. + * \param[in] frame The frame to get the palette for. + * \param[out] table Client buffer to hold the colour table. + * \param[out] entries The number of used entries in the colour table. + * \return true if a palette is returned, false otherwise. */ - uint32_t (*get_rowspan)(nsgif_bitmap_t *bitmap); -} nsgif_bitmap_cb_vt; - -/** - * Convert an error code to a string. - * - * \param[in] err The error code to convert. - * \return String representation of given error code. - */ -const char *nsgif_strerror(nsgif_error err); - -/** - * Create the NSGIF object. - * - * \param[in] bitmap_vt Bitmap operation functions v-table. - * \param[in] bitmap_fmt Bitmap pixel format specification. - * \param[out] gif_out Return \ref nsgif_t object on success. - * - * \return NSGIF_OK on success, or appropriate error otherwise. - */ -nsgif_error nsgif_create( - const nsgif_bitmap_cb_vt *bitmap_vt, - nsgif_bitmap_fmt_t bitmap_fmt, - nsgif_t **gif_out); - -/** - * Free a NSGIF object. - * - * \param[in] gif The NSGIF to free. - */ -void nsgif_destroy(nsgif_t *gif); - -/** - * Scan the source image data. - * - * This is used to feed the source data into LibNSGIF. This must be called - * before calling \ref nsgif_frame_decode. - * - * It can be called multiple times with, with increasing sizes. If it is called - * several times, as more data is available (e.g. slow network fetch) the data - * already given to \ref nsgif_data_scan must be provided each time. - * - * Once all the data has been provided, call \ref nsgif_data_complete. - * - * For example, if you call \ref nsgif_data_scan with 25 bytes of data, and then - * fetch another 10 bytes, you would need to call \ref nsgif_data_scan with a - * size of 35 bytes, and the whole 35 bytes must be contiguous memory. It is - * safe to `realloc` the source buffer between calls to \ref nsgif_data_scan. - * (The actual data pointer is allowed to be different.) - * - * If an error occurs, all previously scanned frames are retained. - * - * Note that an error returned from this function is purely informational. - * So long as at least one frame is available, you can display frames. - * - * \param[in] gif The \ref nsgif_t object. - * \param[in] size Number of bytes in data. - * \param[in] data Raw source GIF data. - * - * \return NSGIF_OK on success, or appropriate error otherwise. - */ -nsgif_error nsgif_data_scan( - nsgif_t *gif, - size_t size, - const uint8_t *data); - -/** - * Tell libnsgif that all the gif data has been provided. - * - * Call this after calling \ref nsgif_data_scan with the the entire GIF - * source data. You can call \ref nsgif_data_scan multiple times up until - * this is called, and after this is called, \ref nsgif_data_scan will - * return an error. - * - * You can decode a GIF before this is called, however, it will fail to - * decode any truncated final frame data and will not perform loops when - * driven via \ref nsgif_frame_prepare (because it doesn't know if there - * will be more frames supplied in future data). - * - * \param[in] gif The \ref nsgif_t object. - */ -void nsgif_data_complete( - nsgif_t *gif); - -/** - * Prepare to show a frame. - * - * If this is the last frame of an animation with a finite loop count, the - * returned `delay_cs` will be \ref NSGIF_INFINITE, indicating that the frame - * should be shown forever. - * - * Note that if \ref nsgif_data_complete has not been called on this gif, - * animated GIFs will not loop back to the start. Instead it will return - * \ref NSGIF_ERR_END_OF_DATA. - * - * \param[in] gif The \ref nsgif_t object. - * \param[out] area The area in pixels that must be redrawn. - * \param[out] delay_cs Time to wait after frame_new before next frame in cs. - * \param[out] frame_new The frame to decode. - * - * \return NSGIF_OK on success, or appropriate error otherwise. - */ -nsgif_error nsgif_frame_prepare( - nsgif_t *gif, - nsgif_rect_t *area, - uint32_t *delay_cs, - uint32_t *frame_new); - -/** - * Decodes a GIF frame. - * - * \param[in] gif The \ref nsgif_t object. - * \param[in] frame The frame number to decode. - * \param[out] bitmap On success, returns pointer to the client-allocated, - * nsgif-owned client bitmap structure. - * - * \return NSGIF_OK on success, or appropriate error otherwise. - */ -nsgif_error nsgif_frame_decode( - nsgif_t *gif, - uint32_t frame, - nsgif_bitmap_t **bitmap); - -/** - * Reset a GIF animation. - * - * Some animations are only meant to loop N times, and then show the - * final frame forever. This function resets the loop and frame counters, - * so that the animation can be replayed without the overhead of recreating - * the \ref nsgif_t object and rescanning the raw data. - * - * \param[in] gif A \ref nsgif_t object. - * - * \return NSGIF_OK on success, or appropriate error otherwise. - */ -nsgif_error nsgif_reset( - nsgif_t *gif); - -/** - * Information about a GIF. - */ -typedef struct nsgif_info { - /** width of GIF (may increase during decoding) */ - uint32_t width; - /** height of GIF (may increase during decoding) */ - uint32_t height; - /** number of frames decoded */ - uint32_t frame_count; - /** number of times to play animation (zero means loop forever) */ - int loop_max; - /** background colour in same pixel format as \ref nsgif_bitmap_t. */ - uint32_t background; - /** whether the GIF has a global colour table */ - bool global_palette; -} nsgif_info_t; - -/** - * Frame disposal method. - * - * Clients do not need to know about this, it is provided purely for dumping - * raw information about GIF frames. - */ -enum nsgif_disposal { - NSGIF_DISPOSAL_UNSPECIFIED, /**< No disposal method specified. */ - NSGIF_DISPOSAL_NONE, /**< Frame remains. */ - NSGIF_DISPOSAL_RESTORE_BG, /**< Clear frame to background colour. */ - NSGIF_DISPOSAL_RESTORE_PREV, /**< Restore previous frame. */ - NSGIF_DISPOSAL_RESTORE_QUIRK, /**< Alias for NSGIF_DISPOSAL_RESTORE_PREV. */ -}; - -/** - * Convert a disposal method to a string. - * - * \param[in] disposal The disposal method to convert. - * \return String representation of given disposal method. - */ -const char *nsgif_str_disposal(enum nsgif_disposal disposal); - -/** - * Information about a GIF frame. - */ -typedef struct nsgif_frame_info { - /** whether the frame should be displayed/animated */ - bool display; - /** whether the frame may have transparency */ - bool transparency; - /** whether the frame has a local colour table */ - bool local_palette; - /** whether the frame is interlaced */ - bool interlaced; - /** Disposal method for previous frame; affects plotting */ - uint8_t disposal; - /** delay (in cs) before animating the frame */ - uint32_t delay; - - /** Frame's redraw rectangle. */ - nsgif_rect_t rect; -} nsgif_frame_info_t; - -/** - * Get information about a GIF from an \ref nsgif_t object. - * - * \param[in] gif The \ref nsgif_t object to get info for. - * - * \return The gif info, or NULL on error. - */ -const nsgif_info_t *nsgif_get_info(const nsgif_t *gif); - -/** - * Get information about a GIF from an \ref nsgif_t object. - * - * \param[in] gif The \ref nsgif_t object to get frame info for. - * \param[in] frame The frame number to get info for. - * - * \return The gif frame info, or NULL on error. - */ -const nsgif_frame_info_t *nsgif_get_frame_info( - const nsgif_t *gif, - uint32_t frame); - -/** - * Get the global colour palette. - * - * If the GIF has no global colour table, this will return the default - * colour palette. - * - * Colours in same pixel format as \ref nsgif_bitmap_t. - * - * \param[in] gif The \ref nsgif_t object. - * \param[out] table Client buffer to hold the colour table. - * \param[out] entries The number of used entries in the colour table. - */ -void nsgif_global_palette( - const nsgif_t *gif, - uint32_t table[NSGIF_MAX_COLOURS], - size_t *entries); - -/** - * Get the local colour palette for a frame. - * - * Frames may have no local palette. In this case they use the global palette. - * This function returns false if the frame has no local palette. - * - * Colours in same pixel format as \ref nsgif_bitmap_t. - * - * \param[in] gif The \ref nsgif_t object. - * \param[in] frame The frame to get the palette for. - * \param[out] table Client buffer to hold the colour table. - * \param[out] entries The number of used entries in the colour table. - * \return true if a palette is returned, false otherwise. - */ -bool nsgif_local_palette( - const nsgif_t *gif, + bool nsgif_local_palette( + const nsgif_t* gif, uint32_t frame, uint32_t table[NSGIF_MAX_COLOURS], - size_t *entries); + size_t* entries); -/** - * Configure handling of small frame delays. - * - * Historically people created GIFs with a tiny frame delay, however the slow - * hardware of the time meant they actually played much slower. As computers - * sped up, to prevent animations playing faster than intended, decoders came - * to ignore overly small frame delays. - * - * By default a \ref nsgif_frame_prepare() managed animation will override - * frame delays of less than 2 centiseconds with a default frame delay of - * 10 centiseconds. This matches the behaviour of web browsers and other - * renderers. - * - * Both the minimum and the default values can be overridden for a given GIF - * by the client. To get frame delays exactly as specified by the GIF file, set - * `delay_min` to zero. - * - * Note that this does not affect the frame delay in the frame info - * (\ref nsgif_frame_info_t) structure, which will always contain values - * specified by the GIF. - * - * \param[in] gif The \ref nsgif_t object to configure. - * \param[in] delay_min The minimum frame delay in centiseconds. - * \param[in] delay_default The delay to use if a frame delay is less than - * `delay_min`. - */ -void nsgif_set_frame_delay_behaviour( - nsgif_t *gif, + /** + * Configure handling of small frame delays. + * + * Historically people created GIFs with a tiny frame delay, however the slow + * hardware of the time meant they actually played much slower. As computers + * sped up, to prevent animations playing faster than intended, decoders came + * to ignore overly small frame delays. + * + * By default a \ref nsgif_frame_prepare() managed animation will override + * frame delays of less than 2 centiseconds with a default frame delay of + * 10 centiseconds. This matches the behaviour of web browsers and other + * renderers. + * + * Both the minimum and the default values can be overridden for a given GIF + * by the client. To get frame delays exactly as specified by the GIF file, set + * `delay_min` to zero. + * + * Note that this does not affect the frame delay in the frame info + * (\ref nsgif_frame_info_t) structure, which will always contain values + * specified by the GIF. + * + * \param[in] gif The \ref nsgif_t object to configure. + * \param[in] delay_min The minimum frame delay in centiseconds. + * \param[in] delay_default The delay to use if a frame delay is less than + * `delay_min`. + */ + void nsgif_set_frame_delay_behaviour( + nsgif_t* gif, uint16_t delay_min, uint16_t delay_default); +#ifdef __cplusplus +} +#endif + #endif