Fix gif files to apply global palette in particular cases

This fix was prompted by Discord users who noticed that certain GIFs
saved with Aseprite and uploaded to Discord chats resulted in
animations whose colors changed unintentionally throughout
the animation. It was discovered that Discord does not correctly
process GIFs with disposal=DO_NOT_DISPOSE + global palette +
local palettes.

This fix essentially defines a global palette in all cases where
the animation (with Color Mode RGBA/GRAYSCALE) can be made with up to
256 global colors. This fixes simple animations from
a Discord perspective.

On the other hand, all other cases (color count > 256) continue to be
processed as before and may present issues in Discord.
This commit is contained in:
Gaspar Capello 2025-06-06 08:48:12 -03:00
parent 41a8249afd
commit 3efd0014e8
5 changed files with 96 additions and 5 deletions

View File

@ -1,5 +1,5 @@
// Aseprite // Aseprite
// Copyright (C) 2018-2023 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This program is distributed under the terms of // This program is distributed under the terms of
@ -1050,7 +1050,47 @@ public:
m_fop->newBlend(), m_fop->newBlend(),
RgbMapAlgorithm::OCTREE, // TODO configurable? RgbMapAlgorithm::OCTREE, // TODO configurable?
false); // Do not add the transparent color yet false); // Do not add the transparent color yet
m_transparentIndex = 0;
m_globalColormapPalette = newPalette;
m_globalColormap = createColorMap(&m_globalColormapPalette);
}
}
// The following "if" block is intended to address cases where
// the Color Mode of the animation is RGB and can be represented by
// an absolute palette because it contains fewer than 256 colors
// throughout the entire animation. In this way the memory space
// used to generate the GIF is much more efficient, since local
// palettes don't need to be inserted in each frame. It also
// fixes a display issue with GIF files (generated by
// Aseprite 1.3.13) on Discord when the GIF parameters were:
// - disposal = DO_NOT_DISPOSE = 1
// - Local palettes exist.
if (m_spec.colorMode() == ColorMode::RGB || m_spec.colorMode() == ColorMode::GRAYSCALE) {
Palette newPalette(0, 512);
render::create_palette_from_sprite(m_sprite,
0,
totalFrames() - 1,
false,
&newPalette,
nullptr,
m_fop->newBlend(),
RgbMapAlgorithm::OCTREE,
false); // No effect on OctreeMap.
// Case: palette with (256 colors + mask color) == 257 but
// the mask color isn't used in the sprite.
if (newPalette.size() == 257 && !m_sprite->isColorUsed(0)) {
// Forcing GIF with background
m_transparentIndex = -1;
m_hasBackground = true;
// Discard the mask color (palette entry = 0)
for (int i = 0; i < 256; i++)
newPalette.setEntry(i, newPalette.getEntry(i + 1));
newPalette.resize(256);
m_globalColormapPalette = newPalette;
m_globalColormap = createColorMap(&m_globalColormapPalette);
}
else if (newPalette.size() <= 256) {
m_transparentIndex = 0; m_transparentIndex = 0;
m_globalColormapPalette = newPalette; m_globalColormapPalette = newPalette;
m_globalColormap = createColorMap(&m_globalColormapPalette); m_globalColormap = createColorMap(&m_globalColormapPalette);
@ -1388,15 +1428,18 @@ private:
if (!m_preservePaletteOrder) { if (!m_preservePaletteOrder) {
const LockImageBits<RgbTraits> srcBits(m_deltaImage.get()); const LockImageBits<RgbTraits> srcBits(m_deltaImage.get());
const LockImageBits<RgbTraits> preBits(m_previousImage, frameBounds);
LockImageBits<IndexedTraits> dstBits(frameImage.get()); LockImageBits<IndexedTraits> dstBits(frameImage.get());
auto srcIt = srcBits.begin(); auto srcIt = srcBits.begin();
auto dstIt = dstBits.begin(); auto dstIt = dstBits.begin();
auto preIt = preBits.begin();
for (int y = 0; y < frameBounds.h; ++y) { for (int y = 0; y < frameBounds.h; ++y) {
for (int x = 0; x < frameBounds.w; ++x, ++srcIt, ++dstIt) { for (int x = 0; x < frameBounds.w; ++x, ++srcIt, ++dstIt, ++preIt) {
ASSERT(srcIt != srcBits.end()); ASSERT(srcIt != srcBits.end());
ASSERT(dstIt != dstBits.end()); ASSERT(dstIt != dstBits.end());
ASSERT(preIt != preBits.end());
color_t color = *srcIt; color_t color = *srcIt;
int i; int i;
@ -1410,9 +1453,19 @@ private:
if (i < 0) if (i < 0)
i = octree.mapColor(color | rgba_a_mask); // alpha=255 i = octree.mapColor(color | rgba_a_mask); // alpha=255
} }
// If the alpha in a pixel from m_deltaImage is < 128, the
// pixel is assumed to be 0. Then it should draw the pixel
// according defined m_transparentIndex or disposal method
else { else {
if (m_transparentIndex >= 0) if (m_transparentIndex >= 0)
i = m_transparentIndex; i = m_transparentIndex;
else if (disposal == DisposalMethod::DO_NOT_DISPOSE) {
i = framePalette.findExactMatch(rgba_getr(*preIt),
rgba_getg(*preIt),
rgba_getb(*preIt),
255,
-1);
}
else else
i = m_bgIndex; i = m_bgIndex;
} }

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // Aseprite Document Library
// Copyright (c) 2018-2023 Igara Studio S.A. // Copyright (c) 2018-2025 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello // Copyright (c) 2001-2016 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -352,6 +352,18 @@ bool is_plain_image_templ(const Image* img, const color_t color)
return true; return true;
} }
template<typename ImageTraits>
bool is_color_used_templ(const Image* img, const doc::color_t color)
{
const LockImageBits<ImageTraits> bits(img);
auto it = bits.begin(), end = bits.end();
for (; it != end; ++it) {
if (*it == color)
return true;
}
return false;
}
template<typename ImageTraits> template<typename ImageTraits>
int count_diff_between_images_templ(const Image* i1, const Image* i2) int count_diff_between_images_templ(const Image* i1, const Image* i2)
{ {
@ -464,6 +476,16 @@ bool is_plain_image(const Image* img, color_t c)
return false; return false;
} }
bool is_color_used(const Image* img, color_t c)
{
ASSERT(img->pixelFormat() == IMAGE_RGB || img->pixelFormat() == IMAGE_GRAYSCALE);
switch (img->pixelFormat()) {
case IMAGE_RGB: return is_color_used_templ<RgbTraits>(img, c);
case IMAGE_GRAYSCALE: return is_color_used_templ<GrayscaleTraits>(img, c);
}
return false;
}
bool is_empty_image(const Image* img) bool is_empty_image(const Image* img)
{ {
color_t c = 0; // alpha = 0 color_t c = 0; // alpha = 0

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // Aseprite Document Library
// Copyright (c) 2018-2023 Igara Studio S.A. // Copyright (c) 2018-2025 Igara Studio S.A.
// Copyright (c) 2001-2016 David Capello // Copyright (c) 2001-2016 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -65,6 +65,7 @@ void fill_ellipse(Image* image,
color_t color); color_t color);
bool is_plain_image(const Image* img, color_t c); bool is_plain_image(const Image* img, color_t c);
bool is_color_used(const Image* img, color_t c);
bool is_empty_image(const Image* img); bool is_empty_image(const Image* img);
int count_diff_between_images(const Image* i1, const Image* i2); int count_diff_between_images(const Image* i1, const Image* i2);

View File

@ -1,5 +1,5 @@
// Aseprite Document Library // Aseprite Document Library
// Copyright (C) 2018-2024 Igara Studio S.A. // Copyright (C) 2018-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello // Copyright (C) 2001-2018 David Capello
// //
// This file is released under the terms of the MIT license. // This file is released under the terms of the MIT license.
@ -223,6 +223,18 @@ bool Sprite::isOpaque() const
return (bg && bg->isVisible()); return (bg && bg->isVisible());
} }
bool Sprite::isColorUsed(const doc::color_t c) const
{
ASSERT(pixelFormat() == IMAGE_RGB || pixelFormat() == IMAGE_GRAYSCALE);
for (Cel* cel : cels()) {
if (cel && cel->image()) {
if (is_color_used(cel->image(), c))
return true;
}
}
return false;
}
bool Sprite::needAlpha() const bool Sprite::needAlpha() const
{ {
switch (pixelFormat()) { switch (pixelFormat()) {

View File

@ -110,6 +110,9 @@ public:
// Returns true if the sprite has a background layer and it's visible // Returns true if the sprite has a background layer and it's visible
bool isOpaque() const; bool isOpaque() const;
// Returns true if the sprite is using a pixel with color c
bool isColorUsed(const doc::color_t c) const;
// Returns true if the rendered images will contain alpha values less // Returns true if the rendered images will contain alpha values less
// than 255. Only RGBA and Grayscale images without background needs // than 255. Only RGBA and Grayscale images without background needs
// alpha channel in the render. // alpha channel in the render.