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
+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;