(Conditionally) add further dithering algorithms

This commit is contained in:
jpirnay
2026-04-06 17:23:29 +02:00
parent 0ba4626157
commit 4f031b00b1
8 changed files with 102 additions and 6 deletions
+2
View File
@@ -32,6 +32,7 @@ inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
return quantizeGray4Level((uint8_t)adjusted); return quantizeGray4Level((uint8_t)adjusted);
} }
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
class DiffusedBayerDitherer { class DiffusedBayerDitherer {
public: public:
explicit DiffusedBayerDitherer(int width) : width(width) { explicit DiffusedBayerDitherer(int width) : width(width) {
@@ -85,3 +86,4 @@ class DiffusedBayerDitherer {
int16_t* errorCurRow; int16_t* errorCurRow;
int16_t* errorNextRow; int16_t* errorNextRow;
}; };
#endif
@@ -13,12 +13,15 @@ struct ImageDimensions {
enum class ImageDitherMode : uint8_t { enum class ImageDitherMode : uint8_t {
Bayer = 0, Bayer = 0,
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
Atkinson = 1, Atkinson = 1,
DiffusedBayer = 2, DiffusedBayer = 2,
#endif
COUNT, COUNT,
}; };
inline ImageDitherMode imageDitherModeFromSetting(uint8_t value) { inline ImageDitherMode imageDitherModeFromSetting(uint8_t value) {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
switch (static_cast<ImageDitherMode>(value)) { switch (static_cast<ImageDitherMode>(value)) {
case ImageDitherMode::Bayer: case ImageDitherMode::Bayer:
case ImageDitherMode::Atkinson: case ImageDitherMode::Atkinson:
@@ -28,14 +31,20 @@ inline ImageDitherMode imageDitherModeFromSetting(uint8_t value) {
default: default:
return ImageDitherMode::Bayer; return ImageDitherMode::Bayer;
} }
#else
(void)value;
return ImageDitherMode::Bayer;
#endif
} }
inline const char* getImageDitherCacheSuffix(ImageDitherMode mode) { inline const char* getImageDitherCacheSuffix(ImageDitherMode mode) {
switch (mode) { switch (mode) {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
case ImageDitherMode::Atkinson: case ImageDitherMode::Atkinson:
return ".atkinson"; return ".atkinson";
case ImageDitherMode::DiffusedBayer: case ImageDitherMode::DiffusedBayer:
return ".diffused-bayer"; return ".diffused-bayer";
#endif
case ImageDitherMode::Bayer: case ImageDitherMode::Bayer:
case ImageDitherMode::COUNT: case ImageDitherMode::COUNT:
default: default:
@@ -1,6 +1,8 @@
#include "JpegToFramebufferConverter.h" #include "JpegToFramebufferConverter.h"
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
#include <BitmapHelpers.h> #include <BitmapHelpers.h>
#endif
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
@@ -40,9 +42,11 @@ struct JpegContext {
PixelCache cache; PixelCache cache;
bool caching; bool caching;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
int currentDitherRow; int currentDitherRow;
AtkinsonDitherer* atkinsonDitherer; AtkinsonDitherer* atkinsonDitherer;
DiffusedBayerDitherer* diffusedBayerDitherer; DiffusedBayerDitherer* diffusedBayerDitherer;
#endif
JpegContext() JpegContext()
: renderer(nullptr), : renderer(nullptr),
@@ -55,17 +59,25 @@ struct JpegContext {
dstHeight(0), dstHeight(0),
fineScaleFP(1 << 16), fineScaleFP(1 << 16),
invScaleFP(1 << 16), invScaleFP(1 << 16),
caching(false), caching(false)
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
,
currentDitherRow(-1), currentDitherRow(-1),
atkinsonDitherer(nullptr), atkinsonDitherer(nullptr),
diffusedBayerDitherer(nullptr) {} diffusedBayerDitherer(nullptr)
#endif
{
}
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
~JpegContext() { ~JpegContext() {
delete atkinsonDitherer; delete atkinsonDitherer;
delete diffusedBayerDitherer; delete diffusedBayerDitherer;
} }
#endif
}; };
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
void prepareDitherRow(JpegContext& ctx, int dstY) { void prepareDitherRow(JpegContext& ctx, int dstY) {
if (!ctx.config || !ctx.config->useDithering) return; if (!ctx.config || !ctx.config->useDithering) return;
@@ -107,6 +119,13 @@ uint8_t ditherGray(JpegContext& ctx, uint8_t gray, int localX, int outX, int out
return applyBayerDither4Level(gray, outX, outY); return applyBayerDither4Level(gray, outX, outY);
} }
#else
uint8_t ditherGray(JpegContext& ctx, uint8_t gray, int localX, int outX, int outY) {
(void)ctx;
(void)localX;
return applyBayerDither4Level(gray, outX, outY);
}
#endif
// File I/O callbacks use pFile->fHandle to access the FsFile*, // File I/O callbacks use pFile->fHandle to access the FsFile*,
// avoiding the need for global file state. // avoiding the need for global file state.
@@ -235,7 +254,9 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
if (fineScaleFP == FP_ONE) { if (fineScaleFP == FP_ONE) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) { for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY; const int outY = cfgY + dstY;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
prepareDitherRow(*ctx, dstY); prepareDitherRow(*ctx, dstY);
#endif
pw.beginRow(outY); pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y); if (caching) cw.beginRow(outY, ctx->config->y);
const uint8_t* row = &pixels[(dstY - blockY) * stride]; const uint8_t* row = &pixels[(dstY - blockY) * stride];
@@ -264,7 +285,9 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) { for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY; const int outY = cfgY + dstY;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
prepareDitherRow(*ctx, dstY); prepareDitherRow(*ctx, dstY);
#endif
pw.beginRow(outY); pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y); if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP; const int32_t srcFyFP = dstY * invScaleFP;
@@ -344,7 +367,9 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
// === Nearest-neighbor (downscale: fineScale < 1.0) === // === Nearest-neighbor (downscale: fineScale < 1.0) ===
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) { for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY; const int outY = cfgY + dstY;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
prepareDitherRow(*ctx, dstY); prepareDitherRow(*ctx, dstY);
#endif
pw.beginRow(outY); pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y); if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP; const int32_t srcFyFP = dstY * invScaleFP;
@@ -507,6 +532,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
} }
if (config.useDithering) { if (config.useDithering) {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
switch (config.ditherMode) { switch (config.ditherMode) {
case ImageDitherMode::Atkinson: case ImageDitherMode::Atkinson:
ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(destWidth); ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(destWidth);
@@ -525,6 +551,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
default: default:
break; break;
} }
#endif
} }
unsigned long decodeStart = millis(); unsigned long decodeStart = millis();
@@ -1,6 +1,8 @@
#include "PngToFramebufferConverter.h" #include "PngToFramebufferConverter.h"
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
#include <BitmapHelpers.h> #include <BitmapHelpers.h>
#endif
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
@@ -37,9 +39,11 @@ struct PngContext {
bool caching; bool caching;
uint8_t* grayLineBuffer; uint8_t* grayLineBuffer;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
int currentDitherRow; int currentDitherRow;
AtkinsonDitherer* atkinsonDitherer; AtkinsonDitherer* atkinsonDitherer;
DiffusedBayerDitherer* diffusedBayerDitherer; DiffusedBayerDitherer* diffusedBayerDitherer;
#endif
PngContext() PngContext()
: renderer(nullptr), : renderer(nullptr),
@@ -53,17 +57,25 @@ struct PngContext {
dstHeight(0), dstHeight(0),
lastDstY(-1), lastDstY(-1),
caching(false), caching(false),
grayLineBuffer(nullptr), grayLineBuffer(nullptr)
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
,
currentDitherRow(-1), currentDitherRow(-1),
atkinsonDitherer(nullptr), atkinsonDitherer(nullptr),
diffusedBayerDitherer(nullptr) {} diffusedBayerDitherer(nullptr)
#endif
{
}
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
~PngContext() { ~PngContext() {
delete atkinsonDitherer; delete atkinsonDitherer;
delete diffusedBayerDitherer; delete diffusedBayerDitherer;
} }
#endif
}; };
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
void prepareDitherRow(PngContext& ctx, int dstY) { void prepareDitherRow(PngContext& ctx, int dstY) {
if (!ctx.config || !ctx.config->useDithering) return; if (!ctx.config || !ctx.config->useDithering) return;
@@ -105,6 +117,13 @@ uint8_t ditherGray(PngContext& ctx, uint8_t gray, int localX, int outX, int outY
return applyBayerDither4Level(gray, outX, outY); return applyBayerDither4Level(gray, outX, outY);
} }
#else
uint8_t ditherGray(PngContext& ctx, uint8_t gray, int localX, int outX, int outY) {
(void)ctx;
(void)localX;
return applyBayerDither4Level(gray, outX, outY);
}
#endif
// File I/O callbacks use pFile->fHandle to access the FsFile*, // File I/O callbacks use pFile->fHandle to access the FsFile*,
// avoiding the need for global file state. // avoiding the need for global file state.
@@ -272,7 +291,9 @@ int pngDrawCallback(PNGDRAW* pDraw) {
cw.beginRow(outY, ctx->config->y); cw.beginRow(outY, ctx->config->y);
} }
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
prepareDitherRow(*ctx, dstY); prepareDitherRow(*ctx, dstY);
#endif
int srcX = 0; int srcX = 0;
int error = 0; int error = 0;
@@ -435,6 +456,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
} }
if (config.useDithering) { if (config.useDithering) {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
switch (config.ditherMode) { switch (config.ditherMode) {
case ImageDitherMode::Atkinson: case ImageDitherMode::Atkinson:
ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(ctx.dstWidth); ctx.atkinsonDitherer = new (std::nothrow) AtkinsonDitherer(ctx.dstWidth);
@@ -453,6 +475,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
default: default:
break; break;
} }
#endif
} }
unsigned long decodeStart = millis(); unsigned long decodeStart = millis();
+4
View File
@@ -137,12 +137,16 @@ class CrossPointSettings {
// Image rendering in EPUB reader // Image rendering in EPUB reader
enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT }; enum IMAGE_RENDERING { IMAGES_DISPLAY = 0, IMAGES_PLACEHOLDER = 1, IMAGES_SUPPRESS = 2, IMAGE_RENDERING_COUNT };
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
enum IMAGE_DITHERING { enum IMAGE_DITHERING {
IMAGE_DITHER_BAYER = 0, IMAGE_DITHER_BAYER = 0,
IMAGE_DITHER_ATKINSON = 1, IMAGE_DITHER_ATKINSON = 1,
IMAGE_DITHER_DIFFUSED_BAYER = 2, IMAGE_DITHER_DIFFUSED_BAYER = 2,
IMAGE_DITHERING_COUNT IMAGE_DITHERING_COUNT
}; };
#else
enum IMAGE_DITHERING { IMAGE_DITHER_BAYER = 0, IMAGE_DITHERING_COUNT };
#endif
// Timezone options (POSIX TZ rules for DST support) // Timezone options (POSIX TZ rules for DST support)
enum TIMEZONE { enum TIMEZONE {
+2
View File
@@ -69,10 +69,12 @@ inline const std::vector<SettingInfo>& getSettingsList() {
SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering, SettingInfo::Enum(StrId::STR_IMAGES, &CrossPointSettings::imageRendering,
{StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS}, {StrId::STR_IMAGES_DISPLAY, StrId::STR_IMAGES_PLACEHOLDER, StrId::STR_IMAGES_SUPPRESS},
"imageRendering", StrId::STR_CAT_READER), "imageRendering", StrId::STR_CAT_READER),
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
SettingInfo::Enum( SettingInfo::Enum(
StrId::STR_IMAGE_DITHERING, &CrossPointSettings::imageDithering, StrId::STR_IMAGE_DITHERING, &CrossPointSettings::imageDithering,
{StrId::STR_IMAGE_DITHER_BAYER, StrId::STR_IMAGE_DITHER_ATKINSON, StrId::STR_IMAGE_DITHER_DIFFUSED_BAYER}, {StrId::STR_IMAGE_DITHER_BAYER, StrId::STR_IMAGE_DITHER_ATKINSON, StrId::STR_IMAGE_DITHER_DIFFUSED_BAYER},
"imageDithering", StrId::STR_CAT_READER), "imageDithering", StrId::STR_CAT_READER),
#endif
SettingInfo::Toggle(StrId::STR_CREATE_FALLBACK_FOR_INVALID_TOC, &CrossPointSettings::syntheticTocFallback, SettingInfo::Toggle(StrId::STR_CREATE_FALLBACK_FOR_INVALID_TOC, &CrossPointSettings::syntheticTocFallback,
"syntheticTocFallback", StrId::STR_CAT_READER), "syntheticTocFallback", StrId::STR_CAT_READER),
// --- Controls --- // --- Controls ---
+27 -2
View File
@@ -20,7 +20,9 @@ constexpr const char* SLEEP_BMP_PATH = "/sleep.bmp";
constexpr const char* SLEEP_BMP_TMP_PATH = "/sleep.bmp.tmp"; constexpr const char* SLEEP_BMP_TMP_PATH = "/sleep.bmp.tmp";
constexpr const char* SLEEP_BMP_BACKUP_PATH = "/sleep.bmp.bak"; constexpr const char* SLEEP_BMP_BACKUP_PATH = "/sleep.bmp.bak";
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
uint8_t normalizeImageDitherModeValue(uint8_t mode) { return static_cast<uint8_t>(imageDitherModeFromSetting(mode)); } uint8_t normalizeImageDitherModeValue(uint8_t mode) { return static_cast<uint8_t>(imageDitherModeFromSetting(mode)); }
#endif
bool isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); } bool isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); }
@@ -87,8 +89,15 @@ bool replaceSleepBmpFromTemp() {
BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path) BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path)
: Activity("BmpViewer", renderer, mappedInput), : Activity("BmpViewer", renderer, mappedInput),
filePath(std::move(path)), filePath(std::move(path))
imageDitherMode(normalizeImageDitherModeValue(SETTINGS.imageDithering)) {} #ifdef ENABLE_IMAGE_DITHERING_EXTENSION
,
imageDitherMode(normalizeImageDitherModeValue(SETTINGS.imageDithering)) {
}
#else
{
}
#endif
bool BmpViewerActivity::renderCurrentImage(const bool showControls) { bool BmpViewerActivity::renderCurrentImage(const bool showControls) {
return isBmpFile(filePath) ? renderBmpImage(showControls) : renderDecodedImage(showControls); return isBmpFile(filePath) ? renderBmpImage(showControls) : renderDecodedImage(showControls);
@@ -139,8 +148,12 @@ bool BmpViewerActivity::renderBmpImage(const bool showControls) {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0); renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0);
if (showControls) { if (showControls) {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
const auto labels = const auto labels =
mappedInput.mapLabels(tr(STR_BACK), "", I18N.get(getCurrentDitherModeLabel()), tr(STR_SET_SLEEP_SCREEN)); mappedInput.mapLabels(tr(STR_BACK), "", I18N.get(getCurrentDitherModeLabel()), tr(STR_SET_SLEEP_SCREEN));
#else
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN));
#endif
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
@@ -179,21 +192,30 @@ bool BmpViewerActivity::renderDecodedImage(const bool showControls) {
config.useExactDimensions = true; config.useExactDimensions = true;
config.useGrayscale = true; config.useGrayscale = true;
config.useDithering = true; config.useDithering = true;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
config.ditherMode = imageDitherModeFromSetting(imageDitherMode); config.ditherMode = imageDitherModeFromSetting(imageDitherMode);
#else
config.ditherMode = ImageDitherMode::Bayer;
#endif
if (!decoder->decodeToFramebuffer(filePath, renderer, config)) { if (!decoder->decodeToFramebuffer(filePath, renderer, config)) {
return false; return false;
} }
if (showControls) { if (showControls) {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
const auto labels = const auto labels =
mappedInput.mapLabels(tr(STR_BACK), "", I18N.get(getCurrentDitherModeLabel()), tr(STR_SET_SLEEP_SCREEN)); mappedInput.mapLabels(tr(STR_BACK), "", I18N.get(getCurrentDitherModeLabel()), tr(STR_SET_SLEEP_SCREEN));
#else
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN));
#endif
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
renderer.displayBuffer(HalDisplay::HALF_REFRESH); renderer.displayBuffer(HalDisplay::HALF_REFRESH);
return true; return true;
} }
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
StrId BmpViewerActivity::getCurrentDitherModeLabel() const { StrId BmpViewerActivity::getCurrentDitherModeLabel() const {
switch (imageDitherModeFromSetting(imageDitherMode)) { switch (imageDitherModeFromSetting(imageDitherMode)) {
case ImageDitherMode::Atkinson: case ImageDitherMode::Atkinson:
@@ -216,6 +238,7 @@ void BmpViewerActivity::cycleDitherMode() {
renderError("Could not render image"); renderError("Could not render image");
} }
} }
#endif
void BmpViewerActivity::renderError(const char* message) { void BmpViewerActivity::renderError(const char* message) {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
@@ -278,10 +301,12 @@ void BmpViewerActivity::loop() {
return; return;
} }
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
if (mappedInput.wasReleased(MappedInputManager::Button::Left)) { if (mappedInput.wasReleased(MappedInputManager::Button::Left)) {
cycleDitherMode(); cycleDitherMode();
return; return;
} }
#endif
// Next/Right button: set this image as the sleep screen // Next/Right button: set this image as the sleep screen
if (mappedInput.wasReleased(MappedInputManager::Button::Right)) { if (mappedInput.wasReleased(MappedInputManager::Button::Right)) {
+4
View File
@@ -19,12 +19,16 @@ class BmpViewerActivity final : public Activity {
private: private:
std::string filePath; std::string filePath;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
uint8_t imageDitherMode; uint8_t imageDitherMode;
#endif
bool renderCurrentImage(bool showControls = true); bool renderCurrentImage(bool showControls = true);
bool renderBmpImage(bool showControls = true); bool renderBmpImage(bool showControls = true);
bool renderDecodedImage(bool showControls = true); bool renderDecodedImage(bool showControls = true);
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
void cycleDitherMode(); void cycleDitherMode();
StrId getCurrentDitherModeLabel() const; StrId getCurrentDitherModeLabel() const;
#endif
void renderError(const char* message); void renderError(const char* message);
void setAsSleepScreen(); void setAsSleepScreen();
}; };