Review comments

This commit is contained in:
jpirnay
2026-04-20 13:28:56 +02:00
parent 3fb9ac9e29
commit 29284c85c9
10 changed files with 129 additions and 127 deletions
+52 -78
View File
@@ -27,6 +27,12 @@ constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + // SECTION_FILE_VERSION
sizeof(uint32_t) + // page LUT offset
sizeof(uint32_t) + // anchor map offset
sizeof(uint32_t); // paragraph LUT offset
// On-disk paragraph LUT entry: u32 xhtmlByteOffset + u16 paragraphIndex.
constexpr uint32_t PARAGRAPH_LUT_ENTRY_SIZE = sizeof(uint32_t) + sizeof(uint16_t);
inline uint32_t paragraphLutEntryOffset(uint32_t lutStart, uint16_t page) {
return lutStart + page * PARAGRAPH_LUT_ENTRY_SIZE;
}
} // namespace
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
@@ -551,36 +557,43 @@ std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) con
return std::nullopt;
}
bool Section::readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32_t& outLutStart) const {
if (!Storage.openFileForRead("SCT", filePath, outFile)) {
return false;
}
const uint32_t fileSize = outFile.size();
outFile.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t paragraphLutOffset;
serialization::readPod(outFile, paragraphLutOffset);
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
outFile.close();
return false;
}
outFile.seek(paragraphLutOffset);
serialization::readPod(outFile, outCount);
if (outCount == 0) {
outFile.close();
return false;
}
outLutStart = paragraphLutOffset + sizeof(uint16_t);
const uint32_t lutEnd = outLutStart + outCount * PARAGRAPH_LUT_ENTRY_SIZE;
if (lutEnd > fileSize) {
outFile.close();
return false;
}
return true;
}
std::optional<uint16_t> Section::getPageForParagraphIndex(const uint16_t pIndex) const {
FsFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
return std::nullopt;
}
const uint32_t fileSize = f.size();
// Read paragraph LUT offset from end of header
f.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t paragraphLutOffset;
serialization::readPod(f, paragraphLutOffset);
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
f.close();
return std::nullopt;
}
f.seek(paragraphLutOffset);
uint16_t count;
serialization::readPod(f, count);
if (count == 0) {
f.close();
return std::nullopt;
}
// Each entry: uint32_t xhtmlByteOffset + uint16_t paragraphIndex
constexpr uint32_t ENTRY_SIZE = sizeof(uint32_t) + sizeof(uint16_t);
const uint32_t lutEnd = paragraphLutOffset + sizeof(uint16_t) + count * ENTRY_SIZE;
if (lutEnd > fileSize) {
f.close();
uint16_t count = 0;
uint32_t lutStart = 0;
if (!readParagraphLutHeader(f, count, lutStart)) {
return std::nullopt;
}
@@ -588,7 +601,7 @@ std::optional<uint16_t> Section::getPageForParagraphIndex(const uint16_t pIndex)
// <p> whose start tag had been seen while page i was being laid out. Paragraph
// P therefore first appears on the smallest i where storedPIdx[i] >= P.
for (uint16_t i = 0; i < count; i++) {
f.seek(paragraphLutOffset + sizeof(uint16_t) + i * ENTRY_SIZE + sizeof(uint32_t));
f.seek(paragraphLutEntryOffset(lutStart, i) + sizeof(uint32_t));
uint16_t pagePIdx;
serialization::readPod(f, pagePIdx);
if (pagePIdx >= pIndex) {
@@ -603,38 +616,18 @@ std::optional<uint16_t> Section::getPageForParagraphIndex(const uint16_t pIndex)
std::optional<uint16_t> Section::getParagraphIndexForPage(const uint16_t page) const {
FsFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
uint16_t count = 0;
uint32_t lutStart = 0;
if (!readParagraphLutHeader(f, count, lutStart)) {
return std::nullopt;
}
const uint32_t fileSize = f.size();
f.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t paragraphLutOffset;
serialization::readPod(f, paragraphLutOffset);
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
f.close();
return std::nullopt;
}
f.seek(paragraphLutOffset);
uint16_t count;
serialization::readPod(f, count);
if (count == 0 || page >= count) {
f.close();
return std::nullopt;
}
// Each entry: uint32_t xhtmlByteOffset + uint16_t paragraphIndex
constexpr uint32_t ENTRY_SIZE = sizeof(uint32_t) + sizeof(uint16_t);
const uint32_t entryEnd = paragraphLutOffset + sizeof(uint16_t) + (page + 1) * ENTRY_SIZE;
if (entryEnd > fileSize) {
if (page >= count) {
f.close();
return std::nullopt;
}
// Seek directly to the paragraphIndex field of the requested entry (skip xhtmlByteOffset)
f.seek(paragraphLutOffset + sizeof(uint16_t) + page * ENTRY_SIZE + sizeof(uint32_t));
f.seek(paragraphLutEntryOffset(lutStart, page) + sizeof(uint32_t));
uint16_t pIdx;
serialization::readPod(f, pIdx);
@@ -644,36 +637,17 @@ std::optional<uint16_t> Section::getParagraphIndexForPage(const uint16_t page) c
std::optional<uint32_t> Section::getXhtmlByteOffsetForPage(const uint16_t page) const {
FsFile f;
if (!Storage.openFileForRead("SCT", filePath, f)) {
uint16_t count = 0;
uint32_t lutStart = 0;
if (!readParagraphLutHeader(f, count, lutStart)) {
return std::nullopt;
}
const uint32_t fileSize = f.size();
f.seek(HEADER_SIZE - sizeof(uint32_t));
uint32_t paragraphLutOffset;
serialization::readPod(f, paragraphLutOffset);
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
if (page >= count) {
f.close();
return std::nullopt;
}
f.seek(paragraphLutOffset);
uint16_t count;
serialization::readPod(f, count);
if (count == 0 || page >= count) {
f.close();
return std::nullopt;
}
constexpr uint32_t ENTRY_SIZE = sizeof(uint32_t) + sizeof(uint16_t);
const uint32_t entryEnd = paragraphLutOffset + sizeof(uint16_t) + (page + 1) * ENTRY_SIZE;
if (entryEnd > fileSize) {
f.close();
return std::nullopt;
}
f.seek(paragraphLutOffset + sizeof(uint16_t) + page * ENTRY_SIZE);
f.seek(paragraphLutEntryOffset(lutStart, page));
uint32_t byteOffset;
serialization::readPod(f, byteOffset);
+6
View File
@@ -32,6 +32,12 @@ class Section {
void buildTocBoundaries(const std::vector<std::pair<std::string, uint16_t>>& anchors);
void buildTocBoundariesFromFile(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
// byte offset of the first entry (just past the count) and `outCount` to the entry count.
// Caller is responsible for closing `outFile`. Returns false on any I/O or validation error.
bool readParagraphLutHeader(FsFile& outFile, uint16_t& outCount, uint32_t& outLutStart) const;
public:
uint16_t pageCount = 0;
int currentPage = 0;
@@ -593,9 +593,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
(self->currentPageNextY + totalImageHeightWithSpacing > self->viewportHeight)) {
LOG_DBG("EHP", "Image page break: currentY=%d needed=%d viewportH=%d", self->currentPageNextY,
totalImageHeightWithSpacing, self->viewportHeight);
const uint32_t byteOff =
self->activeParser ? static_cast<uint32_t>(XML_GetCurrentByteIndex(self->activeParser)) : 0;
self->paragraphLutPerPage.push_back({byteOff, self->xpathParagraphIndex});
self->paragraphLutPerPage.push_back({self->lastBodyChildByteOffset, self->xpathParagraphIndex});
self->completePageFn(std::move(self->currentPage));
self->completedPageCount++;
self->currentPage.reset(new Page());
@@ -689,8 +687,16 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
// check so that hidden <p> elements are still counted, matching ChapterXPathIndexer's
// counting (pure XML, no CSS). This ensures paragraph indices in the section cache LUT
// align with KOReader's crengine XPath indices.
if (self->xpathBodyDepth >= 0 && self->depth == self->xpathBodyDepth + 1 && strcmp(name, "p") == 0) {
self->xpathParagraphIndex++;
// At the same time, record the byte offset of every direct-body-child element start:
// the forward mapper's partial-parse heuristic requires the seek hint to land on a
// body-child boundary, otherwise partialBaseDepth can misidentify wrapped paragraphs.
if (self->xpathBodyDepth >= 0 && self->depth == self->xpathBodyDepth + 1) {
if (self->activeParser) {
self->lastBodyChildByteOffset = static_cast<uint32_t>(XML_GetCurrentByteIndex(self->activeParser));
}
if (strcmp(name, "p") == 0) {
self->xpathParagraphIndex++;
}
}
if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) {
@@ -1438,8 +1444,7 @@ ParsedText::LineProcessResult ChapterHtmlSlimParser::addLineToPage(std::shared_p
}
if (currentPageNextY + lineHeight > viewportHeight) {
const uint32_t byteOff = activeParser ? static_cast<uint32_t>(XML_GetCurrentByteIndex(activeParser)) : 0;
paragraphLutPerPage.push_back({byteOff, xpathParagraphIndex});
paragraphLutPerPage.push_back({lastBodyChildByteOffset, xpathParagraphIndex});
completePageFn(std::move(currentPage));
completedPageCount++;
currentPage.reset(new Page());
@@ -92,9 +92,15 @@ class ChapterHtmlSlimParser {
// without reparsing, and current page can generate an XPath without reparsing.
uint16_t xpathParagraphIndex = 0; // current <p> sibling index (1-based)
int xpathBodyDepth = -1; // depth of the <body> element (-1 = not yet seen)
// Byte offset of the most recent direct-body-child element start (any tag at xpathBodyDepth+1).
// Recorded at the same depth condition that increments xpathParagraphIndex, so the stored
// offset is guaranteed to land on a body-child element boundary. This keeps the XPath forward
// mapper's partial-parse heuristic reliable for wrapped chapters: without this, the offset
// could point mid-way into a nested <div>/<section>, which confuses partialBaseDepth.
uint32_t lastBodyChildByteOffset = 0;
struct ParagraphLutEntry {
uint32_t xhtmlByteOffset; // Expat byte offset at page break — used to seek near target paragraph
uint32_t xhtmlByteOffset; // byte offset of most recent body-child element start at page break
uint16_t paragraphIndex; // 1-based <p> index at page completion
};
std::vector<ParagraphLutEntry> paragraphLutPerPage; // deep LUT: one entry per page