-
-
Notifications
You must be signed in to change notification settings - Fork 170
saver/gif: support an external gif saver #3949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
d0d4952 to
1659a72
Compare
There was a problem hiding this 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.
| #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)); |
Copilot
AI
Nov 11, 2025
There was a problem hiding this comment.
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.
| #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)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Add our own quantization implementation (likely similar to the one in
gif-h), and use it instead ofGifQuantizeBuffer. - Only enable the external GIF saver when
GifQuantizeBufferis available in the system giflib. - 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 |
|---|---|---|---|---|---|
- 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
1659a72 to
3816418
Compare
ac9b628 to
eec09a7
Compare
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:
The implementation of
tvgGifSaver.handtvgGifSaver.cppis mostly identical to the static version,except for the parts handling the
qualitysetting and global palette generation.Additionally,
tvgGifEncoder.handtvgGifEncoder.cpphave 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().
Comparison
1. Binary Size
2. Performance
3. Result
(no bg, 200x200)