diff --git a/lib/KOReaderSync/ChapterXPathReverseMapper.cpp b/lib/KOReaderSync/ChapterXPathReverseMapper.cpp index 4ecc7f5e..c38c8234 100644 --- a/lib/KOReaderSync/ChapterXPathReverseMapper.cpp +++ b/lib/KOReaderSync/ChapterXPathReverseMapper.cpp @@ -148,8 +148,17 @@ struct ReverseState : StackState { const std::string xpath = normalizeXPath(currentXPath(spineIndex)); const int depth = pathDepth(xpath); + const bool targetIsTextSelector = targetTextNodeIndex > 0; + if (xpath == targetNorm) { - tryUpdate(MatchTier::EXACT, depth, "exact", true); + // For /text()[N].M targets, the normalized parent element path is equal to + // targetNorm. Treat that as an ancestor-level anchor so text-node exact + // matching can still determine the real intra-node offset. + if (targetIsTextSelector) { + tryUpdate(MatchTier::ANCESTOR, depth, "text-parent", false); + } else { + tryUpdate(MatchTier::EXACT, depth, "exact", true); + } return; } if (isAncestorPath(xpath, targetNorm)) { @@ -225,8 +234,15 @@ bool findProgressForXPathInternal(const std::shared_ptr& epub, const int s outIntraSpineProgress = std::max(0.0f, std::min(1.0f, outIntraSpineProgress)); } - LOG_DBG("KOX", "Reverse: spine=%d %s match offset=%zu/%zu -> progress=%.3f for '%s'", spineIndex, state.bestTierName, - state.bestOffset, state.totalTextBytes, outIntraSpineProgress, xpath.c_str()); + if (state.targetTextNodeIndex > 0) { + LOG_DBG("KOX", + "Reverse: spine=%d %s match textNode=%d char=%d offset=%zu/%zu -> progress=%.3f for '%s'", + spineIndex, state.bestTierName, state.targetTextNodeIndex, state.targetCharOffset, state.bestOffset, + state.totalTextBytes, outIntraSpineProgress, xpath.c_str()); + } else { + LOG_DBG("KOX", "Reverse: spine=%d %s match offset=%zu/%zu -> progress=%.3f for '%s'", spineIndex, + state.bestTierName, state.bestOffset, state.totalTextBytes, outIntraSpineProgress, xpath.c_str()); + } return true; } diff --git a/lib/KOReaderSync/ProgressMapper.cpp b/lib/KOReaderSync/ProgressMapper.cpp index 55697179..95f0be7c 100644 --- a/lib/KOReaderSync/ProgressMapper.cpp +++ b/lib/KOReaderSync/ProgressMapper.cpp @@ -7,6 +7,44 @@ #include "ChapterXPathIndexer.h" +namespace { +bool resolveFromPercentage(const std::shared_ptr& epub, const float percentage, const int spineCount, + int& outSpineIndex, float& outIntraSpineProgress) { + if (!std::isfinite(percentage) || !epub || spineCount <= 0) { + return false; + } + + const size_t bookSize = epub->getBookSize(); + if (bookSize == 0) { + return false; + } + + const float sanitizedPercentage = std::clamp(percentage, 0.0f, 1.0f); + const size_t targetBytes = static_cast(bookSize * sanitizedPercentage); + + outSpineIndex = spineCount - 1; + for (int i = 0; i < spineCount; i++) { + const size_t cumulativeSize = epub->getCumulativeSpineItemSize(i); + if (cumulativeSize >= targetBytes) { + outSpineIndex = i; + break; + } + } + + outIntraSpineProgress = 0.0f; + const size_t prevCumSize = (outSpineIndex > 0) ? epub->getCumulativeSpineItemSize(outSpineIndex - 1) : 0; + const size_t currentCumSize = epub->getCumulativeSpineItemSize(outSpineIndex); + const size_t spineSize = currentCumSize - prevCumSize; + if (spineSize > 0) { + const size_t bytesIntoSpine = (targetBytes > prevCumSize) ? (targetBytes - prevCumSize) : 0; + outIntraSpineProgress = static_cast(bytesIntoSpine) / static_cast(spineSize); + outIntraSpineProgress = std::clamp(outIntraSpineProgress, 0.0f, 1.0f); + } + + return true; +} +} // namespace + KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr& epub, const CrossPointPosition& pos) { KOReaderPosition result; @@ -58,6 +96,7 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epu float resolvedIntraSpineProgress = -1.0f; bool xpathExactMatch = false; bool usedXPathMapping = false; + bool usedPercentageReconcile = false; int xpathSpineIndex = -1; if (ChapterXPathIndexer::tryExtractSpineIndexFromXPath(koPos.xpath, xpathSpineIndex) && xpathSpineIndex >= 0 && @@ -68,6 +107,30 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epu result.spineIndex = xpathSpineIndex; resolvedIntraSpineProgress = intraFromXPath; usedXPathMapping = true; + + // KOReader's text-node indexing can differ across renderers/parsers in some + // XHTML shapes. When an XPath-resolved position disagrees materially with + // KOReader's percentage but points to the same spine, use percentage-derived + // intra-spine progress as a safer tie-breaker. + if (std::isfinite(koPos.percentage) && resolvedIntraSpineProgress >= 0.0f) { + const float sanitizedPercentage = std::clamp(koPos.percentage, 0.0f, 1.0f); + const float mappedPercentage = epub->calculateProgress(result.spineIndex, resolvedIntraSpineProgress); + const float delta = std::fabs(mappedPercentage - sanitizedPercentage); + + constexpr float kReconcileThreshold = 0.01f; // 1% absolute book progress + if (delta > kReconcileThreshold) { + int percentageSpineIndex = -1; + float percentageIntraSpine = -1.0f; + if (resolveFromPercentage(epub, koPos.percentage, spineCount, percentageSpineIndex, percentageIntraSpine) && + percentageSpineIndex == result.spineIndex && percentageIntraSpine >= 0.0f) { + LOG_DBG("ProgressMapper", + "Reconciling XPath position with percentage: spine=%d xpath=%.3f pct=%.3f delta=%.3f -> %.3f", + result.spineIndex, resolvedIntraSpineProgress, sanitizedPercentage, delta, percentageIntraSpine); + resolvedIntraSpineProgress = percentageIntraSpine; + usedPercentageReconcile = true; + } + } + } } // Extract paragraph index from XPath for direct page lookup via section cache uint16_t pIndex = 0; @@ -78,43 +141,14 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epu } if (!usedXPathMapping) { - const size_t bookSize = epub->getBookSize(); - if (bookSize == 0) { + int percentageSpineIndex = -1; + float percentageIntraSpine = -1.0f; + if (!resolveFromPercentage(epub, koPos.percentage, spineCount, percentageSpineIndex, percentageIntraSpine)) { return result; } - if (!std::isfinite(koPos.percentage)) { - return result; - } - - const float sanitizedPercentage = std::clamp(koPos.percentage, 0.0f, 1.0f); - const size_t targetBytes = static_cast(bookSize * sanitizedPercentage); - - bool spineFound = false; - for (int i = 0; i < spineCount; i++) { - const size_t cumulativeSize = epub->getCumulativeSpineItemSize(i); - if (cumulativeSize >= targetBytes) { - result.spineIndex = i; - spineFound = true; - break; - } - } - - if (!spineFound && spineCount > 0) { - result.spineIndex = spineCount - 1; - } - - if (result.spineIndex < epub->getSpineItemsCount()) { - const size_t prevCumSize = (result.spineIndex > 0) ? epub->getCumulativeSpineItemSize(result.spineIndex - 1) : 0; - const size_t currentCumSize = epub->getCumulativeSpineItemSize(result.spineIndex); - const size_t spineSize = currentCumSize - prevCumSize; - - if (spineSize > 0) { - const size_t bytesIntoSpine = (targetBytes > prevCumSize) ? (targetBytes - prevCumSize) : 0; - resolvedIntraSpineProgress = static_cast(bytesIntoSpine) / static_cast(spineSize); - resolvedIntraSpineProgress = std::max(0.0f, std::min(1.0f, resolvedIntraSpineProgress)); - } - } + result.spineIndex = percentageSpineIndex; + resolvedIntraSpineProgress = percentageIntraSpine; } // Estimate page number within the selected spine item @@ -154,9 +188,15 @@ CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr& epu } } - LOG_DBG("ProgressMapper", "KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d (%s, exact=%s)", - koPos.percentage * 100, koPos.xpath.c_str(), result.spineIndex, result.pageNumber, - usedXPathMapping ? "xpath" : "percentage", xpathExactMatch ? "yes" : "no"); + LOG_DBG("ProgressMapper", "Resolved KOReader position: spine=%d intra=%.3f hasPIdx=%s pIdx=%u", + result.spineIndex, resolvedIntraSpineProgress, result.hasParagraphIndex ? "yes" : "no", + result.paragraphIndex); + + const char* mappingSource = usedXPathMapping ? (usedPercentageReconcile ? "xpath+percentage" : "xpath") + : "percentage"; + LOG_DBG("ProgressMapper", "KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d (%s, exact=%s)", + koPos.percentage * 100, koPos.xpath.c_str(), result.spineIndex, result.pageNumber, mappingSource, + xpathExactMatch ? "yes" : "no"); return result; }