#pragma once // BookPaginator — one open book's FreeInkBook session: container, fonts, // layout parameters, and the per-chapter page cache. This is the engine-side // half of the reader; EpubReaderActivity keeps the UI half (input, menus, // status bar, refresh cadence) and talks to the engine only through this. // // Memory model (ESP32-C3, no PSRAM; the reader sees only ~140 KB free, so // nothing is sized for the worst case): // book arena exact-sized: open() parses into a temporary 48 KB arena to // learn the real footprint (corpus high-water 5-40 KB), then // re-opens into precisely that many bytes // stylesheet compacted to the actual rule array (typically < 2 KB) // index arena 12 KB current chapter's page index + anchor table // page arena 12 KB decoded runs of the page being rendered // open scratch (transient 64 KB) container parse working set // build scratch (transient, ideally 120 KB, stepping down to 96 KB under // fragmentation) only while a chapter (re)paginates; the // index/page arenas are released around it to help the heap // produce one contiguous block // Steady-state page turns touch only the page arena (~2 KB used). #include #include #include #include #include #include #include #include #include "CpFontAdapter.h" #include "FreeInkBookStorage.h" class GfxRenderer; class BookPaginator { public: // Called periodically during a chapter (re)pagination so the UI can show // an indexing popup / keep the watchdog fed. struct BuildProgress { void* ctx; void (*fn)(void* ctx, uint32_t pagesDone); }; // value-initialized ({nullptr, nullptr}) by the default argument BookPaginator() = default; ~BookPaginator() { close(); } BookPaginator(const BookPaginator&) = delete; BookPaginator& operator=(const BookPaginator&) = delete; // Opens the container and builds the font chain. `cacheDir` is the // per-book directory (".crosspoint/epub_"). Plain-text files open // as a one-chapter book with no container — detected by .txt extension, or // forced via `forcePlainText` (markdown files read as plain text). bool open(const std::string& path, const std::string& cacheDir, GfxRenderer& renderer, bool forcePlainText = false); void close(); bool isOpen() const { return open_; } bool isTxt() const { return isTxt_; } freeink::book::Book& book() { return book_; } freeink::book::BookSource* bookSource() { return &source_; } size_t spineCount() const { return isTxt_ ? 1 : book_.spineCount(); } const char* language() const; const char* title() const { return isTxt_ ? "" : book_.metadata().title; } const char* author() const { return isTxt_ ? "" : book_.metadata().author; } // --- TOC (flattened, resolved to spine indices) -------------------------- struct TocItem { const char* title; const char* fragment; // anchor within the chapter, or nullptr int spineIndex; // -1 when the href is not a spine item uint8_t depth; }; size_t tocCount() const { return isTxt_ ? 0 : book_.tocCount(); } TocItem tocItem(size_t index) const; // First TOC entry pointing at `spineIndex` or an earlier chapter (the // chapter's display title); -1 when the TOC has no such entry. int tocIndexForSpine(int spineIndex) const; // --- whole-book progress (uncompressed spine byte weights) --------------- // Fraction of the book at (spine, fraction-within-chapter), 0..1. float bookProgress(int spineIndex, float chapterFraction) const; // Inverse: which spine (and where inside it) a whole-book fraction lands. int spineForBookFraction(float bookFraction, float* chapterFractionOut) const; // Refreshes LayoutParams from SETTINGS and the given content box. Must be // called before ensureChapter() and after any settings/orientation change; // a changed generation makes the next ensureChapter() re-paginate. void configureLayout(int16_t pageWidth, int16_t pageHeight, int16_t marginLeft, int16_t marginRight, int16_t marginTop, int16_t marginBottom); // Everything layout-relevant, hashed — the cache key. uint32_t generation() const; // True when the chapter already has a page cache for the current // generation — i.e. ensureChapter() will be a fast open, not a build. // Callers use this to show the indexing popup before the heavy path. bool isChapterCached(uint16_t spineIndex); // Opens the chapter's page cache for the current generation, laying the // chapter out first when missing or stale. Heavy only on that miss. freeink::book::BookStatus ensureChapter(uint16_t spineIndex, const BuildProgress& progress = BuildProgress()); bool chapterReady() const { return curSpine_ != kNoSpine; } uint16_t currentSpine() const { return curSpine_; } uint32_t pageCount() const { return reader_.pageCount(); } uint32_t totalChars() const { return reader_.totalChars(); } uint32_t charStartOfPage(uint32_t pageIndex) const { return reader_.charStart(pageIndex); } uint32_t pageForChar(uint32_t charOffset) const { return reader_.pageForChar(charOffset); } // Resolve an id="" fragment in the CURRENT chapter to a char offset. bool charForAnchor(const char* fragment, uint32_t* charOut) const; // Decodes one page into the page arena; the returned Page's records stay // valid until the next readPage() call. freeink::book::BookStatus readPage(uint32_t pageIndex, freeink::book::Page* out); // Spine index for a container href (link/TOC targets), -1 when absent. int spineIndexForHref(const char* href) const; // Render-path lockstep: the renderer font id for a run's sizePx, quantized // exactly as CpFontAdapter quantized during layout. int fontIdForRunSize(uint16_t sizePx) const; // The ladder rung (px) a run size resolves to — for underline metrics etc. uint16_t quantizeRunSize(uint16_t sizePx) const { return adapters_[0].quantize(sizePx); } const freeink::book::LayoutParams& layoutParams() const { return params_; } private: static constexpr uint16_t kNoSpine = 0xFFFF; static constexpr size_t kBookArenaSize = 48 * 1024; static constexpr size_t kIndexArenaSize = 12 * 1024; static constexpr size_t kPageArenaSize = 12 * 1024; static constexpr size_t kSheetArenaSize = 12 * 1024; // Container open: one inflate stream + package parse (~47 KB measured // high-water). Deliberately snug: open needs scratch + the 48 KB temp book // arena simultaneously, and the C3's largest free block hovers just above // 112 KB — 64 KB here made the pair miss fitting by a few dozen bytes. // (Chapter builds size their two arenas adaptively in ensureChapter.) static constexpr size_t kOpenScratchSize = 56 * 1024; bool buildFontChain(GfxRenderer& renderer); bool reallocChapterArenas(); void loadHyphenator(); uint32_t fontFingerprint() const; SdBookSource source_; SdCacheStorage cache_; freeink::book::Book book_; freeink::book::LayoutParams params_; freeink::book::PageCacheReader reader_; freeink::book::FontChain chain_; CpFontAdapter adapters_[4] = { CpFontAdapter(EpdFontFamily::REGULAR), CpFontAdapter(EpdFontFamily::BOLD), CpFontAdapter(EpdFontFamily::ITALIC), CpFontAdapter(EpdFontFamily::BOLD_ITALIC), }; freeink::book::Hyphenator hyphenator_; std::unique_ptr hyphBlob_; // SD-loaded patterns (non-English books) std::unique_ptr bookBuf_; // exact-sized book arena backing std::unique_ptr indexBuf_; // released transiently around builds std::unique_ptr pageBuf_; // released transiently around builds std::unique_ptr sheetBuf_; // compacted CssRule array freeink::book::Arena bookArena_; freeink::book::Arena indexArena_; freeink::book::Arena pageArena_; freeink::book::CssStylesheet sheet_{}; // PageCacheReader borrows this for readPage() — must outlive the reader. char cacheName_[64] = ""; int ladderFontIds_[CpFontAdapter::kMaxLadder] = {}; uint16_t ladderSizes_[CpFontAdapter::kMaxLadder] = {}; uint8_t ladderCount_ = 0; uint16_t curSpine_ = kNoSpine; bool open_ = false; bool isTxt_ = false; };