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:
jpirnay
2026-03-22 12:29:30 +01:00
co-authored by Claude Opus 4.6
parent 5b710e960f
commit fd70b3a231
17 changed files with 354 additions and 43 deletions
+37 -13
View File
@@ -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 ===