2019-04-02 08:44:06 +08:00
|
|
|
// Aseprite Render Library
|
2022-06-10 05:28:06 +08:00
|
|
|
// Copyright (c) 2019-2022 Igara Studio S.A
|
2019-04-02 08:44:06 +08:00
|
|
|
// Copyright (c) 2017 David Capello
|
|
|
|
//
|
|
|
|
// This file is released under the terms of the MIT license.
|
|
|
|
// Read LICENSE.txt for more information.
|
|
|
|
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
|
|
#include "config.h"
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#include "render/error_diffusion.h"
|
|
|
|
|
|
|
|
#include "gfx/hsl.h"
|
|
|
|
#include "gfx/rgb.h"
|
|
|
|
|
|
|
|
#include <algorithm>
|
2025-06-12 22:25:20 +08:00
|
|
|
#include <vector>
|
2019-04-02 08:44:06 +08:00
|
|
|
|
|
|
|
namespace render {
|
|
|
|
|
2025-06-12 22:25:20 +08:00
|
|
|
// Predefined error diffusion algorithms
|
|
|
|
class ErrorDiffusionMatrices {
|
|
|
|
public:
|
|
|
|
static const ErrorDiffusionMatrix& getFloydSteinberg()
|
|
|
|
{
|
2025-06-14 00:45:32 +08:00
|
|
|
static const ErrorDiffusionMatrix matrix(3,
|
|
|
|
2,
|
|
|
|
1,
|
|
|
|
0,
|
|
|
|
{
|
|
|
|
{ 0, 0, 7 },
|
|
|
|
{ 3, 5, 1 }
|
2025-06-12 22:25:20 +08:00
|
|
|
},
|
2025-06-14 00:45:32 +08:00
|
|
|
16);
|
2025-06-12 22:25:20 +08:00
|
|
|
return matrix;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const ErrorDiffusionMatrix& getJarvisJudiceNinke()
|
|
|
|
{
|
2025-06-14 00:45:32 +08:00
|
|
|
static const ErrorDiffusionMatrix matrix(5,
|
|
|
|
3,
|
|
|
|
2,
|
|
|
|
0,
|
|
|
|
{
|
|
|
|
{ 0, 0, 0, 7, 5 },
|
|
|
|
{ 3, 5, 7, 5, 3 },
|
|
|
|
{ 1, 3, 5, 3, 1 }
|
2025-06-12 22:25:20 +08:00
|
|
|
},
|
2025-06-14 00:45:32 +08:00
|
|
|
48);
|
2025-06-12 22:25:20 +08:00
|
|
|
return matrix;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const ErrorDiffusionMatrix& getStucki()
|
|
|
|
{
|
2025-06-14 00:45:32 +08:00
|
|
|
static const ErrorDiffusionMatrix matrix(5,
|
|
|
|
3,
|
|
|
|
2,
|
|
|
|
0,
|
|
|
|
{
|
|
|
|
{ 0, 0, 0, 8, 4 },
|
|
|
|
{ 2, 4, 8, 4, 2 },
|
|
|
|
{ 1, 2, 4, 2, 1 }
|
2025-06-12 22:25:20 +08:00
|
|
|
},
|
2025-06-14 00:45:32 +08:00
|
|
|
42);
|
2025-06-12 22:25:20 +08:00
|
|
|
return matrix;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const ErrorDiffusionMatrix& getAtkinson()
|
|
|
|
{
|
2025-06-14 00:45:32 +08:00
|
|
|
static const ErrorDiffusionMatrix matrix(4,
|
|
|
|
3,
|
|
|
|
1,
|
|
|
|
0,
|
|
|
|
{
|
|
|
|
{ 0, 0, 1, 1 },
|
|
|
|
{ 1, 1, 1, 0 },
|
|
|
|
{ 0, 1, 0, 0 }
|
2025-06-12 22:25:20 +08:00
|
|
|
},
|
2025-06-14 00:45:32 +08:00
|
|
|
8);
|
2025-06-12 22:25:20 +08:00
|
|
|
return matrix;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const ErrorDiffusionMatrix& getBurkes()
|
|
|
|
{
|
2025-06-14 00:45:32 +08:00
|
|
|
static const ErrorDiffusionMatrix matrix(5,
|
|
|
|
2,
|
|
|
|
2,
|
|
|
|
0,
|
|
|
|
{
|
|
|
|
{ 0, 0, 0, 8, 4 },
|
|
|
|
{ 2, 4, 8, 4, 2 }
|
2025-06-12 22:25:20 +08:00
|
|
|
},
|
2025-06-14 00:45:32 +08:00
|
|
|
32);
|
2025-06-12 22:25:20 +08:00
|
|
|
return matrix;
|
|
|
|
}
|
|
|
|
|
|
|
|
static const ErrorDiffusionMatrix& getSierra()
|
|
|
|
{
|
2025-06-14 00:45:32 +08:00
|
|
|
static const ErrorDiffusionMatrix matrix(5,
|
|
|
|
3,
|
|
|
|
2,
|
|
|
|
0,
|
|
|
|
{
|
|
|
|
{ 0, 0, 0, 5, 3 },
|
|
|
|
{ 2, 4, 5, 4, 2 },
|
|
|
|
{ 0, 2, 3, 2, 0 }
|
2025-06-12 22:25:20 +08:00
|
|
|
},
|
2025-06-14 00:45:32 +08:00
|
|
|
32);
|
2025-06-12 22:25:20 +08:00
|
|
|
return matrix;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
ErrorDiffusionDither::ErrorDiffusionDither(ErrorDiffusionType type, int transparentIndex)
|
2019-04-02 08:44:06 +08:00
|
|
|
: m_transparentIndex(transparentIndex)
|
2025-06-12 22:25:20 +08:00
|
|
|
, m_diffusionType(type)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
const ErrorDiffusionMatrix& ErrorDiffusionDither::getCurrentMatrix() const
|
2019-04-02 08:44:06 +08:00
|
|
|
{
|
2025-06-12 22:25:20 +08:00
|
|
|
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();
|
|
|
|
}
|
2019-04-02 08:44:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void ErrorDiffusionDither::start(const doc::Image* srcImage,
|
2019-04-04 05:42:10 +08:00
|
|
|
doc::Image* dstImage,
|
|
|
|
const double factor)
|
2019-04-02 08:44:06 +08:00
|
|
|
{
|
|
|
|
m_srcImage = srcImage;
|
|
|
|
m_width = 2 + srcImage->width();
|
2025-06-12 22:25:20 +08:00
|
|
|
|
|
|
|
// Get the current matrix to determine buffer size needed
|
|
|
|
const ErrorDiffusionMatrix& matrix = getCurrentMatrix();
|
2025-06-14 00:45:32 +08:00
|
|
|
const int bufferRows = matrix.height;
|
2025-06-12 22:25:20 +08:00
|
|
|
|
|
|
|
// Resize error buffers to accommodate the matrix height
|
2025-06-14 21:37:36 +08:00
|
|
|
const std::vector<int>::size_type bufferSize = m_width * bufferRows;
|
2019-04-02 11:18:36 +08:00
|
|
|
for (int i = 0; i < kChannels; ++i)
|
2025-06-14 00:45:32 +08:00
|
|
|
m_err[i].resize(bufferSize, 0);
|
2025-06-12 22:25:20 +08:00
|
|
|
|
2019-04-02 08:44:06 +08:00
|
|
|
m_lastY = -1;
|
2025-06-13 21:27:28 +08:00
|
|
|
m_currentRowOffset = 0;
|
2019-04-04 05:42:10 +08:00
|
|
|
m_factor = int(factor * 100.0);
|
2019-04-02 08:44:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
void ErrorDiffusionDither::finish()
|
|
|
|
{
|
|
|
|
}
|
|
|
|
|
|
|
|
doc::color_t ErrorDiffusionDither::ditherRgbToIndex2D(const int x,
|
|
|
|
const int y,
|
|
|
|
const doc::RgbMap* rgbmap,
|
2025-06-13 23:53:42 +08:00
|
|
|
const doc::Palette* palette,
|
|
|
|
const int direction)
|
2019-04-02 08:44:06 +08:00
|
|
|
{
|
2025-06-12 22:25:20 +08:00
|
|
|
const ErrorDiffusionMatrix& matrix = getCurrentMatrix();
|
|
|
|
|
2019-04-02 08:44:06 +08:00
|
|
|
if (y != m_lastY) {
|
2025-06-13 21:27:28 +08:00
|
|
|
// 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
|
2025-06-14 21:37:36 +08:00
|
|
|
const int clearRowIndex = (m_currentRowOffset + matrix.height - 1) % matrix.height;
|
2025-06-13 21:27:28 +08:00
|
|
|
for (int c = 0; c < kChannels; ++c) {
|
|
|
|
int* rowToClear = &m_err[c][m_width * clearRowIndex];
|
|
|
|
std::fill(rowToClear, rowToClear + m_width, 0);
|
2019-04-02 08:44:06 +08:00
|
|
|
}
|
2025-06-13 21:27:28 +08:00
|
|
|
|
2019-04-02 08:44:06 +08:00
|
|
|
m_lastY = y;
|
|
|
|
}
|
|
|
|
|
|
|
|
doc::color_t color = doc::get_pixel_fast<doc::RgbTraits>(m_srcImage, x, y);
|
|
|
|
|
2025-06-12 22:25:20 +08:00
|
|
|
// Get RGB values + quantization error
|
2019-04-02 08:44:06 +08:00
|
|
|
int v[kChannels] = { doc::rgba_getr(color),
|
|
|
|
doc::rgba_getg(color),
|
|
|
|
doc::rgba_getb(color),
|
|
|
|
doc::rgba_geta(color) };
|
2025-06-12 22:25:20 +08:00
|
|
|
|
|
|
|
// Add accumulated error (16-bit fixed point) and convert to 0..255
|
2025-06-13 21:27:28 +08:00
|
|
|
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);
|
2019-04-02 08:44:06 +08:00
|
|
|
|
|
|
|
const doc::color_t index = (rgbmap ?
|
2019-04-03 00:54:39 +08:00
|
|
|
rgbmap->mapColor(v[0], v[1], v[2], v[3]) :
|
|
|
|
palette->findBestfit(v[0], v[1], v[2], v[3], m_transparentIndex));
|
2019-04-02 08:44:06 +08:00
|
|
|
|
|
|
|
doc::color_t palColor = palette->getEntry(index);
|
|
|
|
if (m_transparentIndex == index || doc::rgba_geta(palColor) == 0) {
|
|
|
|
// "color" without alpha
|
|
|
|
palColor = (color & doc::rgba_rgb_mask);
|
|
|
|
}
|
|
|
|
|
|
|
|
const int quantError[kChannels] = { v[0] - doc::rgba_getr(palColor),
|
|
|
|
v[1] - doc::rgba_getg(palColor),
|
|
|
|
v[2] - doc::rgba_getb(palColor),
|
|
|
|
v[3] - doc::rgba_geta(palColor) };
|
|
|
|
|
2025-06-12 22:25:20 +08:00
|
|
|
const int srcWidth = m_srcImage->width();
|
|
|
|
|
|
|
|
// Distribute error using the configurable matrix
|
2025-06-13 21:27:28 +08:00
|
|
|
for (int c = 0; c < kChannels; ++c) {
|
|
|
|
const int qerr = quantError[c] * m_factor / 100;
|
2025-06-12 22:25:20 +08:00
|
|
|
|
|
|
|
for (int my = 0; my < matrix.height; ++my) {
|
2025-06-13 21:27:28 +08:00
|
|
|
// Use circular buffer indexing
|
2025-06-14 21:37:36 +08:00
|
|
|
const int bufferRow = (m_currentRowOffset + my) % matrix.height;
|
|
|
|
const int bufferRowIndex = bufferRow * m_width;
|
2025-06-13 21:27:28 +08:00
|
|
|
|
2025-06-12 22:25:20 +08:00
|
|
|
for (int mx = 0; mx < matrix.width; ++mx) {
|
2025-06-25 00:43:35 +08:00
|
|
|
const int coeff = direction > 0 ? matrix.coefficients[my][mx] :
|
|
|
|
matrix.coefficients[my][matrix.width - 1 - mx];
|
2025-06-12 22:25:20 +08:00
|
|
|
if (coeff == 0)
|
|
|
|
continue;
|
|
|
|
|
2025-06-14 00:45:32 +08:00
|
|
|
const int errorPixelX = x + mx - matrix.centerX;
|
|
|
|
const int errorPixelY = y + my - matrix.centerY;
|
2025-06-12 22:25:20 +08:00
|
|
|
|
|
|
|
// Check bounds
|
|
|
|
if (errorPixelX < 0 || errorPixelX >= srcWidth)
|
|
|
|
continue;
|
|
|
|
if (errorPixelY < y)
|
|
|
|
continue; // Don't go backwards
|
|
|
|
|
|
|
|
// Calculate error as 16-bit fixed point
|
2025-06-14 00:45:32 +08:00
|
|
|
const int errorValue = ((qerr * coeff) << 16) / matrix.divisor;
|
2025-06-13 21:27:28 +08:00
|
|
|
m_err[c][bufferRowIndex + errorPixelX + 1] += errorValue;
|
2025-06-12 22:25:20 +08:00
|
|
|
}
|
2019-04-03 23:14:17 +08:00
|
|
|
}
|
2019-04-02 08:44:06 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return index;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace render
|