Compare commits

...

18 Commits

Author SHA1 Message Date
David Thomas 4168f7dd6d
Merge bd2ae1eb61 into 80fa065bd5 2025-07-26 21:34:11 +02:00
Christian Kaiser 80fa065bd5 [lua] Add sprite.undoHistory
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-25 13:58:52 -03:00
David Capello de1ccb24dd [win] Don't drop text when IME dialog composition is accepted w/Enter
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
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
With #5230, now that we can show the IME dialog on Windows, when we
are selecting a specific word/composition in the IME dialog, if we
press Enter we'll receive that Enter onKeyUp(). It's better if we
process the Enter key onKeyDown() (as the IME enter key is not
received in that case).
2025-07-25 09:19:50 -03:00
David Capello 7d91c4b9d9 [win] Fix dead keys on Windows 2025-07-24 17:45:46 -03:00
Cerallin 6d89a6bc15 fix entry
build-auto / build-auto (Debug, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (Debug, windows-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, macos-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, ubuntu-latest) (push) Has been cancelled Details
build-auto / build-auto (RelWithDebInfo, windows-latest) (push) Has been cancelled Details
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-24 12:50:32 -03:00
Cerallin d4e97b5a96 Add const method Entry::caretPosOnScreen()
This method is for Entry::setTextInput() and IME positioning.
2025-07-24 12:50:32 -03:00
Cerallin 205b18dc0f Make Entry::getCharBoxBounds() a const method 2025-07-24 12:50:32 -03:00
David Thomas bd2ae1eb61
Appease the automatons 2025-06-24 17:43:35 +01:00
David Thomas f5ecead66f
Hide zig zag checkbox when not dithering 2025-06-24 11:08:01 +01:00
David Thomas 8d0fa49044
Fix missing #include <algorithm> 2025-06-24 11:08:01 +01:00
David Thomas 2ba52d6d6c
Appease the gods of const 2025-06-24 11:08:01 +01:00
David Thomas 4e5ae492e5
Factor out dither algorithm-to-name mappings 2025-06-24 11:08:01 +01:00
David Thomas 4c5f5249d8
Fix transposed arg, add consts 2025-06-24 11:08:01 +01:00
David Thomas 484f8d69bf
Check continueTask per-line, not per-pixel 2025-06-24 11:08:01 +01:00
David Thomas 87fc82a978
Flip dither matrix when zig-zagging 2025-06-24 11:08:01 +01:00
David Thomas b43e3e6ac1
Avoid copying error buffers around
# Conflicts:
#	src/render/error_diffusion.cpp
2025-06-24 11:08:01 +01:00
David Thomas d6129359c4
Adopt clang-tidy suggestions 2025-06-24 11:08:01 +01:00
David Thomas 9e9abfab27
Add five new types of diffusion dither and expose zig zag to user
This extends the existing dithering code to add Jarvis-Judice-Ninke,
Stucki, Atkinson, Burkes and Sierra error diffused dithers. It also
exposes the zig zag (serpentine) choice to the user. Diffusion is
tweaked to use fixed point for the error values.
2025-06-24 11:08:01 +01:00
24 changed files with 561 additions and 120 deletions

View File

@ -329,6 +329,7 @@
<section id="quantization">
<option id="with_alpha" type="bool" default="true" />
<option id="dithering_algorithm" type="std::string" />
<option id="zig_zag" type="bool" default="true" />
<option id="dithering_factor" type="int" default="100" />
<option id="to_gray" type="ToGrayAlgorithm" default="ToGrayAlgorithm::DEFAULT" />
<option id="advanced" type="bool" default="false" />

View File

@ -235,8 +235,14 @@ ChangePixelFormat_Grayscale = Grayscale
ChangePixelFormat_Indexed = Indexed
ChangePixelFormat_Indexed_OrderedDithering = Indexed with Ordered Dithering
ChangePixelFormat_Indexed_OldDithering = Indexed with Old Dithering
ChangePixelFormat_Indexed_ErrorDiffusion = Indexed with Floyd-Steinberg Error Diffusion Dithering
ChangePixelFormat_Indexed_FloydSteinberg = Indexed with Floyd-Steinberg Error Diffusion Dithering
ChangePixelFormat_Indexed_JarvisJudiceNinke = Indexed with Jarvis-Judice-Ninke Error Diffusion Dithering
ChangePixelFormat_Indexed_Stucki = Indexed with Stucki Error Diffusion Dithering
ChangePixelFormat_Indexed_Atkinson = Indexed with Atkinson Error Diffusion Dithering
ChangePixelFormat_Indexed_Burkes = Indexed with Burkes Error Diffusion Dithering
ChangePixelFormat_Indexed_Sierra = Indexed with Sierra Error Diffusion Dithering
ChangePixelFormat_MoreOptions = More Options
ChangePixelFormat_ZigZag = Zig Zag
Clear = Clear
ClearCel = Clear Cel
ClearRecentFiles = Clear Recent Files
@ -516,6 +522,11 @@ no_dithering = No Dithering
old_dithering = Old Dithering +\s
ordered_dithering = Ordered Dithering +\s
floyd_steinberg = Floyd-Steinberg Error Diffusion Dithering
jarvis_judice_ninke = Jarvis Judice Ninke Error Diffusion Dithering
stucki = Stucki Error Diffusion Dithering
atkinson = Atkinson Error Diffusion Dithering
burkes = Burkes Error Diffusion Dithering
sierra = Sierra Error Diffusion Dithering
[canvas_size]
title = Canvas Size

2
laf

@ -1 +1 @@
Subproject commit a2bb9ec7fb98354279a2c49870a4a47a67a8e86e
Subproject commit 8ec4b553f1618f7a4b47cdcf4cfc2663266111ac

View File

@ -447,19 +447,13 @@ int CliProcessor::process(Context* ctx)
}
// --dithering-algorithm <algorithm>
else if (opt == &m_options.ditheringAlgorithm()) {
if (value.value() == "none")
ditheringAlgorithm = render::DitheringAlgorithm::None;
else if (value.value() == "ordered")
ditheringAlgorithm = render::DitheringAlgorithm::Ordered;
else if (value.value() == "old")
ditheringAlgorithm = render::DitheringAlgorithm::Old;
else if (value.value() == "error-diffusion")
ditheringAlgorithm = render::DitheringAlgorithm::ErrorDiffusion;
else
ditheringAlgorithm = render::DitheringAlgorithmFromString(value.value());
if (ditheringAlgorithm == render::DitheringAlgorithm::Unknown) {
throw std::runtime_error(
"--dithering-algorithm needs a valid algorithm name\n"
"Usage: --dithering-algorithm <algorithm>\n"
"Where <algorithm> can be none, ordered, old, or error-diffusion");
"Where <algorithm> can be: none, ordered, old, floyd-steinberg, jarvis-judice-ninke, stucki, atkinson, burkes or sierra.");
}
}
// --dithering-matrix <id>
else if (opt == &m_options.ditheringMatrix()) {
@ -477,15 +471,7 @@ int CliProcessor::process(Context* ctx)
}
else if (value.value() == "indexed") {
params.set("format", "indexed");
switch (ditheringAlgorithm) {
case render::DitheringAlgorithm::None: params.set("dithering", "none"); break;
case render::DitheringAlgorithm::Ordered: params.set("dithering", "ordered"); break;
case render::DitheringAlgorithm::Old: params.set("dithering", "old"); break;
case render::DitheringAlgorithm::ErrorDiffusion:
params.set("dithering", "error-diffusion");
break;
}
params.set("dithering", DitheringAlgorithmToString(ditheringAlgorithm).c_str());
if (ditheringAlgorithm != render::DitheringAlgorithm::None &&
!ditheringMatrix.empty()) {
params.set("dithering-matrix", ditheringMatrix.c_str());

View File

@ -41,6 +41,7 @@
#include "render/quantization.h"
#include "render/render.h"
#include "render/task_delegate.h"
#include "ui/button.h"
#include "ui/listitem.h"
#include "ui/paint_event.h"
#include "ui/size_hint_event.h"
@ -177,6 +178,7 @@ public:
, m_imageBuffer(new doc::ImageBuffer)
, m_selectedItem(nullptr)
, m_ditheringSelector(nullptr)
, m_zigZagCheck(nullptr)
, m_mapAlgorithmSelector(nullptr)
, m_bestFitCriteriaSelector(nullptr)
, m_imageJustCreated(true)
@ -204,6 +206,8 @@ public:
m_ditheringSelector = new DitheringSelector(DitheringSelector::SelectBoth);
m_ditheringSelector->setExpansive(true);
m_zigZagCheck = new CheckBox(Strings::commands_ChangePixelFormat_ZigZag());
m_mapAlgorithmSelector = new RgbMapAlgorithmSelector;
m_mapAlgorithmSelector->setExpansive(true);
@ -217,6 +221,9 @@ public:
m_ditheringSelector->setSelectedItemIndex(index);
}
// Select default zig zag
m_zigZagCheck->setSelected(pref.quantization.zigZag());
// Select default RgbMap algorithm
m_mapAlgorithmSelector->algorithm(pref.quantization.rgbmapAlgorithm());
@ -224,6 +231,7 @@ public:
m_bestFitCriteriaSelector->criteria(pref.quantization.fitCriteria());
ditheringPlaceholder()->addChild(m_ditheringSelector);
ditheringPlaceholder()->addChild(m_zigZagCheck);
rgbmapAlgorithmPlaceholder()->addChild(m_mapAlgorithmSelector);
bestFitCriteriaPlaceholder()->addChild(m_bestFitCriteriaSelector);
@ -233,6 +241,7 @@ public:
// Signals
m_ditheringSelector->Change.connect([this] { onIndexParamChange(); });
m_zigZagCheck->Click.connect([this] { onIndexParamChange(); });
m_mapAlgorithmSelector->Change.connect([this] { onIndexParamChange(); });
m_bestFitCriteriaSelector->Change.connect([this] { onIndexParamChange(); });
factor()->Change.connect([this] { onIndexParamChange(); });
@ -281,6 +290,7 @@ public:
if (m_ditheringSelector) {
d.algorithm(m_ditheringSelector->ditheringAlgorithm());
d.matrix(m_ditheringSelector->ditheringMatrix());
d.zigzag(m_zigZagCheck->isSelected());
}
d.factor(double(factor()->getValue()) / 100.0);
return d;
@ -317,16 +327,16 @@ public:
{
auto& pref = Preferences::instance();
// Save the dithering method used for the future
// Save the dithering method and zig-zag setting used for the future
if (m_ditheringSelector) {
if (auto item = m_ditheringSelector->getSelectedItem()) {
pref.quantization.ditheringAlgorithm(item->text());
if (m_ditheringSelector->ditheringAlgorithm() ==
render::DitheringAlgorithm::ErrorDiffusion) {
if (DitheringAlgorithmIsDiffusion(m_ditheringSelector->ditheringAlgorithm()))
pref.quantization.ditheringFactor(factor()->getValue());
}
}
pref.quantization.zigZag(m_zigZagCheck->isEnabled());
}
if (m_mapAlgorithmSelector || m_bestFitCriteriaSelector)
@ -364,9 +374,10 @@ private:
if (m_ditheringSelector) {
const bool toIndexed = (dstColorMode == doc::ColorMode::INDEXED);
m_ditheringSelector->setVisible(toIndexed);
m_zigZagCheck->setVisible(toIndexed);
const bool errorDiff = (m_ditheringSelector->ditheringAlgorithm() ==
render::DitheringAlgorithm::ErrorDiffusion);
const bool errorDiff =
(render::DitheringAlgorithmIsDiffusion(m_ditheringSelector->ditheringAlgorithm()));
amount()->setVisible(toIndexed && errorDiff);
}
@ -460,6 +471,7 @@ private:
std::unique_ptr<ConvertThread> m_bgThread;
ConversionItem* m_selectedItem;
DitheringSelector* m_ditheringSelector;
CheckBox* m_zigZagCheck;
RgbMapAlgorithmSelector* m_mapAlgorithmSelector;
BestFitCriteriaSelector* m_bestFitCriteriaSelector;
bool m_imageJustCreated;
@ -487,6 +499,11 @@ struct ChangePixelFormatParams : public NewParams {
1.0,
{ "ditheringFactor", "dithering-factor" }
};
Param<bool> zigZag{
this,
true,
{ "zigZag", "zig-zag" }
};
Param<doc::RgbMapAlgorithm> rgbmap{ this, RgbMapAlgorithm::DEFAULT, "rgbmap" };
Param<gen::ToGrayAlgorithm> toGray{ this, gen::ToGrayAlgorithm::DEFAULT, "toGray" };
Param<doc::FitCriteria> fitCriteria{ this, doc::FitCriteria::DEFAULT, "fitCriteria" };
@ -568,10 +585,13 @@ void ChangePixelFormatCommand::onExecute(Context* ctx)
if (window.closer() != window.ok())
return;
const auto d = window.dithering();
params().colorMode(window.selectedColorMode());
params().dithering(window.dithering().algorithm());
matrix = window.dithering().matrix();
params().factor(window.dithering().factor());
params().dithering(d.algorithm());
matrix = d.matrix();
params().factor(d.factor());
params().zigZag(d.zigzag());
params().rgbmap(window.rgbMapAlgorithm());
params().fitCriteria(window.fitCriteria());
params().toGray(window.toGray());
@ -615,13 +635,14 @@ void ChangePixelFormatCommand::onExecute(Context* ctx)
}
job.startJobWithCallback([this, &job, sprite, &matrix](Tx& tx) {
tx(new cmd::SetPixelFormat(sprite,
(PixelFormat)params().colorMode(),
render::Dithering(params().dithering(), matrix, params().factor()),
params().rgbmap(),
get_gray_func(params().toGray()),
&job, // SpriteJob is a render::TaskDelegate
params().fitCriteria()));
tx(new cmd::SetPixelFormat(
sprite,
(PixelFormat)params().colorMode(),
render::Dithering(params().dithering(), matrix, params().zigZag(), params().factor()),
params().rgbmap(),
get_gray_func(params().toGray()),
&job, // SpriteJob is a render::TaskDelegate
params().fitCriteria()));
});
job.waitJob();
}
@ -651,8 +672,23 @@ std::string ChangePixelFormatCommand::onGetFriendlyName() const
case render::DitheringAlgorithm::Old:
conversion = Strings::commands_ChangePixelFormat_Indexed_OldDithering();
break;
case render::DitheringAlgorithm::ErrorDiffusion:
conversion = Strings::commands_ChangePixelFormat_Indexed_ErrorDiffusion();
case render::DitheringAlgorithm::FloydSteinberg:
conversion = Strings::commands_ChangePixelFormat_Indexed_FloydSteinberg();
break;
case render::DitheringAlgorithm::JarvisJudiceNinke:
conversion = Strings::commands_ChangePixelFormat_Indexed_JarvisJudiceNinke();
break;
case render::DitheringAlgorithm::Stucki:
conversion = Strings::commands_ChangePixelFormat_Indexed_Stucki();
break;
case render::DitheringAlgorithm::Atkinson:
conversion = Strings::commands_ChangePixelFormat_Indexed_Atkinson();
break;
case render::DitheringAlgorithm::Burkes:
conversion = Strings::commands_ChangePixelFormat_Indexed_Burkes();
break;
case render::DitheringAlgorithm::Sierra:
conversion = Strings::commands_ChangePixelFormat_Indexed_Sierra();
break;
}
break;

