Files
crosspoint-reader-mod/lib/KOReaderSync/ProgressMapper.cpp
jpirnay 00e25b1a90 fix: Fix kosync repositioning issue (#783)
## Summary

* Original implementation had inconsistent positioning logic:
- When XPath parsing succeeded: incorrectly set pageNumber = 0 (always
beginning of chapter)
- When XPath parsing failed: used percentage for positioning (worked
correctly)
- Result: Positions restored to wrong locations depending on XPath
parsing success
  - Mentioned in Issue #581 
* Solution
- Unified ProgressMapper::toCrossPoint() to use percentage-based
positioning exclusively for both spine identification and intra-chapter
page calculation, eliminating unreliable XPath parsing entirely.

## Additional Context

* ProgressMapper.cpp: Simplified toCrossPoint() to always use percentage
for positioning, removed parseDocFragmentIndex() function
* ProgressMapper.h: Updated comments and removed unused function
declaration
* Tests confirmed appropriate positioning
* __Notabene: the syncing to another device will (most probably) end up
at the current chapter of crosspoints reading position. There is not
much we can do about it, as KOReader needs to have the correct XPath
information - we can only provide an apporximate position (plus
percentage) - the percentage information is not used in KOReaders
current implementation__
---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? YES
2026-02-19 21:38:46 +11:00

115 lines
4.9 KiB
C++

#include "ProgressMapper.h"
#include <Logging.h>
#include <cmath>
KOReaderPosition ProgressMapper::toKOReader(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos) {
KOReaderPosition result;
// Calculate page progress within current spine item
float intraSpineProgress = 0.0f;
if (pos.totalPages > 0) {
intraSpineProgress = static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages);
}
// Calculate overall book progress (0.0-1.0)
result.percentage = epub->calculateProgress(pos.spineIndex, intraSpineProgress);
// Generate XPath with estimated paragraph position based on page
result.xpath = generateXPath(pos.spineIndex, pos.pageNumber, pos.totalPages);
// Get chapter info for logging
const int tocIndex = epub->getTocIndexForSpineIndex(pos.spineIndex);
const std::string chapterName = (tocIndex >= 0) ? epub->getTocItem(tocIndex).title : "unknown";
LOG_DBG("ProgressMapper", "CrossPoint -> KOReader: chapter='%s', page=%d/%d -> %.2f%% at %s", chapterName.c_str(),
pos.pageNumber, pos.totalPages, result.percentage * 100, result.xpath.c_str());
return result;
}
CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const KOReaderPosition& koPos,
int currentSpineIndex, int totalPagesInCurrentSpine) {
CrossPointPosition result;
result.spineIndex = 0;
result.pageNumber = 0;
result.totalPages = 0;
const size_t bookSize = epub->getBookSize();
if (bookSize == 0) {
return result;
}
// Use percentage-based lookup for both spine and page positioning
// XPath parsing is unreliable since CrossPoint doesn't preserve detailed HTML structure
const size_t targetBytes = static_cast<size_t>(bookSize * koPos.percentage);
// Find the spine item that contains this byte position
const int spineCount = epub->getSpineItemsCount();
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 no spine item was found (e.g., targetBytes beyond last cumulative size),
// default to the last spine item so we map to the end of the book instead of the beginning.
if (!spineFound && spineCount > 0) {
result.spineIndex = spineCount - 1;
}
// Estimate page number within the spine item using percentage
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;
int estimatedTotalPages = 0;
// If we are in the same spine, use the known total pages
if (result.spineIndex == currentSpineIndex && totalPagesInCurrentSpine > 0) {
estimatedTotalPages = totalPagesInCurrentSpine;
}
// Otherwise try to estimate based on density from current spine
else if (currentSpineIndex >= 0 && currentSpineIndex < epub->getSpineItemsCount() && totalPagesInCurrentSpine > 0) {
const size_t prevCurrCumSize =
(currentSpineIndex > 0) ? epub->getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
const size_t currCumSize = epub->getCumulativeSpineItemSize(currentSpineIndex);
const size_t currSpineSize = currCumSize - prevCurrCumSize;
if (currSpineSize > 0) {
float ratio = static_cast<float>(spineSize) / static_cast<float>(currSpineSize);
estimatedTotalPages = static_cast<int>(totalPagesInCurrentSpine * ratio);
if (estimatedTotalPages < 1) estimatedTotalPages = 1;
}
}
result.totalPages = estimatedTotalPages;
if (spineSize > 0 && estimatedTotalPages > 0) {
const size_t bytesIntoSpine = (targetBytes > prevCumSize) ? (targetBytes - prevCumSize) : 0;
const float intraSpineProgress = static_cast<float>(bytesIntoSpine) / static_cast<float>(spineSize);
const float clampedProgress = std::max(0.0f, std::min(1.0f, intraSpineProgress));
result.pageNumber = static_cast<int>(clampedProgress * estimatedTotalPages);
result.pageNumber = std::max(0, std::min(result.pageNumber, estimatedTotalPages - 1));
}
}
LOG_DBG("ProgressMapper", "KOReader -> CrossPoint: %.2f%% at %s -> spine=%d, page=%d", koPos.percentage * 100,
koPos.xpath.c_str(), result.spineIndex, result.pageNumber);
return result;
}
std::string ProgressMapper::generateXPath(int spineIndex, int pageNumber, int totalPages) {
// Use 0-based DocFragment indices for KOReader
// Use a simple xpath pointing to the DocFragment - KOReader will use the percentage for fine positioning within it
// Avoid specifying paragraph numbers as they may not exist in the target document
return "/body/DocFragment[" + std::to_string(spineIndex) + "]/body";
}