This commit is contained in:
David Thomas 2025-07-26 21:34:11 +02:00 committed by GitHub
commit 4168f7dd6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 426 additions and 109 deletions

View File

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

View File

@ -235,8 +235,14 @@ ChangePixelFormat_Grayscale = Grayscale
ChangePixelFormat_Indexed = Indexed ChangePixelFormat_Indexed = Indexed
ChangePixelFormat_Indexed_OrderedDithering = Indexed with Ordered Dithering ChangePixelFormat_Indexed_OrderedDithering = Indexed with Ordered Dithering
ChangePixelFormat_Indexed_OldDithering = Indexed with Old 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_MoreOptions = More Options
ChangePixelFormat_ZigZag = Zig Zag
Clear = Clear Clear = Clear
ClearCel = Clear Cel ClearCel = Clear Cel
ClearRecentFiles = Clear Recent Files ClearRecentFiles = Clear Recent Files
@ -516,6 +522,11 @@ no_dithering = No Dithering
old_dithering = Old Dithering +\s old_dithering = Old Dithering +\s
ordered_dithering = Ordered Dithering +\s ordered_dithering = Ordered Dithering +\s
floyd_steinberg = Floyd-Steinberg Error Diffusion Dithering 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] [canvas_size]
title = Canvas Size title = Canvas Size

View File