View File

@ -275,14 +275,10 @@ void Param<doc::FitCriteria>::fromString(const std::string& value)
template<>
void Param<render::DitheringAlgorithm>::fromString(const std::string& value)
{
if (base::utf8_icmp(value, "ordered") == 0)
setValue(render::DitheringAlgorithm::Ordered);
else if (base::utf8_icmp(value, "old") == 0)
setValue(render::DitheringAlgorithm::Old);
else if (base::utf8_icmp(value, "error-diffusion") == 0)
setValue(render::DitheringAlgorithm::ErrorDiffusion);
else
setValue(render::DitheringAlgorithm::None);
auto algo = render::DitheringAlgorithmFromString(value);
if (algo == render::DitheringAlgorithm::Unknown)
algo = render::DitheringAlgorithm::None;
setValue(algo);
}
template<>

View File

@ -13,6 +13,8 @@
#include "app/console.h"
#include "app/context.h"
#include "app/context_observer.h"
#include "app/doc.h"
#include "app/doc_undo.h"
#include "app/script/docobj.h"
#include "app/script/engine.h"
#include "app/script/luacpp.h"

View File

@ -64,6 +64,7 @@
#include "doc/tag.h"
#include "doc/tileset.h"
#include "doc/tilesets.h"
#include "undo/undo_state.h"
#include <algorithm>
@ -1029,6 +1030,42 @@ int Sprite_set_useLayerUuids(lua_State* L)
return 0;
}
int Sprite_get_undoHistory(lua_State* L)
{
const auto* sprite = get_docobj<Sprite>(L, 1);
const auto* doc = static_cast<Doc*>(sprite->document());
const auto* history = doc->undoHistory();
if (!history) {
lua_pushnil(L);
return 1;
}
const undo::UndoState* currentState = history->currentState();
const undo::UndoState* s = history->firstState();
const bool canRedo = history->canRedo();
bool pastCurrent = !currentState && canRedo;
int undoSteps = 0;
int redoSteps = 0;
while (s) {
if (pastCurrent && canRedo)
redoSteps++;
else if (currentState || !canRedo)
undoSteps++;
if (s == currentState || !currentState)
pastCurrent = true;
s = s->next();
}
lua_newtable(L);
setfield_integer(L, "undoSteps", undoSteps);
setfield_integer(L, "redoSteps", redoSteps);
return 1;
}
const luaL_Reg Sprite_methods[] = {
{ "__eq", Sprite_eq },
{ "resize", Sprite_resize },
@ -1094,6 +1131,7 @@ const Property Sprite_properties[] = {
{ "events", Sprite_get_events, nullptr },
{ "tileManagementPlugin", Sprite_get_tileManagementPlugin, Sprite_set_tileManagementPlugin },
{ "useLayerUuids", Sprite_get_useLayerUuids, Sprite_set_useLayerUuids },
{ "undoHistory", Sprite_get_undoHistory, nullptr },
{ nullptr, nullptr, nullptr }
};

View File

@ -2577,8 +2577,26 @@ render::DitheringAlgorithmBase* ContextBar::ditheringAlgorithm()
case render::DitheringAlgorithm::None: s_dither.reset(nullptr); break;
case render::DitheringAlgorithm::Ordered: s_dither.reset(new render::OrderedDither2(-1)); break;
case render::DitheringAlgorithm::Old: s_dither.reset(new render::OrderedDither(-1)); break;
case render::DitheringAlgorithm::ErrorDiffusion:
s_dither.reset(new render::ErrorDiffusionDither(-1));
case render::DitheringAlgorithm::FloydSteinberg:
s_dither.reset(
new render::ErrorDiffusionDither(render::ErrorDiffusionType::FloydSteinberg, -1));
break;
case render::DitheringAlgorithm::JarvisJudiceNinke:
s_dither.reset(
new render::ErrorDiffusionDither(render::ErrorDiffusionType::JarvisJudiceNinke, -1));
break;
case render::DitheringAlgorithm::Stucki:
s_dither.reset(new render::ErrorDiffusionDither(render::ErrorDiffusionType::Stucki, -1));
break;
case render::DitheringAlgorithm::Atkinson:
s_dither.reset(new render::ErrorDiffusionDither(render::ErrorDiffusionType::Atkinson, -1));
break;
case render::DitheringAlgorithm::Burkes:
s_dither.reset(new render::ErrorDiffusionDither(render::ErrorDiffusionType::Burkes, -1));
break;
case render::DitheringAlgorithm::Sierra:
s_dither.reset(new render::ErrorDiffusionDither(render::ErrorDiffusionType::Sierra, -1));
break;
break;
}

