Thanks to visit codestin.com
Credit goes to Github.com

Skip to content

Conversation

@Nor-s
Copy link
Collaborator

@Nor-s Nor-s commented Nov 10, 2025

Support External GIF (giflib)

This patch adds support for external GIF encoding using giflib

Implementation Details

This patch integrates giflib as an optional external dependency.
We can control the palette behavior via the quality parameter:

Quality Value Behavior Description
=0 Global Palette Use a single global color palette for all frames
!= 0 Local Palette Use individual local palettes per frame

The implementation of tvgGifSaver.h and tvgGifSaver.cpp is mostly identical to the static version,
except for the parts handling the quality setting and global palette generation.

Additionally, tvgGifEncoder.h and tvgGifEncoder.cpp have been added.
They share a similar interface with the static encoder but include an option to generate a global palette.

giflib Quantization

giflib provides an color quantization API, but it's deprecated. (see source code reference, This doesn't really belong in the core library, was undocumented,
and was removed in 4.2
)

Coomplee the removal of GifQuantizeBuffer().

Version 5.2.0
=============

The undocumented and deprecated GifQuantizeBuffer() entry point
has been moved to the util library to reduce libgif size and attack
surface. Applications needing this function are couraged to link the
util library or make their own copy.

The following obsolete utility programs are no longer installed:
gifecho, giffilter, gifinto, gifsponge. These were either installed in
error or have been obsolesced by modern image-transformation tools
like ImageMagick convert. They may be removed entirely in a future
release.

* Address SourceForge issue #136: Stack-buffer-overflow in gifcolor.c:84

* Address SF bug #134: Giflib fails to slurp significant number of gifs

* Apply SPDX convention for license tagging.

Comparison

1. Binary Size

meson setup builddir --wipe -Dloaders=all -Dsavers=gif -Dtests=true -Dlog=false -Dengines=sw,gl -Dextra=lottie_expressions -Dstatic=false  -Dbuildtype=release -Dtools=all 

meson compile -C builddir
Version Binary Size
static (gif-h) 1520144
external_gif 1512048(-8096)

2. Performance

./builddir/tools/lottie2gif/tvg-lottie2gif ./examples/resources/lottie/ -r 200x200

find examples/resources/lottie -type f -iname "*.gif" -printf '%s\n' | awk '{sum+=$1} END {print sum/1024/1024 " MB"}'
Mode Encode Time gif file size
global palette (quality = 0) 81380.06 ms 52.102 MB
local palette (quality != 0) 21256.928 ms 63.4543 MB
static (gif-h) 53286.036 ms 77.3191 MB

AMD Ryzen 5 7600, 64GB ram, wsl2 ubuntu

3. Result

(no bg, 200x200)

static(gif-h) giflib (global) giflib (local) gifenc
Image Image Image Image
Image Image Image Image
Image Image Image Image
Image Image Image Image
Image Image Image Image
Image Image Image Image
Image Image Image Image
Image Image Image Image
Image Image Image Image
Image Image Image Image
Image Image Image Image

@Nor-s Nor-s requested a review from hermet as a code owner November 10, 2025 16:45
@Nor-s Nor-s force-pushed the support-external-gif branch from d0d4952 to 1659a72 Compare November 10, 2025 16:50
@hermet hermet requested a review from Copilot November 11, 2025 03:34
@hermet hermet added feature New feature additions image Bitmap image features (jpg/png/webp/gif) labels Nov 11, 2025
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR integrates external GIF encoding using giflib as an optional dependency to replace the static GIF encoder. The implementation allows users to control palette behavior through the quality parameter: quality=0 uses a global palette for all frames, while quality≠0 uses local palettes per frame.

  • Adds external_gif saver implementation with giflib dependency
  • Implements global/local palette selection based on quality parameter
  • Falls back to static gif saver when giflib is unavailable

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/savers/meson.build Updated build logic to prefer external_gif when not using static mode, with fallback to static gif
src/savers/external_gif/tvgGifSaver.h Header for external GIF saver module with quality-based palette control
src/savers/external_gif/tvgGifSaver.cpp Implementation of GIF saver using giflib with global/local palette support
src/savers/external_gif/tvgGifEncoder.h Encoder interface with global palette generation capability
src/savers/external_gif/tvgGifEncoder.cpp Core encoding logic using giflib APIs for quantization and frame writing
src/savers/external_gif/meson.build Build configuration for external_gif with giflib dependency detection

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 24 to 45
#include "tvgCommon.h"

#define TRANSPARENT_THRESHOLD 127
#define BIT_DEPTH 8
#define MAX_PALETTESIZE 256

