Merge branch 'feat-ordered-list' of https://github.com/jpirnay/crosspoint-reader into mybuild

This commit is contained in:
jpirnay
2026-03-31 21:26:05 +02:00
8 changed files with 248 additions and 33 deletions
+9 -5
View File
@@ -4,7 +4,7 @@
#include <Logging.h>
#include <Serialization.h>
#include "../converters/DitherUtils.h"
#include "../converters/DirectPixelWriter.h"
#include "../converters/ImageDecoderFactory.h"
// Cache file format:
@@ -66,6 +66,9 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
return false;
}
DirectPixelWriter pw;
pw.init(renderer);
for (int row = 0; row < cachedHeight; row++) {
if (cacheFile.read(rowBuffer, bytesPerRow) != bytesPerRow) {
LOG_ERR("IMG", "Cache read error at row %d", row);
@@ -74,13 +77,14 @@ bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x,
return false;
}
int destY = y + row;
const int destY = y + row;
pw.beginRow(destY);
for (int col = 0; col < cachedWidth; col++) {
int byteIdx = col / 4;
int bitShift = 6 - (col % 4) * 2; // MSB first within byte
const int byteIdx = col >> 2; // col / 4
const int bitShift = 6 - (col & 3) * 2; // MSB first within byte
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
drawPixelWithRenderMode(renderer, x + col, destY, pixelValue);
pw.writePixel(x + col, pixelValue);
}
}
@@ -0,0 +1,156 @@
#pragma once
#include <GfxRenderer.h>
#include <HalDisplay.h>
#include <stdint.h>
// Direct framebuffer writer that eliminates per-pixel overhead from the image
// rendering hot path. Pre-computes orientation transform as linear coefficients
// and caches render-mode state so the inner loop is: one multiply, one add,
// one shift, and one AND per pixel — no branches, no method calls.
//
// Caller is responsible for ensuring (outX, outY) are within screen bounds.
// ImageBlock::render() already validates this before entering the pixel loop,
// and the JPEG/PNG callbacks pre-clamp destination ranges to screen bounds.
struct DirectPixelWriter {
uint8_t* fb;
GfxRenderer::RenderMode mode;
// Orientation is collapsed into a linear transform:
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
// phyY = phyYBase + x * phyYStepX + y * phyYStepY
int phyXBase, phyYBase;
int phyXStepX, phyYStepX; // per logical-X step
int phyXStepY, phyYStepY; // per logical-Y step
// Row-precomputed: the Y-dependent portion of the physical coords
int rowPhyXBase, rowPhyYBase;
void init(GfxRenderer& renderer) {
fb = renderer.getFrameBuffer();
mode = renderer.getRenderMode();
switch (renderer.getOrientation()) {
case GfxRenderer::Portrait:
// phyX = y, phyY = (DISPLAY_HEIGHT-1) - x
phyXBase = 0;
phyYBase = HalDisplay::DISPLAY_HEIGHT - 1;
phyXStepX = 0;
phyYStepX = -1;
phyXStepY = 1;
phyYStepY = 0;
break;
case GfxRenderer::LandscapeClockwise:
// phyX = (DISPLAY_WIDTH-1) - x, phyY = (DISPLAY_HEIGHT-1) - y
phyXBase = HalDisplay::DISPLAY_WIDTH - 1;
phyYBase = HalDisplay::DISPLAY_HEIGHT - 1;
phyXStepX = -1;
phyYStepX = 0;
phyXStepY = 0;
phyYStepY = -1;
break;
case GfxRenderer::PortraitInverted:
// phyX = (DISPLAY_WIDTH-1) - y, phyY = x
phyXBase = HalDisplay::DISPLAY_WIDTH - 1;
phyYBase = 0;
phyXStepX = 0;
phyYStepX = 1;
phyXStepY = -1;
phyYStepY = 0;
break;
case GfxRenderer::LandscapeCounterClockwise:
// phyX = x, phyY = y
phyXBase = 0;
phyYBase = 0;
phyXStepX = 1;
phyYStepX = 0;
phyXStepY = 0;
phyYStepY = 1;
break;
default:
// Fallback to LandscapeCounterClockwise (identity transform)
phyXBase = 0;
phyYBase = 0;
phyXStepX = 1;
phyYStepX = 0;
phyXStepY = 0;
phyYStepY = 1;
break;
}
}
// Call once per row before the column loop.
// Pre-computes the Y-dependent portion so writePixel() only needs the X part.
inline void beginRow(int logicalY) {
rowPhyXBase = phyXBase + logicalY * phyXStepY;
rowPhyYBase = phyYBase + logicalY * phyYStepY;
}
// Write a single 2-bit dithered pixel value to the framebuffer.
// Must be called after beginRow() for the current row.
// No bounds checking — caller guarantees coordinates are valid.
inline void writePixel(int logicalX, uint8_t pixelValue) const {
// Determine whether to draw based on render mode
bool draw;
bool state;
switch (mode) {
case GfxRenderer::BW:
draw = (pixelValue < 3);
state = true;
break;
case GfxRenderer::GRAYSCALE_MSB:
draw = (pixelValue == 1 || pixelValue == 2);
state = false;
break;
case GfxRenderer::GRAYSCALE_LSB:
draw = (pixelValue == 1);
state = false;
break;
default:
return;
}
if (!draw) return;
const int phyX = rowPhyXBase + logicalX * phyXStepX;
const int phyY = rowPhyYBase + logicalX * phyYStepX;
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX >> 3);
const uint8_t bitMask = 1 << (7 - (phyX & 7));
if (state) {
fb[byteIndex] &= ~bitMask; // Clear bit (draw black)
} else {
fb[byteIndex] |= bitMask; // Set bit (draw white)
}
}
};
// Direct cache writer that eliminates per-pixel overhead from PixelCache::setPixel().
// Pre-computes row pointer so the inner loop is just byte index + bit manipulation.
//
// Caller guarantees coordinates are within cache bounds.
struct DirectCacheWriter {
uint8_t* buffer;
int bytesPerRow;
int originX;
uint8_t* rowPtr; // Pre-computed for current row
void init(uint8_t* cacheBuffer, int cacheBytesPerRow, int cacheOriginX) {
buffer = cacheBuffer;
bytesPerRow = cacheBytesPerRow;
originX = cacheOriginX;
rowPtr = nullptr;
}
// Call once per row before the column loop.
inline void beginRow(int screenY, int cacheOriginY) { rowPtr = buffer + (screenY - cacheOriginY) * bytesPerRow; }
// Write a 2-bit pixel value. No bounds checking.
inline void writePixel(int screenX, uint8_t value) const {
const int localX = screenX - originX;
const int byteIdx = localX >> 2; // localX / 4
const int bitShift = 6 - (localX & 3) * 2; // MSB first: pixel 0 at bits 6-7
rowPtr[byteIdx] = (rowPtr[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
}
};
-13
View File
@@ -1,6 +1,5 @@
#pragma once
#include <GfxRenderer.h>
#include <stdint.h>
// 4x4 Bayer matrix for ordered dithering
@@ -26,15 +25,3 @@ inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
if (adjusted < 192) return 2;
return 3;
}
// Draw a pixel respecting the current render mode for grayscale support
inline void drawPixelWithRenderMode(GfxRenderer& renderer, int x, int y, uint8_t pixelValue) {
GfxRenderer::RenderMode renderMode = renderer.getRenderMode();
if (renderMode == GfxRenderer::BW && pixelValue < 3) {
renderer.drawPixel(x, y, true);
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (pixelValue == 1 || pixelValue == 2)) {
renderer.drawPixel(x, y, false);
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && pixelValue == 1) {
renderer.drawPixel(x, y, false);
}
}
@@ -9,6 +9,7 @@
#include <cstdlib>
#include <new>
#include "DirectPixelWriter.h"
#include "DitherUtils.h"
#include "PixelCache.h"
@@ -167,10 +168,21 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
if (dstYStart >= dstYEnd || dstXStart >= dstXEnd) return 1;
// Pre-compute orientation and render-mode state once per callback invocation
DirectPixelWriter pw;
pw.init(renderer);
DirectCacheWriter cw;
if (caching) {
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.originX);
}
// === 1:1 fast path: no scaling math ===
if (fineScaleFP == FP_ONE) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const uint8_t* row = &pixels[(dstY - blockY) * stride];
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
@@ -182,8 +194,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
return 1;
@@ -203,6 +215,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP;
const int32_t fy = srcFyFP & FP_MASK;
const int32_t fyInv = FP_ONE - fy;
@@ -239,8 +253,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
// Interior (no X boundary checks — lx0 and lx0+1 guaranteed in bounds)
@@ -262,8 +276,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
// Right edge (with X boundary clamping)
@@ -288,8 +302,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
return 1;
@@ -298,6 +312,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP;
int ly = (srcFyFP >> FP_SHIFT) - blockY;
if (ly < 0) ly = 0;
@@ -319,8 +335,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
dithered = gray / 85;
if (dithered > 3) dithered = 3;
}
drawPixelWithRenderMode(renderer, outX, outY, dithered);
if (caching) ctx->cache.setPixel(outX, outY, dithered);
pw.writePixel(outX, dithered);
if (caching) cw.writePixel(outX, dithered);
}
}
@@ -9,6 +9,7 @@
#include <cstdlib>
#include <new>
#include "DirectPixelWriter.h"
#include "DitherUtils.h"
#include "PixelCache.h"
@@ -207,6 +208,17 @@ int pngDrawCallback(PNGDRAW* pDraw) {
bool useDithering = ctx->config->useDithering;
bool caching = ctx->caching;
// Pre-compute orientation and render-mode state once per row
DirectPixelWriter pw;
pw.init(*ctx->renderer);
pw.beginRow(outY);
DirectCacheWriter cw;
if (caching) {
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.originX);
cw.beginRow(outY, ctx->config->y);
}
int srcX = 0;
int error = 0;
@@ -222,8 +234,8 @@ int pngDrawCallback(PNGDRAW* pDraw) {
ditheredGray = gray / 85;
if (ditheredGray > 3) ditheredGray = 3;
}
drawPixelWithRenderMode(*ctx->renderer, outX, outY, ditheredGray);
if (caching) ctx->cache.setPixel(outX, outY, ditheredGray);
pw.writePixel(outX, ditheredGray);
if (caching) cw.writePixel(outX, ditheredGray);
}
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
@@ -356,10 +368,18 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
return false;
}
// Allocate cache buffer using SCALED dimensions
// Allocate cache buffer using SCALED dimensions.
// PNG decode is fast enough (~135ms for 400x600) that caching provides minimal benefit
// for larger images, while the cache buffer competes with the 44KB PNG decoder for heap.
// Skip caching when the buffer would exceed the framebuffer size (48KB).
static constexpr size_t PNG_MAX_CACHE_BYTES = 48000;
ctx.caching = !config.cachePath.empty();
if (ctx.caching) {
if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
size_t cacheSize = (size_t)((ctx.dstWidth + 3) / 4) * ctx.dstHeight;
if (cacheSize > PNG_MAX_CACHE_BYTES) {
LOG_DBG("PNG", "Skipping cache: %zu bytes exceeds PNG limit (%zu)", cacheSize, PNG_MAX_CACHE_BYTES);
ctx.caching = false;
} else if (!ctx.cache.allocate(ctx.dstWidth, ctx.dstHeight, config.x, config.y)) {
LOG_ERR("PNG", "Failed to allocate cache buffer, continuing without caching");
ctx.caching = false;
}
@@ -660,6 +660,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
}
}
if (strcmp(name, "ul") == 0 || strcmp(name, "ol") == 0) {
self->listStack.push_back({self->depth, name[0] == 'o', 0});
}
const float emSize = static_cast<float>(self->renderer.getFontAscenderSize(self->fontId));
const auto userAlignmentBlockStyle = BlockStyle::fromCssStyle(
cssStyle, emSize, static_cast<CssTextAlign>(self->paragraphAlignment), self->viewportWidth);
@@ -728,7 +732,14 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->updateEffectiveInlineStyle();
if (strcmp(name, "li") == 0) {
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
char marker[12];
if (!self->listStack.empty() && self->listStack.back().isOrdered) {
self->listStack.back().counter += 1;
snprintf(marker, sizeof(marker), "%d.", self->listStack.back().counter);
} else {
strcpy(marker, "\xe2\x80\xa2");
}
self->currentTextBlock->addWord(marker, EpdFontFamily::REGULAR);
} else if (strcmp(name, "pre") == 0) {
// Record depth so characterData can treat \n as a hard line break inside <pre>.
// depth has not been incremented yet here; it will be after startElement returns.
@@ -1080,6 +1091,11 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
self->depth -= 1;
// Pop list entries whose ul/ol is now out of scope
while (!self->listStack.empty() && self->listStack.back().depth >= self->depth) {
self->listStack.pop_back();
}
// Closing a footnote link — create entry from collected text and href
if (self->insideFootnoteLink && self->depth == self->footnoteLinkDepth) {
if (self->currentFootnoteLinkText[0] != '\0' && self->currentFootnoteLinkHref[0] != '\0') {
@@ -73,6 +73,13 @@ class ChapterHtmlSlimParser {
int tableRowIndex = 0;
int tableColIndex = 0;
struct ListEntry {
int depth;
bool isOrdered;
int counter;
};
std::vector<ListEntry> listStack;
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
int completedPageCount = 0;
std::vector<std::pair<std::string, uint16_t>> anchorData;
+9
View File
@@ -66,6 +66,10 @@ STR_SLEEP_COVER_MODE: "Slaapscherm omslag-modus"
STR_HIDE_BATTERY: "Batterij % verbergen"
STR_EXTRA_SPACING: "Extra regelafstand alinea"
STR_TEXT_AA: "Tekst Anti-Aliasing"
STR_IMAGES: "Afbeeldingen"
STR_IMAGES_DISPLAY: "Weergave"
STR_IMAGES_PLACEHOLDER: "Placeholder"
STR_IMAGES_SUPPRESS: "Verbergen"
STR_SHORT_PWR_BTN: "Korte klik aan/uit-knop"
STR_ORIENTATION: "Leesstand"
STR_SIDE_BTN_LAYOUT: "Indeling zijknoppen (lezer)"
@@ -77,6 +81,7 @@ STR_SCREEN_MARGIN: "Schermmarge lezer"
STR_PARA_ALIGNMENT: "Uitlijning alinea lezer"
STR_HYPHENATION: "Woordafbreking"
STR_TIME_TO_SLEEP: "Tijd tot slaapstand"
STR_SHOW_HIDDEN_FILES: "Toon verborgen bestanden"
STR_REFRESH_FREQ: "Verversingsfrequentie"
STR_KOREADER_SYNC: "KOReader Sync"
STR_CHECK_UPDATES: "Controleren op updates"
@@ -180,6 +185,7 @@ STR_BACK: "« Terug"
STR_EXIT: "« Sluit"
STR_HOME: "« Home"
STR_SELECT: "Kies"
STR_SELECTED: "Geselecteerd"
STR_TOGGLE: "Wissel"
STR_CONFIRM: "Bevestig"
STR_CANCEL: "Annuleer"
@@ -247,6 +253,7 @@ STR_GO_TO_PERCENT: "Ga naar %"
STR_GO_HOME_BUTTON: "Naar Home"
STR_SYNC_PROGRESS: "Voortgang synchroniseren"
STR_DELETE_CACHE: "Boekcache verwijderen"
STR_DELETE: "Verwijder"
STR_DISPLAY_QR: "Pagina als QR tonen"
STR_CHAPTER_PREFIX: "Hoofdstuk: "
STR_PAGES_SEPARATOR: " pagina's | "
@@ -281,3 +288,5 @@ STR_FOOTNOTES: "Voetnoten"
STR_NO_FOOTNOTES: "Geen voetnoten op deze pagina"
STR_LINK: "[link]"
STR_SCREENSHOT_BUTTON: "Screenshot maken"
STR_AUTO_TURN_ENABLED: "Automatisch omslaan ingeschakeld: "
STR_AUTO_TURN_PAGES_PER_MIN: "Autom. omslaan (pagina's per minuut)"