Fix image display inside epubs

This commit is contained in:
jpirnay
2026-05-24 22:04:16 +02:00
parent 28b4ab7b5a
commit 285717fe92
5 changed files with 97 additions and 47 deletions
+20 -4
View File
@@ -30,8 +30,9 @@ void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffse
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);
void PageImage::renderWithForceLoad(GfxRenderer& renderer, const int xOffset, const int yOffset, const bool forceLoad,
const bool monochromeOutput) {
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset, forceLoad, monochromeOutput);
}
bool PageImage::serialize(FsFile& file) {
@@ -205,10 +206,12 @@ std::unique_ptr<PageTableFragment> PageTableFragment::deserialize(FsFile& file)
}
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset,
const bool forceLoadLargeImages) const {
const bool forceLoadLargeImages, const bool skipDecodedImages, const bool monochromeImages) const {
for (auto& element : elements) {
if (element->getTag() == TAG_PageImage) {
static_cast<PageImage&>(*element).renderWithForceLoad(renderer, xOffset, yOffset, forceLoadLargeImages);
auto& pi = static_cast<PageImage&>(*element);
if (skipDecodedImages && !pi.getImageBlock().wouldShowPlaceholder(forceLoadLargeImages)) continue;
pi.renderWithForceLoad(renderer, xOffset, yOffset, forceLoadLargeImages, monochromeImages);
} else {
element->render(renderer, fontId, xOffset, yOffset);
}
@@ -262,6 +265,19 @@ void Page::renderTextOnly(GfxRenderer& renderer, const int fontId, const int xOf
}
}
void Page::renderImagesOnly(GfxRenderer& renderer, const int xOffset, const int yOffset,
const bool forceLoadLargeImages) const {
for (auto& element : elements) {
if (element->getTag() == TAG_PageImage) {
auto& pi = static_cast<PageImage&>(*element);
// Placeholders already drew in the BW pass; the grayscale path is only
// for images with actual decoded pixel data.
if (pi.getImageBlock().wouldShowPlaceholder(forceLoadLargeImages)) continue;
pi.renderWithForceLoad(renderer, xOffset, yOffset, forceLoadLargeImages, false);
}
}
}
bool Page::serialize(FsFile& file) const {
const uint16_t count = elements.size();
serialization::writePod(file, count);
+14 -2
View File
@@ -58,7 +58,8 @@ 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);
void renderWithForceLoad(GfxRenderer& renderer, int xOffset, int yOffset, bool forceLoad,
bool monochromeOutput = false);
bool serialize(FsFile& file) override;
PageElementTag getTag() const override { return TAG_PageImage; }
static std::unique_ptr<PageImage> deserialize(FsFile& file);
@@ -129,8 +130,19 @@ class Page {
footnotes.push_back(entry);
}
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool forceLoadLargeImages = true) const;
// skipDecodedImages=true: PageImages that would decode (not show as a placeholder)
// are skipped. Used by the BW pass when an AA grayscale-image pass will redraw
// them at full 4-level tonality. Placeholders still render in BW (they're just
// a box+text, not dithered pixels).
// monochromeImages=true: render images via the 1-bit Atkinson path. Used when
// no grayscale image pass will follow (AA off or low-mem) so the BW
// DirectPixelWriter `<3` rule yields clean black/white instead of muddy dark.
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset, bool forceLoadLargeImages = true,
bool skipDecodedImages = false, bool monochromeImages = false) const;
void renderTextOnly(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
// Renders only PageImages that have actual decoded pixels (skips placeholders,
// which already rendered in the BW pass as a box+text).
void renderImagesOnly(GfxRenderer& renderer, int xOffset, int yOffset, bool forceLoadLargeImages) const;
// Decode any missing .pxc pixel caches for images on this page. Called before the
// BW render so the large (~60 KB contiguous) PNG decoder allocation runs while heap
// contig is at its peak — before font prewarm and BW backup chunks fragment it.
+15 -13
View File
@@ -25,13 +25,16 @@ bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str());
namespace {
std::string getCachePath(const std::string& imagePath, ImageDitherMode ditherMode) {
std::string getCachePath(const std::string& imagePath, ImageDitherMode ditherMode, bool monochromeOutput) {
// The monochrome (1-bit Atkinson) cache only stores values 0/3, so it can't be
// reused for the 4-level grayscale render path — keep it in a separate file.
const char* monoSuffix = monochromeOutput ? "_mono" : "";
// Replace extension with .pxc (pixel cache)
size_t dotPos = imagePath.rfind('.');
if (dotPos != std::string::npos) {
return imagePath.substr(0, dotPos) + getImageDitherCacheSuffix(ditherMode) + ".pxc";
return imagePath.substr(0, dotPos) + getImageDitherCacheSuffix(ditherMode) + monoSuffix + ".pxc";
}
return imagePath + getImageDitherCacheSuffix(ditherMode) + ".pxc";
return imagePath + getImageDitherCacheSuffix(ditherMode) + monoSuffix + ".pxc";
}
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
@@ -118,7 +121,7 @@ bool ImageBlock::isLargeImage() const {
bool ImageBlock::hasPixelCache() const {
const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering);
return Storage.exists(getCachePath(imagePath, ditherMode).c_str());
return Storage.exists(getCachePath(imagePath, ditherMode, false).c_str());
}
bool ImageBlock::wouldShowPlaceholder(bool forceLoad) const {
@@ -126,12 +129,7 @@ bool ImageBlock::wouldShowPlaceholder(bool forceLoad) const {
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());
return !Storage.exists(getCachePath(imagePath, ditherMode, false).c_str());
}
void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int y) const {
@@ -156,7 +154,8 @@ void ImageBlock::renderPlaceholder(GfxRenderer& renderer, const int x, const int
}
}
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y, const bool forceLoad) {
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y, const bool forceLoad,
const bool monochromeOutput) {
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
const int screenWidth = renderer.getScreenWidth();
@@ -169,9 +168,11 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y, const b
return;
}
// Try to render from pixel cache first (always, regardless of forceLoad)
// Try to render from pixel cache first (always, regardless of forceLoad).
// Mono and 4-level caches are kept in separate files so each render path gets
// the dither output its DirectPixelWriter branch expects.
const ImageDitherMode ditherMode = imageDitherModeFromSetting(SETTINGS.imageDithering);
std::string cachePath = getCachePath(imagePath, ditherMode);
std::string cachePath = getCachePath(imagePath, ditherMode, monochromeOutput);
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
return;
}
@@ -210,6 +211,7 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y, const b
config.performanceMode = false;
config.useExactDimensions = true;
config.cachePath = cachePath;
config.monochromeOutput = monochromeOutput;
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
if (!decoder) {
+5 -1
View File
@@ -38,7 +38,11 @@ class ImageBlock final : public Block {
BlockType getType() override { return IMAGE_BLOCK; }
bool isEmpty() override { return false; }
void render(GfxRenderer& renderer, int x, int y, bool forceLoad = true);
// monochromeOutput=true switches the decode pipeline to a 1-bit Atkinson dither
// that emits only levels 0 and 3, so the BW DirectPixelWriter `<3` rule yields
// clean black/white instead of collapsing mid-greys to black. Used by the BW
// pass when the AA grayscale image pass isn't available (AA off or low-mem).
void render(GfxRenderer& renderer, int x, int y, bool forceLoad = true, bool monochromeOutput = false);
bool serialize(FsFile& file);
static std::unique_ptr<ImageBlock> deserialize(FsFile& file);