feat: whole book page count
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user