feat: whole book page count

This commit is contained in:
Uri Tauber
2026-07-15 17:58:45 +03:00
parent f180069643
commit fd00c185f2
13 changed files with 337 additions and 51 deletions
+73
View File
@@ -0,0 +1,73 @@
#include "BookPages.h"
#include <algorithm>
#include <cmath>
#include <limits>
namespace {
// Coarse seed used only before any section has an exact or live count; replaced
// by the calibrated average as soon as one is available.
constexpr double DEFAULT_BYTES_PER_PAGE = 2000.0;
// A section's serialized pageCount is a uint16_t, so no estimate needs to exceed this.
constexpr int MAX_SECTION_PAGES = std::numeric_limits<uint16_t>::max();
int clampToInt(const uint64_t v) {
return v > static_cast<uint64_t>(std::numeric_limits<int>::max()) ? std::numeric_limits<int>::max()
: static_cast<int>(v);
}
} // namespace
BookPagePosition computeBookPagePosition(const BookPageEntry* entries, const int sectionCount, const int spineIndex,
const int pageInSection, const int liveSectionPages) {
BookPagePosition pos;
if (!entries || sectionCount <= 0) {
return pos;
}
// Calibrate bytes-per-page from every section with an exact count (empty
// known-0 chapters excluded), plus the live estimate for spineIndex when it
// has no exact count yet.
uint64_t knownBytes = 0;
uint64_t knownPages = 0;
for (int i = 0; i < sectionCount; ++i) {
if (entries[i].pages > 0) {
knownBytes += entries[i].bytes;
knownPages += static_cast<uint32_t>(entries[i].pages);
}
}
const bool useLive = spineIndex >= 0 && spineIndex < sectionCount && entries[spineIndex].pages < 0 &&
liveSectionPages > 0 && entries[spineIndex].bytes > 0;
if (useLive) {
knownBytes += entries[spineIndex].bytes;
knownPages += static_cast<uint32_t>(liveSectionPages);
}
const double bytesPerPage = (knownBytes > 0 && knownPages > 0)
? static_cast<double>(knownBytes) / static_cast<double>(knownPages)
: DEFAULT_BYTES_PER_PAGE;
uint64_t total = 0;
uint64_t before = 0;
bool exact = true;
for (int i = 0; i < sectionCount; ++i) {
uint32_t pages;
if (entries[i].pages >= 0) {
pages = static_cast<uint32_t>(entries[i].pages); // exact (0 = genuinely empty chapter)
} else if (i == spineIndex && liveSectionPages > 0) {
pages = static_cast<uint32_t>(std::min(liveSectionPages, MAX_SECTION_PAGES));
exact = false;
} else {
const double raw = std::floor(static_cast<double>(entries[i].bytes) / bytesPerPage + 0.5);
pages = static_cast<uint32_t>(std::clamp(raw, 1.0, static_cast<double>(MAX_SECTION_PAGES)));
exact = false;
}
total += pages;
if (i < spineIndex) {
before += pages;
}
}
pos.totalPages = clampToInt(total);
pos.currentPage = clampToInt(before + static_cast<uint64_t>(std::max(0, pageInSection)) + 1);
pos.isEstimate = !exact;
return pos;
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
// Whole-book page accounting for book-global "page X of Y".
//
// The finalized section cache files (sections/*.bin) are the single source of
// truth for exact per-section counts; the caller harvests them into an array of
// BookPageEntry (nothing is persisted separately). Sections without an exact
// count are estimated from their byte size, calibrated against the sections
// whose counts are known — so the total gets more accurate as more of the book
// is paginated, and becomes exact once every section is.
struct BookPageEntry {
uint32_t bytes = 0; // uncompressed XHTML size, for estimating unknown sections
int32_t pages = -1; // exact count from a finalized section cache; -1 = unknown (0 = empty chapter)
};
struct BookPagePosition {
int currentPage = 0;
int totalPages = 0;
bool isEstimate = true; // true until every section has an exact count
};
// Book-global position: currentPage = pages before spineIndex + pageInSection + 1.
// liveSectionPages is the in-progress build's estimate for spineIndex (see
// Section::estimatedTotalPages); it is used for that section when it has no exact
// count yet and folded into the bytes-per-page calibration. Pure function: no I/O.
BookPagePosition computeBookPagePosition(const BookPageEntry* entries, int sectionCount, int spineIndex,
int pageInSection, int liveSectionPages);
+38 -26
View File
@@ -39,8 +39,34 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) +
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
sizeof(uint32_t) + sizeof(uint32_t);
// Read the render-parameter block of the header (everything between the version
// byte and pageCount), in writeSectionFileHeader order. The file cursor must sit
// just past the version byte.
Section::RenderParams readHeaderRenderParams(HalFile& f) {
Section::RenderParams p;
serialization::readPod(f, p.fontId);
serialization::readPod(f, p.lineCompression);
serialization::readPod(f, p.extraParagraphSpacing);
serialization::readPod(f, p.paragraphAlignment);
serialization::readPod(f, p.viewportWidth);
serialization::readPod(f, p.viewportHeight);
serialization::readPod(f, p.hyphenationEnabled);
serialization::readPod(f, p.embeddedStyle);
serialization::readPod(f, p.imageRendering);
serialization::readPod(f, p.focusReadingEnabled);
return p;
}
} // namespace
bool Section::RenderParams::operator==(const RenderParams& o) const {
return fontId == o.fontId && lineCompression == o.lineCompression &&
extraParagraphSpacing == o.extraParagraphSpacing && paragraphAlignment == o.paragraphAlignment &&
viewportWidth == o.viewportWidth && viewportHeight == o.viewportHeight &&
hyphenationEnabled == o.hyphenationEnabled && embeddedStyle == o.embeddedStyle &&
imageRendering == o.imageRendering && focusReadingEnabled == o.focusReadingEnabled;
}
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
// constructed/destroyed where the parser's full definition is visible.
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
@@ -133,31 +159,11 @@ bool Section::loadSectionFile(const int fontId, const float lineCompression, con
}
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
int fileFontId;
uint16_t fileViewportWidth, fileViewportHeight;
float fileLineCompression;
bool fileExtraParagraphSpacing;
uint8_t fileParagraphAlignment;
bool fileHyphenationEnabled;
bool fileEmbeddedStyle;
uint8_t fileImageRendering;
bool fileFocusReadingEnabled;
serialization::readPod(file, fileFontId);
serialization::readPod(file, fileLineCompression);
serialization::readPod(file, fileExtraParagraphSpacing);
serialization::readPod(file, fileParagraphAlignment);
serialization::readPod(file, fileViewportWidth);
serialization::readPod(file, fileViewportHeight);
serialization::readPod(file, fileHyphenationEnabled);
serialization::readPod(file, fileEmbeddedStyle);
serialization::readPod(file, fileImageRendering);
serialization::readPod(file, fileFocusReadingEnabled);
if (fontId != fileFontId || lineCompression != fileLineCompression ||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
const RenderParams fileParams = readHeaderRenderParams(file);
const RenderParams params = {fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
viewportWidth, viewportHeight, hyphenationEnabled, embeddedStyle,
imageRendering, focusReadingEnabled};
if (!(fileParams == params)) {
file.close();
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
clearCache();
@@ -754,7 +760,7 @@ std::string Section::getTextFromSectionFile() {
return fullText;
}
std::optional<uint16_t> Section::getCachedPageCount() const {
std::optional<uint16_t> Section::getCachedPageCount(const RenderParams* mustMatch) const {
HalFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
return std::nullopt;
@@ -774,6 +780,12 @@ std::optional<uint16_t> Section::getCachedPageCount() const {
return std::nullopt;
}
// A file cached under other render settings paginates differently; its count is
// only stale-valid for rough mapping, so reject it when the caller needs a match.
if (mustMatch && !(readHeaderRenderParams(f) == *mustMatch)) {
return std::nullopt;
}
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
uint16_t count;
serialization::readPod(f, count);
+21 -1
View File
@@ -78,6 +78,23 @@ class Section {
std::unique_ptr<Page> loadPageDuringBuild(int page);
public:
// The render parameters that determine pagination, in section cache header order
// (see writeSectionFileHeader). A cached page count is only valid for one set.
struct RenderParams {
int fontId = 0;
float lineCompression = 0.0f;
bool extraParagraphSpacing = false;
uint8_t paragraphAlignment = 0;
uint16_t viewportWidth = 0;
uint16_t viewportHeight = 0;
bool hyphenationEnabled = false;
bool embeddedStyle = false;
uint8_t imageRendering = 0;
bool focusReadingEnabled = false;
bool operator==(const RenderParams& o) const;
};
uint16_t pageCount = 0;
int currentPage = 0;
@@ -144,7 +161,10 @@ class Section {
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
// Get the page count from the section cache file without fully loading it.
std::optional<uint16_t> getCachedPageCount() const;
// Finalized files only (a partial's count is just a build watermark). When
// `mustMatch` is given, the header's render parameters must equal it, so a
// count cached under different settings is never trusted.
std::optional<uint16_t> getCachedPageCount(const RenderParams* mustMatch = nullptr) const;
// Look up the page number for a synthetic paragraph index from XPath p[N].
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
+1 -1
View File
@@ -229,7 +229,7 @@ STR_OK_BUTTON: "OK"
STR_SLEEP_COVER_FILTER: "Sleep Screen Cover Filter"
STR_FILTER_CONTRAST: "Contrast"
STR_CUSTOMISE_STATUS_BAR: "Customise Status Bar"
STR_CHAPTER_PAGE_COUNT: "Chapter Page Count"
STR_CHAPTER_PAGE_COUNT: "Page Count"
STR_BOOK_PROGRESS_PERCENTAGE: "Book Progress Percentage"
STR_PROGRESS_BAR: "Progress Bar"
STR_PROGRESS_BAR_THICKNESS: "Progress Bar Thickness"