Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
16 kB · 537 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538#include "TxtReaderActivity.h"
#include <BidiUtils.h>#include <FontCacheManager.h>#include <GfxRenderer.h>#include <HalStorage.h>#include <I18n.h>#include <Memory.h>#include <Serialization.h>#include <Utf8.h>
#include "CrossPointSettings.h"#include "ProgressFile.h"#include "ReaderActivity.h"#include "ReaderUtils.h"#include "components/UITheme.h"#include "fontIds.h"
namespace {constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading// Cache file magic and versionconstexpr uint32_t CACHE_MAGIC = 0x54585449; // "TXTI"constexpr uint8_t CACHE_VERSION = 3; // Increment when cache format changes} // namespace
bool TxtReaderActivity::loadBook() { txt = makeUniqueNoThrow<Txt>(bookPath, "/.crosspoint"); if (!txt) { LOG_ERR("TRS", "Failed to allocate TXT object"); return false; } if (!txt->load()) { LOG_ERR("TRS", "Failed to load TXT"); return false; } txt->setupCacheDir(); return true;}
void TxtReaderActivity::initializeReader(GfxRenderer& renderer) { if (initialized) { return; }
// Store current settings for cache validation cachedFontId = SETTINGS.getReaderFontId(); cachedScreenMargin = SETTINGS.screenMargin; cachedParagraphAlignment = SETTINGS.paragraphAlignment;
// Calculate viewport dimensions renderer.getOrientedViewableTRBL(&cachedOrientedMarginTop, &cachedOrientedMarginRight, &cachedOrientedMarginBottom, &cachedOrientedMarginLeft); cachedOrientedMarginTop += cachedScreenMargin; cachedOrientedMarginLeft += cachedScreenMargin; cachedOrientedMarginRight += cachedScreenMargin; cachedOrientedMarginBottom += std::max(cachedScreenMargin, static_cast<uint8_t>(UITheme::getInstance().getStatusBarHeight()));
viewportWidth = renderer.getScreenWidth() - cachedOrientedMarginLeft - cachedOrientedMarginRight; const int viewportHeight = renderer.getScreenHeight() - cachedOrientedMarginTop - cachedOrientedMarginBottom; const int lineHeight = renderer.getLineHeight(cachedFontId);
linesPerPage = viewportHeight / lineHeight; if (linesPerPage < 1) linesPerPage = 1;
LOG_DBG("TRS", "Viewport: %dx%d, lines per page: %d", viewportWidth, viewportHeight, linesPerPage);
// Try to load cached page index first if (!loadPageIndexCache()) { // Cache not found, build page index buildPageIndex(renderer); // Save to cache for next time savePageIndexCache(); }
// Load saved progress loadProgress();
initialized = true;}
void TxtReaderActivity::buildPageIndex(GfxRenderer& renderer) { pageOffsets.clear(); pageOffsets.push_back(0); // First page starts at offset 0
size_t offset = 0; const size_t fileSize = txt->getFileSize();
LOG_DBG("TRS", "Building page index for %zu bytes...", fileSize);
GUI.drawPopup(renderer, tr(STR_INDEXING));
while (offset < fileSize) { std::vector<std::string> tempLines; size_t nextOffset = offset;
if (!loadPageAtOffset(renderer, offset, tempLines, nextOffset)) { break; }
if (nextOffset <= offset) { // No progress made, avoid infinite loop break; }
offset = nextOffset; if (offset < fileSize) { pageOffsets.push_back(offset); }
// Yield to other tasks periodically if (pageOffsets.size() % 20 == 0) { vTaskDelay(1); } }
totalPages = pageOffsets.size(); LOG_DBG("TRS", "Built page index: %d pages", totalPages);}
bool TxtReaderActivity::loadPageAtOffset(const GfxRenderer& renderer, size_t offset, std::vector<std::string>& outLines, size_t& nextOffset) { outLines.clear(); const size_t fileSize = txt->getFileSize();
if (offset >= fileSize) { return false; }
// Read a chunk from file size_t chunkSize = std::min(CHUNK_SIZE, fileSize - offset); auto* buffer = static_cast<uint8_t*>(malloc(chunkSize + 1)); if (!buffer) { LOG_ERR("TRS", "Failed to allocate %zu bytes", chunkSize); return false; }
if (!txt->readContent(buffer, offset, chunkSize)) { free(buffer); return false; } buffer[chunkSize] = '\0';
if (renderer.isSdCardFont(cachedFontId)) { renderer.ensureSdCardFontReady(cachedFontId, reinterpret_cast<const char*>(buffer), /*styleMask=*/0x01); }
// Parse lines from buffer size_t pos = 0;
while (pos < chunkSize && static_cast<int>(outLines.size()) < linesPerPage) { // Find end of line size_t lineEnd = pos; while (lineEnd < chunkSize && buffer[lineEnd] != '\n') { lineEnd++; }
// Check if we have a complete line bool lineComplete = (lineEnd < chunkSize) || (offset + lineEnd >= fileSize);
if (!lineComplete && static_cast<int>(outLines.size()) > 0) { // Incomplete line and we already have some lines, stop here break; }
size_t lineContentLen = lineEnd - pos; bool hasCR = (lineContentLen > 0 && buffer[pos + lineContentLen - 1] == '\r'); size_t displayLen = hasCR ? lineContentLen - 1 : lineContentLen;
std::string line(reinterpret_cast<char*>(buffer + pos), displayLen); size_t lineBytePos = 0;
do { if (line.empty()) { outLines.emplace_back(); break; }
int lineWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
if (lineWidth <= viewportWidth) { outLines.push_back(line); lineBytePos = displayLen; line.clear(); break; }
// Find break point size_t breakPos = line.length(); while (breakPos > 0 && renderer.getTextAdvanceX(cachedFontId, line.substr(0, breakPos).c_str(), EpdFontFamily::REGULAR) > viewportWidth) { // Try to break at space size_t spacePos = line.rfind(' ', breakPos - 1); if (spacePos != std::string::npos && spacePos > 0) { breakPos = spacePos; } else { // Break at character boundary for UTF-8 breakPos--; while (breakPos > 0 && (line[breakPos] & 0xC0) == 0x80) { breakPos--; } } }
if (breakPos == 0) { breakPos = 1; }
outLines.push_back(line.substr(0, breakPos));
size_t skipChars = breakPos; if (breakPos < line.length() && line[breakPos] == ' ') { skipChars++; } lineBytePos += skipChars; line = line.substr(skipChars); } while (!line.empty() && static_cast<int>(outLines.size()) < linesPerPage);
if (line.empty()) { pos = lineEnd + 1; } else { pos = pos + lineBytePos; break; } }
if (pos == 0 && !outLines.empty()) { pos = 1; }
nextOffset = offset + pos; if (nextOffset > fileSize) { nextOffset = fileSize; }
free(buffer); return !outLines.empty();}
void TxtReaderActivity::renderBook() { if (!txt) { return; }
if (!initialized) { initializeReader(renderer); }
if (pageOffsets.empty()) { renderer.clearScreen(); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_EMPTY_FILE), true, EpdFontFamily::BOLD); renderer.displayBuffer(); return; }
// Bounds check if (currentPage < 0) currentPage = 0; if (currentPage >= totalPages) currentPage = totalPages - 1;
// Load current page content size_t offset = pageOffsets[currentPage]; size_t nextOffset; currentPageLines.clear(); if (!loadPageAtOffset(renderer, offset, currentPageLines, nextOffset)) { LOG_ERR("TRS", "Failed to load page at offset %zu", offset); renderer.clearScreen(); renderer.drawCenteredText(UI_12_FONT_ID, 300, tr(STR_PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD); const auto labels = mappedInput.mapLabels(tr(STR_BACK), "", "", ""); GUI.drawButtonHints(renderer, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.displayBuffer(); return; }
renderer.clearScreen(); renderPage(renderer); markPageRendered();
// Save progress saveProgress();}
void TxtReaderActivity::renderPage(GfxRenderer& renderer) { const int lineHeight = renderer.getLineHeight(cachedFontId); const int contentWidth = viewportWidth;
// Render text lines with alignment auto renderLines = [&]() { int y = cachedOrientedMarginTop; for (const auto& line : currentPageLines) { if (!line.empty()) { int x = cachedOrientedMarginLeft; const bool lineIsRtl = BidiUtils::startsWithRtl(line.c_str(), BidiUtils::RTL_PARAGRAPH_PROBE_DEPTH); uint8_t effectiveAlignment = cachedParagraphAlignment; if (lineIsRtl && (effectiveAlignment == CrossPointSettings::LEFT_ALIGN || effectiveAlignment == CrossPointSettings::JUSTIFIED)) { effectiveAlignment = CrossPointSettings::RIGHT_ALIGN; } const int textWidth = renderer.getTextAdvanceX(cachedFontId, line.c_str(), EpdFontFamily::REGULAR);
// Apply text alignment switch (effectiveAlignment) { case CrossPointSettings::LEFT_ALIGN: default: break; case CrossPointSettings::CENTER_ALIGN: { x = cachedOrientedMarginLeft + (contentWidth - textWidth) / 2; break; } case CrossPointSettings::RIGHT_ALIGN: { x = cachedOrientedMarginLeft + contentWidth - textWidth; break; } case CrossPointSettings::JUSTIFIED: break; }
renderer.drawText(cachedFontId, x, y, line.c_str()); } y += lineHeight; } };
// Font prewarm: scan pass accumulates text, then prewarm, then real render auto* fcm = renderer.getFontCacheManager(); auto scope = fcm->createPrewarmScope(); renderLines(); // scan pass renderStatusBar(); // scan: a CJK title joins the batch prewarm scope.endScanAndPrewarm();
// BW rendering renderLines(); renderStatusBar();
if (SETTINGS.textAntiAliasing) { ReaderUtils::displayBaseWithRefreshCycle(renderer, pagesUntilFullRefresh); ReaderUtils::renderAntiAliased(renderer, [&renderLines]() { renderLines(); }); } else { ReaderUtils::displayWithRefreshCycle(renderer, pagesUntilFullRefresh); }}
void TxtReaderActivity::renderStatusBar() const { const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0; std::string title; if (SETTINGS.statusBarSpec().showsTitle()) { title = txt->getTitle(); } GUI.drawStatusBar(renderer, progress, currentPage + 1, totalPages, title);}
bool TxtReaderActivity::pageTurn(bool isForward) { // Ignore paging until initializeReader has established the page index if (!initialized) { return false; } if (isForward) { if (currentPage < totalPages) { currentPage++; return true; } } else { if (currentPage > 0) { currentPage--; return true; } } return false;}
bool TxtReaderActivity::skipPages(int amount) { if (!initialized) { return false; } int newPage = currentPage + amount; if (newPage < 0) newPage = 0; // Clamp to totalPages, not totalPages - 1: pageTurn() lets currentPage reach // totalPages and isAtEndOfBook() treats that as the end-of-book sentinel, so // a forward skip must be able to reach it too. if (newPage > totalPages) newPage = totalPages; if (newPage != currentPage) { currentPage = newPage; return true; } return false;}
bool TxtReaderActivity::isAtEndOfBook() const { return initialized && currentPage >= totalPages; }
void TxtReaderActivity::onReturnFromEndOfBook() { currentPage = totalPages > 0 ? totalPages - 1 : 0; }
void TxtReaderActivity::saveProgress() const { uint8_t data[4]; data[0] = currentPage & 0xFF; data[1] = (currentPage >> 8) & 0xFF; data[2] = 0; data[3] = 0; if (!ProgressFile::writeAtomic(txt->getCachePath(), data, sizeof(data))) { LOG_ERR("TRS", "Failed to save progress: page %d", currentPage); }}
void TxtReaderActivity::loadProgress() { HalFile f; if (Storage.openFileForRead("TRS", txt->getCachePath() + "/progress.bin", f)) { uint8_t data[4]; if (f.read(data, 4) == 4) { currentPage = data[0] + (data[1] << 8); if (currentPage >= totalPages) { currentPage = totalPages - 1; } if (currentPage < 0) { currentPage = 0; } LOG_DBG("TRS", "Loaded progress: page %d/%d", currentPage, totalPages); } }}
bool TxtReaderActivity::loadPageIndexCache() { std::string cachePath = txt->getCachePath() + "/index.bin"; HalFile f; if (!Storage.openFileForRead("TRS", cachePath, f)) { LOG_DBG("TRS", "No page index cache found"); return false; }
uint32_t magic; serialization::readPod(f, magic); if (magic != CACHE_MAGIC) { LOG_DBG("TRS", "Cache magic mismatch, rebuilding"); return false; }
uint8_t version; serialization::readPod(f, version); if (version != CACHE_VERSION) { LOG_DBG("TRS", "Cache version mismatch (%d != %d), rebuilding", version, CACHE_VERSION); return false; }
uint32_t fileSize; serialization::readPod(f, fileSize); if (fileSize != txt->getFileSize()) { LOG_DBG("TRS", "Cache file size mismatch, rebuilding"); return false; }
int32_t cachedWidth; serialization::readPod(f, cachedWidth); if (cachedWidth != viewportWidth) { LOG_DBG("TRS", "Cache viewport width mismatch, rebuilding"); return false; }
int32_t cachedLines; serialization::readPod(f, cachedLines); if (cachedLines != linesPerPage) { LOG_DBG("TRS", "Cache lines per page mismatch, rebuilding"); return false; }
int32_t fontId; serialization::readPod(f, fontId); if (fontId != cachedFontId) { LOG_DBG("TRS", "Cache font ID mismatch (%d != %d), rebuilding", fontId, cachedFontId); return false; }
int32_t margin; serialization::readPod(f, margin); if (margin != cachedScreenMargin) { LOG_DBG("TRS", "Cache screen margin mismatch, rebuilding"); return false; }
uint8_t alignment; serialization::readPod(f, alignment); if (alignment != cachedParagraphAlignment) { LOG_DBG("TRS", "Cache paragraph alignment mismatch, rebuilding"); return false; }
uint32_t numPages; serialization::readPod(f, numPages);
pageOffsets.clear(); pageOffsets.reserve(numPages);
for (uint32_t i = 0; i < numPages; i++) { uint32_t offset; serialization::readPod(f, offset); pageOffsets.push_back(offset); }
totalPages = pageOffsets.size(); LOG_DBG("TRS", "Loaded page index cache: %d pages", totalPages); return true;}
void TxtReaderActivity::savePageIndexCache() const { std::string cachePath = txt->getCachePath() + "/index.bin"; HalFile f; if (!Storage.openFileForWrite("TRS", cachePath, f)) { LOG_ERR("TRS", "Failed to save page index cache"); return; }
serialization::writePod(f, CACHE_MAGIC); serialization::writePod(f, CACHE_VERSION); serialization::writePod(f, static_cast<uint32_t>(txt->getFileSize())); serialization::writePod(f, static_cast<int32_t>(viewportWidth)); serialization::writePod(f, static_cast<int32_t>(linesPerPage)); serialization::writePod(f, static_cast<int32_t>(cachedFontId)); serialization::writePod(f, static_cast<int32_t>(cachedScreenMargin)); serialization::writePod(f, cachedParagraphAlignment); serialization::writePod(f, static_cast<uint32_t>(pageOffsets.size()));
for (size_t offset : pageOffsets) { serialization::writePod(f, static_cast<uint32_t>(offset)); }
LOG_DBG("TRS", "Saved page index cache: %d pages", totalPages);}
ScreenshotInfo TxtReaderActivity::getScreenshotInfo() const { ScreenshotInfo info; info.readerType = ScreenshotInfo::ReaderType::Txt; if (txt) { const std::string t = txt->getTitle(); snprintf(info.title, sizeof(info.title), "%s", t.c_str()); } info.currentPage = currentPage + 1; info.totalPages = totalPages; info.progressPercent = totalPages > 0 ? static_cast<int>((currentPage + 1) * 100.0f / totalPages + 0.5f) : 0; if (info.progressPercent > 100) info.progressPercent = 100; return info;}