Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
35 kB · 930 lines
C++
at master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931#include "EpubReaderActivity.h"
#include <Epub/Page.h>#include <Epub/blocks/TextBlock.h>#include <FontCacheManager.h>#include <FsHelpers.h>#include <GfxRenderer.h>#include <HalStorage.h>#include <I18n.h>#include <Logging.h>#include <esp_system.h>
#include <limits>
#include "CrossPointSettings.h"#include "CrossPointState.h"#include "EpubReaderChapterSelectionActivity.h"#include "EpubReaderFootnotesActivity.h"#include "EpubReaderPercentSelectionActivity.h"#include "KOReaderCredentialStore.h"#include "KOReaderSyncActivity.h"#include "MappedInputManager.h"#include "QrDisplayActivity.h"#include "ReaderUtils.h"#include "RecentBooksStore.h"#include "components/UITheme.h"#include "fontIds.h"#include "util/ScreenshotUtil.h"
namespace {// pagesPerRefresh now comes from SETTINGS.getRefreshFrequency()constexpr unsigned long skipChapterMs = 700;// pages per minute, first item is 1 to prevent division by zero if accessedconst std::vector<int> PAGE_TURN_LABELS = {1, 1, 3, 6, 12};
int clampPercent(int percent) { if (percent < 0) { return 0; } if (percent > 100) { return 100; } return percent;}
} // namespace
void EpubReaderActivity::onEnter() { Activity::onEnter();
if (!epub) { return; }
// Configure screen orientation based on settings // NOTE: This affects layout math and must be applied before any render calls. ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
epub->setupCacheDir();
FsFile f; if (Storage.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[6]; int dataSize = f.read(data, 6); if (dataSize == 4 || dataSize == 6) { currentSpineIndex = data[0] + (data[1] << 8); nextPageNumber = data[2] + (data[3] << 8); if (nextPageNumber == UINT16_MAX) { // UINT16_MAX is an in-memory navigation sentinel for "open previous // chapter on its last page". It should never be treated as persisted // resume state after sleep or reopen. LOG_DBG("ERS", "Ignoring stale last-page sentinel from progress cache"); nextPageNumber = 0; } cachedSpineIndex = currentSpineIndex; LOG_DBG("ERS", "Loaded cache: %d, %d", currentSpineIndex, nextPageNumber); } if (dataSize == 6) { cachedChapterTotalPageCount = data[4] + (data[5] << 8); } } // We may want a better condition to detect if we are opening for the first time. // This will trigger if the book is re-opened at Chapter 0. if (currentSpineIndex == 0) { int textSpineIndex = epub->getSpineIndexForTextReference(); if (textSpineIndex != 0) { currentSpineIndex = textSpineIndex; LOG_DBG("ERS", "Opened for first time, navigating to text reference at index %d", textSpineIndex); } }
// Save current epub as last opened epub and add to recent books APP_STATE.openEpubPath = epub->getPath(); APP_STATE.saveToFile(); RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor(), epub->getThumbBmpPath());
// Trigger first update requestUpdate();}
void EpubReaderActivity::onExit() { Activity::onExit();
// Reset orientation back to portrait for the rest of the UI renderer.setOrientation(GfxRenderer::Orientation::Portrait);
APP_STATE.readerActivityLoadCount = 0; APP_STATE.saveToFile(); section.reset(); epub.reset();}
void EpubReaderActivity::loop() { if (!epub) { // Should never happen finish(); return; }
if (automaticPageTurnActive) { if (mappedInput.wasReleased(MappedInputManager::Button::Confirm) || mappedInput.wasReleased(MappedInputManager::Button::Back)) { automaticPageTurnActive = false; // updates chapter title space to indicate page turn disabled requestUpdate(); return; }
if (!section) { requestUpdate(); return; }
// Skips page turn if renderingMutex is busy if (RenderLock::peek()) { lastPageTurnTime = millis(); return; }
if ((millis() - lastPageTurnTime) >= pageTurnDuration) { pageTurn(true); return; } }
// Enter reader menu activity. if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { const int currentPage = section ? section->currentPage + 1 : 0; const int totalPages = section ? section->pageCount : 0; float bookProgress = 0.0f; if (epub->getBookSize() > 0 && section && section->pageCount > 0) { const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount); bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int bookProgressPercent = clampPercent(static_cast<int>(bookProgress + 0.5f)); startActivityForResult(std::make_unique<EpubReaderMenuActivity>( renderer, mappedInput, epub->getTitle(), currentPage, totalPages, bookProgressPercent, SETTINGS.orientation, !currentPageFootnotes.empty()), [this](const ActivityResult& result) { // Always apply orientation change even if the menu was cancelled const auto& menu = std::get<MenuResult>(result.data); applyOrientation(menu.orientation); toggleAutoPageTurn(menu.pageTurnOption); if (!result.isCancelled) { onReaderMenuConfirm(static_cast<EpubReaderMenuActivity::MenuAction>(menu.action)); } }); }
// Long press BACK (1s+) goes to file selection if (mappedInput.isPressed(MappedInputManager::Button::Back) && mappedInput.getHeldTime() >= ReaderUtils::GO_HOME_MS) { activityManager.goToFileBrowser(epub ? epub->getPath() : ""); return; }
// Short press BACK goes directly to home (or restores position if viewing footnote) if (mappedInput.wasReleased(MappedInputManager::Button::Back) && mappedInput.getHeldTime() < ReaderUtils::GO_HOME_MS) { if (footnoteDepth > 0) { restoreSavedPosition(); return; } onGoHome(); return; }
auto [prevTriggered, nextTriggered] = ReaderUtils::detectPageTurn(mappedInput); if (!prevTriggered && !nextTriggered) { return; }
// At end of the book, forward button goes home and back button returns to last page if (currentSpineIndex > 0 && currentSpineIndex >= epub->getSpineItemsCount()) { if (nextTriggered) { onGoHome(); } else { currentSpineIndex = epub->getSpineItemsCount() - 1; nextPageNumber = 0; pendingPageJump = std::numeric_limits<uint16_t>::max(); requestUpdate(); } return; }
const bool skipChapter = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipChapterMs;
// Don't skip chapter after screenshot if (gpio.wasReleased(HalGPIO::BTN_POWER) && gpio.wasReleased(HalGPIO::BTN_DOWN)) { return; }
if (skipChapter) { lastPageTurnTime = millis(); // We don't want to delete the section mid-render, so grab the semaphore { RenderLock lock(*this); nextPageNumber = 0; currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1; section.reset(); } requestUpdate(); return; }
// No current section, attempt to rerender the book if (!section) { requestUpdate(); return; }
if (prevTriggered) { pageTurn(false); } else { pageTurn(true); }}
// Translate an absolute percent into a spine index plus a normalized position// within that spine so we can jump after the section is loaded.void EpubReaderActivity::jumpToPercent(int percent) { if (!epub) { return; }
const size_t bookSize = epub->getBookSize(); if (bookSize == 0) { return; }
// Normalize input to 0-100 to avoid invalid jumps. percent = clampPercent(percent);
// Convert percent into a byte-like absolute position across the spine sizes. // Use an overflow-safe computation: (bookSize / 100) * percent + (bookSize % 100) * percent / 100 size_t targetSize = (bookSize / 100) * static_cast<size_t>(percent) + (bookSize % 100) * static_cast<size_t>(percent) / 100; if (percent >= 100) { // Ensure the final percent lands inside the last spine item. targetSize = bookSize - 1; }
const int spineCount = epub->getSpineItemsCount(); if (spineCount == 0) { return; }
int targetSpineIndex = spineCount - 1; size_t prevCumulative = 0;
for (int i = 0; i < spineCount; i++) { const size_t cumulative = epub->getCumulativeSpineItemSize(i); if (targetSize <= cumulative) { // Found the spine item containing the absolute position. targetSpineIndex = i; prevCumulative = (i > 0) ? epub->getCumulativeSpineItemSize(i - 1) : 0; break; } }
const size_t cumulative = epub->getCumulativeSpineItemSize(targetSpineIndex); const size_t spineSize = (cumulative > prevCumulative) ? (cumulative - prevCumulative) : 0; // Store a normalized position within the spine so it can be applied once loaded. pendingSpineProgress = (spineSize == 0) ? 0.0f : static_cast<float>(targetSize - prevCumulative) / static_cast<float>(spineSize); if (pendingSpineProgress < 0.0f) { pendingSpineProgress = 0.0f; } else if (pendingSpineProgress > 1.0f) { pendingSpineProgress = 1.0f; }
// Reset state so render() reloads and repositions on the target spine. { RenderLock lock(*this); currentSpineIndex = targetSpineIndex; nextPageNumber = 0; pendingPercentJump = true; section.reset(); }}
void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction action) { switch (action) { case EpubReaderMenuActivity::MenuAction::SELECT_CHAPTER: { const int spineIdx = currentSpineIndex; const std::string path = epub->getPath(); startActivityForResult( std::make_unique<EpubReaderChapterSelectionActivity>(renderer, mappedInput, epub, path, spineIdx), [this](const ActivityResult& result) { if (!result.isCancelled && currentSpineIndex != std::get<ChapterResult>(result.data).spineIndex) { RenderLock lock(*this); currentSpineIndex = std::get<ChapterResult>(result.data).spineIndex; nextPageNumber = 0; section.reset(); } }); break; } case EpubReaderMenuActivity::MenuAction::FOOTNOTES: { startActivityForResult(std::make_unique<EpubReaderFootnotesActivity>(renderer, mappedInput, currentPageFootnotes), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& footnoteResult = std::get<FootnoteResult>(result.data); navigateToHref(footnoteResult.href, true); } requestUpdate(); }); break; } case EpubReaderMenuActivity::MenuAction::GO_TO_PERCENT: { float bookProgress = 0.0f; if (epub && epub->getBookSize() > 0 && section && section->pageCount > 0) { const float chapterProgress = static_cast<float>(section->currentPage) / static_cast<float>(section->pageCount); bookProgress = epub->calculateProgress(currentSpineIndex, chapterProgress) * 100.0f; } const int initialPercent = clampPercent(static_cast<int>(bookProgress + 0.5f)); startActivityForResult( std::make_unique<EpubReaderPercentSelectionActivity>(renderer, mappedInput, initialPercent), [this](const ActivityResult& result) { if (!result.isCancelled) { jumpToPercent(std::get<PercentResult>(result.data).percent); } }); break; } case EpubReaderMenuActivity::MenuAction::DISPLAY_QR: { if (section && section->currentPage >= 0 && section->currentPage < section->pageCount) { auto p = section->loadPageFromSectionFile(); if (p) { std::string fullText; for (const auto& el : p->elements) { if (el->getTag() == TAG_PageLine) { const auto& line = static_cast<const PageLine&>(*el); if (line.getBlock()) { const auto& words = line.getBlock()->getWords(); for (const auto& w : words) { if (!fullText.empty()) fullText += " "; fullText += w; } } } } if (!fullText.empty()) { startActivityForResult(std::make_unique<QrDisplayActivity>(renderer, mappedInput, fullText), [this](const ActivityResult& result) {}); break; } } } // If no text or page loading failed, just close menu requestUpdate(); break; } case EpubReaderMenuActivity::MenuAction::GO_HOME: { onGoHome(); return; } case EpubReaderMenuActivity::MenuAction::DELETE_CACHE: { { RenderLock lock(*this); if (epub && section) { uint16_t backupSpine = currentSpineIndex; uint16_t backupPage = section->currentPage; uint16_t backupPageCount = section->pageCount; section.reset(); epub->clearCache(); epub->setupCacheDir(); saveProgress(backupSpine, backupPage, backupPageCount); } } onGoHome(); return; } case EpubReaderMenuActivity::MenuAction::SCREENSHOT: { { RenderLock lock(*this); pendingScreenshot = true; } requestUpdate(); break; } case EpubReaderMenuActivity::MenuAction::SYNC: { if (KOREADER_STORE.hasCredentials()) { const int currentPage = section ? section->currentPage : nextPageNumber; const int totalPages = section ? section->pageCount : cachedChapterTotalPageCount; std::optional<uint16_t> paragraphIndex; if (section && currentPage >= 0 && currentPage < section->pageCount) { const uint16_t paragraphPage = currentPage > 0 ? static_cast<uint16_t>(currentPage - 1) : static_cast<uint16_t>(currentPage); if (const auto pIdx = section->getParagraphIndexForPage(paragraphPage)) { paragraphIndex = *pIdx; } } startActivityForResult( std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex, currentPage, totalPages, paragraphIndex), [this](const ActivityResult& result) { if (!result.isCancelled) { const auto& sync = std::get<SyncResult>(result.data); if (currentSpineIndex != sync.spineIndex || (section && section->currentPage != sync.page)) { RenderLock lock(*this); currentSpineIndex = sync.spineIndex; nextPageNumber = sync.page; cachedChapterTotalPageCount = 0; // Prevent rescaling sync page pendingPageJump.reset(); saveProgress(currentSpineIndex, nextPageNumber, 0); section.reset(); } } }); } break; } }}
void EpubReaderActivity::applyOrientation(const uint8_t orientation) { // No-op if the selected orientation matches current settings. if (SETTINGS.orientation == orientation) { return; }
// Preserve current reading position so we can restore after reflow. { RenderLock lock(*this); if (section) { cachedSpineIndex = currentSpineIndex; cachedChapterTotalPageCount = section->pageCount; nextPageNumber = section->currentPage; }
// Persist the selection so the reader keeps the new orientation on next launch. SETTINGS.orientation = orientation; SETTINGS.saveToFile();
// Update renderer orientation to match the new logical coordinate system. ReaderUtils::applyOrientation(renderer, SETTINGS.orientation);
// Reset section to force re-layout in the new orientation. section.reset(); }}
void EpubReaderActivity::toggleAutoPageTurn(const uint8_t selectedPageTurnOption) { if (selectedPageTurnOption == 0 || selectedPageTurnOption >= PAGE_TURN_LABELS.size()) { automaticPageTurnActive = false; return; }
lastPageTurnTime = millis(); // calculates page turn duration by dividing by number of pages pageTurnDuration = (1UL * 60 * 1000) / PAGE_TURN_LABELS[selectedPageTurnOption]; automaticPageTurnActive = true;
const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight(); // resets cached section so that space is reserved for auto page turn indicator when None or progress bar only if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { // Preserve current reading position so we can restore after reflow. RenderLock lock(*this); if (section) { cachedSpineIndex = currentSpineIndex; cachedChapterTotalPageCount = section->pageCount; nextPageNumber = section->currentPage; } section.reset(); }}
void EpubReaderActivity::pageTurn(bool isForwardTurn) { if (isForwardTurn) { if (section->currentPage < section->pageCount - 1) { section->currentPage++; } else { // We don't want to delete the section mid-render, so grab the semaphore { RenderLock lock(*this); nextPageNumber = 0; currentSpineIndex++; section.reset(); } } } else { if (section->currentPage > 0) { section->currentPage--; } else if (currentSpineIndex > 0) { // We don't want to delete the section mid-render, so grab the semaphore { RenderLock lock(*this); nextPageNumber = 0; pendingPageJump = std::numeric_limits<uint16_t>::max(); currentSpineIndex--; section.reset(); } } } lastPageTurnTime = millis(); requestUpdate();}
// TODO: Failure handlingvoid EpubReaderActivity::render(RenderLock&& lock) { if (!epub) { return; }
// edge case handling for sub-zero spine index if (currentSpineIndex < 0) { currentSpineIndex = 0; } // based bounds of book, show end of book screen if (currentSpineIndex > epub->getSpineItemsCount()) { currentSpineIndex = epub->getSpineItemsCount(); }
// Show end of book screen if (currentSpineIndex == epub->getSpineItemsCount()) { renderer.clearScreen(); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_END_OF_BOOK), true, EpdFontFamily::BOLD); renderer.displayBuffer(); automaticPageTurnActive = false; return; }
// Apply screen viewable areas and additional padding int orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft; renderer.getOrientedViewableTRBL(&orientedMarginTop, &orientedMarginRight, &orientedMarginBottom, &orientedMarginLeft); orientedMarginTop += SETTINGS.screenMargin; orientedMarginLeft += SETTINGS.screenMargin; orientedMarginRight += SETTINGS.screenMargin;
const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight();
// reserves space for automatic page turn indicator when no status bar or progress bar only if (automaticPageTurnActive && (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight())) { orientedMarginBottom += std::max(SETTINGS.screenMargin, static_cast<uint8_t>(statusBarHeight + UITheme::getInstance().getMetrics().statusBarVerticalMargin)); } else { orientedMarginBottom += std::max(SETTINGS.screenMargin, statusBarHeight); }
const uint16_t viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; const uint16_t viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom;
if (!section) { const auto filepath = epub->getSpineItem(currentSpineIndex).href; LOG_DBG("ERS", "Loading file: %s, index: %d", filepath.c_str(), currentSpineIndex); section = std::unique_ptr<Section>(new Section(epub, currentSpineIndex, renderer));
if (!section->loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) { LOG_DBG("ERS", "Cache not found, building...");
const auto popupFn = [this]() { GUI.drawPopup(renderer, tr(STR_INDEXING)); };
if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering, popupFn)) { LOG_ERR("ERS", "Failed to persist page data to SD"); section.reset(); return; } } else { LOG_DBG("ERS", "Cache found, skipping build..."); }
if (pendingPageJump.has_value()) { if (*pendingPageJump >= section->pageCount && section->pageCount > 0) { section->currentPage = section->pageCount - 1; } else { section->currentPage = *pendingPageJump; } pendingPageJump.reset(); } else { section->currentPage = nextPageNumber; if (section->currentPage < 0) { section->currentPage = 0; } else if (section->currentPage >= section->pageCount && section->pageCount > 0) { LOG_DBG("ERS", "Clamping cached page %d to %d", section->currentPage, section->pageCount - 1); section->currentPage = section->pageCount - 1; } }
if (!pendingAnchor.empty()) { if (const auto page = section->getPageForAnchor(pendingAnchor)) { section->currentPage = *page; LOG_DBG("ERS", "Resolved anchor '%s' to page %d", pendingAnchor.c_str(), *page); } else { LOG_DBG("ERS", "Anchor '%s' not found in section %d", pendingAnchor.c_str(), currentSpineIndex); } pendingAnchor.clear(); }
// handles changes in reader settings and reset to approximate position based on cached progress if (cachedChapterTotalPageCount > 0) { // only goes to relative position if spine index matches cached value if (currentSpineIndex == cachedSpineIndex && section->pageCount != cachedChapterTotalPageCount) { float progress = static_cast<float>(section->currentPage) / static_cast<float>(cachedChapterTotalPageCount); int newPage = static_cast<int>(progress * section->pageCount); section->currentPage = newPage; } cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again }
if (pendingPercentJump && section->pageCount > 0) { // Apply the pending percent jump now that we know the new section's page count. int newPage = static_cast<int>(pendingSpineProgress * static_cast<float>(section->pageCount)); if (newPage >= section->pageCount) { newPage = section->pageCount - 1; } section->currentPage = newPage; pendingPercentJump = false; } }
renderer.clearScreen();
if (section->pageCount == 0) { LOG_DBG("ERS", "No pages to render"); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_CHAPTER), true, EpdFontFamily::BOLD); renderStatusBar(); renderer.displayBuffer(); automaticPageTurnActive = false; return; }
if (section->currentPage < 0 || section->currentPage >= section->pageCount) { LOG_DBG("ERS", "Page out of bounds: %d (max %d)", section->currentPage, section->pageCount); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_OUT_OF_BOUNDS), true, EpdFontFamily::BOLD); renderStatusBar(); renderer.displayBuffer(); automaticPageTurnActive = false; return; }
{ auto p = section->loadPageFromSectionFile(); if (!p) { LOG_ERR("ERS", "Failed to load page from SD - clearing section cache"); section->clearCache(); section.reset(); requestUpdate(); // Try again after clearing cache // TODO: prevent infinite loop if the page keeps failing to load for some reason automaticPageTurnActive = false; return; }
// Collect footnotes from the loaded page currentPageFootnotes = std::move(p->footnotes);
const auto start = millis(); renderContents(std::move(p), orientedMarginTop, orientedMarginRight, orientedMarginBottom, orientedMarginLeft); LOG_DBG("ERS", "Rendered page in %dms", millis() - start); } silentIndexNextChapterIfNeeded(viewportWidth, viewportHeight); saveProgress(currentSpineIndex, section->currentPage, section->pageCount);
if (pendingScreenshot) { pendingScreenshot = false; ScreenshotUtil::takeScreenshot(renderer); }}
void EpubReaderActivity::silentIndexNextChapterIfNeeded(const uint16_t viewportWidth, const uint16_t viewportHeight) { if (!epub || !section || section->pageCount < 2) { return; }
// Build the next chapter cache while the penultimate page is on screen. if (section->currentPage != section->pageCount - 2) { return; }
const int nextSpineIndex = currentSpineIndex + 1; if (nextSpineIndex < 0 || nextSpineIndex >= epub->getSpineItemsCount()) { return; }
Section nextSection(epub, nextSpineIndex, renderer); if (nextSection.loadSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) { return; }
LOG_DBG("ERS", "Silently indexing next chapter: %d", nextSpineIndex); if (!nextSection.createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), SETTINGS.extraParagraphSpacing, SETTINGS.paragraphAlignment, viewportWidth, viewportHeight, SETTINGS.hyphenationEnabled, SETTINGS.embeddedStyle, SETTINGS.imageRendering)) { LOG_ERR("ERS", "Failed silent indexing for chapter: %d", nextSpineIndex); }}
void EpubReaderActivity::saveProgress(int spineIndex, int currentPage, int pageCount) { FsFile f; if (Storage.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { uint8_t data[6]; data[0] = currentSpineIndex & 0xFF; data[1] = (currentSpineIndex >> 8) & 0xFF; data[2] = currentPage & 0xFF; data[3] = (currentPage >> 8) & 0xFF; data[4] = pageCount & 0xFF; data[5] = (pageCount >> 8) & 0xFF; f.write(data, 6); LOG_DBG("ERS", "Progress saved: Chapter %d, Page %d", spineIndex, currentPage); } else { LOG_ERR("ERS", "Could not save progress!"); }}void EpubReaderActivity::renderContents(std::unique_ptr<Page> page, const int orientedMarginTop, const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) { const auto t0 = millis(); auto* fcm = renderer.getFontCacheManager(); fcm->resetStats();
// Font prewarm: scan pass accumulates text, then prewarm, then real render const uint32_t heapBefore = esp_get_free_heap_size(); auto scope = fcm->createPrewarmScope(); page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); // scan pass scope.endScanAndPrewarm(); const uint32_t heapAfter = esp_get_free_heap_size(); fcm->logStats("prewarm"); const auto tPrewarm = millis();
LOG_DBG("ERS", "Heap: before=%lu after=%lu delta=%ld", heapBefore, heapAfter, (int32_t)heapAfter - (int32_t)heapBefore);
// Force special handling for pages with images when anti-aliasing is on bool imagePageWithAA = page->hasImages() && SETTINGS.textAntiAliasing;
page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderStatusBar(); fcm->logStats("bw_render"); const auto tBwRender = millis();
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. // Step 1: Display page with image area blanked (text appears, image area white) // Step 2: Re-render with images and display again (images appear clean) int16_t imgX, imgY, imgW, imgH; if (page->getImageBoundingBox(imgX, imgY, imgW, imgH)) { renderer.fillRect(imgX + orientedMarginLeft, imgY + orientedMarginTop, imgW, imgH, false); renderer.displayBuffer(HalDisplay::FAST_REFRESH);
// Re-render page content to restore images into the blanked area // Status bar is not re-rendered here to avoid reading stale dynamic values (e.g. battery %) page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderer.displayBuffer(HalDisplay::FAST_REFRESH); } else { renderer.displayBuffer(HalDisplay::HALF_REFRESH); } // Double FAST_REFRESH handles ghosting for image pages; don't count toward full refresh cadence } else { ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); } const auto tDisplay = millis();
// Save bw buffer to reset buffer state after grayscale data sync renderer.storeBwBuffer(); const auto tBwStore = millis();
// grayscale rendering // TODO: Only do this if font supports it if (SETTINGS.textAntiAliasing) { renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderer.copyGrayscaleLsbBuffers(); const auto tGrayLsb = millis();
// Render and copy to MSB buffer renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_MSB); page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderer.copyGrayscaleMsbBuffers(); const auto tGrayMsb = millis();
// display grayscale part renderer.displayGrayBuffer(); const auto tGrayDisplay = millis(); renderer.setRenderMode(GfxRenderer::BW); fcm->logStats("gray");
// restore the bw data renderer.restoreBwBuffer(); const auto tBwRestore = millis();
const auto tEnd = millis(); LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums " "gray_lsb=%lums gray_msb=%lums gray_display=%lums bw_restore=%lums total=%lums", tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tGrayLsb - tBwStore, tGrayMsb - tGrayLsb, tGrayDisplay - tGrayMsb, tBwRestore - tGrayDisplay, tEnd - t0); } else { // restore the bw data renderer.restoreBwBuffer(); const auto tBwRestore = millis();
const auto tEnd = millis(); LOG_DBG("ERS", "Page render: prewarm=%lums bw_render=%lums display=%lums bw_store=%lums bw_restore=%lums total=%lums", tPrewarm - t0, tBwRender - tPrewarm, tDisplay - tBwRender, tBwStore - tDisplay, tBwRestore - tBwStore, tEnd - t0); }}
void EpubReaderActivity::renderStatusBar() const { // Calculate progress in book const int currentPage = section->currentPage + 1; const float pageCount = section->pageCount; const float sectionChapterProg = (pageCount > 0) ? (static_cast<float>(currentPage) / pageCount) : 0; const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100;
std::string title;
int textYOffset = 0;
if (automaticPageTurnActive) { title = tr(STR_AUTO_TURN_ENABLED) + std::to_string(60 * 1000 / pageTurnDuration);
// calculates textYOffset when rendering title in status bar const uint8_t statusBarHeight = UITheme::getInstance().getStatusBarHeight();
// offsets text if no status bar or progress bar only if (statusBarHeight == 0 || statusBarHeight == UITheme::getInstance().getProgressBarHeight()) { textYOffset += UITheme::getInstance().getMetrics().statusBarVerticalMargin; }
} else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::CHAPTER_TITLE) { title = tr(STR_UNNAMED); const int tocIndex = epub->getTocIndexForSpineIndex(currentSpineIndex); if (tocIndex != -1) { const auto tocItem = epub->getTocItem(tocIndex); title = tocItem.title; }
} else if (SETTINGS.statusBarTitle == CrossPointSettings::STATUS_BAR_TITLE::BOOK_TITLE) { title = epub->getTitle(); }
GUI.drawStatusBar(renderer, bookProgress, currentPage, pageCount, title, 0, textYOffset);}
void EpubReaderActivity::navigateToHref(const std::string& hrefStr, const bool savePosition) { if (!epub) return;
// Push current position onto saved stack if (savePosition && section && footnoteDepth < MAX_FOOTNOTE_DEPTH) { savedPositions[footnoteDepth] = {currentSpineIndex, section->currentPage}; footnoteDepth++; LOG_DBG("ERS", "Saved position [%d]: spine %d, page %d", footnoteDepth, currentSpineIndex, section->currentPage); }
// Extract fragment anchor (e.g. "#note1" or "chapter2.xhtml#note1") std::string anchor; const auto hashPos = hrefStr.find('#'); if (hashPos != std::string::npos && hashPos + 1 < hrefStr.size()) { anchor = hrefStr.substr(hashPos + 1); }
// Check for same-file anchor reference (#anchor only) bool sameFile = !hrefStr.empty() && hrefStr[0] == '#';
int targetSpineIndex; if (sameFile) { targetSpineIndex = currentSpineIndex; } else { targetSpineIndex = epub->resolveHrefToSpineIndex(hrefStr); }
if (targetSpineIndex < 0) { LOG_DBG("ERS", "Could not resolve href: %s", hrefStr.c_str()); if (savePosition && footnoteDepth > 0) footnoteDepth--; // undo push return; }
{ RenderLock lock(*this); pendingAnchor = std::move(anchor); currentSpineIndex = targetSpineIndex; nextPageNumber = 0; section.reset(); } requestUpdate(); LOG_DBG("ERS", "Navigated to spine %d for href: %s", targetSpineIndex, hrefStr.c_str());}
void EpubReaderActivity::restoreSavedPosition() { if (footnoteDepth <= 0) return; footnoteDepth--; const auto& pos = savedPositions[footnoteDepth]; LOG_DBG("ERS", "Restoring position [%d]: spine %d, page %d", footnoteDepth, pos.spineIndex, pos.pageNumber);
{ RenderLock lock(*this); currentSpineIndex = pos.spineIndex; nextPageNumber = pos.pageNumber; section.reset(); } requestUpdate();}