@ -447,19 +447,13 @@ int CliProcessor::process(Context* ctx)
} }
// --dithering-algorithm <algorithm> // --dithering-algorithm <algorithm>
else if (opt == &m_options.ditheringAlgorithm()) { else if (opt == &m_options.ditheringAlgorithm()) {
if (value.value() == "none") ditheringAlgorithm = render::DitheringAlgorithmFromString(value.value());
ditheringAlgorithm = render::DitheringAlgorithm::None; if (ditheringAlgorithm == render::DitheringAlgorithm::Unknown) {
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
throw std::runtime_error( throw std::runtime_error(
"--dithering-algorithm needs a valid algorithm name\n" "--dithering-algorithm needs a valid algorithm name\n"
"Usage: --dithering-algorithm <algorithm>\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> // --dithering-matrix <id>
else if (opt == &m_options.ditheringMatrix()) { else if (opt == &m_options.ditheringMatrix()) {
@ -477,15 +471,7 @@ int CliProcessor::process(Context* ctx)
} }
else if (value.value() == "indexed") { else if (value.value() == "indexed") {
params.set("format", "indexed"); params.set("format", "indexed");
switch (ditheringAlgorithm) { params.set("dithering", DitheringAlgorithmToString(ditheringAlgorithm).c_str());
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;
}
if (ditheringAlgorithm != render::DitheringAlgorithm::None && if (ditheringAlgorithm != render::DitheringAlgorithm::None &&
!ditheringMatrix.empty()) { !ditheringMatrix.empty()) {
params.set("dithering-matrix", ditheringMatrix.c_str()); params.set("dithering-matrix", ditheringMatrix.c_str());

View File

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

View File

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

View File

@ -2577,8 +2577,26 @@ render::DitheringAlgorithmBase* ContextBar::ditheringAlgorithm()
case render::DitheringAlgorithm::None: s_dither.reset(nullptr); break; case render::DitheringAlgorithm::None: s_dither.reset(nullptr); break;
case render::DitheringAlgorithm::Ordered: s_dither.reset(new render::OrderedDither2(-1)); 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::Old: s_dither.reset(new render::OrderedDither(-1)); break;
case render::DitheringAlgorithm::ErrorDiffusion: case render::DitheringAlgorithm::FloydSteinberg:
s_dither.reset(new render::ErrorDiffusionDither(-1)); 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; break;
} }

View File

@ -231,9 +231,24 @@ void DitheringSelector::regenerate(int selectedItemIndex)
Console::showException(e); Console::showException(e);
} }
} }
addItem(new DitherItem(render::DitheringAlgorithm::ErrorDiffusion, addItem(new DitherItem(render::DitheringAlgorithm::FloydSteinberg,
render::DitheringMatrix(), render::DitheringMatrix(),
Strings::dithering_selector_floyd_steinberg())); 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; break;
case SelectMatrix: case SelectMatrix:
addItem( addItem(

View File

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

View File

@ -18,24 +18,29 @@ class Dithering {
public: public:
Dithering(DitheringAlgorithm algorithm = DitheringAlgorithm::None, Dithering(DitheringAlgorithm algorithm = DitheringAlgorithm::None,
const DitheringMatrix& matrix = DitheringMatrix(), const DitheringMatrix& matrix = DitheringMatrix(),
bool zigzag = true,
double factor = 1.0) double factor = 1.0)
: m_algorithm(algorithm) : m_algorithm(algorithm)
, m_matrix(matrix) , m_matrix(matrix)
, m_zigzag(zigzag)
, m_factor(factor) , m_factor(factor)
{ {
} }
DitheringAlgorithm algorithm() const { return m_algorithm; } DitheringAlgorithm algorithm() const { return m_algorithm; }
DitheringMatrix matrix() const { return m_matrix; } DitheringMatrix matrix() const { return m_matrix; }
bool zigzag() const { return m_zigzag; }
double factor() const { return m_factor; } double factor() const { return m_factor; }
void algorithm(const DitheringAlgorithm algorithm) { m_algorithm = algorithm; } void algorithm(const DitheringAlgorithm algorithm) { m_algorithm = algorithm; }
void matrix(const DitheringMatrix& matrix) { m_matrix = matrix; } void matrix(const DitheringMatrix& matrix) { m_matrix = matrix; }
void zigzag(bool zigzag) { m_zigzag = zigzag; }
void factor(const double factor) { m_factor = factor; } void factor(const double factor) { m_factor = factor; }
private: private:
DitheringAlgorithm m_algorithm; DitheringAlgorithm m_algorithm;
DitheringMatrix m_matrix; DitheringMatrix m_matrix;
bool m_zigzag;
double m_factor; 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 #define RENDER_DITHERING_METHOD_H_INCLUDED
#pragma once #pragma once
#include <string>
namespace render { namespace render {
// Dithering algorithms // Dithering algorithms
@ -16,9 +18,19 @@ enum class DitheringAlgorithm {
None, None,
Ordered, Ordered,
Old, 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 } // namespace render
#endif #endif

View File

@ -15,23 +15,139 @@
#include "gfx/rgb.h" #include "gfx/rgb.h"
#include <algorithm> #include <algorithm>
#include <vector>
namespace render { 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_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, void ErrorDiffusionDither::start(const doc::Image* srcImage,
doc::Image* dstImage, doc::Image* dstImage,
const double factor) const double factor)
{ {
m_srcImage = srcImage; m_srcImage = srcImage;
m_width = 2 + srcImage->width(); 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) 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_lastY = -1;
m_currentRowOffset = 0;
m_factor = int(factor * 100.0); m_factor = int(factor * 100.0);
} }
@ -42,30 +158,39 @@ void ErrorDiffusionDither::finish()
doc::color_t ErrorDiffusionDither::ditherRgbToIndex2D(const int x, doc::color_t ErrorDiffusionDither::ditherRgbToIndex2D(const int x,
const int y, const int y,
const doc::RgbMap* rgbmap, const doc::RgbMap* rgbmap,
const doc::Palette* palette) const doc::Palette* palette,
const int direction)
{ {
const ErrorDiffusionMatrix& matrix = getCurrentMatrix();
if (y != m_lastY) { if (y != m_lastY) {
for (int i = 0; i < kChannels; ++i) { // Instead of shifting all rows, just advance the circular buffer
int* row0 = &m_err[i][0]; // and clear the row that will be reused
int* row1 = row0 + m_width; m_currentRowOffset = (m_currentRowOffset + 1) % matrix.height;
int* end1 = row1 + m_width;
std::copy(row1, end1, row0); // Clear only the row that will be used as the "last" row
std::fill(row1, end1, 0); 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; m_lastY = y;
} }
doc::color_t color = doc::get_pixel_fast<doc::RgbTraits>(m_srcImage, x, 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), int v[kChannels] = { doc::rgba_getr(color),
doc::rgba_getg(color), doc::rgba_getg(color),
doc::rgba_getb(color), doc::rgba_getb(color),
doc::rgba_geta(color) }; doc::rgba_geta(color) };
for (int i = 0; i < kChannels; ++i) {
v[i] += m_err[i][x + 1]; // Add accumulated error (16-bit fixed point) and convert to 0..255
v[i] = std::clamp(v[i], 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 ? const doc::color_t index = (rgbmap ?
rgbmap->mapColor(v[0], v[1], v[2], v[3]) : 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[2] - doc::rgba_getb(palColor),
v[3] - doc::rgba_geta(palColor) }; v[3] - doc::rgba_geta(palColor) };
// TODO using Floyd-Steinberg matrix here but it should be configurable const int srcWidth = m_srcImage->width();
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;
if (y & 1) { // Distribute error using the configurable matrix
err[0] += a; for (int c = 0; c < kChannels; ++c) {
err[m_width + 2] += b; const int qerr = quantError[c] * m_factor / 100;
err[m_width + 1] += c;
err[m_width] += d; for (int my = 0; my < matrix.height; ++my) {
} // Use circular buffer indexing
else { const int bufferRow = (m_currentRowOffset + my) % matrix.height;
err[+2] += a; const int bufferRowIndex = bufferRow * m_width;
err[m_width] += b;
err[m_width + 1] += c; for (int mx = 0; mx < matrix.width; ++mx) {
err[m_width + 2] += d; 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 { 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 { class ErrorDiffusionDither : public DitheringAlgorithmBase {
public: public:
ErrorDiffusionDither(int transparentIndex = -1); ErrorDiffusionDither(ErrorDiffusionType type, int transparentIndex);
int dimensions() const override { return 2; } 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 start(const doc::Image* srcImage, doc::Image* dstImage, const double factor) override;
void finish() override; void finish() override;
doc::color_t ditherRgbToIndex2D(const int x, doc::color_t ditherRgbToIndex2D(const int x,
const int y, const int y,
const doc::RgbMap* rgbmap, const doc::RgbMap* rgbmap,
const doc::Palette* palette) override; const doc::Palette* palette,
const int direction) override;
private: private:
const ErrorDiffusionMatrix& getCurrentMatrix() const;
int m_transparentIndex; int m_transparentIndex;
ErrorDiffusionType m_diffusionType;
const doc::Image* m_srcImage; const doc::Image* m_srcImage;
int m_width, m_lastY; int m_width, m_lastY;
int m_currentRowOffset;
static const int kChannels = 4; static const int kChannels = 4;
std::vector<int> m_err[kChannels]; std::vector<int> m_err[kChannels];
int m_factor; int m_factor;

View File

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

View File

@ -27,7 +27,6 @@ public:
virtual ~DitheringAlgorithmBase() {} virtual ~DitheringAlgorithmBase() {}
virtual int dimensions() const { return 1; } 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) {} 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, virtual doc::color_t ditherRgbToIndex2D(const int x,
const int y, const int y,
const doc::RgbMap* rgbmap, const doc::RgbMap* rgbmap,
const doc::Palette* palette) const doc::Palette* palette,
const int direction)
{ {
return 0; return 0;
} }

View File

@ -159,8 +159,29 @@ Image* convert_pixel_format(const Image* image,
case DitheringAlgorithm::Old: case DitheringAlgorithm::Old:
dither.reset(new OrderedDither(is_background ? -1 : new_mask_color)); dither.reset(new OrderedDither(is_background ? -1 : new_mask_color));
break; break;
case DitheringAlgorithm::ErrorDiffusion: case DitheringAlgorithm::FloydSteinberg:
dither.reset(new ErrorDiffusionDither(is_background ? -1 : new_mask_color)); 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; break;
} }
if (dither) if (dither)

View File

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