Compare commits

...

13 Commits

Author SHA1 Message Date
David Thomas e273a35f04
Merge bd2ae1eb61 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
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
19 changed files with 452 additions and 110 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

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

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

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

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