2025-12-13 19:36:01 +11:00
|
|
|
#include "ChapterHtmlSlimParser.h"
|
2025-12-06 20:57:24 +11:00
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
#include <GfxRenderer.h>
|
2025-12-06 20:57:24 +11:00
|
|
|
#include <HardwareSerial.h>
|
2025-12-30 15:09:30 +10:00
|
|
|
#include <SDCardManager.h>
|
2025-12-06 22:12:01 +11:00
|
|
|
#include <expat.h>
|
2025-12-06 20:57:24 +11:00
|
|
|
|
2025-12-13 19:36:01 +11:00
|
|
|
#include "../Page.h"
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
|
|
|
|
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
|
|
|
|
|
2025-12-28 13:59:44 +09:00
|
|
|
// Minimum file size (in bytes) to show progress bar - smaller chapters don't benefit from it
|
|
|
|
|
constexpr size_t MIN_SIZE_FOR_PROGRESS = 50 * 1024; // 50KB
|
|
|
|
|
|
2025-12-24 22:33:17 +11:00
|
|
|
const char* BLOCK_TAGS[] = {"p", "li", "div", "br", "blockquote"};
|
2025-12-06 20:57:24 +11:00
|
|
|
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
|
|
|
|
|
2025-12-24 22:33:17 +11:00
|
|
|
const char* BOLD_TAGS[] = {"b", "strong"};
|
2025-12-06 20:57:24 +11:00
|
|
|
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
|
|
|
|
|
2025-12-24 22:33:17 +11:00
|
|
|
const char* ITALIC_TAGS[] = {"i", "em"};
|
2025-12-06 20:57:24 +11:00
|
|
|
constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
|
|
|
|
|
|
|
|
|
const char* IMAGE_TAGS[] = {"img"};
|
|
|
|
|
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
|
|
|
|
|
2026-01-15 05:14:59 -07:00
|
|
|
const char* SKIP_TAGS[] = {"head"};
|
2025-12-06 20:57:24 +11:00
|
|
|
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
|
|
|
|
|
|
2025-12-16 13:02:32 +01:00
|
|
|
bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n' || c == '\t'; }
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
// given the start and end of a tag, check to see if it matches a known tag
|
|
|
|
|
bool matches(const char* tag_name, const char* possible_tags[], const int possible_tag_count) {
|
|
|
|
|
for (int i = 0; i < possible_tag_count; i++) {
|
|
|
|
|
if (strcmp(tag_name, possible_tags[i]) == 0) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 03:07:02 -08:00
|
|
|
// flush the contents of partWordBuffer to currentTextBlock
|
|
|
|
|
void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
|
|
|
|
// determine font style
|
|
|
|
|
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
|
|
|
|
if (boldUntilDepth < depth && italicUntilDepth < depth) {
|
|
|
|
|
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
|
|
|
|
} else if (boldUntilDepth < depth) {
|
|
|
|
|
fontStyle = EpdFontFamily::BOLD;
|
|
|
|
|
} else if (italicUntilDepth < depth) {
|
|
|
|
|
fontStyle = EpdFontFamily::ITALIC;
|
|
|
|
|
}
|
|
|
|
|
// flush the buffer
|
|
|
|
|
partWordBuffer[partWordBufferIndex] = '\0';
|
|
|
|
|
currentTextBlock->addWord(partWordBuffer, fontStyle);
|
|
|
|
|
partWordBufferIndex = 0;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 20:57:24 +11:00
|
|
|
// start a new text block if needed
|
2025-12-31 12:11:36 +10:00
|
|
|
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) {
|
2025-12-06 20:57:24 +11:00
|
|
|
if (currentTextBlock) {
|
|
|
|
|
// already have a text block running and it is empty - just reuse it
|
|
|
|
|
if (currentTextBlock->isEmpty()) {
|
|
|
|
|
currentTextBlock->setStyle(style);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
makePages();
|
|
|
|
|
}
|
2026-01-19 17:56:26 +05:00
|
|
|
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled));
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 19:36:01 +11:00
|
|
|
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
|
|
|
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
// Middle of skip
|
|
|
|
|
if (self->skipUntilDepth < self->depth) {
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-15 05:14:59 -07:00
|
|
|
// Special handling for tables - show placeholder text instead of dropping silently
|
|
|
|
|
if (strcmp(name, "table") == 0) {
|
|
|
|
|
// Add placeholder text
|
|
|
|
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
|
|
|
|
if (self->currentTextBlock) {
|
|
|
|
|
self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Skip table contents
|
|
|
|
|
self->skipUntilDepth = self->depth;
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 20:57:24 +11:00
|
|
|
if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
2025-12-13 19:36:01 +11:00
|
|
|
// TODO: Start processing image tags
|
2026-01-15 13:21:46 +01:00
|
|
|
std::string alt;
|
|
|
|
|
if (atts != nullptr) {
|
|
|
|
|
for (int i = 0; atts[i]; i += 2) {
|
|
|
|
|
if (strcmp(atts[i], "alt") == 0) {
|
2026-01-27 12:12:40 +01:00
|
|
|
// add " " (counts as whitespace) at the end of alt
|
|
|
|
|
// so the corresponding text block ends.
|
|
|
|
|
// TODO: A zero-width breaking space would be more appropriate (once/if we support it)
|
|
|
|
|
alt = "[Image: " + std::string(atts[i + 1]) + "] ";
|
2026-01-15 13:21:46 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
|
|
|
|
|
|
|
|
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
|
|
|
|
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
self->characterData(userData, alt.c_str(), alt.length());
|
2026-01-27 12:12:40 +01:00
|
|
|
return;
|
2026-01-15 13:21:46 +01:00
|
|
|
} else {
|
|
|
|
|
// Skip for now
|
|
|
|
|
self->skipUntilDepth = self->depth;
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
|
|
|
|
// start skip
|
|
|
|
|
self->skipUntilDepth = self->depth;
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-19 01:11:03 +11:00
|
|
|
// Skip blocks with role="doc-pagebreak" and epub:type="pagebreak"
|
|
|
|
|
if (atts != nullptr) {
|
|
|
|
|
for (int i = 0; atts[i]; i += 2) {
|
|
|
|
|
if (strcmp(atts[i], "role") == 0 && strcmp(atts[i + 1], "doc-pagebreak") == 0 ||
|
|
|
|
|
strcmp(atts[i], "epub:type") == 0 && strcmp(atts[i + 1], "pagebreak") == 0) {
|
|
|
|
|
self->skipUntilDepth = self->depth;
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 20:57:24 +11:00
|
|
|
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
2025-12-12 22:13:34 +11:00
|
|
|
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
2025-12-30 23:18:51 +11:00
|
|
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
2025-12-06 20:57:24 +11:00
|
|
|
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
|
|
|
|
if (strcmp(name, "br") == 0) {
|
2026-01-27 03:07:02 -08:00
|
|
|
if (self->partWordBufferIndex > 0) {
|
|
|
|
|
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
|
|
|
|
self->flushPartWordBuffer();
|
|
|
|
|
}
|
2025-12-06 20:57:24 +11:00
|
|
|
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
|
|
|
|
} else {
|
2026-01-02 01:21:48 -06:00
|
|
|
self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment);
|
2026-01-14 06:23:03 -06:00
|
|
|
if (strcmp(name, "li") == 0) {
|
|
|
|
|
self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR);
|
|
|
|
|
}
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
} else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
2025-12-30 23:18:51 +11:00
|
|
|
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
2025-12-06 20:57:24 +11:00
|
|
|
} else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
2025-12-30 23:18:51 +11:00
|
|
|
self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth);
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self->depth += 1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 19:36:01 +11:00
|
|
|
void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char* s, const int len) {
|
|
|
|
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
// Middle of skip
|
|
|
|
|
if (self->skipUntilDepth < self->depth) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < len; i++) {
|
|
|
|
|
if (isWhitespace(s[i])) {
|
|
|
|
|
// Currently looking at whitespace, if there's anything in the partWordBuffer, flush it
|
|
|
|
|
if (self->partWordBufferIndex > 0) {
|
2026-01-27 03:07:02 -08:00
|
|
|
self->flushPartWordBuffer();
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
// Skip the whitespace char
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 12:38:30 +01:00
|
|
|
// Skip Zero Width No-Break Space / BOM (U+FEFF) = 0xEF 0xBB 0xBF
|
|
|
|
|
const XML_Char FEFF_BYTE_1 = static_cast<XML_Char>(0xEF);
|
|
|
|
|
const XML_Char FEFF_BYTE_2 = static_cast<XML_Char>(0xBB);
|
|
|
|
|
const XML_Char FEFF_BYTE_3 = static_cast<XML_Char>(0xBF);
|
|
|
|
|
|
|
|
|
|
if (s[i] == FEFF_BYTE_1) {
|
|
|
|
|
// Check if the next two bytes complete the 3-byte sequence
|
|
|
|
|
if ((i + 2 < len) && (s[i + 1] == FEFF_BYTE_2) && (s[i + 2] == FEFF_BYTE_3)) {
|
|
|
|
|
// Sequence 0xEF 0xBB 0xBF found!
|
|
|
|
|
i += 2; // Skip the next two bytes
|
|
|
|
|
continue; // Move to the next iteration
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-06 20:57:24 +11:00
|
|
|
// If we're about to run out of space, then cut the word off and start a new one
|
2025-12-08 22:06:09 +11:00
|
|
|
if (self->partWordBufferIndex >= MAX_WORD_SIZE) {
|
2026-01-27 03:07:02 -08:00
|
|
|
self->flushPartWordBuffer();
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self->partWordBuffer[self->partWordBufferIndex++] = s[i];
|
|
|
|
|
}
|
2025-12-21 13:43:19 +11:00
|
|
|
|
|
|
|
|
// If we have > 750 words buffered up, perform the layout and consume out all but the last line
|
|
|
|
|
// There should be enough here to build out 1-2 full pages and doing this will free up a lot of
|
|
|
|
|
// memory.
|
|
|
|
|
// Spotted when reading Intermezzo, there are some really long text blocks in there.
|
|
|
|
|
if (self->currentTextBlock->size() > 750) {
|
|
|
|
|
Serial.printf("[%lu] [EHP] Text block too long, splitting into multiple pages\n", millis());
|
|
|
|
|
self->currentTextBlock->layoutAndExtractLines(
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
self->renderer, self->fontId, self->viewportWidth,
|
2025-12-21 13:43:19 +11:00
|
|
|
[self](const std::shared_ptr<TextBlock>& textBlock) { self->addLineToPage(textBlock); }, false);
|
|
|
|
|
}
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-13 19:36:01 +11:00
|
|
|
void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* name) {
|
|
|
|
|
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
if (self->partWordBufferIndex > 0) {
|
|
|
|
|
// Only flush out part word buffer if we're closing a block tag or are at the top of the HTML file.
|
|
|
|
|
// We don't want to flush out content when closing inline tags like <span>.
|
|
|
|
|
// Currently this also flushes out on closing <b> and <i> tags, but they are line tags so that shouldn't happen,
|
|
|
|
|
// text styling needs to be overhauled to fix it.
|
2025-12-06 22:12:01 +11:00
|
|
|
const bool shouldBreakText =
|
|
|
|
|
matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) ||
|
|
|
|
|
matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1;
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
if (shouldBreakText) {
|
2026-01-27 03:07:02 -08:00
|
|
|
self->flushPartWordBuffer();
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self->depth -= 1;
|
|
|
|
|
|
|
|
|
|
// Leaving skip
|
|
|
|
|
if (self->skipUntilDepth == self->depth) {
|
|
|
|
|
self->skipUntilDepth = INT_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Leaving bold
|
|
|
|
|
if (self->boldUntilDepth == self->depth) {
|
|
|
|
|
self->boldUntilDepth = INT_MAX;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Leaving italic
|
|
|
|
|
if (self->italicUntilDepth == self->depth) {
|
|
|
|
|
self->italicUntilDepth = INT_MAX;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 19:36:01 +11:00
|
|
|
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
2026-01-02 01:21:48 -06:00
|
|
|
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
const XML_Parser parser = XML_ParserCreate(nullptr);
|
|
|
|
|
int done;
|
|
|
|
|
|
|
|
|
|
if (!parser) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [EHP] Couldn't allocate memory for parser\n", millis());
|
2025-12-06 20:57:24 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 15:09:30 +10:00
|
|
|
FsFile file;
|
|
|
|
|
if (!SdMan.openFileForRead("EHP", filepath, file)) {
|
2025-12-08 00:39:17 +11:00
|
|
|
XML_ParserFree(parser);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2025-12-06 20:57:24 +11:00
|
|
|
|
2025-12-28 13:59:44 +09:00
|
|
|
// Get file size for progress calculation
|
|
|
|
|
const size_t totalSize = file.size();
|
|
|
|
|
size_t bytesRead = 0;
|
|
|
|
|
int lastProgress = -1;
|
|
|
|
|
|
2025-12-22 17:16:39 +11:00
|
|
|
XML_SetUserData(parser, this);
|
|
|
|
|
XML_SetElementHandler(parser, startElement, endElement);
|
|
|
|
|
XML_SetCharacterDataHandler(parser, characterData);
|
|
|
|
|
|
2025-12-06 20:57:24 +11:00
|
|
|
do {
|
|
|
|
|
void* const buf = XML_GetBuffer(parser, 1024);
|
|
|
|
|
if (!buf) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [EHP] Couldn't allocate memory for buffer\n", millis());
|
2025-12-22 17:16:39 +11:00
|
|
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
|
|
|
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
|
|
|
|
XML_SetCharacterDataHandler(parser, nullptr);
|
2025-12-06 20:57:24 +11:00
|
|
|
XML_ParserFree(parser);
|
2025-12-23 14:14:10 +11:00
|
|
|
file.close();
|
2025-12-06 20:57:24 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-30 23:18:51 +11:00
|
|
|
const size_t len = file.read(buf, 1024);
|
2025-12-06 20:57:24 +11:00
|
|
|
|
2025-12-30 23:18:51 +11:00
|
|
|
if (len == 0 && file.available() > 0) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [EHP] File read error\n", millis());
|
2025-12-22 17:16:39 +11:00
|
|
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
|
|
|
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
|
|
|
|
XML_SetCharacterDataHandler(parser, nullptr);
|
2025-12-06 20:57:24 +11:00
|
|
|
XML_ParserFree(parser);
|
2025-12-23 14:14:10 +11:00
|
|
|
file.close();
|
2025-12-06 20:57:24 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-28 13:59:44 +09:00
|
|
|
// Update progress (call every 10% change to avoid too frequent updates)
|
|
|
|
|
// Only show progress for larger chapters where rendering overhead is worth it
|
|
|
|
|
bytesRead += len;
|
|
|
|
|
if (progressFn && totalSize >= MIN_SIZE_FOR_PROGRESS) {
|
|
|
|
|
const int progress = static_cast<int>((bytesRead * 100) / totalSize);
|
|
|
|
|
if (lastProgress / 10 != progress / 10) {
|
|
|
|
|
lastProgress = progress;
|
|
|
|
|
progressFn(progress);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-23 14:14:10 +11:00
|
|
|
done = file.available() == 0;
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
if (XML_ParseBuffer(parser, static_cast<int>(len), done) == XML_STATUS_ERROR) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [EHP] Parse error at line %lu:\n%s\n", millis(), XML_GetCurrentLineNumber(parser),
|
2025-12-06 20:57:24 +11:00
|
|
|
XML_ErrorString(XML_GetErrorCode(parser)));
|
2025-12-22 17:16:39 +11:00
|
|
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
|
|
|
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
|
|
|
|
XML_SetCharacterDataHandler(parser, nullptr);
|
2025-12-06 20:57:24 +11:00
|
|
|
XML_ParserFree(parser);
|
2025-12-23 14:14:10 +11:00
|
|
|
file.close();
|
2025-12-06 20:57:24 +11:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} while (!done);
|
|
|
|
|
|
2025-12-22 17:16:39 +11:00
|
|
|
XML_StopParser(parser, XML_FALSE); // Stop any pending processing
|
|
|
|
|
XML_SetElementHandler(parser, nullptr, nullptr); // Clear callbacks
|
|
|
|
|
XML_SetCharacterDataHandler(parser, nullptr);
|
2025-12-06 20:57:24 +11:00
|
|
|
XML_ParserFree(parser);
|
2025-12-23 14:14:10 +11:00
|
|
|
file.close();
|
2025-12-06 20:57:24 +11:00
|
|
|
|
|
|
|
|
// Process last page if there is still text
|
|
|
|
|
if (currentTextBlock) {
|
|
|
|
|
makePages();
|
2025-12-12 22:13:34 +11:00
|
|
|
completePageFn(std::move(currentPage));
|
|
|
|
|
currentPage.reset();
|
|
|
|
|
currentTextBlock.reset();
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 20:10:16 +11:00
|
|
|
void ChapterHtmlSlimParser::addLineToPage(std::shared_ptr<TextBlock> line) {
|
|
|
|
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
if (currentPageNextY + lineHeight > viewportHeight) {
|
2025-12-13 20:10:16 +11:00
|
|
|
completePageFn(std::move(currentPage));
|
|
|
|
|
currentPage.reset(new Page());
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
currentPageNextY = 0;
|
2025-12-13 20:10:16 +11:00
|
|
|
}
|
|
|
|
|
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
currentPage->elements.push_back(std::make_shared<PageLine>(line, 0, currentPageNextY));
|
2025-12-13 20:10:16 +11:00
|
|
|
currentPageNextY += lineHeight;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-13 19:36:01 +11:00
|
|
|
void ChapterHtmlSlimParser::makePages() {
|
2025-12-06 20:57:24 +11:00
|
|
|
if (!currentTextBlock) {
|
2025-12-08 22:39:23 +11:00
|
|
|
Serial.printf("[%lu] [EHP] !! No text block to make pages for !!\n", millis());
|
2025-12-06 20:57:24 +11:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!currentPage) {
|
2025-12-12 22:13:34 +11:00
|
|
|
currentPage.reset(new Page());
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
currentPageNextY = 0;
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|
|
|
|
|
|
2025-12-08 22:06:09 +11:00
|
|
|
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
2025-12-13 20:10:16 +11:00
|
|
|
currentTextBlock->layoutAndExtractLines(
|
Rotation Support (#77)
• What is the goal of this PR?
Implement a horizontal EPUB reading mode so books can be read in
landscape orientation (both 90° and 270°), while keeping the rest of the
UI in portrait.
• What changes are included?
◦ Rendering / Display
▪ Added an orientation model to GfxRenderer (Portrait, LandscapeNormal,
LandscapeFlipped) and made:
▪ drawPixel, drawImage, displayWindow map logical coordinates
differently depending on orientation.
▪ getScreenWidth() / getScreenHeight() return orientation‑aware logical
dimensions (480×800 in portrait, 800×480 in landscape).
◦ Settings / Configuration
▪ Extended CrossPointSettings with:
▪ landscapeReading (toggle for portrait vs. landscape EPUB reading).
▪ landscapeFlipped (toggle to flip landscape 180° so both horizontal
holding directions are supported).
▪ Updated settings serialization/deserialization to persist these fields
while remaining backward‑compatible with existing settings files.
▪ Updated SettingsActivity to expose two new toggles:
▪ “Landscape Reading”
▪ “Flip Landscape (swap top/bottom)”
◦ EPUB Reader
▪ In EpubReaderActivity:
▪ On onEnter, set GfxRenderer orientation based on the new settings
(Portrait, LandscapeNormal, or LandscapeFlipped).
▪ On onExit, reset orientation back to Portrait so Home, WiFi, Settings,
etc. continue to render as before.
▪ Adjusted renderStatusBar to position the status bar and battery
indicator relative to GfxRenderer::getScreenHeight() instead of
hard‑coded Y coordinates, so it stays correctly at the bottom in both
portrait and landscape.
◦ EPUB Caching / Layout
▪ Extended Section cache metadata (section.bin) to include the logical
screenWidth and screenHeight used when pages were generated; bumped
SECTION_FILE_VERSION.
▪ Updated loadCacheMetadata to compare:
▪ font/margins/line compression/extraParagraphSpacing and screen
dimensions; mismatches now invalidate and clear the cache.
▪ Updated persistPageDataToSD and all call sites in EpubReaderActivity
to pass the current GfxRenderer::getScreenWidth() / getScreenHeight() so
portrait and landscape caches are kept separate and correctly sized.
Additional Context
• Cache behavior / migration
◦ Existing section.bin files (old SECTION_FILE_VERSION) will be detected
as incompatible and their caches cleared and rebuilt once per chapter
when first opened after this change.
◦ Within a given orientation, caches will be reused as before. Switching
orientation (portrait ↔ landscape) will cause a one‑time re‑index of
each chapter in the new orientation.
• Scope and risks
◦ Orientation changes are scoped to the EPUB reader; the Home screen,
Settings, WiFi selection, sleep screens, and web server UI continue to
assume portrait orientation.
◦ The renderer’s orientation is a static/global setting; if future code
uses GfxRenderer outside the reader while a reader instance is active,
it should be aware that orientation is no longer implicitly fixed.
◦ All drawing primitives now go through orientation‑aware coordinate
transforms; any code that previously relied on edge‑case behavior or
out‑of‑bounds writes might surface as logged “Outside range” warnings
instead.
• Testing suggestions / areas to focus on
◦ Verify in hardware:
▪ Portrait mode still renders correctly (boot, home, settings, WiFi,
reader).
▪ Landscape reading in both directions:
▪ Landscape Reading = ON, Flip Landscape = OFF.
▪ Landscape Reading = ON, Flip Landscape = ON.
▪ Status bar (page X/Y, % progress, battery icon) is fully visible and
aligned at the bottom in all three combinations.
◦ Open the same book:
▪ In portrait first, then switch to landscape and reopen it.
▪ Confirm that:
▪ Old portrait caches are rebuilt once for landscape (you should see the
“Indexing…” page).
▪ Progress save/restore still works (resume opens to the correct page in
the current orientation).
◦ Ensure grayscale rendering (the secondary pass in
EpubReaderActivity::renderContents) still looks correct in both
orientations.
---------
Co-authored-by: Dave Allie <dave@daveallie.com>
2025-12-28 05:33:20 -05:00
|
|
|
renderer, fontId, viewportWidth,
|
2025-12-13 20:10:16 +11:00
|
|
|
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
2025-12-15 13:16:46 +01:00
|
|
|
// Extra paragraph spacing if enabled
|
|
|
|
|
if (extraParagraphSpacing) {
|
|
|
|
|
currentPageNextY += lineHeight / 2;
|
|
|
|
|
}
|
2025-12-06 20:57:24 +11:00
|
|
|
}
|