# FreeInkBook FreeInkBook (`libs/book/FreeInkBook`) turns an EPUB file on external storage into typeset, cached, tappable pages on an e-paper panel. It is a complete reading engine — container parsing, CSS, layout, pagination, fonts, images, links — built as a freestanding C++17 library: no Arduino or ESP-IDF dependency in the core, every byte of working memory supplied by the caller, and the entire pipeline runs (and is regression-tested) on a desktop host. The design rationale, phase history, and prior-art notes live in [freeink-book-design.md](freeink-book-design.md). This document is the user guide. ## The four rules Everything in the engine follows from four rules, each enforced by host tests rather than convention: 1. **Never a DOM.** Chapters parse as a stream of SAX events feeding a layout state machine. RAM is O(paragraph + page) — layout peak memory is byte-identical between a 1 KB chapter and a 19,000-page omnibus. 2. **Arenas only.** All memory comes from caller-sized bump allocators that reset wholesale at book/chapter/page boundaries. There is no `free()`, so there is no fragmentation; exhaustion returns a status, never aborts. 3. **Layout once, render many.** Pagination runs once per (book, settings) generation and serializes compact page records. A page turn is one small read + a glyph blit — no ZIP, no XML, no layout, ~2 KB of scratch. 4. **Host-testable end to end.** `test/host/run.sh` builds four test binaries covering container, layout, cache, and fonts — including CI-asserted memory ceilings and O(1) proofs. ## Quick start ```cpp #include #include #include #include #include using namespace freeink::book; // 1. Adapters: the engine never touches a filesystem directly. MyBookSource source; // BookSource: readAt()/size() over the .epub MyCacheStorage cache; // CacheStorage: read/streaming-write cache files // 2. Arenas: the caller decides every budget (PSRAM on ESP32-class parts). Arena bookArena(bookBuf, 512 * 1024); // retained while the book is open Arena scratch(scratchBuf, 512 * 1024); // transient; released per call // 3. Open: ZIP catalog, OPF, TOC — metadata/spine/toc are now queryable. Book book; if (book.open(source, bookArena, scratch) != BookStatus::Ok) ...; // 4. Fonts: any TTF/OTF, style-aware, per-codepoint fallback. TtfFont serif; serif.init(ttfBytes, ttfLen, glyphArena); FontChain fonts; fonts.add(&serif); // + bold/italic faces, CJK... // 5. Layout params = every reader setting in one struct. LayoutParams params; params.pageWidth = 480; params.pageHeight = 800; // logical (portrait) params.baseSizePx = 18; params.font = &fonts; params.defaultAlign = TextAlign::Justify; params.hyphenator = &hyphenator; // see Hyphenation // 6. Paginate into the cache (once per generation), then read pages forever. char name[64]; const uint32_t gen = layoutGenerationHash(params, fontFingerprint); pageCacheName(spineIndex, gen, name, sizeof(name)); PageCacheReader reader; if (reader.open(cache, name, gen, indexArena) != BookStatus::Ok) { PageCacheWriter writer; writer.begin(cache, name, gen, scratch); uint32_t totalChars = 0; ChapterLayout::layout(source, book.zip(), *entry, item->href, params, scratch, writer, nullptr, &totalChars); writer.setTotalChars(totalChars); writer.finish(); reader.open(cache, name, gen, indexArena); } // 7. Render a page into a 1-bit (or Gray8) framebuffer. Page page{}; reader.readPage(pageIndex, scratch, &page); FrameTarget frame{fb, panelW, panelH, panelWBytes, FrameFormat::Mono1Dithered, FrameRotation::Portrait}; PageRenderer::render(page, fonts, source, book.zip(), scratch, frame); ``` A complete firmware consuming all of this (screens, settings, progress, gestures) is a few hundred lines; the storage adapters are ~80. ## Module map | Area | Headers | What it does | |---|---|---| | Container | `FreeInkBook.h`, `epub/ZipCatalog.h` | ZIP catalog + streaming inflate; OPF metadata/manifest/spine; nav + NCX TOC; DRM detection | | CSS | `css/Css.h` | Tolerant subset cascade: element/`.class` selectors, ~15 properties, inline `style=""`, chapter `