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