Render larger images on demand not by default

This commit is contained in:
jpirnay
2026-05-19 09:28:36 +02:00
parent d4b2a24e2a
commit d922b5a67a
9 changed files with 134 additions and 20 deletions
+22 -3
View File
@@ -27,10 +27,13 @@ std::unique_ptr<PageLine> PageLine::deserialize(FsFile& file) {
}
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
// Images don't use fontId or text rendering
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
}
void PageImage::renderWithForceLoad(GfxRenderer& renderer, const int xOffset, const int yOffset, const bool forceLoad) {
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset, forceLoad);
}
bool PageImage::serialize(FsFile& file) {
serialization::writePod(file, xPos);
serialization::writePod(file, yPos);
@@ -201,12 +204,28 @@ std::unique_ptr<PageTableFragment> PageTableFragment::deserialize(FsFile& file)
new PageTableFragment(columnCount, totalWidth, totalHeight, colWidths, std::move(rows), xPos, yPos));
}
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset,
const bool forceLoadLargeImages) const {
for (auto& element : elements) {
element->render(renderer, fontId, xOffset, yOffset);
if (element->getTag() == TAG_PageImage) {
static_cast<PageImage&>(*element).renderWithForceLoad(renderer, xOffset, yOffset, forceLoadLargeImages);
} else {
element->render(renderer, fontId, xOffset, yOffset);
}
}
}
bool Page::hasPlaceholderImages(const bool forceLoadLargeImages) const {
for (const auto& el : elements) {
if (el->getTag() == TAG_PageImage) {
if (static_cast<const PageImage&>(*el).getImageBlock().wouldShowPlaceholder(forceLoadLargeImages)) {
return true;
}
}
}
return false;
}
void Page::renderTextOnly(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
for (auto& element : elements) {
if (element->getTag() == TAG_PageLine) {
+3 -1
View File
@@ -58,6 +58,7 @@ class PageImage final : public PageElement {
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
void renderWithForceLoad(GfxRenderer& renderer, int xOffset, int yOffset, bool forceLoad);
bool serialize(FsFile& file) override;
PageElementTag getTag() const override { return TAG_PageImage; }
static std::unique_ptr<PageImage> deserialize(FsFile& file);
@@ -128,8 +129,9 @@ class Page {
footnotes.push_back(entry);
}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool forceLoadLargeImages = true) const;
void renderTextOnly(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
bool hasPlaceholderImages(bool forceLoadLargeImages) const;
bool serialize(FsFile& file) const;
static std::unique_ptr<Page> deserialize(FsFile& file);
+61 -10
View File
@@ -1,12 +1,16 @@
#include "ImageBlock.h"
#include <FsHelpers.h>
#include <GfxRenderer.h>
#include <Logging.h>
#include <Serialization.h>
#include "../../../../src/CrossPointSettings.h"
#include "../../../../src/fontIds.h"
#include "../converters/DirectPixelWriter.h"
#include "../converters/ImageDecoderFactory.h"
#include "../converters/JpegToFramebufferConverter.h"
#include "../converters/PngToFramebufferConverter.h"
// Cache file format:
// - uint16_t width
@@ -97,7 +101,51 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
} // namespace
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
bool ImageBlock::isLargeImage() const {
if (largeImageCached != 0) return largeImageCached == 1;
ImageDimensions dims{0, 0};
const bool ok = FsHelpers::hasJpgExtension(imagePath)
? JpegToFramebufferConverter::getDimensionsStatic(imagePath, dims)
: PngToFramebufferConverter::getDimensionsStatic(imagePath, dims);
if (ok && dims.width > 0 && dims.height > 0) {
largeImageCached = (int32_t(dims.width) * dims.height > LARGE_IMAGE_PIXEL_THRESHOLD) ? 1 : -1;
} else {
largeImageCached = -1; // unreadable header → assume not large, render normally
}
return largeImageCached == 1;
}
bool ImageBlock::wouldShowPlaceholder(bool forceLoad) const {
if (forceLoad) return false;
if (!isLargeImage()) return false;
// If the pixel cache already exists the render is instant — no placeholder needed
const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering);
const std::string pxcPath = [&] {
size_t dot = imagePath.rfind('.');
return (dot != std::string::npos ? imagePath.substr(0, dot) : imagePath) + getImageDitherCacheSuffix(ditherMode) +
".pxc";
}();
return !Storage.exists(pxcPath.c_str());
}
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
constexpr int BORDER = 1;
constexpr int PADDING = 6;
renderer.drawRect(x, y, width, height, BORDER, true);
const int lineH = renderer.getLineHeight(UI_10_FONT_ID);
const int totalTextH = lineH * 2;
if (lineH > 0 && width > PADDING * 2 && height > totalTextH + PADDING * 2) {
const int textX = x + PADDING;
const int textY = y + (height - totalTextH) / 2;
renderer.drawText(UI_10_FONT_ID, textX, textY, "Image");
renderer.drawText(UI_10_FONT_ID, textX, textY + lineH, "Press OK to load");
}
}
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y, const bool forceLoad) {
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
const int screenWidth = renderer.getScreenWidth();
@@ -110,15 +158,21 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
return;
}
// Try to render from cache first
// Try to render from pixel cache first (always, regardless of forceLoad)
const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering);
std::string cachePath = getCachePath(imagePath, ditherMode);
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
return; // Successfully rendered from cache
return;
}
// No cache - need to decode the image
// Check if image file exists
// No pixel cache — check if this is a large image that should show a placeholder
if (wouldShowPlaceholder(forceLoad)) {
LOG_DBG("IMG", "Large image placeholder at %d,%d (%dx%d): %s", x, y, width, height, imagePath.c_str());
renderPlaceholder(renderer, x, y);
return;
}
// Proceed with full decode
FsFile file;
if (!Storage.openFileForRead("IMG", imagePath, file)) {
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
@@ -143,8 +197,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
config.useDithering = true;
config.ditherMode = ditherMode;
config.performanceMode = false;
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
config.cachePath = cachePath; // Enable caching during decode
config.useExactDimensions = true;
config.cachePath = cachePath;
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
if (!decoder) {
@@ -157,10 +211,7 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
if (!success) {
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
return;
}
LOG_DBG("IMG", "Decode successful");
}
bool ImageBlock::serialize(FsFile& file) {
+18 -1
View File
@@ -6,6 +6,11 @@
#include "Block.h"
// Source pixel area above which an image is considered "large" and rendered
// as a placeholder until the user explicitly requests it.
// 800x600 covers most full-page illustrations that take >1s to dither on ESP32.
static constexpr int32_t LARGE_IMAGE_PIXEL_THRESHOLD = 800 * 600;
class ImageBlock final : public Block {
public:
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
@@ -17,10 +22,18 @@ class ImageBlock final : public Block {
bool imageExists() const;
// Returns true if the source image dimensions exceed LARGE_IMAGE_PIXEL_THRESHOLD.
// Result is cached after the first call to avoid repeated header reads.
bool isLargeImage() const;
// Returns true if this image would be shown as a placeholder given forceLoad.
// False when: forceLoad is true, image is not large, or pixel cache already exists.
bool wouldShowPlaceholder(bool forceLoad) const;
BlockType getType() override { return IMAGE_BLOCK; }
bool isEmpty() override { return false; }
void render(GfxRenderer& renderer, const int x, const int y);
void render(GfxRenderer& renderer, int x, int y, bool forceLoad = true);
bool serialize(FsFile& file);
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
@@ -28,4 +41,8 @@ class ImageBlock final : public Block {
std::string imagePath;
int16_t width;
int16_t height;
mutable int8_t largeImageCached = 0; // 0=unchecked, 1=large, -1=not large
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
};
+1
View File
@@ -102,6 +102,7 @@ STR_IMAGES: "Images"
STR_IMAGES_DISPLAY: "Display"
STR_IMAGES_PLACEHOLDER: "Placeholder"
STR_IMAGES_SUPPRESS: "Suppress"
STR_LARGE_IMAGE_PLACEHOLDER: "Placeholder for large images"
STR_IMAGE_DITHERING: "Image Dithering"
STR_IMAGE_DITHER_BAYER: "Bayer"
STR_IMAGE_DITHER_ATKINSON: "Atkinson"