mirror of https://github.com/aseprite/aseprite.git
Compare commits
13 Commits
92b254c70d
...
e273a35f04
Author | SHA1 | Date |
---|---|---|
|
e273a35f04 | |
|
2ba051b59b | |
|
bd2ae1eb61 | |
|
f5ecead66f | |
|
8d0fa49044 | |
|
2ba52d6d6c | |
|
4e5ae492e5 | |
|
4c5f5249d8 | |
|
484f8d69bf | |
|
87fc82a978 | |
|
b43e3e6ac1 | |
|
d6129359c4 | |
|
9e9abfab27 |
|
@ -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" />
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,9 +635,10 @@ void ChangePixelFormatCommand::onExecute(Context* ctx)
|
|||
}
|
||||
|
||||
job.startJobWithCallback([this, &job, sprite, &matrix](Tx& tx) {
|
||||
tx(new cmd::SetPixelFormat(sprite,
|
||||
tx(new cmd::SetPixelFormat(
|
||||
sprite,
|
||||
(PixelFormat)params().colorMode(),
|
||||
render::Dithering(params().dithering(), matrix, params().factor()),
|
||||
render::Dithering(params().dithering(), matrix, params().zigZag(), params().factor()),
|
||||
params().rgbmap(),
|
||||
get_gray_func(params().toGray()),
|
||||
&job, // SpriteJob is a render::TaskDelegate
|
||||
|
@ -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;
|
||||
|
|
|
@ -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<>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -15,12 +15,119 @@
|
|||
#include "gfx/rgb.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
namespace render {
|
||||
|
||||
ErrorDiffusionDither::ErrorDiffusionDither(int transparentIndex)
|
||||
: m_transparentIndex(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,
|
||||
|
@ -29,9 +136,18 @@ void ErrorDiffusionDither::start(const doc::Image* srcImage,
|
|||
{
|
||||
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;
|
||||
// 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;
|
||||
}
|
||||
else {
|
||||
err[+2] += a;
|
||||
err[m_width] += b;
|
||||
err[m_width + 1] += c;
|
||||
err[m_width + 2] += d;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
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);
|
||||
*dstIt = algorithm.ditherRgbToIndex2D(x, y, rgbmap, palette, +1);
|
||||
}
|
||||
}
|
||||
|
||||
if (delegate) {
|
||||
if (!delegate->continueTask())
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (delegate) {
|
||||
delegate->notifyTaskProgress(double(y + 1) / double(h));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Aseprite Rener Library
|
||||
// Aseprite Render Library
|
||||
// Copyright (c) 2019-2021 Igara Studio S.A.
|
||||
// Copyright (c) 2001-2017 David Capello
|
||||
//
|
||||
|
|
Loading…
Reference in New Issue