6 Commits

Author SHA1 Message Date
cottongin
18be265a4a fix: Re-apply upstream PRs #1005, #1010, #1003
Re-applies changes that were accidentally discarded during a prior
dry-run cherry-pick reset (git checkout -- .).

- PR #1005: Use HalPowerManager for battery percentage (uint16_t return
  type, remove Battery.h, update theme files)
- PR #1010: Fix dangling pointer in onGoToReader()
- PR #1003: Render image placeholders while waiting for decode (adds
  isCached, renderPlaceholder, renderTextOnly, countUncachedImages,
  renderImagePlaceholders)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 22:31:07 -05:00
cottongin
3a0641889f perf: Port upstream font drawing performance optimization (PR #978)
Cherry-pick upstream commit 07d715e which refactors renderChar and
drawTextRotated90CW into a template-based renderCharImpl, hoisting
the is2Bit branch outside inner pixel loops for 15-23% speedup.

Additionally extends the template with Rotated90CCW to fix two bugs
in the mod's drawTextRotated90CCW: operator precedence in bmpVal
calculation and missing compressed font support via getGlyphBitmap.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 22:20:44 -05:00
cottongin
ad282cadfe fix: Align double FAST_REFRESH image rendering with upstream PR #957
Reorder refresh branches so image+AA pages always use the double
FAST_REFRESH technique instead of occasionally falling through to
HALF_REFRESH when the refresh counter expires. Image pages no longer
count toward the full refresh cadence. Remove experimental Method B
toggle (USE_IMAGE_DOUBLE_FAST_REFRESH / displayWindow).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 14:30:10 -05:00
cottongin
c8ba4fe973 fix: Port upstream CSS-aware image sizing (PR #1002)
Parse CSS height/width into CssStyle for images and use aspect-ratio-
preserving logic when CSS dimensions are set. Falls back to viewport-fit
scaling when no CSS dimensions are present. Includes divide-by-zero
guards and viewport clamping with aspect ratio rescaling.

- Add imageHeight field to CssStyle/CssPropertyFlags
- Parse CSS height declarations into imageHeight
- Add imageHeight + width to cache serialization (bump cache v2->v3)
- Replace viewport-fit-only image scaling with CSS-aware sizing

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 14:21:31 -05:00
cottongin
c1b8e53138 fix: Port upstream 1.1.0-rc fixes (glyph null-safety, PNGdec wide image buffer)
Cherry-pick two bug fixes from upstream PR #992:

- fix(GfxRenderer): Null-safety in getSpaceWidth/getTextAdvanceX to
  prevent Load access fault when bold/italic font variants lack certain
  glyphs (upstream 3e2c518)
- fix(PNGdec): Increase PNG_MAX_BUFFERED_PIXELS to 16416 for 2048px
  wide images and add pre-decode buffer overflow guard (upstream b8e743e)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 13:20:30 -05:00
cottongin
0fda9031fd fix: Use double FAST_REFRESH for dithered letterbox sleep covers
Replace HALF_REFRESH with double FAST_REFRESH technique for the BW
pass when dithered letterbox fill is active. This avoids the e-ink
crosstalk and image corruption that occurred when HALF_REFRESH drove
large areas of dithered gray pixels simultaneously.

Revert the hash-based block dithering workaround (bayerCrossesBwBoundary,
hashBlockDither) back to standard Bayer dithering for all gray ranges,
since the root cause was HALF_REFRESH rather than the dithering pattern
itself.

Letterbox fill is now included in all three render passes (BW, LSB, MSB)
so the greyscale LUT treats letterbox pixels identically to cover pixels,
maintaining color-matched edges.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-19 11:33:45 -05:00
20 changed files with 386 additions and 358 deletions

View File

@@ -61,6 +61,49 @@ std::unique_ptr<PageImage> PageImage::deserialize(FsFile& file) {
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
}
bool PageImage::isCached() const { return imageBlock->isCached(); }
void PageImage::renderPlaceholder(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
int x = xPos + xOffset;
int y = yPos + yOffset;
int w = imageBlock->getWidth();
int h = imageBlock->getHeight();
renderer.fillRect(x, y, w, h, true);
if (w > 2 && h > 2) {
renderer.fillRect(x + 1, y + 1, w - 2, h - 2, 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) {
element->render(renderer, fontId, xOffset, yOffset);
}
}
}
int Page::countUncachedImages() const {
int count = 0;
for (auto& element : elements) {
if (element->getTag() == TAG_PageImage) {
auto* img = static_cast<PageImage*>(element.get());
if (!img->isCached()) {
count++;
}
}
}
return count;
}
void Page::renderImagePlaceholders(GfxRenderer& renderer, const int xOffset, const int yOffset) const {
for (auto& element : elements) {
if (element->getTag() == TAG_PageImage) {
auto* img = static_cast<PageImage*>(element.get());
img->renderPlaceholder(renderer, xOffset, yOffset);
}
}
}
// ---------------------------------------------------------------------------
// PageTableRow
// ---------------------------------------------------------------------------

View File

@@ -80,6 +80,8 @@ class PageImage final : public PageElement {
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
bool serialize(FsFile& file) override;
PageElementTag getTag() const override { return TAG_PageImage; }
bool isCached() const;
void renderPlaceholder(GfxRenderer& renderer, int xOffset, int yOffset) const;
static std::unique_ptr<PageImage> deserialize(FsFile& file);
// Helper to get image block dimensions (needed for bounding box calculation)
@@ -104,4 +106,8 @@ class Page {
// Returns true if page has images and fills out the bounding box coordinates.
// If no images, returns false.
bool getImageBoundingBox(int& outX, int& outY, int& outWidth, int& outHeight) const;
void renderTextOnly(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
int countUncachedImages() const;
void renderImagePlaceholders(GfxRenderer& renderer, int xOffset, int yOffset) const;
};

View File

@@ -93,6 +93,11 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
} // namespace
bool ImageBlock::isCached() const {
std::string cachePath = getCachePath(imagePath);
return Storage.exists(cachePath.c_str());
}
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);

View File

@@ -16,6 +16,7 @@ class ImageBlock final : public Block {
int16_t getHeight() const { return height; }
bool imageExists() const;
bool isCached() const;
BlockType getType() override { return IMAGE_BLOCK; }
bool isEmpty() override { return false; }

View File

@@ -90,6 +90,32 @@ int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
// PNGdec keeps TWO scanlines in its internal ucPixels buffer (current + previous)
// and each scanline includes a leading filter byte.
// Required storage is therefore approximately: 2 * (pitch + 1) + alignment slack.
// If PNG_MAX_BUFFERED_PIXELS is smaller than this requirement for a given image,
// PNGdec can overrun its internal buffer before our draw callback executes.
int bytesPerPixelFromType(int pixelType) {
switch (pixelType) {
case PNG_PIXEL_TRUECOLOR:
return 3;
case PNG_PIXEL_GRAY_ALPHA:
return 2;
case PNG_PIXEL_TRUECOLOR_ALPHA:
return 4;
case PNG_PIXEL_GRAYSCALE:
case PNG_PIXEL_INDEXED:
default:
return 1;
}
}
int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
return ((pitch + 1) * 2) + 32;
}
// Convert entire source line to grayscale with alpha blending to white background.
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
@@ -304,6 +330,18 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
ctx.scale, png->getBpp());
const int pixelType = png->getPixelType();
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType);
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
LOG_ERR("PNG",
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
png->close();
delete png;
return false;
}
if (png->getBpp() != 8) {
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
}

