mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf9fea5923 |
No files matched your search
@@ -33,7 +33,10 @@ namespace {
|
|||||||
// v35: Persist a uint32_t visible-text start offset for every page.
|
// v35: Persist a uint32_t visible-text start offset for every page.
|
||||||
// v36: Ruby and CJK justification layout changes invalidate cached word positions.
|
// v36: Ruby and CJK justification layout changes invalidate cached word positions.
|
||||||
// v37: Footnote href records grew from 96 to 256 bytes.
|
// v37: Footnote href records grew from 96 to 256 bytes.
|
||||||
constexpr uint8_t SECTION_FILE_VERSION = 37;
|
// v38: Image top margin is clamped so a full-viewport-height image cannot
|
||||||
|
// overflow the page bottom; older caches can hold placements that panels
|
||||||
|
// with no bottom inset refuse to draw.
|
||||||
|
constexpr uint8_t SECTION_FILE_VERSION = 38;
|
||||||
// Written into the version field while a build is in progress; patched to
|
// Written into the version field while a build is in progress; patched to
|
||||||
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
|
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
|
||||||
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
|
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
#include "ImageToFramebufferDecoder.h"
|
#include "ImageToFramebufferDecoder.h"
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
#include <Logging.h>
|
#include <Logging.h>
|
||||||
|
|
||||||
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||||
if (width * height > MAX_SOURCE_PIXELS) {
|
// 64-bit multiply: a malformed header (e.g. 40000x60000) overflows a plain
|
||||||
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
|
// int product before the comparison can reject it.
|
||||||
format.c_str(), MAX_SOURCE_PIXELS);
|
const int64_t pixels = static_cast<int64_t>(width) * static_cast<int64_t>(height);
|
||||||
|
if (pixels > MAX_SOURCE_PIXELS) {
|
||||||
|
LOG_ERR("IMG", "Image too large (%dx%d = %lld pixels %s), max supported: %lld pixels", width, height,
|
||||||
|
static_cast<long long>(pixels), format.c_str(), static_cast<long long>(MAX_SOURCE_PIXELS));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ImageToFramebufferDecoder::yieldDuringDecode(uint32_t& lastYieldMs) {
|
||||||
|
const uint32_t now = millis();
|
||||||
|
if (now - lastYieldMs >= 250) {
|
||||||
|
lastYieldMs = now;
|
||||||
|
vTaskDelay(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||||
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
||||||
imagePath.c_str());
|
imagePath.c_str());
|
||||||
|
|||||||
@@ -31,9 +31,21 @@ class ImageToFramebufferDecoder {
|
|||||||
|
|
||||||
virtual const char* getFormatName() const = 0;
|
virtual const char* getFormatName() const = 0;
|
||||||
|
|
||||||
|
// Call from per-row/per-MCU decode callbacks (free functions, hence public):
|
||||||
|
// yields one tick at most every 250 ms so multi-second decodes keep the idle
|
||||||
|
// task (and its watchdog) fed. `lastYieldMs` is caller-held state,
|
||||||
|
// initialized to the decode start time.
|
||||||
|
static void yieldDuringDecode(uint32_t& lastYieldMs);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// Size validation helpers
|
// Size validation helpers. The cap bounds decode TIME, not memory: both decoders
|
||||||
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
|
// stream (JPEG in MCU bands at 1/2..1/8 coarse scale, PNG scanline-by-scanline
|
||||||
|
// with its own width-based row-buffer guard), so RAM never scales with source
|
||||||
|
// area. 8 MP admits real-world ebook covers (KDP recommends 1600x2560 and
|
||||||
|
// 2000x3000) while keeping a worst-case single decode in single-digit seconds;
|
||||||
|
// the row callbacks yield periodically so a long decode cannot starve the idle
|
||||||
|
// task's watchdog.
|
||||||
|
static constexpr int64_t MAX_SOURCE_PIXELS = 8388608; // 8 MP (e.g. 2048 * 4096)
|
||||||
|
|
||||||
bool validateImageDimensions(int width, int height, const std::string& format);
|
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||||
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ struct JpegContext {
|
|||||||
|
|
||||||
PixelCache cache;
|
PixelCache cache;
|
||||||
bool caching{false};
|
bool caching{false};
|
||||||
|
|
||||||
|
uint32_t lastYieldMs{0}; // throttle state for yieldDuringDecode()
|
||||||
};
|
};
|
||||||
|
|
||||||
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||||
@@ -122,6 +124,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
|||||||
JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser);
|
JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser);
|
||||||
if (!ctx || !ctx->config || !ctx->renderer) return 0;
|
if (!ctx || !ctx->config || !ctx->renderer) return 0;
|
||||||
|
|
||||||
|
ImageToFramebufferDecoder::yieldDuringDecode(ctx->lastYieldMs);
|
||||||
|
|
||||||
// In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values
|
// In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values
|
||||||
// Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed
|
// Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed
|
||||||
uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
|
uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
|
||||||
@@ -494,6 +498,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsigned long decodeStart = millis();
|
unsigned long decodeStart = millis();
|
||||||
|
ctx.lastYieldMs = decodeStart;
|
||||||
rc = jpeg->decode(0, 0, jpegScaleOption);
|
rc = jpeg->decode(0, 0, jpegScaleOption);
|
||||||
unsigned long decodeTime = millis() - decodeStart;
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ struct PngContext {
|
|||||||
bool caching{false};
|
bool caching{false};
|
||||||
|
|
||||||
uint8_t* grayLineBuffer{nullptr};
|
uint8_t* grayLineBuffer{nullptr};
|
||||||
|
|
||||||
|
uint32_t lastYieldMs{0}; // throttle state for yieldDuringDecode()
|
||||||
};
|
};
|
||||||
|
|
||||||
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||||
@@ -206,6 +208,8 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
|||||||
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
||||||
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
||||||
|
|
||||||
|
ImageToFramebufferDecoder::yieldDuringDecode(ctx->lastYieldMs);
|
||||||
|
|
||||||
int srcY = pDraw->y;
|
int srcY = pDraw->y;
|
||||||
int srcWidth = ctx->srcWidth;
|
int srcWidth = ctx->srcWidth;
|
||||||
|
|
||||||
@@ -432,6 +436,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsigned long decodeStart = millis();
|
unsigned long decodeStart = millis();
|
||||||
|
ctx.lastYieldMs = decodeStart;
|
||||||
rc = png->decode(&ctx, 0);
|
rc = png->decode(&ctx, 0);
|
||||||
unsigned long decodeTime = millis() - decodeStart;
|
unsigned long decodeTime = millis() - decodeStart;
|
||||||
|
|
||||||
|
|||||||
@@ -793,7 +793,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
|||||||
self->currentPageVisibleOffsetSet = false;
|
self->currentPageVisibleOffsetSet = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply top margin from container block
|
// Apply top margin from container block. Clamp it so the image never
|
||||||
|
// overflows the page bottom: a full-viewport-height image leaves no room
|
||||||
|
// for the margin, and the break above only fires on non-empty pages, so a
|
||||||
|
// fresh page would otherwise place the image at y=marginTop and run
|
||||||
|
// marginTop pixels past viewportHeight. A large bottom reserve (status
|
||||||
|
// bar / big screen margin) absorbs that overflow silently, but with a
|
||||||
|
// thin reserve it crosses the physical screen edge and fails
|
||||||
|
// ImageBlock::render's bounds check, dropping the image entirely.
|
||||||
|
if (self->currentPageNextY + imageMarginTop + displayHeight > self->viewportHeight) {
|
||||||
|
const int room = self->viewportHeight - displayHeight - self->currentPageNextY;
|
||||||
|
imageMarginTop = static_cast<int16_t>(room > 0 ? room : 0);
|
||||||
|
}
|
||||||
self->currentPageNextY += imageMarginTop;
|
self->currentPageNextY += imageMarginTop;
|
||||||
|
|
||||||
// Create ImageBlock and add to page
|
// Create ImageBlock and add to page
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "GfxRenderer.h"
|
#include "GfxRenderer.h"
|
||||||
|
|
||||||
#include <BidiUtils.h>
|
#include <BidiUtils.h>
|
||||||
|
#include <BoardConfig.h>
|
||||||
#include <BuildScratch.h>
|
#include <BuildScratch.h>
|
||||||
#include <FontDecompressor.h>
|
#include <FontDecompressor.h>
|
||||||
#include <HalGPIO.h>
|
#include <HalGPIO.h>
|
||||||
@@ -2220,30 +2221,33 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
|
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
|
||||||
|
// Board truth: the bezel insets live in the active profile (panel-native
|
||||||
|
// portrait frame); this only rotates them into the current orientation.
|
||||||
|
const BoardConfig::ViewableInsets& vi = BoardConfig::ACTIVE.viewableInsets;
|
||||||
switch (orientation) {
|
switch (orientation) {
|
||||||
case Portrait:
|
case Portrait:
|
||||||
*outTop = VIEWABLE_MARGIN_TOP;
|
*outTop = vi.top;
|
||||||
*outRight = VIEWABLE_MARGIN_RIGHT;
|
*outRight = vi.right;
|
||||||
*outBottom = VIEWABLE_MARGIN_BOTTOM;
|
*outBottom = vi.bottom;
|
||||||
*outLeft = VIEWABLE_MARGIN_LEFT;
|
*outLeft = vi.left;
|
||||||
break;
|
break;
|
||||||
case LandscapeClockwise:
|
case LandscapeClockwise:
|
||||||
*outTop = VIEWABLE_MARGIN_LEFT;
|
*outTop = vi.left;
|
||||||
*outRight = VIEWABLE_MARGIN_TOP;
|
*outRight = vi.top;
|
||||||
*outBottom = VIEWABLE_MARGIN_RIGHT;
|
*outBottom = vi.right;
|
||||||
*outLeft = VIEWABLE_MARGIN_BOTTOM;
|
*outLeft = vi.bottom;
|
||||||
break;
|
break;
|
||||||
case PortraitInverted:
|
case PortraitInverted:
|
||||||
*outTop = VIEWABLE_MARGIN_BOTTOM;
|
*outTop = vi.bottom;
|
||||||
*outRight = VIEWABLE_MARGIN_LEFT;
|
*outRight = vi.left;
|
||||||
*outBottom = VIEWABLE_MARGIN_TOP;
|
*outBottom = vi.top;
|
||||||
*outLeft = VIEWABLE_MARGIN_RIGHT;
|
*outLeft = vi.right;
|
||||||
break;
|
break;
|
||||||
case LandscapeCounterClockwise:
|
case LandscapeCounterClockwise:
|
||||||
*outTop = VIEWABLE_MARGIN_RIGHT;
|
*outTop = vi.right;
|
||||||
*outRight = VIEWABLE_MARGIN_BOTTOM;
|
*outRight = vi.bottom;
|
||||||
*outBottom = VIEWABLE_MARGIN_LEFT;
|
*outBottom = vi.left;
|
||||||
*outLeft = VIEWABLE_MARGIN_TOP;
|
*outLeft = vi.top;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -109,11 +109,6 @@ class GfxRenderer {
|
|||||||
: display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {}
|
: display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {}
|
||||||
~GfxRenderer() { freeBwBufferChunks(); }
|
~GfxRenderer() { freeBwBufferChunks(); }
|
||||||
|
|
||||||
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
|
||||||
static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
|
|
||||||
static constexpr int VIEWABLE_MARGIN_BOTTOM = 3;
|
|
||||||
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
|
|
||||||
|
|
||||||
// Setup
|
// Setup
|
||||||
void begin(); // must be called right after display.begin()
|
void begin(); // must be called right after display.begin()
|
||||||
void insertFont(int fontId, EpdFontFamily font);
|
void insertFont(int fontId, EpdFontFamily font);
|
||||||
|
|||||||
@@ -1619,28 +1619,14 @@ void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int or
|
|||||||
const auto tBwRender = millis();
|
const auto tBwRender = millis();
|
||||||
|
|
||||||
if (pageHasImages) {
|
if (pageHasImages) {
|
||||||
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
|
// Single FAST refresh with the image included, straight into the gray pass.
|
||||||
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
|
// FAST (not HALF) because HALF sets particles too firmly for the grayscale
|
||||||
// Instead, blank only the image area and do two fast refreshes.
|
// LUT to adjust (#1011). If large light-gray images wash out, the fix to
|
||||||
// Step 1: Display page with image area blanked (text appears, image area white)
|
// restore is #957's prep: blank the image bounding box, FAST, re-render,
|
||||||
// Step 2: Re-render with images and display again (images appear clean)
|
// FAST again, so the region reaches the gray pass from a uniform white.
|
||||||
int16_t imgX, imgY, imgW, imgH;
|
// Image pages intentionally bypass the regular refresh cadence; preserve a
|
||||||
if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) {
|
// pending clean base (silent restart / manual refresh) before the pipeline.
|
||||||
// Image pages intentionally bypass the regular refresh cadence. Preserve
|
renderer.displayBuffer(cleanImageBasePending ? HalDisplay::HALF_REFRESH : HalDisplay::FAST_REFRESH);
|
||||||
// a pending clean base before their double-FAST grayscale pipeline.
|
|
||||||
if (cleanImageBasePending) {
|
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
||||||
}
|
|
||||||
renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false);
|
|
||||||
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
||||||
|
|
||||||
// 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 %)
|
|
||||||
page->render(renderer, fontId, orientedMarginLeft, orientedMarginTop);
|
|
||||||
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
|
|
||||||
} else {
|
|
||||||
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
|
|
||||||
}
|
|
||||||
// The image's own page is handled above and doesn't count toward the full
|
// The image's own page is handled above and doesn't count toward the full
|
||||||
// refresh cadence. But the grayscale pass below leaves gray charge in the
|
// refresh cadence. But the grayscale pass below leaves gray charge in the
|
||||||
// image region that a plain fast diff on the *next* page can't clear, so
|
// image region that a plain fast diff on the *next* page can't clear, so
|
||||||
|
|||||||
Reference in new issue
Block a user