Merge pull request #241 from jpirnay/feat-large-image-placeholder
feat: Render larger images on demand not by default
This commit is contained in:
@@ -11,7 +11,7 @@
|
|||||||
#include "FsHelpers.h"
|
#include "FsHelpers.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr uint8_t BOOK_CACHE_VERSION = 9;
|
constexpr uint8_t BOOK_CACHE_VERSION = 10;
|
||||||
constexpr char bookBinFile[] = "/book.bin";
|
constexpr char bookBinFile[] = "/book.bin";
|
||||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||||
|
|||||||
+35
-3
@@ -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) {
|
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);
|
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) {
|
bool PageImage::serialize(FsFile& file) {
|
||||||
serialization::writePod(file, xPos);
|
serialization::writePod(file, xPos);
|
||||||
serialization::writePod(file, yPos);
|
serialization::writePod(file, yPos);
|
||||||
@@ -201,12 +204,41 @@ std::unique_ptr<PageTableFragment> PageTableFragment::deserialize(FsFile& file)
|
|||||||
new PageTableFragment(columnCount, totalWidth, totalHeight, colWidths, std::move(rows), xPos, yPos));
|
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) {
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Page::allImagesArePlaceholders(const bool forceLoadLargeImages) const {
|
||||||
|
bool anyImage = false;
|
||||||
|
for (const auto& el : elements) {
|
||||||
|
if (el->getTag() == TAG_PageImage) {
|
||||||
|
anyImage = true;
|
||||||
|
if (!static_cast<const PageImage&>(*el).getImageBlock().wouldShowPlaceholder(forceLoadLargeImages)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return anyImage;
|
||||||
|
}
|
||||||
|
|
||||||
void Page::renderTextOnly(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
void Page::renderTextOnly(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||||
for (auto& element : elements) {
|
for (auto& element : elements) {
|
||||||
if (element->getTag() == TAG_PageLine) {
|
if (element->getTag() == TAG_PageLine) {
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ class PageImage final : public PageElement {
|
|||||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
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;
|
bool serialize(FsFile& file) override;
|
||||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||||
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
static std::unique_ptr<PageImage> deserialize(FsFile& file);
|
||||||
@@ -128,8 +129,10 @@ class Page {
|
|||||||
footnotes.push_back(entry);
|
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;
|
void renderTextOnly(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||||
|
bool hasPlaceholderImages(bool forceLoadLargeImages) const;
|
||||||
|
bool allImagesArePlaceholders(bool forceLoadLargeImages) const;
|
||||||
bool serialize(FsFile& file) const;
|
bool serialize(FsFile& file) const;
|
||||||
static std::unique_ptr<Page> deserialize(FsFile& file);
|
static std::unique_ptr<Page> deserialize(FsFile& file);
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,25 @@
|
|||||||
#include "ImageBlock.h"
|
#include "ImageBlock.h"
|
||||||
|
|
||||||
|
#include <FsHelpers.h>
|
||||||
#include <GfxRenderer.h>
|
#include <GfxRenderer.h>
|
||||||
|
#include <I18n.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
#include <Serialization.h>
|
#include <Serialization.h>
|
||||||
|
|
||||||
#include "../../../../src/CrossPointSettings.h"
|
#include "../../../../src/CrossPointSettings.h"
|
||||||
|
#include "../../../../src/fontIds.h"
|
||||||
#include "../converters/DirectPixelWriter.h"
|
#include "../converters/DirectPixelWriter.h"
|
||||||
#include "../converters/ImageDecoderFactory.h"
|
#include "../converters/ImageDecoderFactory.h"
|
||||||
|
#include "../converters/JpegToFramebufferConverter.h"
|
||||||
|
#include "../converters/PngToFramebufferConverter.h"
|
||||||
|
|
||||||
// Cache file format:
|
// Cache file format:
|
||||||
// - uint16_t width
|
// - uint16_t width
|
||||||
// - uint16_t height
|
// - uint16_t height
|
||||||
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||||
|
|
||||||
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height, const std::string& altText)
|
||||||
: imagePath(imagePath), width(width), height(height) {}
|
: imagePath(imagePath), altText(altText), width(width), height(height) {}
|
||||||
|
|
||||||
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
||||||
|
|
||||||
@@ -97,7 +102,56 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
|
|||||||
|
|
||||||
} // namespace
|
} // 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 bool hasAlt = !altText.empty();
|
||||||
|
const int lineCount = hasAlt ? 3 : 2;
|
||||||
|
const int totalTextH = lineH * lineCount;
|
||||||
|
|
||||||
|
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, tr(STR_LARGE_IMAGE));
|
||||||
|
if (hasAlt) {
|
||||||
|
renderer.drawText(UI_10_FONT_ID, textX, textY + lineH, altText.c_str());
|
||||||
|
}
|
||||||
|
renderer.drawText(UI_10_FONT_ID, textX, textY + lineH * (lineCount - 1), tr(STR_PRESS_CONFIRM_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);
|
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
||||||
|
|
||||||
const int screenWidth = renderer.getScreenWidth();
|
const int screenWidth = renderer.getScreenWidth();
|
||||||
@@ -110,15 +164,21 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to render from cache first
|
// Try to render from pixel cache first (always, regardless of forceLoad)
|
||||||
const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering);
|
const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering);
|
||||||
std::string cachePath = getCachePath(imagePath, ditherMode);
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
// No cache - need to decode the image
|
// No pixel cache — check if this is a large image that should show a placeholder
|
||||||
// Check if image file exists
|
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;
|
FsFile file;
|
||||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||||
@@ -143,8 +203,8 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
config.useDithering = true;
|
config.useDithering = true;
|
||||||
config.ditherMode = ditherMode;
|
config.ditherMode = ditherMode;
|
||||||
config.performanceMode = false;
|
config.performanceMode = false;
|
||||||
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
config.useExactDimensions = true;
|
||||||
config.cachePath = cachePath; // Enable caching during decode
|
config.cachePath = cachePath;
|
||||||
|
|
||||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||||
if (!decoder) {
|
if (!decoder) {
|
||||||
@@ -157,16 +217,14 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
|||||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_DBG("IMG", "Decode successful");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ImageBlock::serialize(FsFile& file) {
|
bool ImageBlock::serialize(FsFile& file) {
|
||||||
serialization::writeString(file, imagePath);
|
serialization::writeString(file, imagePath);
|
||||||
serialization::writePod(file, width);
|
serialization::writePod(file, width);
|
||||||
serialization::writePod(file, height);
|
serialization::writePod(file, height);
|
||||||
|
serialization::writeString(file, altText);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,5 +234,7 @@ std::unique_ptr<ImageBlock> ImageBlock::deserialize(FsFile& file) {
|
|||||||
int16_t w, h;
|
int16_t w, h;
|
||||||
serialization::readPod(file, w);
|
serialization::readPod(file, w);
|
||||||
serialization::readPod(file, h);
|
serialization::readPod(file, h);
|
||||||
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
std::string alt;
|
||||||
|
serialization::readString(file, alt);
|
||||||
|
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h, alt));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,26 +6,45 @@
|
|||||||
|
|
||||||
#include "Block.h"
|
#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 {
|
class ImageBlock final : public Block {
|
||||||
public:
|
public:
|
||||||
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
|
ImageBlock(const std::string& imagePath, int16_t width, int16_t height, const std::string& altText = "");
|
||||||
~ImageBlock() override = default;
|
~ImageBlock() override = default;
|
||||||
|
|
||||||
const std::string& getImagePath() const { return imagePath; }
|
const std::string& getImagePath() const { return imagePath; }
|
||||||
int16_t getWidth() const { return width; }
|
int16_t getWidth() const { return width; }
|
||||||
int16_t getHeight() const { return height; }
|
int16_t getHeight() const { return height; }
|
||||||
|
const std::string& getAltText() const { return altText; }
|
||||||
|
|
||||||
bool imageExists() const;
|
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; }
|
BlockType getType() override { return IMAGE_BLOCK; }
|
||||||
bool isEmpty() override { return false; }
|
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);
|
bool serialize(FsFile& file);
|
||||||
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
|
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string imagePath;
|
std::string imagePath;
|
||||||
|
std::string altText;
|
||||||
int16_t width;
|
int16_t width;
|
||||||
int16_t height;
|
int16_t height;
|
||||||
|
|
||||||
|
mutable int8_t largeImageCached = 0; // 0=unchecked, 1=large, -1=not large
|
||||||
|
|
||||||
|
void renderPlaceholder(GfxRenderer& renderer, int x, int y) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -849,7 +849,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->currentPageNextY += imageSpacingTop;
|
self->currentPageNextY += imageSpacingTop;
|
||||||
|
|
||||||
// Create ImageBlock and add to page
|
// Create ImageBlock and add to page
|
||||||
auto imageBlock = std::make_shared<ImageBlock>(cachedImagePath, displayWidth, displayHeight);
|
auto imageBlock = std::make_shared<ImageBlock>(cachedImagePath, displayWidth, displayHeight, alt);
|
||||||
if (!imageBlock) {
|
if (!imageBlock) {
|
||||||
LOG_ERR("EHP", "Failed to create ImageBlock");
|
LOG_ERR("EHP", "Failed to create ImageBlock");
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -102,6 +102,9 @@ 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_LARGE_IMAGE_PLACEHOLDER: "Placeholder for large images"
|
||||||
|
STR_LARGE_IMAGE: "Large Image"
|
||||||
|
STR_PRESS_CONFIRM_TO_LOAD: "Press Confirm to load"
|
||||||
STR_IMAGE_DITHERING: "Image Dithering"
|
STR_IMAGE_DITHERING: "Image Dithering"
|
||||||
STR_IMAGE_DITHER_BAYER: "Bayer"
|
STR_IMAGE_DITHER_BAYER: "Bayer"
|
||||||
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
|
STR_IMAGE_DITHER_ATKINSON: "Atkinson"
|
||||||
|
|||||||
@@ -267,6 +267,9 @@ class CrossPointSettings {
|
|||||||
uint8_t showFileExtensions = 0;
|
uint8_t showFileExtensions = 0;
|
||||||
// Image rendering mode in EPUB reader
|
// Image rendering mode in EPUB reader
|
||||||
uint8_t imageRendering = IMAGES_DISPLAY;
|
uint8_t imageRendering = IMAGES_DISPLAY;
|
||||||
|
// Show a placeholder for large images (>800×600 source pixels) instead of decoding immediately.
|
||||||
|
// The user can press OK on the placeholder page to decode the image on demand.
|
||||||
|
uint8_t largeImagePlaceholder = 1;
|
||||||
// Dithering mode for decoded images (EPUB/JPG/PNG)
|
// Dithering mode for decoded images (EPUB/JPG/PNG)
|
||||||
uint8_t imageDithering = IMAGE_DITHER_BAYER;
|
uint8_t imageDithering = IMAGE_DITHER_BAYER;
|
||||||
// Tilt-based page turning (X3 only — requires QMI8658 IMU)
|
// Tilt-based page turning (X3 only — requires QMI8658 IMU)
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ inline const std::vector<SettingInfo> list = {
|
|||||||
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),
|
||||||
|
SettingInfo::Toggle(StrId::STR_LARGE_IMAGE_PLACEHOLDER, &CrossPointSettings::largeImagePlaceholder,
|
||||||
|
"largeImagePlaceholder", StrId::STR_CAT_READER),
|
||||||
SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin",
|
SettingInfo::Value(StrId::STR_SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}, "screenMargin",
|
||||||
StrId::STR_CAT_READER)
|
StrId::STR_CAT_READER)
|
||||||
.withSubmenu(StrId::STR_MENU_READER_SPACING),
|
.withSubmenu(StrId::STR_MENU_READER_SPACING),
|
||||||
|
|||||||
@@ -407,6 +407,12 @@ void EpubReaderActivity::loop() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ev.type == ButtonEventManager::PressType::Short) {
|
if (ev.type == ButtonEventManager::PressType::Short) {
|
||||||
|
if (pageHasPlaceholders) {
|
||||||
|
forceLoadLargeImages = true;
|
||||||
|
pageHasPlaceholders = false;
|
||||||
|
requestUpdate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
openReaderMenu();
|
openReaderMenu();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -607,6 +613,8 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
|
|||||||
: std::nullopt;
|
: std::nullopt;
|
||||||
if (resolvedPage) {
|
if (resolvedPage) {
|
||||||
section->currentPage = *resolvedPage;
|
section->currentPage = *resolvedPage;
|
||||||
|
forceLoadLargeImages = false;
|
||||||
|
pageHasPlaceholders = false;
|
||||||
} else {
|
} else {
|
||||||
navTarget =
|
navTarget =
|
||||||
chapter.tocIndex ? NavigationTarget::makeTocIndex(*chapter.tocIndex) : NavigationTarget::makePage(0);
|
chapter.tocIndex ? NavigationTarget::makeTocIndex(*chapter.tocIndex) : NavigationTarget::makePage(0);
|
||||||
@@ -1512,6 +1520,8 @@ bool EpubReaderActivity::stepPageState(const bool isForwardTurn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lastPageTurnTime = millis();
|
lastPageTurnTime = millis();
|
||||||
|
forceLoadLargeImages = false;
|
||||||
|
pageHasPlaceholders = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1782,6 +1792,8 @@ void EpubReaderActivity::render(RenderLock&& lock) {
|
|||||||
|
|
||||||
navTarget.resolveInto(*section, currentSpineIndex);
|
navTarget.resolveInto(*section, currentSpineIndex);
|
||||||
navTarget = NavigationTarget::makePage(section->currentPage);
|
navTarget = NavigationTarget::makePage(section->currentPage);
|
||||||
|
forceLoadLargeImages = false;
|
||||||
|
pageHasPlaceholders = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer.clearScreen();
|
renderer.clearScreen();
|
||||||
@@ -1949,15 +1961,20 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
}
|
}
|
||||||
lastRenderStats.textAntiAliasing = aaEnabledForThisRender;
|
lastRenderStats.textAntiAliasing = aaEnabledForThisRender;
|
||||||
|
|
||||||
// Force special handling for pages with images when anti-aliasing is on
|
const bool effectiveForceLoad = forceLoadLargeImages || !SETTINGS.largeImagePlaceholder;
|
||||||
bool imagePageWithAA = page->hasImages() && aaEnabledForThisRender;
|
pageHasPlaceholders = page->hasPlaceholderImages(effectiveForceLoad);
|
||||||
|
|
||||||
|
// Force special handling for pages with at least one real (decoded) image when anti-aliasing is on.
|
||||||
|
// Mixed pages (some decoded, some placeholder) still need the AA codepath.
|
||||||
|
bool imagePageWithAA =
|
||||||
|
page->hasImages() && !page->allImagesArePlaceholders(effectiveForceLoad) && aaEnabledForThisRender;
|
||||||
bool forceHalfRefreshThisPage = pendingHalfRefreshAfterImagePage && SETTINGS.halfRefreshAfterImagePage;
|
bool forceHalfRefreshThisPage = pendingHalfRefreshAfterImagePage && SETTINGS.halfRefreshAfterImagePage;
|
||||||
pendingHalfRefreshAfterImagePage = false;
|
pendingHalfRefreshAfterImagePage = false;
|
||||||
lastRenderStats.imagePageWithAA = imagePageWithAA;
|
lastRenderStats.imagePageWithAA = imagePageWithAA;
|
||||||
lastRenderStats.forcedHalfRefresh = forceHalfRefreshThisPage;
|
lastRenderStats.forcedHalfRefresh = forceHalfRefreshThisPage;
|
||||||
|
|
||||||
logReaderMemSnapshot("before_bw_render");
|
logReaderMemSnapshot("before_bw_render");
|
||||||
page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop, effectiveForceLoad);
|
||||||
renderStatusBar();
|
renderStatusBar();
|
||||||
if (showTruncatedSectionHintThisRender) {
|
if (showTruncatedSectionHintThisRender) {
|
||||||
const int hintX = orientedMarginLeft + 4;
|
const int hintX = orientedMarginLeft + 4;
|
||||||
@@ -1993,7 +2010,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
|
|
||||||
// Re-render page content to restore images into the blanked area
|
// Re-render page content to restore images into the blanked area
|
||||||
// Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %)
|
// Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %)
|
||||||
page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop);
|
page->render(renderer, getEffectiveReaderFontId(), orientedMarginLeft, contentTop, effectiveForceLoad);
|
||||||
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
||||||
} else {
|
} else {
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
||||||
@@ -2051,7 +2068,10 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)");
|
LOG_INF("ERS", "Skipping grayscale/BW-restore for this page (insufficient heap for BW snapshot)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (page->hasImages() && getEffectiveImageRendering() != CrossPointSettings::IMAGES_SUPPRESS) {
|
// Only schedule the half-refresh if at least one real image was decoded on this page.
|
||||||
|
// Placeholder-only pages don't deposit grayscale data that needs settling.
|
||||||
|
if (page->hasImages() && !page->allImagesArePlaceholders(effectiveForceLoad) &&
|
||||||
|
getEffectiveImageRendering() != CrossPointSettings::IMAGES_SUPPRESS) {
|
||||||
pendingHalfRefreshAfterImagePage = true;
|
pendingHalfRefreshAfterImagePage = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2137,8 +2157,8 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::renderPageContentOnly(const Page& page, const int orientedMarginTop,
|
void EpubReaderActivity::renderPageContentOnly(const Page& page, const int orientedMarginTop,
|
||||||
const int orientedMarginRight,
|
const int orientedMarginRight, const int orientedMarginBottom,
|
||||||
const int orientedMarginBottom, const int orientedMarginLeft) {
|
const int orientedMarginLeft) {
|
||||||
auto* fcm = renderer.getFontCacheManager();
|
auto* fcm = renderer.getFontCacheManager();
|
||||||
fcm->resetStats();
|
fcm->resetStats();
|
||||||
|
|
||||||
@@ -2155,8 +2175,8 @@ void EpubReaderActivity::renderPageContentOnly(const Page& page, const int orien
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EpubReaderActivity::displayPreRenderedPage(const Page& page, const int orientedMarginTop,
|
void EpubReaderActivity::displayPreRenderedPage(const Page& page, const int orientedMarginTop,
|
||||||
const int orientedMarginRight,
|
const int orientedMarginRight, const int orientedMarginBottom,
|
||||||
const int orientedMarginBottom, const int orientedMarginLeft) {
|
const int orientedMarginLeft) {
|
||||||
const int viewportHeight = std::max(0, renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom);
|
const int viewportHeight = std::max(0, renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom);
|
||||||
const int contentTop = orientedMarginTop + getImageOnlyPageYOffset(page, viewportHeight);
|
const int contentTop = orientedMarginTop + getImageOnlyPageYOffset(page, viewportHeight);
|
||||||
|
|
||||||
@@ -2509,6 +2529,8 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION
|
|||||||
: std::nullopt;
|
: std::nullopt;
|
||||||
if (resolvedPage) {
|
if (resolvedPage) {
|
||||||
section->currentPage = *resolvedPage;
|
section->currentPage = *resolvedPage;
|
||||||
|
forceLoadLargeImages = false;
|
||||||
|
pageHasPlaceholders = false;
|
||||||
} else {
|
} else {
|
||||||
navTarget = chapter.tocIndex ? NavigationTarget::makeTocIndex(*chapter.tocIndex)
|
navTarget = chapter.tocIndex ? NavigationTarget::makeTocIndex(*chapter.tocIndex)
|
||||||
: NavigationTarget::makePage(0);
|
: NavigationTarget::makePage(0);
|
||||||
@@ -2535,6 +2557,8 @@ void EpubReaderActivity::onButtonAction(const CrossPointSettings::BUTTON_ACTION
|
|||||||
if (newSpineIndex == currentSpineIndex) {
|
if (newSpineIndex == currentSpineIndex) {
|
||||||
if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) {
|
if (const auto resolvedPage = section->getPageForTocIndex(nextTocIndex)) {
|
||||||
section->currentPage = *resolvedPage;
|
section->currentPage = *resolvedPage;
|
||||||
|
forceLoadLargeImages = false;
|
||||||
|
pageHasPlaceholders = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
navTarget = NavigationTarget::makeTocIndex(nextTocIndex);
|
navTarget = NavigationTarget::makeTocIndex(nextTocIndex);
|
||||||
|
|||||||
@@ -109,6 +109,11 @@ class EpubReaderActivity final : public Activity {
|
|||||||
unsigned long lastPageTurnTime = 0UL;
|
unsigned long lastPageTurnTime = 0UL;
|
||||||
unsigned long pageTurnDuration = 0UL;
|
unsigned long pageTurnDuration = 0UL;
|
||||||
bool pendingHalfRefreshAfterImagePage = false;
|
bool pendingHalfRefreshAfterImagePage = false;
|
||||||
|
// When true, large images on the current page are decoded instead of shown as placeholders.
|
||||||
|
// Reset to false on every page turn so the next image page starts with a placeholder again.
|
||||||
|
bool forceLoadLargeImages = false;
|
||||||
|
// Set after each render: true if the current page contains at least one placeholder image.
|
||||||
|
bool pageHasPlaceholders = false;
|
||||||
// Temporary AA suspension when BW snapshot allocation fails under memory pressure.
|
// Temporary AA suspension when BW snapshot allocation fails under memory pressure.
|
||||||
// Automatically lifted once heap recovers above hysteresis thresholds.
|
// Automatically lifted once heap recovers above hysteresis thresholds.
|
||||||
bool antiAliasingSuspendedLowMemory = false;
|
bool antiAliasingSuspendedLowMemory = false;
|
||||||
@@ -254,8 +259,8 @@ class EpubReaderActivity final : public Activity {
|
|||||||
// Draws the status bar over the current frame buffer and flushes to the display.
|
// Draws the status bar over the current frame buffer and flushes to the display.
|
||||||
// Handles the refresh cycle and grayscale AA pass. page must be the same page
|
// Handles the refresh cycle and grayscale AA pass. page must be the same page
|
||||||
// that was last rendered into the buffer (needed for image AA re-render).
|
// that was last rendered into the buffer (needed for image AA re-render).
|
||||||
void displayPreRenderedPage(const Page& page, int orientedMarginTop, int orientedMarginRight, int orientedMarginBottom,
|
void displayPreRenderedPage(const Page& page, int orientedMarginTop, int orientedMarginRight,
|
||||||
int orientedMarginLeft);
|
int orientedMarginBottom, int orientedMarginLeft);
|
||||||
void renderStatusBar() const;
|
void renderStatusBar() const;
|
||||||
void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight);
|
void silentIndexNextChapterIfNeeded(uint16_t viewportWidth, uint16_t viewportHeight);
|
||||||
void saveProgress(int spineIndex, int currentPage, int pageCount);
|
void saveProgress(int spineIndex, int currentPage, int pageCount);
|
||||||
|
|||||||
Reference in New Issue
Block a user