View File

@@ -295,6 +295,9 @@ void CssParser::parseDeclarationIntoStyle(const std::string& decl, CssStyle& sty
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
1;
}
} else if (propNameBuf == "height") {
style.imageHeight = interpretLength(propValueBuf);
style.defined.imageHeight = 1;
} else if (propNameBuf == "width") {
style.width = interpretLength(propValueBuf);
style.defined.width = 1;
@@ -565,7 +568,7 @@ CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return par
// Cache serialization
// Cache format version - increment when format changes
constexpr uint8_t CSS_CACHE_VERSION = 2;
constexpr uint8_t CSS_CACHE_VERSION = 3;
constexpr char rulesCache[] = "/css_rules.cache";
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
@@ -616,6 +619,8 @@ bool CssParser::saveToCache() const {
writeLength(style.paddingBottom);
writeLength(style.paddingLeft);
writeLength(style.paddingRight);
writeLength(style.imageHeight);
writeLength(style.width);
// Write defined flags as uint16_t
uint16_t definedBits = 0;
@@ -632,6 +637,8 @@ bool CssParser::saveToCache() const {
if (style.defined.paddingBottom) definedBits |= 1 << 10;
if (style.defined.paddingLeft) definedBits |= 1 << 11;
if (style.defined.paddingRight) definedBits |= 1 << 12;
if (style.defined.width) definedBits |= 1 << 13;
if (style.defined.imageHeight) definedBits |= 1 << 14;
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
}
@@ -733,7 +740,8 @@ bool CssParser::loadFromCache() {
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) {
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight) ||
!readLength(style.imageHeight) || !readLength(style.width)) {
rulesBySelector_.clear();
file.close();
return false;
@@ -759,6 +767,8 @@ bool CssParser::loadFromCache() {
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
style.defined.paddingRight = (definedBits & 1 << 12) != 0;
style.defined.width = (definedBits & 1 << 13) != 0;
style.defined.imageHeight = (definedBits & 1 << 14) != 0;
rulesBySelector_[selector] = style;
}

View File

@@ -70,6 +70,7 @@ struct CssPropertyFlags {
uint16_t paddingLeft : 1;
uint16_t paddingRight : 1;
uint16_t width : 1;
uint16_t imageHeight : 1;
CssPropertyFlags()
: textAlign(0),
@@ -85,18 +86,20 @@ struct CssPropertyFlags {
paddingBottom(0),
paddingLeft(0),
paddingRight(0),
width(0) {}
width(0),
imageHeight(0) {}
[[nodiscard]] bool anySet() const {
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width;
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || width ||
imageHeight;
}
void clearAll() {
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
marginTop = marginBottom = marginLeft = marginRight = 0;
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
width = 0;
width = imageHeight = 0;
}
};
@@ -118,7 +121,8 @@ struct CssStyle {
CssLength paddingBottom; // Padding after
CssLength paddingLeft; // Padding left
CssLength paddingRight; // Padding right
CssLength width; // Element width (used for table columns/cells)
CssLength width; // Element width (used for table columns/cells and image sizing)
CssLength imageHeight; // Height for img (e.g. 2em) -- width derived from aspect ratio when only height set
CssPropertyFlags defined; // Tracks which properties were explicitly set
@@ -181,6 +185,10 @@ struct CssStyle {
width = base.width;
defined.width = 1;
}
if (base.hasImageHeight()) {
imageHeight = base.imageHeight;
defined.imageHeight = 1;
}
}
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
@@ -197,6 +205,7 @@ struct CssStyle {
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
[[nodiscard]] bool hasWidth() const { return defined.width; }
[[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; }
void reset() {
textAlign = CssTextAlign::Left;
@@ -207,6 +216,7 @@ struct CssStyle {
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
width = CssLength{};
imageHeight = CssLength{};
defined.clearAll();
}
};

View File

@@ -418,7 +418,47 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
if (decoder->getDimensions(cachedImagePath, dims)) {
LOG_DBG("EHP", "Image dimensions: %dx%d", dims.width, dims.height);
// Scale to fit viewport while maintaining aspect ratio
int displayWidth = 0;
int displayHeight = 0;
const float emSize =
static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
CssStyle imgStyle = self->cssParser ? self->cssParser->resolveStyle("img", classAttr) : CssStyle{};
if (!styleAttr.empty()) {
imgStyle.applyOver(CssParser::parseInlineStyle(styleAttr));
}
const bool hasCssHeight = imgStyle.hasImageHeight();
const bool hasCssWidth = imgStyle.hasWidth();
if (hasCssHeight && dims.width > 0 && dims.height > 0) {
displayHeight = static_cast<int>(
imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f);
if (displayHeight < 1) displayHeight = 1;
displayWidth =
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
if (displayWidth > self->viewportWidth) {
displayWidth = self->viewportWidth;
displayHeight =
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
if (displayHeight < 1) displayHeight = 1;
}
if (displayWidth < 1) displayWidth = 1;
LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight);
} else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) {
displayWidth = static_cast<int>(
imgStyle.width.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth;
if (displayWidth < 1) displayWidth = 1;
displayHeight =
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
if (displayHeight > self->viewportHeight) {
displayHeight = self->viewportHeight;
displayWidth =
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
if (displayWidth < 1) displayWidth = 1;
}
if (displayHeight < 1) displayHeight = 1;
LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight);
} else {
int maxWidth = self->viewportWidth;
int maxHeight = self->viewportHeight;
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
@@ -426,10 +466,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
float scale = (scaleX < scaleY) ? scaleX : scaleY;
if (scale > 1.0f) scale = 1.0f;
int displayWidth = (int)(dims.width * scale);
int displayHeight = (int)(dims.height * scale);
displayWidth = (int)(dims.width * scale);
displayHeight = (int)(dims.height * scale);
LOG_DBG("EHP", "Display size: %dx%d (scale %.2f)", displayWidth, displayHeight, scale);
}
// Create page for image - only break if image won't fit remaining space
if (self->currentPage && !self->currentPage->elements.empty() &&

View File

@@ -59,6 +59,130 @@ static inline void rotateCoordinates(const GfxRenderer::Orientation orientation,
}
}
enum class TextRotation { None, Rotated90CW, Rotated90CCW };
// Shared glyph rendering logic for normal and rotated text.
// Coordinate mapping and cursor advance direction are selected at compile time via the template parameter.
template <TextRotation rotation>
static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode renderMode,
const EpdFontFamily& fontFamily, const uint32_t cp, int* cursorX, int* cursorY,
const bool pixelState, const EpdFontFamily::Style style) {
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
if (!glyph) {
glyph = fontFamily.getGlyph(REPLACEMENT_GLYPH, style);
}
if (!glyph) {
LOG_ERR("GFX", "No glyph for codepoint %d", cp);
return;
}
const EpdFontData* fontData = fontFamily.getData(style);
const bool is2Bit = fontData->is2Bit;
const uint8_t width = glyph->width;
const uint8_t height = glyph->height;
const int left = glyph->left;
const int top = glyph->top;
const uint8_t* bitmap = renderer.getGlyphBitmap(fontData, glyph);
if (bitmap != nullptr) {
int outerBase, innerBase;
if constexpr (rotation == TextRotation::Rotated90CW) {
outerBase = *cursorX + fontData->ascender - top; // screenX = outerBase + glyphY
innerBase = *cursorY - left; // screenY = innerBase - glyphX
} else if constexpr (rotation == TextRotation::Rotated90CCW) {
outerBase = *cursorX + fontData->advanceY - 1 - fontData->ascender + top; // screenX = outerBase - glyphY
innerBase = *cursorY + left; // screenY = innerBase + glyphX
} else {
outerBase = *cursorY - top; // screenY = outerBase + glyphY
innerBase = *cursorX + left; // screenX = innerBase + glyphX
}
if (is2Bit) {
int pixelPosition = 0;
for (int glyphY = 0; glyphY < height; glyphY++) {
int outerCoord;
if constexpr (rotation == TextRotation::Rotated90CCW) {
outerCoord = outerBase - glyphY;
} else {
outerCoord = outerBase + glyphY;
}
for (int glyphX = 0; glyphX < width; glyphX++, pixelPosition++) {
int screenX, screenY;
if constexpr (rotation == TextRotation::Rotated90CW) {
screenX = outerCoord;
screenY = innerBase - glyphX;
} else if constexpr (rotation == TextRotation::Rotated90CCW) {
screenX = outerCoord;
screenY = innerBase + glyphX;
} else {
screenX = innerBase + glyphX;
screenY = outerCoord;
}
const uint8_t byte = bitmap[pixelPosition >> 2];
const uint8_t bit_index = (3 - (pixelPosition & 3)) * 2;
// the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black
// we swap this to better match the way images and screen think about colors:
// 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white
const uint8_t bmpVal = 3 - ((byte >> bit_index) & 0x3);
if (renderMode == GfxRenderer::BW && bmpVal < 3) {
// Black (also paints over the grays in BW mode)
renderer.drawPixel(screenX, screenY, pixelState);
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
// Light gray (also mark the MSB if it's going to be a dark gray too)
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
renderer.drawPixel(screenX, screenY, false);
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) {
// Dark gray
renderer.drawPixel(screenX, screenY, false);
}
}
}
} else {
int pixelPosition = 0;
for (int glyphY = 0; glyphY < height; glyphY++) {
int outerCoord;
if constexpr (rotation == TextRotation::Rotated90CCW) {
outerCoord = outerBase - glyphY;
} else {
outerCoord = outerBase + glyphY;
}
for (int glyphX = 0; glyphX < width; glyphX++, pixelPosition++) {
int screenX, screenY;
if constexpr (rotation == TextRotation::Rotated90CW) {
screenX = outerCoord;
screenY = innerBase - glyphX;
} else if constexpr (rotation == TextRotation::Rotated90CCW) {
screenX = outerCoord;
screenY = innerBase + glyphX;
} else {
screenX = innerBase + glyphX;
screenY = outerCoord;
}
const uint8_t byte = bitmap[pixelPosition >> 3];
const uint8_t bit_index = 7 - (pixelPosition & 7);
if ((byte >> bit_index) & 1) {
renderer.drawPixel(screenX, screenY, pixelState);
}
}
}
}
}
if constexpr (rotation == TextRotation::Rotated90CW) {
*cursorY -= glyph->advanceX;
} else if constexpr (rotation == TextRotation::Rotated90CCW) {
*cursorY += glyph->advanceX;
} else {
*cursorX += glyph->advanceX;
}
}
// IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and
// efficient as possible.
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
@@ -115,7 +239,7 @@ void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* te
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
const EpdFontFamily::Style style) const {
const int yPos = y + getFontAscenderSize(fontId);
int yPos = y + getFontAscenderSize(fontId);
int xpos = x;
// cannot draw a NULL / empty string
@@ -824,7 +948,8 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
return 0;
}
return fontIt->second.getGlyph(' ', style)->advanceX;
const EpdGlyph* spaceGlyph = fontIt->second.getGlyph(' ', style);
return spaceGlyph ? spaceGlyph->advanceX : 0;
}
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFontFamily::Style style) const {
@@ -838,7 +963,9 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFo
int width = 0;
const auto& font = fontIt->second;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
width += font.getGlyph(cp, style)->advanceX;
const EpdGlyph* glyph = font.getGlyph(cp, style);
if (!glyph) glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
if (glyph) width += glyph->advanceX;
}
return width;
}
@@ -887,68 +1014,12 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
const auto& font = fontIt->second;
// For 90° clockwise rotation:
// Original (glyphX, glyphY) -> Rotated (glyphY, -glyphX)
// Text reads from bottom to top
int yPos = y; // Current Y position (decreases as we draw characters)
int xPos = x;
int yPos = y;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
const EpdGlyph* glyph = font.getGlyph(cp, style);
if (!glyph) {
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
}
if (!glyph) {
continue;
}
const EpdFontData* fontData = font.getData(style);
const int is2Bit = fontData->is2Bit;
const uint8_t width = glyph->width;
const uint8_t height = glyph->height;
const int left = glyph->left;
const int top = glyph->top;
const uint8_t* bitmap = getGlyphBitmap(fontData, glyph);
if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) {
for (int glyphX = 0; glyphX < width; glyphX++) {
const int pixelPosition = glyphY * width + glyphX;
// 90° clockwise rotation transformation:
// screenX = x + (ascender - top + glyphY)
// screenY = yPos - (left + glyphX)
const int screenX = x + (fontData->ascender - top + glyphY);
const int screenY = yPos - left - glyphX;
if (is2Bit) {
const uint8_t byte = bitmap[pixelPosition / 4];
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
if (renderMode == BW && bmpVal < 3) {
drawPixel(screenX, screenY, black);
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
drawPixel(screenX, screenY, false);
}
} else {
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((byte >> bit_index) & 1) {
drawPixel(screenX, screenY, black);
}
}
}
}
}
// Move to next character position (going up, so decrease Y)
yPos -= glyph->advanceX;
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, &xPos, &yPos, black, style);
}
}
@@ -959,77 +1030,20 @@ void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int
return;
}
if (fontMap.count(fontId) == 0) {
const auto fontIt = fontMap.find(fontId);
if (fontIt == fontMap.end()) {
LOG_ERR("GFX", "Font %d not found", fontId);
return;
}
const auto font = fontMap.at(fontId);
// For 90° counter-clockwise rotation:
// Mirror of CW: glyphY maps to -X direction, glyphX maps to +Y direction
// Text reads from top to bottom
const auto& font = fontIt->second;
const int advanceY = font.getData(style)->advanceY;
const int ascender = font.getData(style)->ascender;
int yPos = y; // Current Y position (increases as we draw characters)
int xPos = x;
int yPos = y;
uint32_t cp;
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
const EpdGlyph* glyph = font.getGlyph(cp, style);
if (!glyph) {
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
}
if (!glyph) {
continue;
}
const int is2Bit = font.getData(style)->is2Bit;
const uint32_t offset = glyph->dataOffset;
const uint8_t width = glyph->width;
const uint8_t height = glyph->height;
const int left = glyph->left;
const int top = glyph->top;
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) {
for (int glyphX = 0; glyphX < width; glyphX++) {
const int pixelPosition = glyphY * width + glyphX;
// 90° counter-clockwise rotation transformation:
// screenX = mirrored CW X (right-to-left within advanceY span)
// screenY = yPos + (left + glyphX) (downward)
const int screenX = x + advanceY - 1 - (ascender - top + glyphY);
const int screenY = yPos + left + glyphX;
if (is2Bit) {
const uint8_t byte = bitmap[pixelPosition / 4];
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
if (renderMode == BW && bmpVal < 3) {
drawPixel(screenX, screenY, black);
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
drawPixel(screenX, screenY, false);
}
} else {
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((byte >> bit_index) & 1) {
drawPixel(screenX, screenY, black);
}
}
}
}
}
// Move to next character position (going down, so increase Y)
yPos += glyph->advanceX;
renderCharImpl<TextRotation::Rotated90CCW>(*this, renderMode, font, cp, &xPos, &yPos, black, style);
}
}
@@ -1094,7 +1108,7 @@ bool GfxRenderer::storeBwBuffer() {
* Uses chunked restoration to match chunked storage.
*/
void GfxRenderer::restoreBwBuffer() {
// Check if any all chunks are allocated
// Check if all chunks are allocated
bool missingChunks = false;
for (const auto& bwBufferChunk : bwBufferChunks) {
if (!bwBufferChunk) {
@@ -1109,13 +1123,6 @@ void GfxRenderer::restoreBwBuffer() {
}
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
// Check if chunk is missing
if (!bwBufferChunks[i]) {
LOG_ERR("GFX", "!! BW buffer chunks not stored - this is likely a bug");
freeBwBufferChunks();
return;
}
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
}
@@ -1136,66 +1143,9 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
}
}
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y,
const bool pixelState, const EpdFontFamily::Style style) const {
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
if (!glyph) {
glyph = fontFamily.getGlyph(REPLACEMENT_GLYPH, style);
}
// no glyph?
if (!glyph) {
LOG_ERR("GFX", "No glyph for codepoint %d", cp);
return;
}
const EpdFontData* fontData = fontFamily.getData(style);
const int is2Bit = fontData->is2Bit;
const uint8_t width = glyph->width;
const uint8_t height = glyph->height;
const int left = glyph->left;
const uint8_t* bitmap = getGlyphBitmap(fontData, glyph);
if (bitmap != nullptr) {
for (int glyphY = 0; glyphY < height; glyphY++) {
const int screenY = *y - glyph->top + glyphY;
for (int glyphX = 0; glyphX < width; glyphX++) {
const int pixelPosition = glyphY * width + glyphX;
const int screenX = *x + left + glyphX;
if (is2Bit) {
const uint8_t byte = bitmap[pixelPosition / 4];
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
// the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black
// we swap this to better match the way images and screen think about colors:
// 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
if (renderMode == BW && bmpVal < 3) {
// Black (also paints over the grays in BW mode)
drawPixel(screenX, screenY, pixelState);
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
// Light gray (also mark the MSB if it's going to be a dark gray too)
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
drawPixel(screenX, screenY, false);
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
// Dark gray
drawPixel(screenX, screenY, false);
}
} else {
const uint8_t byte = bitmap[pixelPosition / 8];
const uint8_t bit_index = 7 - (pixelPosition % 8);
if ((byte >> bit_index) & 1) {
drawPixel(screenX, screenY, pixelState);
}
}
}
}
}
*x += glyph->advanceX;
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
EpdFontFamily::Style style) const {
renderCharImpl<TextRotation::None>(*this, renderMode, fontFamily, cp, x, y, pixelState, style);
}
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {

View File

@@ -38,10 +38,9 @@ class GfxRenderer {
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap;
FontDecompressor* fontDecompressor = nullptr;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState,
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, int* y, bool pixelState,
EpdFontFamily::Style style) const;
void freeBwBufferChunks();
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
template <Color color>
void drawPixelDither(int x, int y) const;
template <Color color>
@@ -136,6 +135,9 @@ class GfxRenderer {
void restoreBwBuffer(); // Restore and free the stored buffer
void cleanupGrayscaleWithFrameBuffer() const;
// Font helpers
const uint8_t* getGlyphBitmap(const EpdFontData* fontData, const EpdGlyph* glyph) const;
// Low level functions
uint8_t* getFrameBuffer() const;
static size_t getBufferSize();

View File

@@ -78,7 +78,7 @@ void HalPowerManager::startDeepSleep(HalGPIO& gpio) const {
esp_deep_sleep_start();
}
int HalPowerManager::getBatteryPercentage() const {
uint16_t HalPowerManager::getBatteryPercentage() const {
static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0);
return battery.readPercentage();
}

View File

@@ -28,7 +28,7 @@ class HalPowerManager {
void startDeepSleep(HalGPIO& gpio) const;
// Get battery percentage (range 0-100)
int getBatteryPercentage() const;
uint16_t getBatteryPercentage() const;
// RAII lock to prevent low-power mode during critical work (e.g. rendering)
class Lock {

View File

@@ -2,7 +2,7 @@
default_envs = default
[crosspoint]
version = 1.0.0
version = 1.1.1-rc
[base]
platform = espressif32 @ 6.12.0
@@ -31,9 +31,9 @@ build_flags =
-std=gnu++2a
# Enable UTF-8 long file names in SdFat
-DUSE_UTF8_LONG_NAMES=1
# Increase PNG scanline buffer to support up to 800px wide images
# Increase PNG scanline buffer to support up to 2048px wide images
# Default is (320*4+1)*2=2562, we need more for larger images
-DPNG_MAX_BUFFERED_PIXELS=6402
-DPNG_MAX_BUFFERED_PIXELS=16416
build_unflags =
-std=gnu++11

View File

@@ -1,6 +0,0 @@
#pragma once
#include <BatteryMonitor.h>
#define BAT_GPIO0 0 // Battery voltage
static BatteryMonitor battery(BAT_GPIO0);

View File

@@ -21,6 +21,11 @@
#include "util/BookSettings.h"
#include "util/StringUtils.h"
// Sleep cover refresh strategy when dithered letterbox fill is active:
// 1 = Double FAST_REFRESH (clear to white, then render content -- avoids HALF_REFRESH crosstalk)
// 0 = Standard HALF_REFRESH (original behavior)
#define USE_SLEEP_DOUBLE_FAST_REFRESH 1
namespace {
// Number of source pixels along the image edge to average for the dominant color
@@ -74,37 +79,6 @@ uint8_t quantizeBayerDither(int gray, int x, int y) {
}
}
// Check whether a gray value would produce a dithered mix that crosses the
// level-2 / level-3 boundary. This is the ONLY boundary where some dithered
// pixels are BLACK (level ≤ 2) and others are WHITE (level 3) in the BW pass,
// creating a high-frequency checkerboard that causes e-ink display crosstalk
// and washes out adjacent content during HALF_REFRESH.
// Gray values 171-254 produce a level-2/level-3 mix via Bayer dithering.
bool bayerCrossesBwBoundary(uint8_t gray) { return gray > 170 && gray < 255; }
// Hash-based block dithering for BW-boundary gray values (171-254).
// Each blockSize×blockSize pixel block gets a single uniform level (2 or 3),
// determined by a deterministic spatial hash. The proportion of level-3 blocks
// approximates the target gray. Unlike Bayer, the pattern is irregular
// (noise-like), making it much less visually obvious at the same block size.
// The hash is purely spatial (depends only on x, y, blockSize) so it produces
// identical levels across BW, LSB, and MSB render passes.
static constexpr int BW_DITHER_BLOCK = 2;
uint8_t hashBlockDither(uint8_t avg, int x, int y) {
const int bx = x / BW_DITHER_BLOCK;
const int by = y / BW_DITHER_BLOCK;
// Fast mixing hash (splitmix32-inspired)
uint32_t h = (uint32_t)bx * 2654435761u ^ (uint32_t)by * 2246822519u;
h ^= h >> 16;
h *= 0x45d9f3bu;
h ^= h >> 16;
// Proportion of level-3 blocks needed to approximate the target gray
const float ratio = (avg - 170.0f) / 85.0f;
const uint32_t threshold = (uint32_t)(ratio * 4294967295.0f);
return (h < threshold) ? 3 : 2;
}
// --- Edge average cache ---
// Caches the computed edge averages alongside the cover BMP so we don't rescan on every sleep.
constexpr uint8_t EDGE_CACHE_VERSION = 2;
@@ -278,19 +252,6 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
const bool isSolid = (fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_SOLID);
// For DITHERED mode with gray values in 171-254 (the level-2/level-3 BW boundary):
// Pixel-level Bayer dithering creates a regular high-frequency checkerboard in
// the BW pass that causes e-ink display crosstalk during HALF_REFRESH.
//
// Solution: HASH-BASED BLOCK DITHERING. Each 2x2 pixel block gets a single
// level (2 or 3) determined by a spatial hash, with the proportion of level-3
// blocks tuned to approximate the target gray. The 2px minimum run avoids BW
// crosstalk, and the irregular hash pattern is much less visible than a regular
// Bayer grid at the same block size.
const bool hashA = !isSolid && bayerCrossesBwBoundary(data.avgA);
const bool hashB = !isSolid && bayerCrossesBwBoundary(data.avgB);
// For solid mode: snap to nearest e-ink level
const uint8_t levelA = isSolid ? snapToEinkLevel(data.avgA) / 85 : 0;
const uint8_t levelB = isSolid ? snapToEinkLevel(data.avgB) / 85 : 0;
@@ -298,13 +259,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
if (data.letterboxA > 0) {
for (int y = 0; y < data.letterboxA; y++)
for (int x = 0; x < renderer.getScreenWidth(); x++) {
uint8_t lv;
if (isSolid)
lv = levelA;
else if (hashA)
lv = hashBlockDither(data.avgA, x, y);
else
lv = quantizeBayerDither(data.avgA, x, y);
const uint8_t lv = isSolid ? levelA : quantizeBayerDither(data.avgA, x, y);
renderer.drawPixelGray(x, y, lv);
}
}
@@ -312,13 +267,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
const int start = renderer.getScreenHeight() - data.letterboxB;
for (int y = start; y < renderer.getScreenHeight(); y++)
for (int x = 0; x < renderer.getScreenWidth(); x++) {
uint8_t lv;
if (isSolid)
lv = levelB;
else if (hashB)
lv = hashBlockDither(data.avgB, x, y);
else
lv = quantizeBayerDither(data.avgB, x, y);
const uint8_t lv = isSolid ? levelB : quantizeBayerDither(data.avgB, x, y);
renderer.drawPixelGray(x, y, lv);
}
}
@@ -326,13 +275,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
if (data.letterboxA > 0) {
for (int x = 0; x < data.letterboxA; x++)
for (int y = 0; y < renderer.getScreenHeight(); y++) {
uint8_t lv;
if (isSolid)
lv = levelA;
else if (hashA)
lv = hashBlockDither(data.avgA, x, y);
else
lv = quantizeBayerDither(data.avgA, x, y);
const uint8_t lv = isSolid ? levelA : quantizeBayerDither(data.avgA, x, y);
renderer.drawPixelGray(x, y, lv);
}
}
@@ -340,13 +283,7 @@ void drawLetterboxFill(GfxRenderer& renderer, const LetterboxFillData& data, uin
const int start = renderer.getScreenWidth() - data.letterboxB;
for (int x = start; x < renderer.getScreenWidth(); x++)
for (int y = 0; y < renderer.getScreenHeight(); y++) {
uint8_t lv;
if (isSolid)
lv = levelB;
else if (hashB)
lv = hashBlockDither(data.avgB, x, y);
else
lv = quantizeBayerDither(data.avgB, x, y);
const uint8_t lv = isSolid ? levelB : quantizeBayerDither(data.avgB, x, y);
renderer.drawPixelGray(x, y, lv);
}
}
@@ -543,35 +480,47 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap, const std::str
const bool hasGreyscale = bitmap.hasGreyscale() &&
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER;
// Draw letterbox fill (BW pass)
if (fillData.valid) {
drawLetterboxFill(renderer, fillData, fillMode);
}
const bool isInverted =
SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE;
#if USE_SLEEP_DOUBLE_FAST_REFRESH
const bool useDoubleFast =
fillData.valid && fillMode == CrossPointSettings::SLEEP_SCREEN_LETTERBOX_FILL::LETTERBOX_DITHERED;
#else
const bool useDoubleFast = false;
#endif
if (useDoubleFast) {
// Double FAST_REFRESH technique: avoids HALF_REFRESH crosstalk with dithered letterbox.
// Pass 1: clear to white baseline
renderer.clearScreen();
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
// Pass 2: render actual content and display
if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
if (SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) {
renderer.invertScreen();
}
if (isInverted) renderer.invertScreen();
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
} else {
// Standard path: single HALF_REFRESH
if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
if (isInverted) renderer.invertScreen();
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
}
if (hasGreyscale) {
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB);
if (fillData.valid) {
drawLetterboxFill(renderer, fillData, fillMode);
}
if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
renderer.copyGrayscaleLsbBuffers();
bitmap.rewindToData();
renderer.clearScreen(0x00);
renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB);
if (fillData.valid) {
drawLetterboxFill(renderer, fillData, fillMode);
}
if (fillData.valid) drawLetterboxFill(renderer, fillData, fillMode);
renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY);
renderer.copyGrayscaleMsbBuffers();

View File

@@ -13,7 +13,6 @@
#include <cstring>
#include <vector>
#include "Battery.h"
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "MappedInputManager.h"

View File

@@ -22,11 +22,6 @@
#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;
@@ -1022,13 +1017,16 @@ 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 special handling for pages with images when anti-aliasing is on
bool imagePageWithAA = page->hasImages() && 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;
if (page->countUncachedImages() > 0) {
page->renderTextOnly(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
page->renderImagePlaceholders(renderer, orientedMarginLeft, orientedMarginTop);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer();
renderer.clearScreen();
}
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop);
@@ -1048,42 +1046,26 @@ 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).
if (imagePageWithAA) {
// Double FAST_REFRESH with selective image blanking (pablohc's technique):
// HALF_REFRESH sets particles too firmly for the grayscale LUT to adjust.
// Instead, blank only the image area and do two fast refreshes.
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);
renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false);
renderer.displayBuffer(HalDisplay::FAST_REFRESH);
#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--;
// Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence
} else if (pagesUntilFullRefresh <= 1) {
renderer.displayBuffer(HalDisplay::HALF_REFRESH);
pagesUntilFullRefresh = SETTINGS.getRefreshFrequency();
} else {
// Normal page without images, or images without anti-aliasing
renderer.displayBuffer();
pagesUntilFullRefresh--;
}

