Compare commits

...

6 Commits

Author SHA1 Message Date
Gaspar Capello 21b327ee74
Merge 3efd0014e8 into 2ba051b59b 2025-07-23 18:49:17 -04:00
David Capello 2ba051b59b Fix crash deselecting moved pixels when using certain extensions (fix #5280)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
Although the issue refers to deselecting MovingPixelsState, the same
crash could happen when canceling/finishing WritingTextState or
MovingSelectionState. This fixes the crash for all these states.
2025-07-23 19:01:37 -03:00
Christian Kaiser 3fcb000eb1 Fix slice transformations not updating editors
build / build (Debug, macos-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, macos-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, lua, cli) (push) Has been cancelled Details
build / build (Debug, windows-latest, noscripts, cli) (push) Has been cancelled Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Has been cancelled Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Has been cancelled Details
2025-07-22 02:11:07 -03:00
David Capello af9dc3c817 Fix brush boundaries accumulation switching brush type only (fix #5281)
build / build (Debug, macos-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, macos-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, ubuntu-latest, noscripts, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, lua, cli) (push) Waiting to run Details
build / build (Debug, windows-latest, noscripts, cli) (push) Waiting to run Details
build / build (RelWithDebInfo, macos-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, ubuntu-latest, lua, gui) (push) Waiting to run Details
build / build (RelWithDebInfo, windows-latest, lua, gui) (push) Waiting to run Details
2025-07-21 17:28:11 -03:00
David Capello 250dfdc86a Convert the brush generation counter into an atomic var 2025-07-21 17:27:23 -03:00
Gaspar Capello 3efd0014e8 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.
2025-06-06 08:48:12 -03:00
10 changed files with 130 additions and 11 deletions

View File

@ -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<RgbTraits> srcBits(m_deltaImage.get());
const LockImageBits<RgbTraits> preBits(m_previousImage, frameBounds);
LockImageBits<IndexedTraits> 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;
}

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2018 David Capello
//
// This program is distributed under the terms of
@ -457,6 +457,8 @@ void BrushPreview::show(const gfx::Point& screenPos)
// Here we re-use the cached surface
if (!cached && m_uiLayer->surface()) {
m_uiLayer->surface()->clear();
gfx::Rect layerBounds = m_uiLayer->surface()->bounds();
ui::Graphics g(display, m_uiLayer->surface(), 0, 0);

View File

@ -58,6 +58,7 @@
#include "app/util/tile_flags_utils.h"
#include "base/chrono.h"
#include "base/convert_to.h"
#include "base/scoped_value.h"
#include "doc/doc.h"
#include "doc/mask_boundaries.h"
#include "doc/slice.h"
@ -266,6 +267,23 @@ void Editor::setStateInternal(const EditorStatePtr& newState)
{
m_brushPreview.hide();
// Some onLeaveState impls (like the ones from MovingPixelsState,
// WritingTextState, MovingSelectionState) might generate a
// Tx/Transaction::commit(), which will add a new undo state,
// triggering a sprite change scripting event
// (SpriteEvents::onAddUndoState). This event could be handled by an
// extension and that extension might want to save the current
// sprite (e.g. calling Sprite_saveCopyAs, the kind of extension
// that takes snapshots after each sprite change). That will be a
// new Context::executeCommand() for the save command, generating a
// BeforeCommandExecution signal, getting back to onLeaveState
// again. In that case, we just ignore the reentry as the first
// onLeaveState should handle everything (to avoid an stack
// overflow/infinite recursion).
if (m_leavingState)
return;
base::ScopedValue leaving(m_leavingState, true);
// Fire before change state event, set the state, and fire after
// change state event.
EditorState::LeaveAction leaveAction = m_state->onLeaveState(this, newState.get());

View File

@ -1,5 +1,5 @@
// Aseprite
// Copyright (C) 2018-2024 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
@ -458,6 +458,13 @@ private:
DocView* m_docView;
// Special flag to avoid re-entering a new state when we are leaving
// the current one. This avoids an infinite onLeaveState() recursion
// in some special cases when an extension (third-party code)
// creates a new sprite change in the same sprite change scripting
// event.
bool m_leavingState = false;
// Last known mouse position received by this editor when the
// mouse button was pressed. Used for auto-scrolling. To get the
// current mouse position on the editor you can use

View File

@ -428,8 +428,8 @@ bool MovingSliceState::onMouseMove(Editor* editor, MouseMessage* msg)
if (editor->slicesTransforms())
drawExtraCel();
// Redraw the editor.
editor->invalidate();
// Notify changes
m_site.document()->notifyGeneralUpdate();
// Use StandbyState implementation
return StandbyState::onMouseMove(editor, msg);

View File

@ -1,5 +1,5 @@
// Aseprite Document Library
// Copyright (C) 2019-2024 Igara Studio S.A.
// Copyright (C) 2019-2025 Igara Studio S.A.
// Copyright (C) 2001-2016 David Capello
//
// This file is released under the terms of the MIT license.
@ -20,11 +20,12 @@
#include "doc/primitives.h"
#include <algorithm>
#include <atomic>
#include <cmath>
namespace doc {
static int generation = 0;
static std::atomic<int> g_generation = 0;
Brush::Brush()
{
@ -300,7 +301,7 @@ void Brush::setCenter(const gfx::Point& center)
// Cleans the brush's data (image and region).
void Brush::clean()
{
m_gen = ++generation;
m_gen = ++g_generation;
m_image.reset();
m_maskBitmap.reset();
m_backupImage.reset();

View File

@ -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<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>
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<RgbTraits>(img, c);
case IMAGE_GRAYSCALE: return is_color_used_templ<GrayscaleTraits>(img, c);
}
return false;
}
bool is_empty_image(const Image* img)
{
color_t c = 0; // alpha = 0

View File

@ -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);

View File

@ -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()) {

View File

@ -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.