Add dithering algorithms

This commit is contained in:
jpirnay
2026-04-06 13:00:27 +02:00
parent f45b780900
commit eb95cacad4
10 changed files with 335 additions and 59 deletions
@@ -1,5 +1,6 @@
#include "PngToFramebufferConverter.h"
#include <BitmapHelpers.h>
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <HalStorage.h>
@@ -36,6 +37,9 @@ struct PngContext {
bool caching;
uint8_t* grayLineBuffer;
int currentDitherRow;
AtkinsonDitherer* atkinsonDitherer;
DiffusedBayerDitherer* diffusedBayerDitherer;
PngContext()
: renderer(nullptr),
@@ -49,9 +53,59 @@ struct PngContext {
dstHeight(0),
lastDstY(-1),
caching(false),
grayLineBuffer(nullptr) {}
grayLineBuffer(nullptr),
currentDitherRow(-1),
atkinsonDitherer(nullptr),
diffusedBayerDitherer(nullptr) {}
~PngContext() {
delete atkinsonDitherer;
delete diffusedBayerDitherer;
}
};
void prepareDitherRow(PngContext& ctx, int dstY) {
if (!ctx.config || !ctx.config->useDithering) return;
if (ctx.currentDitherRow == -1 || dstY < ctx.currentDitherRow) {
if (ctx.atkinsonDitherer) ctx.atkinsonDitherer->reset();
if (ctx.diffusedBayerDitherer) ctx.diffusedBayerDitherer->reset();
ctx.currentDitherRow = dstY;
return;
}
while (ctx.currentDitherRow < dstY) {
if (ctx.atkinsonDitherer) ctx.atkinsonDitherer->nextRow();
if (ctx.diffusedBayerDitherer) ctx.diffusedBayerDitherer->nextRow();
ctx.currentDitherRow++;
}
}
uint8_t ditherGray(PngContext& ctx, uint8_t gray, int localX, int outX, int outY) {
if (!ctx.config || !ctx.config->useDithering) {
return quantizeGray4Level(gray);
}
switch (ctx.config->ditherMode) {
case ImageDitherMode::Atkinson:
if (ctx.atkinsonDitherer) {
return ctx.atkinsonDitherer->processPixel(gray, localX);
}
break;
case ImageDitherMode::DiffusedBayer:
if (ctx.diffusedBayerDitherer) {
return ctx.diffusedBayerDitherer->processPixel(gray, localX, outX, outY);
}
break;
case ImageDitherMode::Bayer:
case ImageDitherMode::COUNT:
default:
break;
}
return applyBayerDither4Level(gray, outX, outY);
}
// File I/O callbacks use pFile->fHandle to access the FsFile*,
// avoiding the need for global file state.
void* pngOpenWithHandle(const char* filename, int32_t* size) {
@@ -205,7 +259,6 @@ int pngDrawCallback(PNGDRAW* pDraw) {
int dstWidth = ctx->dstWidth;
int outXBase = ctx->config->x;
int screenWidth = ctx->screenWidth;
bool useDithering = ctx->config->useDithering;
bool caching = ctx->caching;
// Pre-compute orientation and render-mode state once per row
@@ -219,6 +272,8 @@ int pngDrawCallback(PNGDRAW* pDraw) {
cw.beginRow(outY, ctx->config->y);
}
prepareDitherRow(*ctx, dstY);
int srcX = 0;
int error = 0;
@@ -227,13 +282,7 @@ int pngDrawCallback(PNGDRAW* pDraw) {
if (outX < screenWidth) {
uint8_t gray = ctx->grayLineBuffer[srcX];
uint8_t ditheredGray;
if (useDithering) {
ditheredGray = applyBayerDither4Level(gray, outX, outY);
} else {
ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3;
}
uint8_t ditheredGray = ditherGray(*ctx, gray, dstX, outX, outY);
pw.writePixel(outX, ditheredGray);
if (caching) cw.writePixel(outX, ditheredGray);
}
@@ -385,6 +434,27 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
}
}
if (config.useDithering) {
switch (config.ditherMode) {
case ImageDitherMode::Atkinson:
ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(ctx.dstWidth);
if (!ctx.atkinsonDitherer) {
LOG_ERR("PNG", "Failed to allocate Atkinson ditherer, falling back to Bayer");
}
break;
case ImageDitherMode::DiffusedBayer:
ctx.diffusedBayerDitherer = new (std::nothrow) DiffusedBayerDitherer(ctx.dstWidth);
if (!ctx.diffusedBayerDitherer) {
LOG_ERR("PNG", "Failed to allocate diffused Bayer ditherer, falling back to Bayer");
}
break;
case ImageDitherMode::Bayer:
case ImageDitherMode::COUNT:
default:
break;
}
}
unsigned long decodeStart = millis();
rc = png->decode(&ctx, 0);
unsigned long decodeTime = millis() - decodeStart;