View File

@@ -1,6 +1,7 @@
#include "BaseTheme.h"
#include <GfxRenderer.h>
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <Logging.h>
#include <Utf8.h>
@@ -9,7 +10,6 @@
#include <ctime>
#include <string>
#include "Battery.h"
#include "CrossPointSettings.h"
#include "I18n.h"
#include "RecentBooksStore.h"
@@ -49,7 +49,7 @@ void drawBatteryIcon(const GfxRenderer& renderer, int x, int y, int battWidth, i
void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
// Left aligned: icon on left, percentage on right (reader mode)
const uint16_t percentage = battery.readPercentage();
const uint16_t percentage = powerManager.getBatteryPercentage();
const int y = rect.y + 6;
if (showPercentage) {
@@ -64,7 +64,7 @@ void BaseTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bo
void BaseTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
// Right aligned: percentage on left, icon on right (UI headers)
// rect.x is already positioned for the icon (drawHeader calculated it)
const uint16_t percentage = battery.readPercentage();
const uint16_t percentage = powerManager.getBatteryPercentage();
const int y = rect.y + 6;
if (showPercentage) {

View File

@@ -1,6 +1,7 @@
#include "LyraTheme.h"
#include <GfxRenderer.h>
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <I18n.h>
#include <Utf8.h>
@@ -10,7 +11,6 @@
#include <string>
#include <vector>
#include "Battery.h"
#include "CrossPointSettings.h"
#include "RecentBooksStore.h"
#include "components/UITheme.h"
@@ -88,7 +88,7 @@ const uint8_t* iconForName(UIIcon icon, int size) {
void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
// Left aligned: icon on left, percentage on right (reader mode)
const uint16_t percentage = battery.readPercentage();
const uint16_t percentage = powerManager.getBatteryPercentage();
const int y = rect.y + 6;
const int battWidth = LyraMetrics::values.batteryWidth;
@@ -125,7 +125,7 @@ void LyraTheme::drawBatteryLeft(const GfxRenderer& renderer, Rect rect, const bo
void LyraTheme::drawBatteryRight(const GfxRenderer& renderer, Rect rect, const bool showPercentage) const {
// Right aligned: percentage on left, icon on right (UI headers)
const uint16_t percentage = battery.readPercentage();
const uint16_t percentage = powerManager.getBatteryPercentage();
const int y = rect.y + 6;
const int battWidth = LyraMetrics::values.batteryWidth;

View File

@@ -15,7 +15,6 @@
#include <cstring>
#include <ctime>
#include "Battery.h"
#include "CrossPointSettings.h"
#include "CrossPointState.h"
#include "KOReaderCredentialStore.h"
@@ -227,9 +226,9 @@ void onGoHome();
void onGoToMyLibraryWithPath(const std::string& path);
void onGoToRecentBooks();
void onGoToReader(const std::string& initialEpubPath) {
const std::string bookPath = initialEpubPath; // Copy before exitActivity() invalidates the reference
exitActivity();
enterNewActivity(
new ReaderActivity(renderer, mappedInputManager, initialEpubPath, onGoHome, onGoToMyLibraryWithPath));
enterNewActivity(new ReaderActivity(renderer, mappedInputManager, bookPath, onGoHome, onGoToMyLibraryWithPath));
}
void onGoToFileTransfer() {