static ColorMapObject* _quantize(std::vector<GifByteType>& r, std::vector<GifByteType>& g, std::vector<GifByteType>& b, GifByteType* outputBuffer)
{
int paletteSize = MAX_PALETTESIZE - 1;
std::vector<GifColorType> palette(paletteSize, {0, 0, 0});
std::vector<GifByteType> tempIndex;
if (outputBuffer == nullptr) {
tempIndex.resize(r.size(), 0);
outputBuffer = tempIndex.data();
}
if (r.size() > 0 && GifQuantizeBuffer((int)r.size(), 1, &paletteSize, r.data(), g.data(), b.data(), outputBuffer, palette.data()) == GIF_ERROR) {
return nullptr;
}

int colorMapSize = 1 << GifBitSize(paletteSize + 1); // +1: transparent index
auto *colorMap = GifMakeMapObject(colorMapSize, nullptr);
if (colorMap) memcpy(colorMap->Colors, palette.data(), paletteSize * sizeof(GifColorType));
Copy link

Copilot AI Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code uses GifQuantizeBuffer() which according to the PR description was removed in giflib 5.2.0 (released years ago). This will cause compilation failures with modern giflib versions. An alternative quantization implementation is needed, or the minimum supported giflib version should be documented and enforced.

Suggested change
#include "tvgCommon.h"
#define TRANSPARENT_THRESHOLD 127
#define BIT_DEPTH 8
#define MAX_PALETTESIZE 256
static ColorMapObject* _quantize(std::vector<GifByteType>& r, std::vector<GifByteType>& g, std::vector<GifByteType>& b, GifByteType* outputBuffer)
{
int paletteSize = MAX_PALETTESIZE - 1;
std::vector<GifColorType> palette(paletteSize, {0, 0, 0});
std::vector<GifByteType> tempIndex;
if (outputBuffer == nullptr) {
tempIndex.resize(r.size(), 0);
outputBuffer = tempIndex.data();
}
if (r.size() > 0 && GifQuantizeBuffer((int)r.size(), 1, &paletteSize, r.data(), g.data(), b.data(), outputBuffer, palette.data()) == GIF_ERROR) {
return nullptr;
}
int colorMapSize = 1 << GifBitSize(paletteSize + 1); // +1: transparent index
auto *colorMap = GifMakeMapObject(colorMapSize, nullptr);
if (colorMap) memcpy(colorMap->Colors, palette.data(), paletteSize * sizeof(GifColorType));
#include <algorithm>
#include <cstdint>
#include "tvgCommon.h"
#define TRANSPARENT_THRESHOLD 127
#define BIT_DEPTH 8
#define MAX_PALETTESIZE 256
// Simple median cut quantizer for GIF palette generation
static void medianCutQuantize(const std::vector<GifByteType>& r, const std::vector<GifByteType>& g, const std::vector<GifByteType>& b,
int maxColors, std::vector<GifColorType>& palette, std::vector<GifByteType>& outIndices)
{
struct Color {
GifByteType r, g, b;
int idx;
};
std::vector<Color> colors(r.size());
for (size_t i = 0; i < r.size(); ++i) {
colors[i] = {r[i], g[i], b[i], (int)i};
}
// Each box is a vector of indices into colors
std::vector<std::vector<int>> boxes;
boxes.push_back({});
for (size_t i = 0; i < colors.size(); ++i) boxes[0].push_back((int)i);
// Split boxes until we have enough
while ((int)boxes.size() < maxColors) {
// Find the box with the largest color range
int bestBox = -1, bestRange = -1, bestChannel = 0;
for (size_t b = 0; b < boxes.size(); ++b) {
int minR = 255, maxR = 0, minG = 255, maxG = 0, minB = 255, maxB = 0;
for (int idx : boxes[b]) {
minR = std::min(minR, (int)colors[idx].r); maxR = std::max(maxR, (int)colors[idx].r);
minG = std::min(minG, (int)colors[idx].g); maxG = std::max(maxG, (int)colors[idx].g);
minB = std::min(minB, (int)colors[idx].b); maxB = std::max(maxB, (int)colors[idx].b);
}
int rangeR = maxR - minR, rangeG = maxG - minG, rangeB = maxB - minB;
int range = std::max({rangeR, rangeG, rangeB});
if (range > bestRange) {
bestRange = range;
bestBox = (int)b;
if (rangeR >= rangeG && rangeR >= rangeB) bestChannel = 0;
else if (rangeG >= rangeR && rangeG >= rangeB) bestChannel = 1;
else bestChannel = 2;
}
}
if (bestBox == -1 || (int)boxes[bestBox].size() <= 1) break;
// Sort the box along the best channel and split in half
auto& box = boxes[bestBox];
std::sort(box.begin(), box.end(), [&](int a, int b) {
if (bestChannel == 0) return colors[a].r < colors[b].r;
if (bestChannel == 1) return colors[a].g < colors[b].g;
return colors[a].b < colors[b].b;
});
size_t mid = box.size() / 2;
std::vector<int> newBox(box.begin() + mid, box.end());
box.erase(box.begin() + mid, box.end());
boxes.push_back(std::move(newBox));
}
// Compute palette as the average color in each box
palette.clear();
palette.reserve(boxes.size());
outIndices.resize(colors.size());
for (size_t b = 0; b < boxes.size(); ++b) {
uint64_t sumR = 0, sumG = 0, sumB = 0;
for (int idx : boxes[b]) {
sumR += colors[idx].r; sumG += colors[idx].g; sumB += colors[idx].b;
}
int n = (int)boxes[b].size();
GifColorType col;
col.Red = n ? (GifByteType)(sumR / n) : 0;
col.Green = n ? (GifByteType)(sumG / n) : 0;
col.Blue = n ? (GifByteType)(sumB / n) : 0;
palette.push_back(col);
for (int idx : boxes[b]) {
outIndices[idx] = (GifByteType)b;
}
}
}
static ColorMapObject* _quantize(std::vector<GifByteType>& r, std::vector<GifByteType>& g, std::vector<GifByteType>& b, GifByteType* outputBuffer)
{
int paletteSize = MAX_PALETTESIZE - 1;
std::vector<GifColorType> palette;
std::vector<GifByteType> tempIndex;
if (outputBuffer == nullptr) {
tempIndex.resize(r.size(), 0);
outputBuffer = tempIndex.data();
}
if (r.size() > 0) {
medianCutQuantize(r, g, b, paletteSize, palette, tempIndex);
if (outputBuffer == tempIndex.data()) {
// Copy to outputBuffer
memcpy(outputBuffer, tempIndex.data(), tempIndex.size() * sizeof(GifByteType));
} else {
// outputBuffer is provided, fill it
memcpy(outputBuffer, tempIndex.data(), tempIndex.size() * sizeof(GifByteType));
}
}
int colorMapSize = 1 << GifBitSize((int)palette.size() + 1); // +1: transparent index
auto *colorMap = GifMakeMapObject(colorMapSize, nullptr);
if (colorMap && !palette.empty()) memcpy(colorMap->Colors, palette.data(), palette.size() * sizeof(GifColorType));

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

@Nor-s Nor-s Nov 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hermet

I updated the Meson build so that it now disables the build when GifQuantizeBuffer is not found.
Currently, the GitHub Actions CI does not test the external_gif build, since macOS uses giflib 5.2.2, which no longer provides GifQuantizeBuffer. (see: https://github.com/thorvg/thorvg/actions/runs/19264273695/job/55076167615#step:5:26)

Given this situation, I think we have a few options going forward:

  1. Add our own quantization implementation (likely similar to the one in gif-h), and use it instead of GifQuantizeBuffer.
  2. Only enable the external GIF saver when GifQuantizeBuffer is available in the system giflib.
  3. Drop support for the external GIF saver and rely only on the static implementation.

Below are the results after applying the quantization code generated by Copilot.
Thank you.

Train Ghost Gradient Background Partying Face Emoji UI/UX Jolly Walker
train ghost gradient_background 27746-joypixels-partying-face-emoji-animation uiux jolly_walker
  • no background, 200x200
  • examples/resources/lottie:
    • encode time: 309219.29 ms,
    • total gif size: 102.533 MB
  • libthorvg.so.1.0.0 size: 1524336 (-4192)

This patch introduces support for an external GIF saver
using giflib(https://giflib.sourceforge.net/)

- Introduced `saver/external_gif` module
- Integrated giflib for GIF encoding
- Added Meson build option
- Implemented **quality** option:
  - `=0`: use global palette
  - `!=0`: use local palette

issue: thorvg#2166
@Nor-s Nor-s force-pushed the support-external-gif branch from 1659a72 to 3816418 Compare November 11, 2025 11:32
@github-actions github-actions bot removed the image Bitmap image features (jpg/png/webp/gif) label Nov 11, 2025
@Nor-s Nor-s added the image Bitmap image features (jpg/png/webp/gif) label Nov 11, 2025
@hermet hermet force-pushed the main branch 5 times, most recently from ac9b628 to eec09a7 Compare November 20, 2025 15:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New feature additions image Bitmap image features (jpg/png/webp/gif)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants