Recognize and use printed pages info

This commit is contained in:
jpirnay
2026-05-23 17:53:45 +02:00
parent 6717c02173
commit 3671bdba0b
20 changed files with 869 additions and 39 deletions
+112
View File
@@ -6,6 +6,7 @@
#include <JpegToBmpConverter.h>
#include <Logging.h>
#include <PngToBmpConverter.h>
#include <Serialization.h>
#include <ZipFile.h>
#include <cctype>
@@ -13,11 +14,37 @@
#include "Epub/parsers/ContainerParser.h"
#include "Epub/parsers/ContentOpfParser.h"
#include "Epub/parsers/PageMapParser.h"
#include "Epub/parsers/TocNavParser.h"
#include "Epub/parsers/TocNcxParser.h"
namespace {
// Serialise a list of printed-page entries (href, anchor, label) to pagelist.bin in the
// book cache. Templated on the parser's entry type so both NCX <pageList> and EPUB 3
// <nav epub:type="page-list"> share the same writer.
template <typename Entry>
void writePageListBin(const std::string& cachePath, const std::vector<Entry>& pageList) {
if (pageList.empty()) {
return;
}
const auto pageListPath = cachePath + "/pagelist.bin";
FsFile pageListFile;
if (!Storage.openFileForWrite("EBP", pageListPath, pageListFile)) {
LOG_ERR("EBP", "Could not write pagelist.bin");
return;
}
serialization::writePod(pageListFile, static_cast<uint16_t>(pageList.size()));
for (const auto& entry : pageList) {
serialization::writeString(pageListFile, entry.href);
serialization::writeString(pageListFile, entry.anchor);
serialization::writeString(pageListFile, entry.label);
}
pageListFile.flush();
pageListFile.close();
LOG_DBG("EBP", "Wrote pagelist.bin with %u entries", static_cast<unsigned>(pageList.size()));
}
enum class CoverImageFormat { Unknown, Jpeg, Png };
CoverImageFormat detectCoverImageFormat(FsFile& imageFile) {
@@ -311,6 +338,10 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, OpfCac
tocNavItem = opfParser.tocNavPath;
}
if (!opfParser.pageMapPath.empty()) {
pageMapItem = opfParser.pageMapPath;
}
if (!opfParser.cssFiles.empty()) {
cssFiles = opfParser.cssFiles;
}
@@ -373,6 +404,12 @@ bool Epub::parseTocNcxFile() const {
tempNcxFile.close();
Storage.remove(tmpNcxPath.c_str());
// Persist the printed-page list (NCX <pageList>) to a small cache file so the
// section builder can stamp printed-page labels onto rendered pages without
// re-parsing the NCX. Format: u16 count, then per entry: writeString(href),
// writeString(anchor), writeString(label).
writePageListBin(getCachePath(), ncxParser.getPageList());
LOG_DBG("EBP", "Parsed TOC items");
return true;
}
@@ -430,10 +467,74 @@ bool Epub::parseTocNavFile() const {
tempNavFile.close();
Storage.remove(tmpNavPath.c_str());
// Persist EPUB 3 <nav epub:type="page-list"> entries to pagelist.bin (same format
// as the NCX writer); the section builder consumes either source uniformly.
writePageListBin(getCachePath(), navParser.getPageList());
LOG_DBG("EBP", "Parsed TOC nav items");
return true;
}
bool Epub::parsePageMapFile() const {
// EPUB 2.01 page-map.xml: a separate top-level manifest item with media-type
// "application/oebps-page-map+xml". Structure is a flat list of <page name="..." href="..."/>.
if (pageMapItem.empty()) {
LOG_DBG("EBP", "No page-map file specified");
return false;
}
LOG_DBG("EBP", "Parsing page-map file: %s", pageMapItem.c_str());
const auto tmpPageMapPath = getCachePath() + "/page-map.xml";
FsFile tempPageMapFile;
if (!Storage.openFileForWrite("EBP", tmpPageMapPath, tempPageMapFile)) {
return false;
}
readItemContentsToStream(pageMapItem, tempPageMapFile, 1024);
tempPageMapFile.close();
if (!Storage.openFileForRead("EBP", tmpPageMapPath, tempPageMapFile)) {
return false;
}
const auto pageMapSize = tempPageMapFile.size();
// page-map hrefs are relative to the page-map file itself (typically content.opf's dir).
const std::string pageMapBasePath = pageMapItem.substr(0, pageMapItem.find_last_of('/') + 1);
PageMapParser pageMapParser(pageMapBasePath, pageMapSize);
if (!pageMapParser.setup()) {
LOG_ERR("EBP", "Could not setup page-map parser");
tempPageMapFile.close();
return false;
}
const auto pageMapBuffer = static_cast<uint8_t*>(malloc(1024));
if (!pageMapBuffer) {
LOG_ERR("EBP", "Could not allocate memory for page-map parser");
tempPageMapFile.close();
return false;
}
while (tempPageMapFile.available()) {
const auto readSize = tempPageMapFile.read(pageMapBuffer, 1024);
if (readSize == 0) break;
const auto processedSize = pageMapParser.write(pageMapBuffer, readSize);
if (processedSize != readSize) {
LOG_ERR("EBP", "Could not process all page-map data");
free(pageMapBuffer);
tempPageMapFile.close();
return false;
}
}
free(pageMapBuffer);
tempPageMapFile.close();
Storage.remove(tmpPageMapPath.c_str());
writePageListBin(getCachePath(), pageMapParser.getPageList());
LOG_DBG("EBP", "Parsed page-map entries");
return true;
}
void Epub::parseCssFiles() const {
// Maximum CSS file size we'll attempt to parse (uncompressed)
// Larger files risk memory exhaustion on ESP32
@@ -607,6 +708,17 @@ bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
// Continue anyway - book will work without TOC
}
// EPUB 2.01 page-map.xml — only parse if neither NCX <pageList> nor nav page-list
// wrote a pagelist.bin already (so an explicit NCX/nav printed-page list always wins).
if (!pageMapItem.empty()) {
const auto pageListPath = getCachePath() + "/pagelist.bin";
if (!Storage.exists(pageListPath.c_str())) {
parsePageMapFile();
} else {
LOG_DBG("EBP", "page-map.xml present but pagelist.bin already written by NCX/nav; skipping");
}
}
if (!bookMetadataCache->endTocPass()) {
LOG_ERR("EBP", "Could not end writing toc pass");
return false;
+3
View File
@@ -17,6 +17,8 @@ class Epub {
std::string tocNcxItem;
// the nav file (EPUB 3)
std::string tocNavItem;
// the page-map.xml file (EPUB 2.01 printed page list, separate from NCX <pageList>)
std::string pageMapItem;
// where is the EPUBfile?
std::string filepath;
// the base path for items in the EPUB file
@@ -38,6 +40,7 @@ class Epub {
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, OpfCacheMode cacheMode);
bool parseTocNcxFile() const;
bool parseTocNavFile() const;
bool parsePageMapFile() const;
void parseCssFiles() const;
public:
+167 -3
View File
@@ -32,7 +32,8 @@ constexpr uint32_t kParseComplete = kImageRendering + sizeof(uint8_t);
constexpr uint32_t kPageCount = kParseComplete + sizeof(bool);
constexpr uint32_t kPageLut = kPageCount + sizeof(uint16_t);
constexpr uint32_t kAnchorMap = kPageLut + sizeof(uint32_t);
constexpr uint32_t kParagraphLut = kAnchorMap + sizeof(uint32_t);
constexpr uint32_t kPageBreakMap = kAnchorMap + sizeof(uint32_t);
constexpr uint32_t kParagraphLut = kPageBreakMap + sizeof(uint32_t);
constexpr uint32_t kSize = kParagraphLut + sizeof(uint32_t);
} // namespace header
@@ -211,7 +212,7 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
sizeof(viewportWidth) + sizeof(viewportHeight) + sizeof(hyphenationEnabled) +
sizeof(embeddedStyle) + sizeof(bionicReadingEnabled) + sizeof(imageRendering) +
sizeof(bool) + sizeof(pageCount) + sizeof(uint32_t) + sizeof(uint32_t) +
sizeof(uint32_t),
sizeof(uint32_t) + sizeof(uint32_t),
"Header size mismatch");
serialization::writePod(file, SECTION_FILE_VERSION);
serialization::writePod(file, fontId);
@@ -228,6 +229,8 @@ void Section::writeSectionFileHeader(const int fontId, const float lineCompressi
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
serialization::writePod(file,
static_cast<uint32_t>(0)); // Placeholder for page break label map offset (patched later)
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for paragraph LUT offset (patched later)
}
@@ -339,6 +342,7 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
// Build TOC boundaries by scanning anchor data from the still-open file,
// matching only the TOC anchors we need (avoids loading all anchors into memory).
buildTocBoundariesFromFile(file);
buildPageBreakLabelsFromFile(file);
// File is intentionally left open; subsequent loadPageFromSectionFile() calls
// seek within this handle instead of re-opening the file each time.
@@ -456,11 +460,39 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
}
}
// Load printed-page list entries (NCX <pageList> or EPUB 3 nav page-list) for this
// chapter's href, if any. Format: u16 count, then per entry: writeString(href),
// writeString(anchor), writeString(label).
std::vector<std::pair<std::string, std::string>> externalPageBreakAnchors;
{
const auto pageListPath = epub->getCachePath() + "/pagelist.bin";
FsFile pageListFile;
if (Storage.exists(pageListPath.c_str()) && Storage.openFileForRead("SCT", pageListPath, pageListFile)) {
uint16_t count = 0;
serialization::readPod(pageListFile, count);
for (uint16_t i = 0; i < count; i++) {
std::string href, anchor, label;
serialization::readString(pageListFile, href);
serialization::readString(pageListFile, anchor);
serialization::readString(pageListFile, label);
if (href == localPath) {
externalPageBreakAnchors.emplace_back(std::move(anchor), std::move(label));
}
}
pageListFile.close();
LOG_DBG("SCT", "Loaded %u printed-page anchors for %s (of %u total in pagelist.bin)",
static_cast<unsigned>(externalPageBreakAnchors.size()), localPath.c_str(), static_cast<unsigned>(count));
} else {
LOG_DBG("SCT", "No pagelist.bin in cache (skipping printed-page labels)");
}
}
ChapterHtmlSlimParser visitor(
epub, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight,
hyphenationEnabled, bionicReadingEnabled,
[this, &lut](std::unique_ptr<Page> page) { lut.emplace_back(this->onPageComplete(std::move(page))); },
embeddedStyle, contentBase, imageBasePath, imageRendering, std::move(tocAnchors), progressFn, cssParser);
visitor.setExternalPageBreakAnchors(std::move(externalPageBreakAnchors));
Hyphenator::setPreferredLanguage(epub->getLanguage());
if (!visitor.setup(inflatedSize)) {
@@ -551,6 +583,15 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
serialization::writePod(file, page);
}
// Write printed page label map for EPUB pagebreak markers.
const uint32_t pageBreakMapOffset = file.position();
const auto& pageBreakLabels = visitor.getPageBreakLabels();
serialization::writePod(file, static_cast<uint16_t>(pageBreakLabels.size()));
for (const auto& [page, label] : pageBreakLabels) {
serialization::writePod(file, page);
serialization::writeString(file, label);
}
// Write per-page paragraph LUT: count + array of {xhtmlByteOffset(u32), paragraphIndex(u16)}.
// The byte offset lets findXPathForParagraph seek near the target paragraph without scanning
// from the beginning of the XHTML file, reducing SD reads on large chapters.
@@ -582,11 +623,13 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
serialization::writePod(file, pageCount);
serialization::writePod(file, lutOffset);
serialization::writePod(file, anchorMapOffset);
serialization::writePod(file, pageBreakMapOffset);
serialization::writePod(file, paragraphLutOffset);
file.flush();
const size_t expectedHeaderPatchEnd = headerPatchStart + sizeof(parseComplete) + sizeof(pageCount) +
sizeof(lutOffset) + sizeof(anchorMapOffset) + sizeof(paragraphLutOffset);
sizeof(lutOffset) + sizeof(anchorMapOffset) + sizeof(pageBreakMapOffset) +
sizeof(paragraphLutOffset);
if (file.position() != expectedHeaderPatchEnd) {
LOG_ERR("SCT", "Section header patch write failed: wrote %u bytes at offset %u",
static_cast<unsigned>(file.position() - headerPatchStart), static_cast<unsigned>(headerPatchStart));
@@ -601,6 +644,16 @@ bool Section::createSectionFile(const int fontId, const float lineCompression, c
buildTocBoundaries(anchors);
// Populate in-memory pageBreakLabels from the just-completed parse so the status bar
// can show printed-page labels without having to reload the section cache from disk.
// Without this, labels only appear after a subsequent open (via buildPageBreakLabelsFromFile).
this->pageBreakLabels.clear();
for (const auto& entry : visitor.getPageBreakLabels()) {
this->pageBreakLabels.emplace_back(entry.first, entry.second);
}
LOG_DBG("SCT", "Recorded %u printed-page labels for spine=%d", static_cast<unsigned>(this->pageBreakLabels.size()),
spineIndex);
file.close();
// Cache the LUT in memory and open the file for reading so that
@@ -758,6 +811,27 @@ void Section::buildTocBoundariesFromFile(FsFile& f) {
[](const TocBoundary& a, const TocBoundary& b) { return a.startPage < b.startPage; });
}
void Section::buildPageBreakLabelsFromFile(FsFile& f) {
pageBreakLabels.clear();
f.seek(header::kPageBreakMap);
uint32_t pageBreakMapOffset;
serialization::readPod(f, pageBreakMapOffset);
if (pageBreakMapOffset == 0 || pageBreakMapOffset >= f.size()) {
return;
}
f.seek(pageBreakMapOffset);
uint16_t count;
serialization::readPod(f, count);
for (uint16_t i = 0; i < count; i++) {
uint16_t page;
std::string label;
serialization::readPod(f, page);
serialization::readString(f, label);
pageBreakLabels.emplace_back(page, std::move(label));
}
}
int Section::getTocIndexForPage(const int page) const {
if (tocBoundaries.empty()) {
return epub->getTocIndexForSpineIndex(spineIndex);
@@ -825,6 +899,96 @@ std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) con
return std::nullopt;
}
std::optional<std::string> Section::getPrintedPageLabelFromCache(const std::string& sectionsDir, int spineIndex,
uint16_t page) {
// Find any cache variant for spineIndex. Filename format: "<spineIndex>_<hash>.bin".
// We pick the first match — all variants for the same spine share the same printed-page
// anchors (those are content-derived, not render-parameter-derived).
char prefix[16];
snprintf(prefix, sizeof(prefix), "%d_", spineIndex);
const auto files = Storage.listFiles(sectionsDir.c_str(), 50);
std::string match;
for (const auto& f : files) {
if (f.startsWith(prefix) && f.endsWith(".bin")) {
match = f.c_str();
break;
}
}
if (match.empty()) {
return std::nullopt;
}
FsFile file;
if (!Storage.openFileForRead("SCT", sectionsDir + "/" + match, file)) {
return std::nullopt;
}
// Header version guard — refuse to read a cache written by a different layout.
uint8_t version = 0;
file.seek(header::kVersion);
serialization::readPod(file, version);
if (version != SECTION_FILE_VERSION) {
file.close();
return std::nullopt;
}
file.seek(header::kPageBreakMap);
uint32_t pageBreakMapOffset = 0;
serialization::readPod(file, pageBreakMapOffset);
if (pageBreakMapOffset == 0 || pageBreakMapOffset >= file.size()) {
file.close();
return std::nullopt;
}
file.seek(pageBreakMapOffset);
uint16_t count = 0;
serialization::readPod(file, count);
std::vector<std::string> labelsOnPage;
for (uint16_t i = 0; i < count; i++) {
uint16_t entryPage = 0;
std::string label;
serialization::readPod(file, entryPage);
serialization::readString(file, label);
if (entryPage == page) {
labelsOnPage.push_back(std::move(label));
} else if (entryPage > page) {
break;
}
}
file.close();
if (labelsOnPage.empty()) {
return std::nullopt;
}
if (labelsOnPage.size() == 1 || labelsOnPage.front() == labelsOnPage.back()) {
return std::string("(") + labelsOnPage.front() + ")";
}
return std::string("(") + labelsOnPage.front() + "/" + labelsOnPage.back() + ")";
}
std::optional<std::string> Section::getPrintedPageLabelForPage(uint16_t page) const {
// Collect every printed-page label whose anchor lands on this exact rendered page.
// Multiple labels can co-occur when a short device page contains more than one EPUB
// pagebreak marker (e.g. printed pages 7 and 8 both starting within the same device page).
// pageBreakLabels is recorded in document order, so we can short-circuit once we pass `page`.
std::vector<std::string> labels;
for (const auto& [labelPage, label] : pageBreakLabels) {
if (labelPage == page) {
labels.push_back(label);
} else if (labelPage > page) {
break;
}
}
if (labels.empty()) {
return std::nullopt;
}
if (labels.size() == 1 || labels.front() == labels.back()) {
return std::string("(") + labels.front() + ")";
}
return std::string("(") + labels.front() + "/" + labels.back() + ")";
}
bool Section::readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32_t& outLutStart) const {
if (!Storage.openFileForRead("SCT", filePath, outFile)) {
return false;
+14
View File
@@ -30,9 +30,11 @@ class Section {
uint16_t startPage = 0;
};
std::vector<TocBoundary> tocBoundaries;
std::vector<std::pair<uint16_t, std::string>> pageBreakLabels;
void buildTocBoundaries(const std::vector<std::pair<std::string, uint16_t>>& anchors);
void buildTocBoundariesFromFile(FsFile& f);
void buildPageBreakLabelsFromFile(FsFile& f);
// Open the section file and seek to the first paragraph LUT entry, validating the header
// and LUT bounds against fileSize. On success, returns true with `outLutStart` set to the
@@ -88,6 +90,18 @@ class Section {
// Look up the page number for an anchor id from the section cache file.
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
// Returns the printed-page label for a rendered page, wrapped in parens (e.g. "(42)"), if
// one or more EPUB pagebreak markers / NCX <pageList> / page-map entries land on it.
// Returns nullopt when no printed-page anchor falls on this exact page.
std::optional<std::string> getPrintedPageLabelForPage(uint16_t page) const;
// Standalone lookup that doesn't require a loaded Section. Walks the book's sections cache
// directory, finds any cache variant for `spineIndex`, reads its printed-page label map,
// and returns the parenthesised label for `page` if one is recorded. Returns nullopt when
// no cache exists or the page carries no printed-page anchor. Used by SleepActivity to
// augment the overlay without instantiating a full Section + render parameters.
static std::optional<std::string> getPrintedPageLabelFromCache(const std::string& sectionsDir, int spineIndex,
uint16_t page);
// Look up the page number for a paragraph index (1-based, from XPath p[N]).
// Uses the per-page paragraph LUT stored in the section cache.
@@ -338,6 +338,33 @@ void ChapterHtmlSlimParser::emitPage(uint32_t xhtmlByteOffset) {
currentPageNextY = 0;
}
void ChapterHtmlSlimParser::recordPageBreakLabel(const std::string& label) {
if (label.empty()) {
return;
}
// Record the printed page label for the current rendered section page.
// Do not alter pagination; the reader keeps its own page breaks.
pageBreakLabels.emplace_back(static_cast<uint16_t>(completedPageCount), label);
}
void ChapterHtmlSlimParser::setExternalPageBreakAnchors(std::vector<std::pair<std::string, std::string>> anchors) {
externalPageBreakAnchors.clear();
topOfFilePageLabel.clear();
topOfFilePageLabelEmitted = false;
for (auto& [id, label] : anchors) {
if (id.empty()) {
// NCX pageTarget with no fragment (e.g. "OEBPS/c9_split_000.xhtml") — applies to the
// first rendered page of this chapter. Keep only the first such entry if multiple.
if (topOfFilePageLabel.empty()) {
topOfFilePageLabel = std::move(label);
}
} else {
externalPageBreakAnchors.emplace_back(std::move(id), std::move(label));
}
}
}
// start a new text block if needed
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
nextWordContinues = false; // New block = new paragraph, no continuation
@@ -424,9 +451,13 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
return;
}
// Extract class, style, and id attributes
// Extract class, style, id, and pagebreak metadata attributes
std::string classAttr;
std::string styleAttr;
std::string idAttr;
std::string ariaLabel;
std::string titleAttr;
bool isPageBreakMarker = false;
if (atts != nullptr) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "class") == 0) {
@@ -434,13 +465,57 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
} else if (strcmp(atts[i], "style") == 0) {
styleAttr = atts[i + 1];
} else if (strcmp(atts[i], "id") == 0) {
// Defer both anchor recording and TOC page breaks until startNewTextBlock,
// after the previous block is flushed to pages via makePages().
self->pendingAnchorId = atts[i + 1];
idAttr = atts[i + 1];
} else if (strcmp(atts[i], "aria-label") == 0) {
ariaLabel = atts[i + 1];
} else if (strcmp(atts[i], "title") == 0) {
titleAttr = atts[i + 1];
} else if (strcmp(atts[i], "role") == 0 && strcmp(atts[i + 1], "doc-pagebreak") == 0) {
isPageBreakMarker = true;
} else if (strcmp(atts[i], "epub:type") == 0 && strcmp(atts[i + 1], "pagebreak") == 0) {
isPageBreakMarker = true;
}
}
}
// Emit any "top-of-file" printed-page label as soon as we see real markup. NCX entries
// without a fragment refer to the start of this XHTML; record now so the label lands on
// page 0 (completedPageCount is still 0 until the first emitPage()).
if (!self->topOfFilePageLabelEmitted && !self->topOfFilePageLabel.empty()) {
self->recordPageBreakLabel(self->topOfFilePageLabel);
self->topOfFilePageLabelEmitted = true;
}
// Match id against NCX-supplied pagebreak anchors (printed page list). If matched,
// treat this element as if it carried an inline doc-pagebreak marker.
std::string externalLabel;
if (!isPageBreakMarker && !idAttr.empty() && !self->externalPageBreakAnchors.empty()) {
for (const auto& [extId, extLabel] : self->externalPageBreakAnchors) {
if (extId == idAttr) {
externalLabel = extLabel;
isPageBreakMarker = true;
break;
}
}
}
if (isPageBreakMarker) {
std::string label = !ariaLabel.empty() ? ariaLabel : titleAttr;
if (label.empty()) {
label = std::move(externalLabel);
}
self->recordPageBreakLabel(label);
if (!idAttr.empty()) {
self->anchorData.emplace_back(idAttr, static_cast<uint16_t>(self->completedPageCount));
}
}
// Defer generic anchor recording until startNewTextBlock, after the previous block
// is flushed to pages via makePages(). Skip pagebreak anchors since they were already recorded.
if (!isPageBreakMarker && !idAttr.empty()) {
self->pendingAnchorId = idAttr;
}
auto centeredBlockStyle = BlockStyle();
centeredBlockStyle.textAlignDefined = true;
centeredBlockStyle.alignment = CssTextAlign::Center;
@@ -108,6 +108,20 @@ class ChapterHtmlSlimParser final : public Print {
std::string pendingAnchorId; // deferred until after previous text block is flushed
std::vector<std::string> tocAnchors;
// External printed-page labels sourced from NCX <pageList> or EPUB 3 nav page-list.
// Keyed by HTML id (anchor fragment). When the parser encounters an element whose id
// matches one of these, it records the label as if the element were an inline
// doc-pagebreak marker. Anchors already labeled this way are not re-recorded if the
// same element also carries an inline pagebreak attribute.
std::vector<std::pair<std::string, std::string>> externalPageBreakAnchors;
// Optional label for the start of this XHTML file (NCX entries with no fragment).
std::string topOfFilePageLabel;
bool topOfFilePageLabelEmitted = false;
// Page break label mapping: stores the printed page label from EPUB pagebreak markers
// and the section page index where that printed page begins.
std::vector<std::pair<uint16_t, std::string>> pageBreakLabels;
// Paragraph index tracking for XPath-to-page lookup table.
// Counts <p> sibling indices (1-based, matching XPath convention) during page building.
// Stored per page in the section cache so that XPath p[N] can be resolved to a page
@@ -173,6 +187,7 @@ class ChapterHtmlSlimParser final : public Print {
// in lockstep. Every page break MUST go through this helper; open-coded completePageFn
// calls risk desynchronising paragraphLutPerPage and failing the size check in Section.cpp.
void emitPage(uint32_t xhtmlByteOffset);
void recordPageBreakLabel(const std::string& label);
// XML callbacks
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
@@ -227,5 +242,10 @@ class ChapterHtmlSlimParser final : public Print {
ParsedText::LineProcessResult addLineToPage(std::shared_ptr<TextBlock> line, bool lineEndsWithHyphenatedWord,
bool suppressHyphenationRetry);
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
const std::vector<std::pair<uint16_t, std::string>>& getPageBreakLabels() const { return pageBreakLabels; }
const std::vector<ParagraphLutEntry>& getParagraphLutPerPage() const { return paragraphLutPerPage; }
// Supplies printed-page labels from NCX <pageList> for this chapter. `anchors` maps
// HTML id -> label; an entry with an empty id applies to the first page of this file.
void setExternalPageBreakAnchors(std::vector<std::pair<std::string, std::string>> anchors);
};
@@ -9,6 +9,7 @@
namespace {
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
constexpr char MEDIA_TYPE_CSS[] = "text/css";
constexpr char MEDIA_TYPE_PAGEMAP[] = "application/oebps-page-map+xml";
constexpr char itemCacheFile[] = "/.items.bin";
constexpr size_t MAX_DESCRIPTION_LENGTH = 1024;
@@ -340,6 +341,16 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
}
}
// EPUB 2.01 page-map.xml — separate top-level file mapping printed page numbers to spine
// locations (e.g. <page name="1" href="OEBPS/c9_split_000.xhtml"/>). Spine references it
// via <spine page-map="..."> but only the manifest item carries the canonical href.
if (mediaType == MEDIA_TYPE_PAGEMAP) {
if (self->pageMapPath.empty()) {
self->pageMapPath = href;
LOG_DBG("COF", "Found EPUB 2.01 page-map: %s", href.c_str());
}
}
// Collect CSS files
if (mediaType == MEDIA_TYPE_CSS) {
self->cssFiles.push_back(href);
+2 -1
View File
@@ -80,7 +80,8 @@ class ContentOpfParser final : public Print {
std::string series;
std::string seriesIndex;
std::string tocNcxPath;
std::string tocNavPath; // EPUB 3 nav document path
std::string tocNavPath; // EPUB 3 nav document path
std::string pageMapPath; // EPUB 2.01 page-map.xml document path
std::string coverItemHref;
std::string guideCoverPageHref; // Guide reference with type="cover" or "cover-page" (points to XHTML wrapper)
std::string textReferenceHref;
+97
View File
@@ -0,0 +1,97 @@
#include "PageMapParser.h"
#include <FsHelpers.h>
#include <Logging.h>
bool PageMapParser::setup() {
parser = XML_ParserCreate(nullptr);
if (!parser) {
LOG_DBG("PMP", "Couldn't allocate memory for parser");
return false;
}
XML_SetUserData(parser, this);
XML_SetElementHandler(parser, startElement, nullptr);
return true;
}
PageMapParser::~PageMapParser() {
if (parser) {
XML_StopParser(parser, XML_FALSE);
XML_SetElementHandler(parser, nullptr, nullptr);
XML_ParserFree(parser);
parser = nullptr;
}
}
size_t PageMapParser::write(const uint8_t data) { return write(&data, 1); }
size_t PageMapParser::write(const uint8_t* buffer, const size_t size) {
if (!parser) return 0;
const uint8_t* currentBufferPos = buffer;
auto remainingInBuffer = size;
while (remainingInBuffer > 0) {
void* const buf = XML_GetBuffer(parser, 1024);
if (!buf) {
LOG_DBG("PMP", "Couldn't allocate memory for buffer");
XML_StopParser(parser, XML_FALSE);
XML_SetElementHandler(parser, nullptr, nullptr);
XML_ParserFree(parser);
parser = nullptr;
return 0;
}
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
memcpy(buf, currentBufferPos, toRead);
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
LOG_DBG("PMP", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
XML_ErrorString(XML_GetErrorCode(parser)));
XML_StopParser(parser, XML_FALSE);
XML_SetElementHandler(parser, nullptr, nullptr);
XML_ParserFree(parser);
parser = nullptr;
return 0;
}
currentBufferPos += toRead;
remainingInBuffer -= toRead;
remainingSize -= toRead;
}
return size;
}
void XMLCALL PageMapParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
auto* self = static_cast<PageMapParser*>(userData);
// We only care about <page name="..." href="..."/> elements. The wrapping <page-map>
// root is ignored (no need for a state machine — every page element carries its data).
if (strcmp(name, "page") != 0) {
return;
}
std::string label;
std::string rawHref;
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "name") == 0) {
label = atts[i + 1];
} else if (strcmp(atts[i], "href") == 0) {
rawHref = atts[i + 1];
}
}
if (label.empty() || rawHref.empty()) {
return;
}
std::string href = FsHelpers::normalisePath(self->baseContentPath + rawHref);
std::string anchor;
const size_t pos = href.find('#');
if (pos != std::string::npos) {
anchor = href.substr(pos + 1);
href = href.substr(0, pos);
}
self->pageList.push_back({std::move(href), std::move(anchor), std::move(label)});
}
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <Print.h>
#include <expat.h>
#include <string>
#include <vector>
// Parser for EPUB 2.01 page-map.xml. Each <page name="X" href="...#anchor"/> element
// maps a printed page number to a spine location. Same output shape as TocNcxParser
// and TocNavParser so all three feed the shared pagelist.bin writer.
class PageMapParser final : public Print {
public:
struct PageListEntry {
std::string href;
std::string anchor;
std::string label;
};
private:
const std::string& baseContentPath;
size_t remainingSize;
XML_Parser parser = nullptr;
std::vector<PageListEntry> pageList;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
public:
explicit PageMapParser(const std::string& baseContentPath, const size_t xmlSize)
: baseContentPath(baseContentPath), remainingSize(xmlSize) {}
~PageMapParser() override;
bool setup();
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
const std::vector<PageListEntry>& getPageList() const { return pageList; }
};
+92 -14
View File
@@ -83,18 +83,53 @@ void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, co
return;
}
// Look for <nav epub:type="toc"> anywhere in body (or nested elements)
if (self->state >= IN_BODY && strcmp(name, "nav") == 0) {
// Look for <nav epub:type="toc"> or <nav epub:type="page-list"> anywhere in body.
// Both navs are siblings under <body>; we don't expect them to nest.
if (self->state >= IN_BODY && self->state != IN_NAV_TOC && self->state != IN_NAV_PAGE_LIST &&
strcmp(name, "nav") == 0) {
for (int i = 0; atts[i]; i += 2) {
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
self->state = IN_NAV_TOC;
LOG_DBG("NAV", "Found nav toc element");
return;
if (strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) {
if (strcmp(atts[i + 1], "toc") == 0) {
self->state = IN_NAV_TOC;
LOG_DBG("NAV", "Found nav toc element");
return;
}
if (strcmp(atts[i + 1], "page-list") == 0) {
self->state = IN_NAV_PAGE_LIST;
LOG_DBG("NAV", "Found nav page-list element");
return;
}
}
}
return;
}
// Page-list nav: parallel state machine (independent ol/li/a tracking).
if (self->state >= IN_NAV_PAGE_LIST && self->state <= IN_PL_ANCHOR) {
if (strcmp(name, "ol") == 0) {
self->plOlDepth++;
self->state = IN_PL_OL;
return;
}
if (self->state == IN_PL_OL && strcmp(name, "li") == 0) {
self->state = IN_PL_LI;
self->currentPageLabel.clear();
self->currentPageHref.clear();
return;
}
if (self->state == IN_PL_LI && strcmp(name, "a") == 0) {
self->state = IN_PL_ANCHOR;
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "href") == 0) {
self->currentPageHref = atts[i + 1];
break;
}
}
return;
}
return;
}
// Only process ol/li/a if we're inside the toc nav
if (self->state < IN_NAV_TOC) {
return;
@@ -129,15 +164,59 @@ void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, co
void XMLCALL TocNavParser::characterData(void* userData, const XML_Char* s, const int len) {
auto* self = static_cast<TocNavParser*>(userData);
// Only collect text when inside an anchor within the TOC nav
// Collect text inside the anchor of either nav (TOC or page-list).
if (self->state == IN_ANCHOR) {
self->currentLabel.append(s, len);
} else if (self->state == IN_PL_ANCHOR) {
self->currentPageLabel.append(s, len);
}
}
void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) {
auto* self = static_cast<TocNavParser*>(userData);
// ---- Page-list nav close handlers (checked before TOC handlers because IN_PL_* states
// sort after IN_NAV_TOC, but we want exact-state matching either way).
if (strcmp(name, "a") == 0 && self->state == IN_PL_ANCHOR) {
if (!self->currentPageLabel.empty() && !self->currentPageHref.empty()) {
std::string href = FsHelpers::normalisePath(self->baseContentPath + self->currentPageHref);
std::string anchor;
const size_t pos = href.find('#');
if (pos != std::string::npos) {
anchor = href.substr(pos + 1);
href = href.substr(0, pos);
}
self->pageList.push_back({std::move(href), std::move(anchor), std::move(self->currentPageLabel)});
self->currentPageLabel.clear();
self->currentPageHref.clear();
}
self->state = IN_PL_LI;
return;
}
if (strcmp(name, "li") == 0 && (self->state == IN_PL_LI || self->state == IN_PL_OL)) {
self->state = IN_PL_OL;
return;
}
if (strcmp(name, "ol") == 0 &&
(self->state == IN_PL_OL || self->state == IN_PL_LI || self->state == IN_NAV_PAGE_LIST)) {
if (self->plOlDepth > 0) {
self->plOlDepth--;
}
self->state = (self->plOlDepth == 0) ? IN_NAV_PAGE_LIST : IN_PL_LI;
return;
}
if (strcmp(name, "nav") == 0 &&
(self->state == IN_NAV_PAGE_LIST || self->state == IN_PL_OL || self->state == IN_PL_LI)) {
self->state = IN_BODY;
self->plOlDepth = 0;
LOG_DBG("NAV", "Finished parsing nav page-list");
return;
}
// ---- TOC nav close handlers
if (strcmp(name, "a") == 0 && self->state == IN_ANCHOR) {
// Create TOC entry when closing anchor tag (we have all data now)
if (!self->currentLabel.empty() && !self->currentHref.empty()) {
@@ -167,18 +246,17 @@ void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) {
return;
}
if (strcmp(name, "ol") == 0 && self->state >= IN_NAV_TOC) {
self->olDepth--;
if (self->olDepth == 0) {
self->state = IN_NAV_TOC;
} else {
self->state = IN_LI; // Back to parent li
if (strcmp(name, "ol") == 0 && (self->state == IN_OL || self->state == IN_LI || self->state == IN_NAV_TOC)) {
if (self->olDepth > 0) {
self->olDepth--;
}
self->state = (self->olDepth == 0) ? IN_NAV_TOC : IN_LI;
return;
}
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
if (strcmp(name, "nav") == 0 && (self->state == IN_NAV_TOC || self->state == IN_OL || self->state == IN_LI)) {
self->state = IN_BODY;
self->olDepth = 0;
LOG_DBG("NAV", "Finished parsing nav toc");
return;
}
+31 -5
View File
@@ -3,22 +3,39 @@
#include <expat.h>
#include <string>
#include <vector>
class BookMetadataCache;
// Parser for EPUB 3 nav.xhtml navigation documents
// Parses HTML5 nav elements with epub:type="toc" to extract table of contents
// Parses HTML5 nav elements with epub:type="toc" (table of contents) and
// epub:type="page-list" (printed page list, EPUB 3 equivalent of NCX <pageList>).
class TocNavParser final : public Print {
enum ParserState {
START,
IN_HTML,
IN_BODY,
IN_NAV_TOC, // Inside <nav epub:type="toc">
IN_OL, // Inside <ol>
IN_LI, // Inside <li>
IN_ANCHOR, // Inside <a>
IN_NAV_TOC, // Inside <nav epub:type="toc">
IN_OL, // Inside <ol> (within toc nav)
IN_LI, // Inside <li> (within toc nav)
IN_ANCHOR, // Inside <a> (within toc nav)
IN_NAV_PAGE_LIST, // Inside <nav epub:type="page-list">
IN_PL_OL, // Inside <ol> (within page-list nav)
IN_PL_LI, // Inside <li> (within page-list nav)
IN_PL_ANCHOR, // Inside <a> (within page-list nav)
};
public:
// One printed-page entry from <nav epub:type="page-list">: file href (normalised),
// anchor fragment, and visible label. Matches TocNcxParser::PageListEntry in shape so
// both parsers can feed the same pagelist.bin writer.
struct PageListEntry {
std::string href;
std::string anchor;
std::string label;
};
private:
const std::string& baseContentPath;
size_t remainingSize;
XML_Parser parser = nullptr;
@@ -31,6 +48,13 @@ class TocNavParser final : public Print {
std::string currentLabel;
std::string currentHref;
// Page-list collection state (independent of TOC; structurally identical handlers but
// a separate label/href pair so a malformed nav with overlapping navs cannot mix them).
uint8_t plOlDepth = 0;
std::string currentPageLabel;
std::string currentPageHref;
std::vector<PageListEntry> pageList;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void characterData(void* userData, const XML_Char* s, int len);
static void endElement(void* userData, const XML_Char* name);
@@ -44,4 +68,6 @@ class TocNavParser final : public Print {
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
const std::vector<PageListEntry>& getPageList() const { return pageList; }
};
+70
View File
@@ -96,6 +96,40 @@ void XMLCALL TocNcxParser::startElement(void* userData, const XML_Char* name, co
return;
}
// <pageList> is a sibling of <navMap> and contains <pageTarget> elements that map
// printed page numbers to spine locations (e.g. "OEBPS/c9_split_000.xhtml#page_3").
if (self->state == IN_NCX && strcmp(name, "pageList") == 0) {
self->state = IN_PAGE_LIST;
return;
}
if (self->state == IN_PAGE_LIST && strcmp(name, "pageTarget") == 0) {
self->state = IN_PAGE_TARGET;
self->currentPageLabel.clear();
self->currentPageSrc.clear();
return;
}
if (self->state == IN_PAGE_TARGET && strcmp(name, "navLabel") == 0) {
self->state = IN_PAGE_TARGET_LABEL;
return;
}
if (self->state == IN_PAGE_TARGET_LABEL && strcmp(name, "text") == 0) {
self->state = IN_PAGE_TARGET_LABEL_TEXT;
return;
}
if (self->state == IN_PAGE_TARGET && strcmp(name, "content") == 0) {
for (int i = 0; atts[i]; i += 2) {
if (strcmp(atts[i], "src") == 0) {
self->currentPageSrc = atts[i + 1];
break;
}
}
return;
}
// Handles both top-level and nested navPoints
if ((self->state == IN_NAV_MAP || self->state == IN_NAV_POINT) && strcmp(name, "navPoint") == 0) {
self->state = IN_NAV_POINT;
@@ -131,6 +165,8 @@ void XMLCALL TocNcxParser::characterData(void* userData, const XML_Char* s, cons
auto* self = static_cast<TocNcxParser*>(userData);
if (self->state == IN_NAV_LABEL_TEXT) {
self->currentLabel.append(s, len);
} else if (self->state == IN_PAGE_TARGET_LABEL_TEXT) {
self->currentPageLabel.append(s, len);
}
}
@@ -177,5 +213,39 @@ void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
self->currentLabel.clear();
self->currentSrc.clear();
}
return;
}
// <pageList> closing handlers
if (self->state == IN_PAGE_TARGET_LABEL_TEXT && strcmp(name, "text") == 0) {
self->state = IN_PAGE_TARGET_LABEL;
return;
}
if (self->state == IN_PAGE_TARGET_LABEL && strcmp(name, "navLabel") == 0) {
self->state = IN_PAGE_TARGET;
return;
}
if (self->state == IN_PAGE_TARGET && strcmp(name, "pageTarget") == 0) {
if (!self->currentPageLabel.empty() && !self->currentPageSrc.empty()) {
std::string href = FsHelpers::normalisePath(self->baseContentPath + self->currentPageSrc);
std::string anchor;
const size_t pos = href.find('#');
if (pos != std::string::npos) {
anchor = href.substr(pos + 1);
href = href.substr(0, pos);
}
self->pageList.push_back({std::move(href), std::move(anchor), self->currentPageLabel});
}
self->currentPageLabel.clear();
self->currentPageSrc.clear();
self->state = IN_PAGE_LIST;
return;
}
if (self->state == IN_PAGE_LIST && strcmp(name, "pageList") == 0) {
self->state = IN_NCX;
return;
}
}
+30 -1
View File
@@ -3,12 +3,34 @@
#include <expat.h>
#include <string>
#include <vector>
class BookMetadataCache;
class TocNcxParser final : public Print {
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
enum ParserState {
START,
IN_NCX,
IN_NAV_MAP,
IN_NAV_POINT,
IN_NAV_LABEL,
IN_NAV_LABEL_TEXT,
IN_CONTENT,
IN_PAGE_LIST,
IN_PAGE_TARGET,
IN_PAGE_TARGET_LABEL,
IN_PAGE_TARGET_LABEL_TEXT,
};
public:
// One printed-page reference from <pageList>: file href (normalised) + anchor fragment + visible label.
struct PageListEntry {
std::string href; // normalised path to spine item
std::string anchor; // fragment (empty = top of file)
std::string label; // value shown to the reader (e.g. "1", "iv")
};
private:
const std::string& baseContentPath;
size_t remainingSize;
XML_Parser parser = nullptr;
@@ -19,6 +41,11 @@ class TocNcxParser final : public Print {
std::string currentSrc;
uint8_t currentDepth = 0;
// <pageList> collection state
std::string currentPageLabel;
std::string currentPageSrc;
std::vector<PageListEntry> pageList;
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
static void characterData(void* userData, const XML_Char* s, int len);
static void endElement(void* userData, const XML_Char* name);
@@ -32,4 +59,6 @@ class TocNcxParser final : public Print {
size_t write(uint8_t) override;
size_t write(const uint8_t* buffer, size_t size) override;
const std::vector<PageListEntry>& getPageList() const { return pageList; }
};