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
+3 -1
View File
@@ -37,7 +37,9 @@ struct PageResult {
struct SyncResult {
int spineIndex = 0;
int page = 0;
int page = 0; // estimated page (fallback)
uint16_t paragraphIndex = 0; // 1-based <p> index from XPath
bool hasParagraphIndex = false; // true when paragraphIndex is available
};
enum class NetworkMode;
+25 -1
View File
@@ -384,9 +384,18 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
if (KOREADER_STORE.hasCredentials()) {
const int currentPage = section ? section->currentPage : 0;
const int totalPages = section ? section->pageCount : 0;
// Look up paragraph index from section cache for accurate XPath generation on upload
uint16_t paragraphIdx = 0;
bool hasParagraphIdx = false;
if (section) {
if (const auto pIdx = section->getParagraphIndexForPage(currentPage)) {
paragraphIdx = *pIdx;
hasParagraphIdx = true;
}
}
startActivityForResult(
std::make_unique<KOReaderSyncActivity>(renderer, mappedInput, epub, epub->getPath(), currentSpineIndex,
currentPage, totalPages),
currentPage, totalPages, paragraphIdx, hasParagraphIdx),
[this](const ActivityResult& result) {
if (!result.isCancelled) {
const auto& sync = std::get<SyncResult>(result.data);
@@ -394,6 +403,10 @@ void EpubReaderActivity::onReaderMenuConfirm(EpubReaderMenuActivity::MenuAction
RenderLock lock(*this);
currentSpineIndex = sync.spineIndex;
nextPageNumber = sync.page;
if (sync.hasParagraphIndex) {
pendingParagraphLookup = true;
pendingParagraphIndex = sync.paragraphIndex;
}
section.reset();
}
}
@@ -574,6 +587,17 @@ void EpubReaderActivity::render(RenderLock&& lock) {
pendingAnchor.clear();
}
// Resolve pending KOReader sync paragraph index to accurate page via Section paragraph LUT
if (pendingParagraphLookup) {
if (const auto page = section->getPageForParagraphIndex(pendingParagraphIndex)) {
section->currentPage = *page;
LOG_DBG("ERS", "Resolved p[%u] to page %d (was %d)", pendingParagraphIndex, *page, nextPageNumber);
} else {
LOG_DBG("ERS", "Paragraph LUT not available, using estimated page %d", nextPageNumber);
}
pendingParagraphLookup = false;
}
// handles changes in reader settings and reset to approximate position based on cached progress
if (cachedChapterTotalPageCount > 0) {
// only goes to relative position if spine index matches cached value
@@ -24,6 +24,9 @@ class EpubReaderActivity final : public Activity {
bool pendingPercentJump = false;
// Normalized 0.0-1.0 progress within the target spine item, computed from book percentage.
float pendingSpineProgress = 0.0f;
// Pending paragraph index from KOReader sync (resolved to page via Section paragraph LUT)
bool pendingParagraphLookup = false;
uint16_t pendingParagraphIndex = 0;
bool pendingScreenshot = false;
bool skipNextButtonCheck = false; // Skip button processing for one frame after subactivity exit
bool automaticPageTurnActive = false;
+18 -3
View File
@@ -132,11 +132,24 @@ void KOReaderSyncActivity::performSync() {
// Convert remote progress to CrossPoint position
hasRemoteProgress = true;
{
RenderLock lock(*this);
statusMessage = tr(STR_MAPPING_REMOTE);
}
requestUpdateAndWait();
KOReaderPosition koPos = {remoteProgress.progress, remoteProgress.percentage};
remotePosition = ProgressMapper::toCrossPoint(epub, koPos, currentSpineIndex, totalPagesInSpine);
// Calculate local progress in KOReader format (for display)
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
{
RenderLock lock(*this);
statusMessage = tr(STR_MAPPING_LOCAL);
}
requestUpdateAndWait();
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine, localParagraphIndex,
hasLocalParagraphIndex};
localProgress = ProgressMapper::toKOReader(epub, localPos);
{
@@ -162,7 +175,8 @@ void KOReaderSyncActivity::performUpload() {
requestUpdateAndWait();
// Convert current position to KOReader format
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine};
CrossPointPosition localPos = {currentSpineIndex, currentPage, totalPagesInSpine, localParagraphIndex,
hasLocalParagraphIndex};
KOReaderPosition koPos = ProgressMapper::toKOReader(epub, localPos);
KOReaderProgress progress;
@@ -360,7 +374,8 @@ void KOReaderSyncActivity::loop() {
if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) {
if (selectedOption == 0) {
// Wifi will be turned off in onExit()
setResult(SyncResult{remotePosition.spineIndex, remotePosition.pageNumber});
setResult(SyncResult{remotePosition.spineIndex, remotePosition.pageNumber, remotePosition.paragraphIndex,
remotePosition.hasParagraphIndex});
finish();
} else if (selectedOption == 1) {
// Upload local progress
+6 -1
View File
@@ -22,13 +22,16 @@ class KOReaderSyncActivity final : public Activity {
public:
explicit KOReaderSyncActivity(GfxRenderer& renderer, MappedInputManager& mappedInput,
const std::shared_ptr<Epub>& epub, const std::string& epubPath, int currentSpineIndex,
int currentPage, int totalPagesInSpine)
int currentPage, int totalPagesInSpine, uint16_t paragraphIndex = 0,
bool hasParagraphIndex = false)
: Activity("KOReaderSync", renderer, mappedInput),
epub(epub),
epubPath(epubPath),
currentSpineIndex(currentSpineIndex),
currentPage(currentPage),
totalPagesInSpine(totalPagesInSpine),
localParagraphIndex(paragraphIndex),
hasLocalParagraphIndex(hasParagraphIndex),
remoteProgress{},
remotePosition{},
localProgress{} {}
@@ -57,6 +60,8 @@ class KOReaderSyncActivity final : public Activity {
int currentSpineIndex;
int currentPage;
int totalPagesInSpine;
uint16_t localParagraphIndex;
bool hasLocalParagraphIndex;
State state = WIFI_SELECTION;
std::string statusMessage;