From 3efd0014e82f1a8c94ed07bf1cecc74a17ac1953 Mon Sep 17 00:00:00 2001 From: Gaspar Capello Date: Fri, 6 Jun 2025 08:48:12 -0300 Subject: [PATCH] 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. --- src/app/file/gif_format.cpp | 57 +++++++++++++++++++++++++++++++++++-- src/doc/primitives.cpp | 24 +++++++++++++++- src/doc/primitives.h | 3 +- src/doc/sprite.cpp | 14 ++++++++- src/doc/sprite.h | 3 ++ 5 files changed, 96 insertions(+), 5 deletions(-) diff --git a/src/app/file/gif_format.cpp b/src/app/file/gif_format.cpp index 9be49d665..9202bf275 100644 --- a/src/app/file/gif_format.cpp +++ b/src/app/file/gif_format.cpp @@ -1,5 +1,5 @@ // Aseprite -// Copyright (C) 2018-2023 Igara Studio S.A. +// Copyright (C) 2018-2025 Igara Studio S.A. // Copyright (C) 2001-2018 David Capello // // This program is distributed under the terms of @@ -1050,7 +1050,47 @@ public: m_fop->newBlend(), RgbMapAlgorithm::OCTREE, // TODO configurable? 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_globalColormapPalette = newPalette; m_globalColormap = createColorMap(&m_globalColormapPalette); @@ -1388,15 +1428,18 @@ private: if (!m_preservePaletteOrder) { const LockImageBits srcBits(m_deltaImage.get()); + const LockImageBits preBits(m_previousImage, frameBounds); LockImageBits dstBits(frameImage.get()); auto srcIt = srcBits.begin(); auto dstIt = dstBits.begin(); + auto preIt = preBits.begin(); 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(dstIt != dstBits.end()); + ASSERT(preIt != preBits.end()); color_t color = *srcIt; int i; @@ -1410,9 +1453,19 @@ private: if (i < 0) 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 { if (m_transparentIndex >= 0) 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 i = m_bgIndex; } diff --git a/src/doc/primitives.cpp b/src/doc/primitives.cpp index be10329fe..c8df70f8c 100644 --- a/src/doc/primitives.cpp +++ b/src/doc/primitives.cpp @@ -1,5 +1,5 @@ // 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 // // 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; } +template +bool is_color_used_templ(const Image* img, const doc::color_t color) +{ + const LockImageBits bits(img); + auto it = bits.begin(), end = bits.end(); + for (; it != end; ++it) { + if (*it == color) + return true; + } + return false; +} + template 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; } +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(img, c); + case IMAGE_GRAYSCALE: return is_color_used_templ(img, c); + } + return false; +} + bool is_empty_image(const Image* img) { color_t c = 0; // alpha = 0 diff --git a/src/doc/primitives.h b/src/doc/primitives.h index f8348d704..f12323547 100644 --- a/src/doc/primitives.h +++ b/src/doc/primitives.h @@ -1,5 +1,5 @@ // 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 // // This file is released under the terms of the MIT license. @@ -65,6 +65,7 @@ void fill_ellipse(Image* image, color_t color); 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); int count_diff_between_images(const Image* i1, const Image* i2); diff --git a/src/doc/sprite.cpp b/src/doc/sprite.cpp index a698eb572..dea6f3574 100644 --- a/src/doc/sprite.cpp +++ b/src/doc/sprite.cpp @@ -1,5 +1,5 @@ // 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 // // This file is released under the terms of the MIT license. @@ -223,6 +223,18 @@ bool Sprite::isOpaque() const 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 { switch (pixelFormat()) { diff --git a/src/doc/sprite.h b/src/doc/sprite.h index f4b6852f5..c2022cc68 100644 --- a/src/doc/sprite.h +++ b/src/doc/sprite.h @@ -110,6 +110,9 @@ public: // Returns true if the sprite has a background layer and it's visible 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 // than 255. Only RGBA and Grayscale images without background needs // alpha channel in the render.