Add paragraph index LUT for accurate KOReader position sync
Store per-page paragraph indices in section cache to enable precise XPath-to-page and page-to-XPath mapping without reparsing XHTML. Forward path (upload): generates XPath directly from paragraph LUT instead of byte-offset estimation, eliminating drift in chapters with non-uniform content density. Reverse path (download): resolves incoming KOReader XPath p[N] to the exact page via paragraph LUT lookup. Paragraph counter counts all <p> elements including display:none to match ChapterXPathIndexer and crengine's standard XPath counting. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
5b710e960f
commit
fd70b3a231
@@ -36,8 +36,10 @@ via a KOReader contributor mapping spine items to DocFragment numbers.
|
||||
Implemented in `ProgressMapper::toKOReader`.
|
||||
|
||||
1. Compute overall `percentage` from chapter/page.
|
||||
2. Attempt to compute a real element-level XPath via `ChapterXPathIndexer::findXPathForProgress`.
|
||||
3. If XPath extraction fails, fallback to synthetic chapter path:
|
||||
2. If a paragraph index is available from the section cache LUT (`CrossPointPosition::hasParagraphIndex`),
|
||||
generate an XPath directly: `/body/DocFragment[spineIndex + 1]/body/p[paragraphIndex]`.
|
||||
3. Otherwise, attempt byte-offset estimation via `ChapterXPathIndexer::findXPathForProgress`.
|
||||
4. If XPath extraction fails, fallback to synthetic chapter path:
|
||||
- `/body/DocFragment[spineIndex + 1]/body`
|
||||
|
||||
### KOReader -> CrossPoint
|
||||
@@ -46,8 +48,15 @@ Implemented in `ProgressMapper::toCrossPoint`.
|
||||
|
||||
1. Attempt to parse `DocFragment[N]` from incoming XPath; convert N to 0-based `spineIndex = N - 1`.
|
||||
2. If valid, attempt XPath-to-offset mapping via `ChapterXPathIndexer::findProgressForXPath`.
|
||||
3. Convert resolved intra-spine progress to page estimate.
|
||||
4. If XPath path is invalid/unresolvable, fallback to percentage-based chapter/page estimation.
|
||||
3. Extract paragraph index from XPath via `ChapterXPathIndexer::tryExtractParagraphIndexFromXPath`
|
||||
(e.g. `/body/DocFragment[7]/body/p[685]/text().96` → `paragraphIndex = 685`).
|
||||
4. Convert resolved intra-spine progress to page estimate.
|
||||
5. If XPath path is invalid/unresolvable, fallback to percentage-based chapter/page estimation.
|
||||
|
||||
When a paragraph index is available, `EpubReaderActivity` refines the page estimate using
|
||||
the section cache's per-page paragraph LUT (`Section::getPageForParagraphIndex`). This finds
|
||||
the first page whose recorded paragraph index is >= the target, giving a more accurate
|
||||
landing position than byte-offset-based estimation alone.
|
||||
|
||||
## ChapterXPathIndexer Design
|
||||
|
||||
@@ -81,9 +90,27 @@ The implementation intentionally avoids full DOM storage.
|
||||
- Free XML parser and chapter byte buffer on all success/failure paths.
|
||||
- No persistent cache structures are introduced by this module.
|
||||
|
||||
## Paragraph Index LUT
|
||||
|
||||
The section cache stores a per-page paragraph index LUT built during page layout
|
||||
(`ChapterHtmlSlimParser`). Each entry records the 1-based `<p>` sibling index
|
||||
(direct children of `<body>`, matching XPath convention) at the time each page was completed.
|
||||
|
||||
This enables two lookups without reparsing:
|
||||
|
||||
- **XPath → page** (`Section::getPageForParagraphIndex`): finds the first page where the
|
||||
recorded paragraph index >= target. Used when applying remote KOReader progress.
|
||||
- **Page → XPath** (`Section::getParagraphIndexForPage`): returns the paragraph index for
|
||||
a given page. Used when uploading local progress to KOReader.
|
||||
|
||||
The paragraph counter in `ChapterHtmlSlimParser` counts **all** `<p>` elements at body-child
|
||||
level, including `display:none` elements. This matches `ChapterXPathIndexer` and crengine's
|
||||
standard XPath same-name sibling counting.
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- Page number on reverse mapping is still an estimate (renderer differences).
|
||||
The paragraph LUT refines this but cannot guarantee exact page matching.
|
||||
- XPath mapping intentionally uses original spine XHTML while pagination comes from distilled renderer output, so minor roundtrip page drift is expected.
|
||||
- Image-only/low-text chapters may yield coarse anchors.
|
||||
- Extremely malformed XHTML can force fallback behavior.
|
||||
|
||||
+37
-13
@@ -104,7 +104,7 @@ if (parsedSize != fileSize) {
|
||||
|
||||
## `section.bin`
|
||||
|
||||
### Version 8
|
||||
### Version 20
|
||||
|
||||
ImHex Pattern:
|
||||
|
||||
@@ -114,7 +114,7 @@ import std.string;
|
||||
import std.core;
|
||||
|
||||
// === Configuration ===
|
||||
#define EXPECTED_VERSION 8
|
||||
#define EXPECTED_VERSION 20
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
|
||||
// === String Structure ===
|
||||
@@ -175,36 +175,60 @@ struct Page {
|
||||
PageElement elements[elementCount] [[inline]];
|
||||
};
|
||||
|
||||
// === Anchor Map Entry ===
|
||||
|
||||
struct AnchorEntry {
|
||||
String anchorId [[comment("HTML id attribute value")]];
|
||||
u16 pageNumber [[comment("Page where the anchor appears")]];
|
||||
};
|
||||
|
||||
// === Section Bin Structure ===
|
||||
|
||||
struct SectionBin {
|
||||
// Header
|
||||
u8 version [[comment("Format version"), color("FFD93D")]];
|
||||
|
||||
|
||||
// Version validation
|
||||
if (version != EXPECTED_VERSION) {
|
||||
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
||||
}
|
||||
|
||||
|
||||
// Cache busting parameters
|
||||
s32 fontId;
|
||||
float lineCompression;
|
||||
bool extraParagraphSpacing;
|
||||
u8 paragraphAlignment;
|
||||
u16 viewportWidth;
|
||||
u16 vieportHeight;
|
||||
u16 viewportHeight;
|
||||
u16 pageCount;
|
||||
u32 lutOffset;
|
||||
|
||||
bool hyphenationEnabled;
|
||||
bool embeddedStyle;
|
||||
u8 imageRendering;
|
||||
u32 pageLutOffset [[comment("Offset to page offset LUT")]];
|
||||
u32 anchorMapOffset [[comment("Offset to anchor map")]];
|
||||
u32 paragraphLutOffset [[comment("Offset to per-page paragraph index LUT")]];
|
||||
|
||||
Page page[pageCount];
|
||||
|
||||
|
||||
// === Page Offset LUT ===
|
||||
// Validate LUT offset alignment
|
||||
u32 currentOffset = $;
|
||||
if (currentOffset != lutOffset) {
|
||||
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
||||
if (currentOffset != pageLutOffset) {
|
||||
std::warning(std::format("Page LUT offset mismatch: expected 0x{:X}, got 0x{:X}", pageLutOffset, currentOffset));
|
||||
}
|
||||
|
||||
// Lookup Tables
|
||||
u32 lut[pageCount];
|
||||
|
||||
u32 pageOffsets[pageCount] [[comment("File offsets to serialized pages")]];
|
||||
|
||||
// === Anchor Map ===
|
||||
u16 anchorCount;
|
||||
AnchorEntry anchors[anchorCount];
|
||||
|
||||
// === Paragraph Index LUT ===
|
||||
// One entry per page: the 1-based <p> sibling index (XPath convention)
|
||||
// at the time each page was completed during parsing.
|
||||
// Used to resolve KOReader XPath p[N] positions to page numbers.
|
||||
u16 paragraphEntryCount;
|
||||
u16 paragraphIndexPerPage[paragraphEntryCount] [[comment("1-based <p> index at page completion")]];
|
||||
};
|
||||
|
||||
// === File Parsing ===
|
||||
|
||||
Reference in New Issue
Block a user