View File

@ -231,9 +231,24 @@ void DitheringSelector::regenerate(int selectedItemIndex)
Console::showException(e);
}
}
addItem(new DitherItem(render::DitheringAlgorithm::ErrorDiffusion,
addItem(new DitherItem(render::DitheringAlgorithm::FloydSteinberg,
render::DitheringMatrix(),
Strings::dithering_selector_floyd_steinberg()));
addItem(new DitherItem(render::DitheringAlgorithm::JarvisJudiceNinke,
render::DitheringMatrix(),
Strings::dithering_selector_jarvis_judice_ninke()));
addItem(new DitherItem(render::DitheringAlgorithm::Stucki,
render::DitheringMatrix(),
Strings::dithering_selector_stucki()));
addItem(new DitherItem(render::DitheringAlgorithm::Atkinson,
render::DitheringMatrix(),
Strings::dithering_selector_atkinson()));
addItem(new DitherItem(render::DitheringAlgorithm::Burkes,
render::DitheringMatrix(),
Strings::dithering_selector_burkes()));
addItem(new DitherItem(render::DitheringAlgorithm::Sierra,
render::DitheringMatrix(),
Strings::dithering_selector_sierra()));
break;
case SelectMatrix:
addItem(

View File

@ -443,12 +443,7 @@ bool WritingTextState::onSetCursor(Editor* editor, const gfx::Point& mouseScreen
return true;
}
bool WritingTextState::onKeyDown(Editor*, KeyMessage*)
{
return false;
}
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
bool WritingTextState::onKeyDown(Editor*, KeyMessage* msg)
{
// Cancel loop pressing Esc key
if (msg->scancode() == ui::kKeyEsc) {
@ -457,7 +452,17 @@ bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
// Drop text pressing Enter key
else if (msg->scancode() == ui::kKeyEnter) {
drop();
return true;
}
return false;
}
bool WritingTextState::onKeyUp(Editor*, KeyMessage* msg)
{
// Note: We cannot process kKeyEnter key here to drop the text as it
// could be received after the Enter key is pressed in the IME
// dialog to accept the composition (not to accept the text). So we
// process kKeyEnter in onKeyDown().
return true;
}

View File

@ -3,6 +3,7 @@
# Copyright (C) 2001-2018 David Capello
add_library(render-lib
dithering_algorithm.cpp
error_diffusion.cpp
get_sprite_pixel.cpp
gradient.cpp

View File

@ -18,24 +18,29 @@ class Dithering {
public:
Dithering(DitheringAlgorithm algorithm = DitheringAlgorithm::None,
const DitheringMatrix& matrix = DitheringMatrix(),
bool zigzag = true,
double factor = 1.0)
: m_algorithm(algorithm)
, m_matrix(matrix)
, m_zigzag(zigzag)
, m_factor(factor)
{
}
DitheringAlgorithm algorithm() const { return m_algorithm; }
DitheringMatrix matrix() const { return m_matrix; }
bool zigzag() const { return m_zigzag; }
double factor() const { return m_factor; }
void algorithm(const DitheringAlgorithm algorithm) { m_algorithm = algorithm; }
void matrix(const DitheringMatrix& matrix) { m_matrix = matrix; }
void zigzag(bool zigzag) { m_zigzag = zigzag; }
void factor(const double factor) { m_factor = factor; }
private:
DitheringAlgorithm m_algorithm;
DitheringMatrix m_matrix;
bool m_zigzag;
double m_factor;
};

View File

@ -0,0 +1,53 @@
// Aseprite
// Copyright (C) 2025 Igara Studio S.A.
//
// This program is distributed under the terms of
// the End-User License Agreement for Aseprite.
#include <algorithm>
#include <string>
#include <unordered_map>
#include "base/string.h"
#include "render/dithering_algorithm.h"
namespace render {
static const std::unordered_map<DitheringAlgorithm, std::string> names = {
{ DitheringAlgorithm::None, "none" },
{ DitheringAlgorithm::Ordered, "ordered" },
{ DitheringAlgorithm::Old, "old" },
{ DitheringAlgorithm::FloydSteinberg, "floyd-steinberg" },
{ DitheringAlgorithm::FloydSteinberg, "error-diffusion" }, // alias
{ DitheringAlgorithm::JarvisJudiceNinke, "jarvis-judice-ninke" },
{ DitheringAlgorithm::Stucki, "stucki" },
{ DitheringAlgorithm::Atkinson, "atkinson" },
{ DitheringAlgorithm::Burkes, "burkes" },
{ DitheringAlgorithm::Sierra, "sierra" },
};
bool DitheringAlgorithmIsDiffusion(DitheringAlgorithm algo)
{
switch (algo) {
case DitheringAlgorithm::None:
case DitheringAlgorithm::Ordered:
case DitheringAlgorithm::Old: return false;
default: return true;
}
}
std::string DitheringAlgorithmToString(DitheringAlgorithm algo)
{
auto it = names.find(algo);
return (it != names.end()) ? it->second : "unknown";
}
DitheringAlgorithm DitheringAlgorithmFromString(const std::string& name)
{
auto it = std::find_if(names.begin(), names.end(), [name](const auto& pair) {
return base::utf8_icmp(pair.second, name) == 0;
});
return (it != names.end()) ? it->first : DitheringAlgorithm::Unknown;
}
} // namespace render

View File

@ -9,6 +9,8 @@
#define RENDER_DITHERING_METHOD_H_INCLUDED
#pragma once
#include <string>
namespace render {
// Dithering algorithms
@ -16,9 +18,19 @@ enum class DitheringAlgorithm {
None,
Ordered,
Old,
ErrorDiffusion,
FloydSteinberg,
JarvisJudiceNinke,
Stucki,
Atkinson,
Burkes,
Sierra,
Unknown
};
bool DitheringAlgorithmIsDiffusion(DitheringAlgorithm algo);
std::string DitheringAlgorithmToString(DitheringAlgorithm algo);
DitheringAlgorithm DitheringAlgorithmFromString(const std::string& name);
} // namespace render
#endif

View File

@ -15,23 +15,139 @@
#include "gfx/rgb.h"
#include <algorithm>
#include <vector>
namespace render {
ErrorDiffusionDither::ErrorDiffusionDither(int transparentIndex)
// Predefined error diffusion algorithms
class ErrorDiffusionMatrices {
public:
static const ErrorDiffusionMatrix& getFloydSteinberg()
{
static const ErrorDiffusionMatrix matrix(3,
2,
1,
0,
{
{ 0, 0, 7 },
{ 3, 5, 1 }
},
16);
return matrix;
}
static const ErrorDiffusionMatrix& getJarvisJudiceNinke()
{
static const ErrorDiffusionMatrix matrix(5,
3,
2,
0,
{
{ 0, 0, 0, 7, 5 },
{ 3, 5, 7, 5, 3 },
{ 1, 3, 5, 3, 1 }
},
48);
return matrix;
}
static const ErrorDiffusionMatrix& getStucki()
{
static const ErrorDiffusionMatrix matrix(5,
3,
2,
0,
{
{ 0, 0, 0, 8, 4 },
{ 2, 4, 8, 4, 2 },
{ 1, 2, 4, 2, 1 }
},
42);
return matrix;
}
static const ErrorDiffusionMatrix& getAtkinson()
{
static const ErrorDiffusionMatrix matrix(4,
3,
1,
0,
{
{ 0, 0, 1, 1 },
{ 1, 1, 1, 0 },
{ 0, 1, 0, 0 }
},
8);
return matrix;
}
static const ErrorDiffusionMatrix& getBurkes()
{
static const ErrorDiffusionMatrix matrix(5,
2,
2,
0,
{
{ 0, 0, 0, 8, 4 },
{ 2, 4, 8, 4, 2 }
},
32);
return matrix;
}
static const ErrorDiffusionMatrix& getSierra()
{
static const ErrorDiffusionMatrix matrix(5,
3,
2,
0,
{
{ 0, 0, 0, 5, 3 },
{ 2, 4, 5, 4, 2 },
{ 0, 2, 3, 2, 0 }
},
32);
return matrix;
}
};
ErrorDiffusionDither::ErrorDiffusionDither(ErrorDiffusionType type, int transparentIndex)
: m_transparentIndex(transparentIndex)
, m_diffusionType(type)
{
}
const ErrorDiffusionMatrix& ErrorDiffusionDither::getCurrentMatrix() const
{
switch (m_diffusionType) {
case ErrorDiffusionType::JarvisJudiceNinke:
return ErrorDiffusionMatrices::getJarvisJudiceNinke();
case ErrorDiffusionType::Stucki: return ErrorDiffusionMatrices::getStucki();
case ErrorDiffusionType::Atkinson: return ErrorDiffusionMatrices::getAtkinson();
case ErrorDiffusionType::Burkes: return ErrorDiffusionMatrices::getBurkes();
case ErrorDiffusionType::Sierra: return ErrorDiffusionMatrices::getSierra();
case ErrorDiffusionType::FloydSteinberg: return ErrorDiffusionMatrices::getFloydSteinberg();
}
}
void ErrorDiffusionDither::start(const doc::Image* srcImage,
doc::Image* dstImage,
const double factor)
{
m_srcImage = srcImage;
m_width = 2 + srcImage->width();
// Get the current matrix to determine buffer size needed
const ErrorDiffusionMatrix& matrix = getCurrentMatrix();
const int bufferRows = matrix.height;
// Resize error buffers to accommodate the matrix height
const std::vector<int>::size_type bufferSize = m_width * bufferRows;
for (int i = 0; i < kChannels; ++i)
m_err[i].resize(m_width * 2, 0);
m_err[i].resize(bufferSize, 0);
m_lastY = -1;
m_currentRowOffset = 0;
m_factor = int(factor * 100.0);
}
@ -42,30 +158,39 @@ void ErrorDiffusionDither::finish()
doc::color_t ErrorDiffusionDither::ditherRgbToIndex2D(const int x,
const int y,
const doc::RgbMap* rgbmap,
const doc::Palette* palette)
const doc::Palette* palette,
const int direction)
{
const ErrorDiffusionMatrix& matrix = getCurrentMatrix();
if (y != m_lastY) {
for (int i = 0; i < kChannels; ++i) {
int* row0 = &m_err[i][0];
int* row1 = row0 + m_width;
int* end1 = row1 + m_width;
std::copy(row1, end1, row0);
std::fill(row1, end1, 0);
// Instead of shifting all rows, just advance the circular buffer
// and clear the row that will be reused
m_currentRowOffset = (m_currentRowOffset + 1) % matrix.height;
// Clear only the row that will be used as the "last" row
const int clearRowIndex = (m_currentRowOffset + matrix.height - 1) % matrix.height;
for (int c = 0; c < kChannels; ++c) {
int* rowToClear = &m_err[c][m_width * clearRowIndex];
std::fill(rowToClear, rowToClear + m_width, 0);
}
m_lastY = y;
}
doc::color_t color = doc::get_pixel_fast<doc::RgbTraits>(m_srcImage, x, y);
// Get RGB values + quatization error
// Get RGB values + quantization error
int v[kChannels] = { doc::rgba_getr(color),
doc::rgba_getg(color),
doc::rgba_getb(color),
doc::rgba_geta(color) };
for (int i = 0; i < kChannels; ++i) {
v[i] += m_err[i][x + 1];
v[i] = std::clamp(v[i], 0, 255);
}
// Add accumulated error (16-bit fixed point) and convert to 0..255
for (int c = 0; c < kChannels; ++c)
v[c] = std::clamp(((v[c] << 16) + m_err[c][m_width * m_currentRowOffset + x + 1] + 32767) >> 16,
0,
255);
const doc::color_t index = (rgbmap ?
rgbmap->mapColor(v[0], v[1], v[2], v[3]) :
@ -82,26 +207,36 @@ doc::color_t ErrorDiffusionDither::ditherRgbToIndex2D(const int x,
v[2] - doc::rgba_getb(palColor),
v[3] - doc::rgba_geta(palColor) };
// TODO using Floyd-Steinberg matrix here but it should be configurable
for (int i = 0; i < kChannels; ++i) {
int* err = &m_err[i][x];
const int q = quantError[i] * m_factor / 100;
const int a = q * 7 / 16;
const int b = q * 3 / 16;
const int c = q * 5 / 16;
const int d = q * 1 / 16;
const int srcWidth = m_srcImage->width();
if (y & 1) {
err[0] += a;
err[m_width + 2] += b;
err[m_width + 1] += c;
err[m_width] += d;
}
else {
err[+2] += a;
err[m_width] += b;
err[m_width + 1] += c;
err[m_width + 2] += d;
// Distribute error using the configurable matrix
for (int c = 0; c < kChannels; ++c) {
const int qerr = quantError[c] * m_factor / 100;
for (int my = 0; my < matrix.height; ++my) {
// Use circular buffer indexing
const int bufferRow = (m_currentRowOffset + my) % matrix.height;
const int bufferRowIndex = bufferRow * m_width;
for (int mx = 0; mx < matrix.width; ++mx) {
const int coeff = direction > 0 ? matrix.coefficients[my][mx] :
matrix.coefficients[my][matrix.width - 1 - mx];
if (coeff == 0)
continue;
const int errorPixelX = x + mx - matrix.centerX;
const int errorPixelY = y + my - matrix.centerY;
// Check bounds
if (errorPixelX < 0 || errorPixelX >= srcWidth)
continue;
if (errorPixelY < y)
continue; // Don't go backwards
// Calculate error as 16-bit fixed point
const int errorValue = ((qerr * coeff) << 16) / matrix.divisor;
m_err[c][bufferRowIndex + errorPixelX + 1] += errorValue;
}
}
}

View File

@ -16,22 +16,58 @@
namespace render {
enum class ErrorDiffusionType : uint8_t {
FloydSteinberg,
JarvisJudiceNinke,
Stucki,
Atkinson,
Burkes,
Sierra
};
// Error diffusion matrix structure
struct ErrorDiffusionMatrix {
int width, height;
int centerX, centerY;
std::vector<std::vector<int>> coefficients;
int divisor;
ErrorDiffusionMatrix(int w,
int h,
int cx,
int cy,
const std::vector<std::vector<int>>& coeff,
int div)
: width(w)
, height(h)
, centerX(cx)
, centerY(cy)
, coefficients(coeff)
, divisor(div)
{
}
};
class ErrorDiffusionDither : public DitheringAlgorithmBase {
public:
ErrorDiffusionDither(int transparentIndex = -1);
ErrorDiffusionDither(ErrorDiffusionType type, int transparentIndex);
int dimensions() const override { return 2; }
bool zigZag() const override { return true; }
void start(const doc::Image* srcImage, doc::Image* dstImage, const double factor) override;
void finish() override;
doc::color_t ditherRgbToIndex2D(const int x,
const int y,
const doc::RgbMap* rgbmap,
const doc::Palette* palette) override;
const doc::Palette* palette,
const int direction) override;
private:
const ErrorDiffusionMatrix& getCurrentMatrix() const;
int m_transparentIndex;
ErrorDiffusionType m_diffusionType;
const doc::Image* m_srcImage;
int m_width, m_lastY;
int m_currentRowOffset;
static const int kChannels = 4;
std::vector<int> m_err[kChannels];
int m_factor;

View File

@ -240,47 +240,38 @@ void dither_rgb_image_to_indexed(DitheringAlgorithmBase& algorithm,
ASSERT(srcIt != srcBits.end());
ASSERT(dstIt != dstBits.end());
*dstIt = algorithm.ditherRgbPixelToIndex(dithering.matrix(), *srcIt, x, y, rgbmap, palette);
if (delegate) {
if (!delegate->continueTask())
return;
}
}
if (delegate) {
if (!delegate->continueTask())
return;
delegate->notifyTaskProgress(double(y + 1) / double(h));
}
}
}
else {
auto dstIt = doc::get_pixel_address_fast<doc::IndexedTraits>(dstImage, 0, 0);
const bool zigZag = algorithm.zigZag();
auto zigZag = dithering.zigzag();
for (int y = 0; y < h; ++y) {
if (zigZag && (y & 1)) { // Odd row: go from right-to-left
dstIt += w - 1;
for (int x = w - 1; x >= 0; --x, --dstIt) {
ASSERT(dstIt == doc::get_pixel_address_fast<doc::IndexedTraits>(dstImage, x, y));
*dstIt = algorithm.ditherRgbToIndex2D(x, y, rgbmap, palette);
if (delegate) {
if (!delegate->continueTask())
return;
}
*dstIt = algorithm.ditherRgbToIndex2D(x, y, rgbmap, palette, -1);
}
dstIt += w + 1;
}
else { // Even row: go fromo left-to-right
else { // Even row: go from left-to-right
for (int x = 0; x < w; ++x, ++dstIt) {
ASSERT(dstIt == doc::get_pixel_address_fast<doc::IndexedTraits>(dstImage, x, y));
*dstIt = algorithm.ditherRgbToIndex2D(x, y, rgbmap, palette);
if (delegate) {
if (!delegate->continueTask())
return;
}
*dstIt = algorithm.ditherRgbToIndex2D(x, y, rgbmap, palette, +1);
}
}
if (delegate) {
if (!delegate->continueTask())
return;
delegate->notifyTaskProgress(double(y + 1) / double(h));
}
}

View File

@ -27,7 +27,6 @@ public:
virtual ~DitheringAlgorithmBase() {}
virtual int dimensions() const { return 1; }
virtual bool zigZag() const { return false; }
virtual void start(const doc::Image* srcImage, doc::Image* dstImage, const double factor) {}
@ -46,7 +45,8 @@ public:
virtual doc::color_t ditherRgbToIndex2D(const int x,
const int y,
const doc::RgbMap* rgbmap,
const doc::Palette* palette)
const doc::Palette* palette,
const int direction)
{
return 0;
}

View File

@ -159,8 +159,29 @@ Image* convert_pixel_format(const Image* image,
case DitheringAlgorithm::Old:
dither.reset(new OrderedDither(is_background ? -1 : new_mask_color));
break;
case DitheringAlgorithm::ErrorDiffusion:
dither.reset(new ErrorDiffusionDither(is_background ? -1 : new_mask_color));
case DitheringAlgorithm::FloydSteinberg:
dither.reset(new ErrorDiffusionDither(ErrorDiffusionType::FloydSteinberg,
is_background ? -1 : new_mask_color));
break;
case DitheringAlgorithm::JarvisJudiceNinke:
dither.reset(new ErrorDiffusionDither(ErrorDiffusionType::JarvisJudiceNinke,
is_background ? -1 : new_mask_color));
break;
case DitheringAlgorithm::Stucki:
dither.reset(new ErrorDiffusionDither(ErrorDiffusionType::Stucki,
is_background ? -1 : new_mask_color));
break;
case DitheringAlgorithm::Atkinson:
dither.reset(new ErrorDiffusionDither(ErrorDiffusionType::Atkinson,
is_background ? -1 : new_mask_color));
break;
case DitheringAlgorithm::Burkes:
dither.reset(new ErrorDiffusionDither(ErrorDiffusionType::Burkes,
is_background ? -1 : new_mask_color));
break;
case DitheringAlgorithm::Sierra:
dither.reset(new ErrorDiffusionDither(ErrorDiffusionType::Sierra,
is_background ? -1 : new_mask_color));
break;
}
if (dither)

View File

@ -1,4 +1,4 @@
// Aseprite Rener Library
// Aseprite Render Library
// Copyright (c) 2019-2021 Igara Studio S.A.
// Copyright (c) 2001-2017 David Capello
//

View File

@ -128,6 +128,15 @@ int Entry::lastCaretPos() const
return int(m_boxes.size() - 1);
}
gfx::Point Entry::caretPosOnScreen() const
{
const gfx::Point caretPos = getCharBoxBounds(m_caret).point2();
const os::Window* nativeWindow = display()->nativeWindow();
const gfx::Point pos = nativeWindow->pointToScreen(caretPos + bounds().origin());
return pos;
}
void Entry::setCaretPos(const int pos)
{
gfx::Size caretSize = theme()->getEntryCaretSize(this);
@ -160,6 +169,8 @@ void Entry::setCaretPos(const int pos)
startTimer();
m_state = true;
os::System::instance()->setTextInput(true, caretPosOnScreen());
invalidate();
}
@ -251,7 +262,7 @@ gfx::Rect Entry::getEntryTextBounds() const
return onGetEntryTextBounds();
}
gfx::Rect Entry::getCharBoxBounds(const int i)
gfx::Rect Entry::getCharBoxBounds(const int i) const
{
ASSERT(i >= 0 && i < int(m_boxes.size()));
if (i >= 0 && i < int(m_boxes.size()))
@ -288,8 +299,9 @@ bool Entry::onProcessMessage(Message* msg)
}
// Start processing dead keys
if (m_translate_dead_keys)
os::System::instance()->setTextInput(true);
if (m_translate_dead_keys) {
os::System::instance()->setTextInput(true, caretPosOnScreen());
}
break;
case kFocusLeaveMessage:

View File

@ -43,6 +43,7 @@ public:
int caretPos() const { return m_caret; }
int lastCaretPos() const;
gfx::Point caretPosOnScreen() const;
void setCaretPos(int pos);
void setCaretToEnd();
@ -76,7 +77,7 @@ public:
obs::signal<void()> Change;
protected:
gfx::Rect getCharBoxBounds(int i);
gfx::Rect getCharBoxBounds(int i) const;
// Events
bool onProcessMessage(Message* msg) override;

View File

@ -228,3 +228,69 @@ do
c = app.open(fn)
assert(c.tileManagementPlugin == nil)
end
-- Undo History
function test_undo_history()
local sprite = Sprite(1, 1)
assert(sprite.undoHistory.undoSteps == 0)
assert(sprite.undoHistory.redoSteps == 0)
sprite:resize(10, 10)
assert(sprite.undoHistory.undoSteps == 1)
assert(sprite.undoHistory.redoSteps == 0)
sprite:resize(10, 15)
assert(sprite.undoHistory.undoSteps == 2)
assert(sprite.undoHistory.redoSteps == 0)
sprite:resize(10, 30)
assert(sprite.undoHistory.undoSteps == 3)
assert(sprite.undoHistory.redoSteps == 0)
app.undo()
assert(sprite.undoHistory.undoSteps == 2)
assert(sprite.undoHistory.redoSteps == 1)
app.undo()
assert(sprite.undoHistory.undoSteps == 1)
assert(sprite.undoHistory.redoSteps == 2)
app.redo()
assert(sprite.undoHistory.undoSteps == 2)
assert(sprite.undoHistory.redoSteps == 1)
app.undo()
app.undo()
assert(sprite.undoHistory.undoSteps == 0)
assert(sprite.undoHistory.redoSteps == 3)
sprite:resize(10, 30)
if (app.preferences.undo.allow_nonlinear_history) then
assert(sprite.undoHistory.undoSteps == 4)
assert(sprite.undoHistory.redoSteps == 0)
else
assert(sprite.undoHistory.undoSteps == 1)
assert(sprite.undoHistory.redoSteps == 0)
end
end
do
local prevSetting = app.preferences.undo.allow_nonlinear_history
app.preferences.undo.allow_nonlinear_history = true
test_undo_history()
app.preferences.undo.allow_nonlinear_history = prevSetting
end
do
local prevSetting = app.preferences.undo.allow_nonlinear_history
app.preferences.undo.allow_nonlinear_history = false
test_undo_history()
app.preferences.undo.allow_nonlinear_history = prevSetting
end