Merge pull request #26 from jpirnay/feat-extend-image-viewer

feat: Extend image viewer to support jpg/png and introduce alternative dither algorithms
This commit is contained in:
jpirnay
2026-04-06 19:06:49 +02:00
committed by GitHub
17 changed files with 663 additions and 149 deletions
+7 -4
View File
@@ -4,6 +4,7 @@
#include <Logging.h> #include <Logging.h>
#include <Serialization.h> #include <Serialization.h>
#include "../../../../src/CrossPointSettings.h"
#include "../converters/DirectPixelWriter.h" #include "../converters/DirectPixelWriter.h"
#include "../converters/ImageDecoderFactory.h" #include "../converters/ImageDecoderFactory.h"
@@ -19,13 +20,13 @@ bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str());
namespace { namespace {
std::string getCachePath(const std::string& imagePath) { std::string getCachePath(const std::string& imagePath, ImageDitherMode ditherMode) {
// Replace extension with .pxc (pixel cache) // Replace extension with .pxc (pixel cache)
size_t dotPos = imagePath.rfind('.'); size_t dotPos = imagePath.rfind('.');
if (dotPos != std::string::npos) { if (dotPos != std::string::npos) {
return imagePath.substr(0, dotPos) + ".pxc"; return imagePath.substr(0, dotPos) + getImageDitherCacheSuffix(ditherMode) + ".pxc";
} }
return imagePath + ".pxc"; return imagePath + getImageDitherCacheSuffix(ditherMode) + ".pxc";
} }
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth, bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
@@ -110,7 +111,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
} }
// Try to render from cache first // Try to render from cache first
std::string cachePath = getCachePath(imagePath); const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering);
std::string cachePath = getCachePath(imagePath, ditherMode);
if (renderFromCache(renderer, cachePath, x, y, width, height)) { if (renderFromCache(renderer, cachePath, x, y, width, height)) {
return; // Successfully rendered from cache return; // Successfully rendered from cache
} }
@@ -139,6 +141,7 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
config.maxHeight = height; config.maxHeight = height;
config.useGrayscale = true; config.useGrayscale = true;
config.useDithering = true; config.useDithering = true;
config.ditherMode = ditherMode;
config.performanceMode = false; config.performanceMode = false;
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
config.cachePath = cachePath; // Enable caching during decode config.cachePath = cachePath; // Enable caching during decode
+61 -4
View File
@@ -2,6 +2,8 @@
#include <stdint.h> #include <stdint.h>
#include <cstring>
// 4x4 Bayer matrix for ordered dithering // 4x4 Bayer matrix for ordered dithering
inline const uint8_t bayer4x4[4][4] = { inline const uint8_t bayer4x4[4][4] = {
{0, 8, 2, 10}, {0, 8, 2, 10},
@@ -10,6 +12,8 @@ inline const uint8_t bayer4x4[4][4] = {
{15, 7, 13, 5}, {15, 7, 13, 5},
}; };
inline uint8_t quantizeGray4Level(uint8_t gray) { return gray >= 213 ? 3 : static_cast<uint8_t>((gray + 42) / 85); }
// Apply Bayer dithering and quantize to 4 levels (0-3) // Apply Bayer dithering and quantize to 4 levels (0-3)
// Stateless - works correctly with any pixel processing order // Stateless - works correctly with any pixel processing order
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) { inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
@@ -20,8 +24,61 @@ inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
if (adjusted < 0) adjusted = 0; if (adjusted < 0) adjusted = 0;
if (adjusted > 255) adjusted = 255; if (adjusted > 255) adjusted = 255;
if (adjusted < 64) return 0; return quantizeGray4Level((uint8_t)adjusted);
if (adjusted < 128) return 1;
if (adjusted < 192) return 2;
return 3;
} }
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
class DiffusedBayerDitherer {
public:
explicit DiffusedBayerDitherer(int width) : width(width) {
errorCurRow = new int16_t[width + 2]();
errorNextRow = new int16_t[width + 2]();
}
~DiffusedBayerDitherer() {
delete[] errorCurRow;
delete[] errorNextRow;
}
DiffusedBayerDitherer(const DiffusedBayerDitherer&) = delete;
DiffusedBayerDitherer& operator=(const DiffusedBayerDitherer&) = delete;
uint8_t processPixel(int gray, int x, int screenX, int screenY) {
int adjusted = gray + errorCurRow[x + 1];
if (adjusted < 0) adjusted = 0;
if (adjusted > 255) adjusted = 255;
int thresholdAdjusted = adjusted + (bayer4x4[screenY & 3][screenX & 3] - 8) * 5;
if (thresholdAdjusted < 0) thresholdAdjusted = 0;
if (thresholdAdjusted > 255) thresholdAdjusted = 255;
uint8_t quantized = quantizeGray4Level((uint8_t)thresholdAdjusted);
int quantizedValue = quantized * 85;
int error = adjusted - quantizedValue;
errorCurRow[x + 2] += (error * 7) / 16;
errorNextRow[x] += (error * 3) / 16;
errorNextRow[x + 1] += (error * 5) / 16;
errorNextRow[x + 2] += error / 16;
return quantized;
}
void nextRow() {
int16_t* tmp = errorCurRow;
errorCurRow = errorNextRow;
errorNextRow = tmp;
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
}
void reset() {
memset(errorCurRow, 0, (width + 2) * sizeof(int16_t));
memset(errorNextRow, 0, (width + 2) * sizeof(int16_t));
}
private:
int width;
int16_t* errorCurRow;
int16_t* errorNextRow;
};
#endif
@@ -11,11 +11,53 @@ struct ImageDimensions {
int16_t height; int16_t height;
}; };
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:
case ImageDitherMode::DiffusedBayer:
return static_cast<ImageDitherMode>(value);
case ImageDitherMode::COUNT:
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:
return ".bayer";
}
}
struct RenderConfig { struct RenderConfig {
int x, y; int x, y;
int maxWidth, maxHeight; int maxWidth, maxHeight;
bool useGrayscale = true; bool useGrayscale = true;
bool useDithering = true; bool useDithering = true;
ImageDitherMode ditherMode = ImageDitherMode::Bayer;
bool performanceMode = false; bool performanceMode = false;
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation) bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
std::string cachePath; // If non-empty, decoder will write pixel cache to this path std::string cachePath; // If non-empty, decoder will write pixel cache to this path
@@ -1,5 +1,8 @@
#include "JpegToFramebufferConverter.h" #include "JpegToFramebufferConverter.h"
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
#include <BitmapHelpers.h>
#endif
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
@@ -39,6 +42,12 @@ struct JpegContext {
PixelCache cache; PixelCache cache;
bool caching; bool caching;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
int currentDitherRow;
AtkinsonDitherer* atkinsonDitherer;
DiffusedBayerDitherer* diffusedBayerDitherer;
#endif
JpegContext() JpegContext()
: renderer(nullptr), : renderer(nullptr),
config(nullptr), config(nullptr),
@@ -50,9 +59,74 @@ 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),
atkinsonDitherer(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;
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(JpegContext& 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);
}
#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.
void* jpegOpen(const char* filename, int32_t* size) { void* jpegOpen(const char* filename, int32_t* size) {
@@ -136,7 +210,6 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
if (stride <= 0 || blockH <= 0 || validW <= 0) return 1; if (stride <= 0 || blockH <= 0 || validW <= 0) return 1;
const bool useDithering = ctx->config->useDithering;
const bool caching = ctx->caching; const bool caching = ctx->caching;
const int32_t fineScaleFP = ctx->fineScaleFP; const int32_t fineScaleFP = ctx->fineScaleFP;
const int32_t invScaleFP = ctx->invScaleFP; const int32_t invScaleFP = ctx->invScaleFP;
@@ -181,19 +254,16 @@ 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);
#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];
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) { for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX; const int outX = cfgX + dstX;
uint8_t gray = row[dstX - blockX]; uint8_t gray = row[dstX - blockX];
uint8_t dithered; uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered); pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered); if (caching) cw.writePixel(outX, dithered);
} }
@@ -215,6 +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);
#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;
@@ -246,13 +319,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT; int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT); uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered; uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered); pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered); if (caching) cw.writePixel(outX, dithered);
} }
@@ -269,13 +336,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx0 + 1] * fx) >> FP_SHIFT; int bot = ((int)row1[lx0] * fxInv + (int)row1[lx0 + 1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT); uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered; uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered); pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered); if (caching) cw.writePixel(outX, dithered);
} }
@@ -295,13 +356,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT; int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT); uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
uint8_t dithered; uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered); pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered); if (caching) cw.writePixel(outX, dithered);
} }
@@ -312,6 +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);
#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;
@@ -328,13 +386,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
if (lx >= validW) lx = validW - 1; if (lx >= validW) lx = validW - 1;
uint8_t gray = row[lx]; uint8_t gray = row[lx];
uint8_t dithered; uint8_t dithered = ditherGray(*ctx, gray, dstX, outX, outY);
if (useDithering) {
dithered = applyBayerDither4Level(gray, outX, outY);
} else {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
pw.writePixel(outX, dithered); pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered); if (caching) cw.writePixel(outX, dithered);
} }
@@ -479,6 +531,29 @@ 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);
if (!ctx.atkinsonDitherer) {
LOG_ERR("JPG", "Failed to allocate Atkinson ditherer, falling back to Bayer");
}
break;
case ImageDitherMode::DiffusedBayer:
ctx.diffusedBayerDitherer = new (std::nothrow) DiffusedBayerDitherer(destWidth);
if (!ctx.diffusedBayerDitherer) {
LOG_ERR("JPG", "Failed to allocate diffused Bayer ditherer, falling back to Bayer");
}
break;
case ImageDitherMode::Bayer:
case ImageDitherMode::COUNT:
default:
break;
}
#endif
}
unsigned long decodeStart = millis(); unsigned long decodeStart = millis();
rc = jpeg->decode(0, 0, jpegScaleOption); rc = jpeg->decode(0, 0, jpegScaleOption);
unsigned long decodeTime = millis() - decodeStart; unsigned long decodeTime = millis() - decodeStart;
@@ -1,5 +1,8 @@
#include "PngToFramebufferConverter.h" #include "PngToFramebufferConverter.h"
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
#include <BitmapHelpers.h>
#endif
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
@@ -36,6 +39,11 @@ struct PngContext {
bool caching; bool caching;
uint8_t* grayLineBuffer; uint8_t* grayLineBuffer;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
int currentDitherRow;
AtkinsonDitherer* atkinsonDitherer;
DiffusedBayerDitherer* diffusedBayerDitherer;
#endif
PngContext() PngContext()
: renderer(nullptr), : renderer(nullptr),
@@ -49,9 +57,74 @@ 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),
atkinsonDitherer(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;
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);
}
#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.
void* pngOpenWithHandle(const char* filename, int32_t* size) { void* pngOpenWithHandle(const char* filename, int32_t* size) {
@@ -205,7 +278,6 @@ int pngDrawCallback(PNGDRAW* pDraw) {
int dstWidth = ctx->dstWidth; int dstWidth = ctx->dstWidth;
int outXBase = ctx->config->x; int outXBase = ctx->config->x;
int screenWidth = ctx->screenWidth; int screenWidth = ctx->screenWidth;
bool useDithering = ctx->config->useDithering;
bool caching = ctx->caching; bool caching = ctx->caching;
// Pre-compute orientation and render-mode state once per row // Pre-compute orientation and render-mode state once per row
@@ -219,6 +291,10 @@ int pngDrawCallback(PNGDRAW* pDraw) {
cw.beginRow(outY, ctx->config->y); cw.beginRow(outY, ctx->config->y);
} }
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
prepareDitherRow(*ctx, dstY);
#endif
int srcX = 0; int srcX = 0;
int error = 0; int error = 0;
@@ -227,13 +303,7 @@ int pngDrawCallback(PNGDRAW* pDraw) {
if (outX < screenWidth) { if (outX < screenWidth) {
uint8_t gray = ctx->grayLineBuffer[srcX]; uint8_t gray = ctx->grayLineBuffer[srcX];
uint8_t ditheredGray; uint8_t ditheredGray = ditherGray(*ctx, gray, dstX, outX, outY);
if (useDithering) {
ditheredGray = applyBayerDither4Level(gray, outX, outY);
} else {
ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3;
}
pw.writePixel(outX, ditheredGray); pw.writePixel(outX, ditheredGray);
if (caching) cw.writePixel(outX, ditheredGray); if (caching) cw.writePixel(outX, ditheredGray);
} }
@@ -385,6 +455,29 @@ 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);
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;
}
#endif
}
unsigned long decodeStart = millis(); unsigned long decodeStart = millis();
rc = png->decode(&ctx, 0); rc = png->decode(&ctx, 0);
unsigned long decodeTime = millis() - decodeStart; unsigned long decodeTime = millis() - decodeStart;
+7
View File
@@ -70,6 +70,10 @@ STR_IMAGES: "Images"
STR_IMAGES_DISPLAY: "Display" STR_IMAGES_DISPLAY: "Display"
STR_IMAGES_PLACEHOLDER: "Placeholder" STR_IMAGES_PLACEHOLDER: "Placeholder"
STR_IMAGES_SUPPRESS: "Suppress" STR_IMAGES_SUPPRESS: "Suppress"
STR_IMAGE_DITHERING: "Image Dithering"
STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Diffused Bayer"
STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Create fallback for invalid TOC" STR_CREATE_FALLBACK_FOR_INVALID_TOC: "Create fallback for invalid TOC"
STR_SHORT_PWR_BTN: "Short Power Button Click" STR_SHORT_PWR_BTN: "Short Power Button Click"
STR_ORIENTATION: "Reading Orientation" STR_ORIENTATION: "Reading Orientation"
@@ -442,6 +446,9 @@ STR_UPTIME: "Uptime"
STR_CHARGING: "Charging" STR_CHARGING: "Charging"
STR_GATHERING_DATA: "Gathering data..." STR_GATHERING_DATA: "Gathering data..."
STR_READING: "Reading..." STR_READING: "Reading..."
STR_UNSUPPORTED_IMAGE_FORMAT: "Unsupported image format"
STR_COULD_NOT_RENDER_IMAGE: "Could not render image"
STR_FAILED_TO_SET_SLEEP_SCREEN: "Failed to set sleep screen"
STR_SET_SLEEP_SCREEN: "Set Sleep" STR_SET_SLEEP_SCREEN: "Set Sleep"
STR_SLEEP_SCREEN_SET: "Sleep screen updated!" STR_SLEEP_SCREEN_SET: "Sleep screen updated!"
STR_WEATHER_MOON_INFO: "Moon" STR_WEATHER_MOON_INFO: "Moon"
+9
View File
@@ -66,6 +66,10 @@ STR_SLEEP_COVER_MODE: "Standby-Bildmodus"
STR_HIDE_BATTERY: "Batterie % ausblenden" STR_HIDE_BATTERY: "Batterie % ausblenden"
STR_EXTRA_SPACING: "Absatzabstand" STR_EXTRA_SPACING: "Absatzabstand"
STR_TEXT_AA: "Schriftglättung" STR_TEXT_AA: "Schriftglättung"
STR_IMAGE_DITHERING: "Bild-Dithering"
STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
STR_IMAGE_DITHER_DIFFUSED_BAYER: "Diffundiertes Bayer"
STR_SHORT_PWR_BTN: "An-Taste kurz drücken" STR_SHORT_PWR_BTN: "An-Taste kurz drücken"
STR_ORIENTATION: "Leseausrichtung" STR_ORIENTATION: "Leseausrichtung"
STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)" STR_SIDE_BTN_LAYOUT: "Seitliche Tasten (Lesen)"
@@ -320,6 +324,11 @@ STR_UPTIME: "Laufzeit"
STR_CHARGING: "Lädt" STR_CHARGING: "Lädt"
STR_GATHERING_DATA: "Daten werden gesammelt..." STR_GATHERING_DATA: "Daten werden gesammelt..."
STR_READING: "Lese..." STR_READING: "Lese..."
STR_UNSUPPORTED_IMAGE_FORMAT: "Nicht unterstütztes Bildformat"
STR_COULD_NOT_RENDER_IMAGE: "Bild konnte nicht dargestellt werden"
STR_FAILED_TO_SET_SLEEP_SCREEN: "Standby-Bild konnte nicht gesetzt werden"
STR_SET_SLEEP_SCREEN: "Standby-Bild setzen"
STR_SLEEP_SCREEN_SET: "Standby-Bild aktualisiert!"
STR_WEATHER: "Wetter" STR_WEATHER: "Wetter"
STR_WEATHER_LOCATION: "Ort" STR_WEATHER_LOCATION: "Ort"
+1
View File
@@ -25,6 +25,7 @@ build_flags =
-DEINK_DISPLAY_SINGLE_BUFFER_MODE=1 -DEINK_DISPLAY_SINGLE_BUFFER_MODE=1
-DDISABLE_FS_H_WARNING=1 -DDISABLE_FS_H_WARNING=1
-DDESTRUCTOR_CLOSES_FILE=1 -DDESTRUCTOR_CLOSES_FILE=1
-DENABLE_IMAGE_DITHERING_EXTENSION
# https://libexpat.github.io/doc/api/latest/#XML_GE # https://libexpat.github.io/doc/api/latest/#XML_GE
-DXML_GE=0 -DXML_GE=0
-DXML_CONTEXT_BYTES=1024 -DXML_CONTEXT_BYTES=1024
+12
View File
@@ -137,6 +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 {
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) // Timezone options (POSIX TZ rules for DST support)
enum TIMEZONE { enum TIMEZONE {
@@ -223,6 +233,8 @@ class CrossPointSettings {
uint8_t showHiddenFiles = 0; uint8_t showHiddenFiles = 0;
// Image rendering mode in EPUB reader // Image rendering mode in EPUB reader
uint8_t imageRendering = IMAGES_DISPLAY; uint8_t imageRendering = IMAGES_DISPLAY;
// Dithering mode for decoded images (EPUB/JPG/PNG)
uint8_t imageDithering = IMAGE_DITHER_BAYER;
// Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled) // Enable synthetic TOC fallback for malformed/sparse TOC books (1 = enabled, 0 = disabled)
uint8_t syntheticTocFallback = 1; uint8_t syntheticTocFallback = 1;
// Show clock in the reader status bar // Show clock in the reader status bar
+6
View File
@@ -69,6 +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(
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, SettingInfo::Toggle(StrId::STR_CREATE_FALLBACK_FOR_INVALID_TOC, &CrossPointSettings::syntheticTocFallback,
"syntheticTocFallback", StrId::STR_CAT_READER), "syntheticTocFallback", StrId::STR_CAT_READER),
// --- Controls --- // --- Controls ---
+15 -16
View File
@@ -187,6 +187,21 @@ void SleepActivity::renderCustomSleepScreen() const {
const bool shouldLoadOverlayInfo = const bool shouldLoadOverlayInfo =
SETTINGS.sleepCoverOverlay != 0 && APP_STATE.lastSleepFromReader && !APP_STATE.openEpubPath.empty(); SETTINGS.sleepCoverOverlay != 0 && APP_STATE.lastSleepFromReader && !APP_STATE.openEpubPath.empty();
// An explicitly selected custom sleep image should override random images from /.sleep or /sleep.
FsFile explicitSleepFile;
if (Storage.openFileForRead("SLP", "/sleep.bmp", explicitSleepFile)) {
Bitmap bitmap(explicitSleepFile, true);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
LOG_DBG("SLP", "Loading explicit custom sleep image: /sleep.bmp");
const BookOverlayInfo resolvedOverlayInfo =
shouldLoadOverlayInfo ? getBookOverlayInfo(APP_STATE.openEpubPath) : overlayInfo;
renderBitmapSleepScreen(bitmap, resolvedOverlayInfo);
explicitSleepFile.close();
return;
}
explicitSleepFile.close();
}
// Check if we have a /.sleep (preferred) or /sleep directory // Check if we have a /.sleep (preferred) or /sleep directory
const char* sleepDir = nullptr; const char* sleepDir = nullptr;
auto dir = Storage.open("/.sleep"); auto dir = Storage.open("/.sleep");
@@ -260,22 +275,6 @@ void SleepActivity::renderCustomSleepScreen() const {
} }
if (dir) dir.close(); if (dir) dir.close();
// Look for sleep.bmp on the root of the sd card to determine if we should
// render a custom sleep screen instead of the default.
FsFile file;
if (Storage.openFileForRead("SLP", "/sleep.bmp", file)) {
Bitmap bitmap(file, true);
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
LOG_DBG("SLP", "Loading: /sleep.bmp");
const BookOverlayInfo resolvedOverlayInfo =
shouldLoadOverlayInfo ? getBookOverlayInfo(APP_STATE.openEpubPath) : overlayInfo;
renderBitmapSleepScreen(bitmap, resolvedOverlayInfo);
file.close();
return;
}
file.close();
}
renderDefaultSleepScreen(); renderDefaultSleepScreen();
} }
+2 -1
View File
@@ -96,7 +96,8 @@ void FileBrowserActivity::loadFiles() {
std::string_view filename{name}; std::string_view filename{name};
if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) || if (FsHelpers::hasEpubExtension(filename) || FsHelpers::hasXtcExtension(filename) ||
FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) || FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename) ||
FsHelpers::hasBmpExtension(filename)) { FsHelpers::hasBmpExtension(filename) || FsHelpers::hasJpgExtension(filename) ||
FsHelpers::hasPngExtension(filename)) {
files.emplace_back(filename); files.emplace_back(filename);
} }
} }
+4 -2
View File
@@ -31,7 +31,9 @@ bool ReaderActivity::isTxtFile(const std::string& path) {
FsHelpers::hasMarkdownExtension(path); // Treat .md as txt files (until we have a markdown reader) FsHelpers::hasMarkdownExtension(path); // Treat .md as txt files (until we have a markdown reader)
} }
bool ReaderActivity::isBmpFile(const std::string& path) { return FsHelpers::hasBmpExtension(path); } bool ReaderActivity::isImageFile(const std::string& path) {
return FsHelpers::hasBmpExtension(path) || FsHelpers::hasJpgExtension(path) || FsHelpers::hasPngExtension(path);
}
std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) { std::unique_ptr<Epub> ReaderActivity::loadEpub(const std::string& path) {
if (!Storage.exists(path.c_str())) { if (!Storage.exists(path.c_str())) {
@@ -120,7 +122,7 @@ void ReaderActivity::onEnter() {
} }
currentBookPath = initialBookPath; currentBookPath = initialBookPath;
if (isBmpFile(initialBookPath)) { if (isImageFile(initialBookPath)) {
onGoToBmpViewer(initialBookPath); onGoToBmpViewer(initialBookPath);
} else if (isXtcFile(initialBookPath)) { } else if (isXtcFile(initialBookPath)) {
renderer.clearScreen(); renderer.clearScreen();
+1 -1
View File
@@ -16,7 +16,7 @@ class ReaderActivity final : public Activity {
static std::unique_ptr<Txt> loadTxt(const std::string& path); static std::unique_ptr<Txt> loadTxt(const std::string& path);
static bool isXtcFile(const std::string& path); static bool isXtcFile(const std::string& path);
static bool isTxtFile(const std::string& path); static bool isTxtFile(const std::string& path);
static bool isBmpFile(const std::string& path); static bool isImageFile(const std::string& path);
static std::string extractFolderPath(const std::string& filePath); static std::string extractFolderPath(const std::string& filePath);
void goToLibrary(const std::string& fromBookPath = ""); void goToLibrary(const std::string& fromBookPath = "");
+262 -73
View File
@@ -1,110 +1,292 @@
#include "BmpViewerActivity.h" #include "BmpViewerActivity.h"
#include <Bitmap.h> #include <Bitmap.h>
#include <Epub/converters/ImageDecoderFactory.h>
#include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HalStorage.h> #include <HalStorage.h>
#include <I18n.h> #include <I18n.h>
#include <cmath>
#include "../reader/ReaderUtils.h" #include "../reader/ReaderUtils.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "components/UITheme.h" #include "components/UITheme.h"
#include "fontIds.h" #include "fontIds.h"
#include "util/ScreenshotUtil.h"
namespace { namespace {
constexpr const char* SLEEP_BMP_PATH = "/sleep.bmp"; 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); }
bool isSupportedImageFile(const std::string& path) {
return FsHelpers::hasBmpExtension(path) || FsHelpers::hasJpgExtension(path) || FsHelpers::hasPngExtension(path);
}
void computeCenteredImagePlacement(const int imageWidth, const int imageHeight, const int pageWidth,
const int pageHeight, int& x, int& y, int& renderWidth, int& renderHeight) {
renderWidth = imageWidth;
renderHeight = imageHeight;
if (imageWidth > pageWidth || imageHeight > pageHeight) {
const float ratio = static_cast<float>(imageWidth) / static_cast<float>(imageHeight);
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
if (ratio > screenRatio) {
renderWidth = pageWidth;
renderHeight = std::max(1, static_cast<int>(std::round(static_cast<float>(pageWidth) / ratio)));
} else {
renderHeight = pageHeight;
renderWidth = std::max(1, static_cast<int>(std::round(static_cast<float>(pageHeight) * ratio)));
}
x = std::max(0, (pageWidth - renderWidth) / 2);
y = std::max(0, (pageHeight - renderHeight) / 2);
return;
}
renderWidth = std::max(1, renderWidth);
renderHeight = std::max(1, renderHeight);
x = std::max(0, (pageWidth - renderWidth) / 2);
y = std::max(0, (pageHeight - renderHeight) / 2);
}
bool replaceSleepBmpFromTemp() {
const bool hadExistingTarget = Storage.exists(SLEEP_BMP_PATH);
bool movedExistingToBackup = false;
if (Storage.exists(SLEEP_BMP_BACKUP_PATH)) {
Storage.remove(SLEEP_BMP_BACKUP_PATH);
}
if (hadExistingTarget) {
movedExistingToBackup = Storage.rename(SLEEP_BMP_PATH, SLEEP_BMP_BACKUP_PATH);
if (!movedExistingToBackup) {
return false;
}
}
if (Storage.rename(SLEEP_BMP_TMP_PATH, SLEEP_BMP_PATH)) {
if (movedExistingToBackup) {
Storage.remove(SLEEP_BMP_BACKUP_PATH);
}
return true;
}
if (movedExistingToBackup) {
Storage.rename(SLEEP_BMP_BACKUP_PATH, SLEEP_BMP_PATH);
}
return false;
}
} // namespace } // namespace
BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path) BmpViewerActivity::BmpViewerActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string path)
: Activity("BmpViewer", renderer, mappedInput), filePath(std::move(path)) {} : Activity("BmpViewer", renderer, mappedInput),
filePath(std::move(path))
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
,
imageDitherMode(normalizeImageDitherModeValue(SETTINGS.imageDithering)),
initialImageDitherMode(imageDitherMode),
imageDitherSettingsDirty(false) {
}
#else
{
}
#endif
bool BmpViewerActivity::renderCurrentImage(const bool showControls) {
return isBmpFile(filePath) ? renderBmpImage(showControls) : renderDecodedImage(showControls);
}
void BmpViewerActivity::onEnter() { void BmpViewerActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
// Removed the redundant initial renderer.clearScreen() if (!isSupportedImageFile(filePath)) {
renderError(tr(STR_UNSUPPORTED_IMAGE_FORMAT));
return;
}
FsFile file; const bool rendered = renderCurrentImage();
if (!rendered) {
const auto pageWidth = renderer.getScreenWidth(); renderError(tr(STR_COULD_NOT_RENDER_IMAGE));
const auto pageHeight = renderer.getScreenHeight();
Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
GUI.fillPopupProgress(renderer, popupRect, 20); // Initial 20% progress
// 1. Open the file
if (Storage.openFileForRead("BMP", filePath, file)) {
Bitmap bitmap(file, true);
// 2. Parse headers to get dimensions
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
int x, y;
if (bitmap.getWidth() > pageWidth || bitmap.getHeight() > pageHeight) {
float ratio = static_cast<float>(bitmap.getWidth()) / static_cast<float>(bitmap.getHeight());
const float screenRatio = static_cast<float>(pageWidth) / static_cast<float>(pageHeight);
if (ratio > screenRatio) {
// Wider than screen
x = 0;
y = std::round((static_cast<float>(pageHeight) - static_cast<float>(pageWidth) / ratio) / 2);
} else {
// Taller than screen
x = std::round((static_cast<float>(pageWidth) - static_cast<float>(pageHeight) * ratio) / 2);
y = 0;
}
} else {
// Center small images
x = (pageWidth - bitmap.getWidth()) / 2;
y = (pageHeight - bitmap.getHeight()) / 2;
}
// 4. Prepare Rendering
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN));
GUI.fillPopupProgress(renderer, popupRect, 50);
renderer.clearScreen();
// Assuming drawBitmap defaults to 0,0 crop if omitted, or pass explicitly: drawBitmap(bitmap, x, y, pageWidth,
// pageHeight, 0, 0)
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0);
// Draw UI hints on the base layer
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Single pass for non-grayscale images
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} else {
// Handle file parsing error
renderer.clearScreen();
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Invalid BMP File");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
file.close();
} else {
// Handle file open error
renderer.clearScreen();
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Could not open file");
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
} }
} }
void BmpViewerActivity::onExit() { void BmpViewerActivity::onExit() {
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
saveDitherSettingsIfNeeded();
#endif
Activity::onExit(); Activity::onExit();
renderer.clearScreen(); renderer.clearScreen();
renderer.displayBuffer(HalDisplay::FULL_REFRESH); renderer.displayBuffer(HalDisplay::FULL_REFRESH);
} }
bool BmpViewerActivity::renderBmpImage(const bool showControls) {
FsFile file;
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
GUI.fillPopupProgress(renderer, popupRect, 20);
if (!Storage.openFileForRead("BMP", filePath, file)) {
return false;
}
Bitmap bitmap(file, true);
if (bitmap.parseHeaders() != BmpReaderError::Ok) {
file.close();
return false;
}
int x, y, renderWidth, renderHeight;
computeCenteredImagePlacement(bitmap.getWidth(), bitmap.getHeight(), pageWidth, pageHeight, x, y, renderWidth,
renderHeight);
GUI.fillPopupProgress(renderer, popupRect, 50);
renderer.clearScreen();
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, 0, 0);
if (showControls) {
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", tr(STR_SET_SLEEP_SCREEN));
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
}
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
file.close();
return true;
}
bool BmpViewerActivity::renderDecodedImage(const bool showControls) {
const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight();
Rect popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
GUI.fillPopupProgress(renderer, popupRect, 20);
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(filePath);
if (!decoder) {
return false;
}
ImageDimensions dims{};
if (!decoder->getDimensions(filePath, dims) || dims.width <= 0 || dims.height <= 0) {
return false;
}
int x, y, renderWidth, renderHeight;
computeCenteredImagePlacement(dims.width, dims.height, pageWidth, pageHeight, x, y, renderWidth, renderHeight);
GUI.fillPopupProgress(renderer, popupRect, 50);
renderer.clearScreen();
RenderConfig config{};
config.x = x;
config.y = y;
config.maxWidth = renderWidth;
config.maxHeight = renderHeight;
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:
return StrId::STR_IMAGE_DITHER_ATKINSON;
case ImageDitherMode::DiffusedBayer:
return StrId::STR_IMAGE_DITHER_DIFFUSED_BAYER;
case ImageDitherMode::Bayer:
case ImageDitherMode::COUNT:
default:
return StrId::STR_IMAGE_DITHER_BAYER;
}
}
void BmpViewerActivity::cycleDitherMode() {
imageDitherMode = (imageDitherMode + 1) % CrossPointSettings::IMAGE_DITHERING_COUNT;
SETTINGS.imageDithering = imageDitherMode;
imageDitherSettingsDirty = (imageDitherMode != initialImageDitherMode);
if (!renderCurrentImage()) {
renderError(tr(STR_COULD_NOT_RENDER_IMAGE));
}
}
void BmpViewerActivity::saveDitherSettingsIfNeeded() {
if (!imageDitherSettingsDirty) {
return;
}
SETTINGS.imageDithering = imageDitherMode;
SETTINGS.saveToFile();
initialImageDitherMode = imageDitherMode;
imageDitherSettingsDirty = false;
}
#endif
void BmpViewerActivity::renderError(const char* message) {
const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen();
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, message);
const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", "");
GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
void BmpViewerActivity::setAsSleepScreen() { void BmpViewerActivity::setAsSleepScreen() {
// Only copy if the source isn't already /sleep.bmp bool success = false;
if (filePath != SLEEP_BMP_PATH) {
if (!Storage.copyFile("BMP", filePath, SLEEP_BMP_PATH)) { if (FsHelpers::hasBmpExtension(filePath)) {
LOG_ERR("BMP", "Failed to copy %s to %s", filePath.c_str(), SLEEP_BMP_PATH); success = (filePath == SLEEP_BMP_PATH) ? true : Storage.copyFile("BMP", filePath, SLEEP_BMP_PATH);
return; } else {
const bool renderedForCapture = renderDecodedImage(false);
if (renderedForCapture) {
Storage.remove(SLEEP_BMP_TMP_PATH);
if (ScreenshotUtil::saveFramebufferAsBmp(SLEEP_BMP_TMP_PATH, renderer.getFrameBuffer(), display.getDisplayWidth(),
display.getDisplayHeight())) {
success = replaceSleepBmpFromTemp();
}
}
if (!success && Storage.exists(SLEEP_BMP_TMP_PATH)) {
Storage.remove(SLEEP_BMP_TMP_PATH);
} }
} }
// Switch sleep screen mode to CUSTOM so the copied image is used if (!success) {
LOG_ERR("BMP", "Failed to set %s as sleep screen", filePath.c_str());
GUI.drawPopup(renderer, tr(STR_FAILED_TO_SET_SLEEP_SCREEN));
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
return;
}
SETTINGS.sleepScreen = CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM; SETTINGS.sleepScreen = CrossPointSettings::SLEEP_SCREEN_MODE::CUSTOM;
SETTINGS.saveToFile(); SETTINGS.saveToFile();
LOG_INF("BMP", "Set %s as sleep screen", filePath.c_str()); LOG_INF("BMP", "Set %s as sleep screen", filePath.c_str());
@@ -130,6 +312,13 @@ void BmpViewerActivity::loop() {
return; return;
} }
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
if (!isBmpFile(filePath) && mappedInput.wasReleased(MappedInputManager::Button::Left)) {
cycleDitherMode();
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)) {
setAsSleepScreen(); setAsSleepScreen();
+18 -1
View File
@@ -1,5 +1,8 @@
#pragma once #pragma once
#include <Epub/converters/ImageToFramebufferDecoder.h>
#include <I18nKeys.h>
#include <functional> #include <functional>
#include <string> #include <string>
@@ -16,5 +19,19 @@ class BmpViewerActivity final : public Activity {
private: private:
std::string filePath; std::string filePath;
#ifdef ENABLE_IMAGE_DITHERING_EXTENSION
uint8_t imageDitherMode;
uint8_t initialImageDitherMode;
bool imageDitherSettingsDirty;
#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;
void saveDitherSettingsIfNeeded();
#endif
void renderError(const char* message);
void setAsSleepScreen(); void setAsSleepScreen();
}; };
+2 -1
View File
@@ -115,7 +115,8 @@ UIIcon UITheme::getFileIcon(const std::string& filename) {
if (FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename)) { if (FsHelpers::hasTxtExtension(filename) || FsHelpers::hasMarkdownExtension(filename)) {
return Text; return Text;
} }
if (FsHelpers::hasBmpExtension(filename)) { if (FsHelpers::hasBmpExtension(filename) || FsHelpers::hasJpgExtension(filename) ||
FsHelpers::hasPngExtension(filename)) {
return Image; return Image;
} }
return File; return File;