mirror of https://github.com/aseprite/aseprite.git
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:
parent
41a8249afd
commit
3efd0014e8
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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()) {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in New Issue