feat: Add EPUB embedded image support (JPEG/PNG)

Cherry-pick merge from pablohc/crosspoint-reader@2d8cbcf, based on
upstream PR #556 by martinbrook with pablohc's refresh optimization.

- Add JPEG decoder (picojpeg) and PNG decoder (PNGdec) with 4-level
  grayscale Bayer dithering for e-ink display
- Add pixel caching system (.pxc files) for fast image re-rendering
- Integrate image extraction from EPUB HTML parser (<img> tag support)
- Add ImageBlock/PageImage types with serialization support
- Add image-aware refresh optimization (double FAST_REFRESH technique)
- Add experimental displayWindow() partial refresh support
- Bump section cache version 12->13 to invalidate stale caches
- Resolve TAG_PageImage=3 to avoid conflict with mod's TAG_PageTableRow=2

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
cottongin
2026-02-15 17:29:39 -05:00
parent f90aebc891
commit 19004eefaa
26 changed files with 1984 additions and 30 deletions

View File

@@ -22,6 +22,11 @@
#include "util/BookmarkStore.h"
#include "util/Dictionary.h"
// Image refresh optimization strategy:
// 0 = Use double FAST_REFRESH technique (default, feels snappier)
// 1 = Use displayWindow() for partial refresh (experimental)
#define USE_IMAGE_DOUBLE_FAST_REFRESH 0
namespace {
// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()
constexpr unsigned long skipChapterMs = 700;
@@ -972,6 +977,14 @@ void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageC
void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop,
const int orientedMarginRight, const int orientedMarginBottom,
const int orientedMarginLeft) {
// Determine if this page needs special image handling
bool pageHasImages = page->hasImages();
bool useAntiAliasing = SETTINGS.textAntiAliasing;
// Force half refresh for pages with images when anti-aliasing is on,
// as grayscale tones require half refresh to display correctly
bool forceFullRefresh = pageHasImages && useAntiAliasing;
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
// Draw bookmark ribbon indicator in top-right corner if current page is bookmarked
@@ -990,10 +1003,42 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
}
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
// Check if half-refresh is needed (either entering Reader or pages counter reached)
if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else if (forceFullRefresh) {
// OPTIMIZATION: For image pages with anti-aliasing, use fast double-refresh technique
// to reduce perceived lag. Only when pagesUntilFullRefresh > 1 (screen already clean).
int imgX, imgY, imgW, imgH;
if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) {
int screenX = imgX + orientedMarginLeft;
int screenY = imgY + orientedMarginTop;
LOG_DBG("ERS", "Image page: fast double-refresh (page bbox: %d,%d %dx%d, screen: %d,%d %dx%d)",
imgX, imgY, imgW, imgH, screenX, screenY, imgW, imgH);
#if USE_IMAGE_DOUBLE_FAST_REFRESH == 0
// Method A: Fill blank area + two FAST_REFRESH operations
renderer.fillRect(screenX, screenY, imgW, imgH, false);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
#else
// Method B (experimental): Use displayWindow() for partial refresh
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayWindow(screenX, screenY, imgW, imgH, HalDisplay::FAST_REFRESH);
#endif
} else {
LOG_DBG("ERS", "Image page but no bbox, using standard half refresh");
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
pagesUntilFullRefresh--;
} else {
// Normal page without images, or images without anti-aliasing
renderer.displayBuffer();
pagesUntilFullRefresh--;
}