import UIKit extension RDEPUBReaderController { func pageNumber(for location: RDEPUBLocation, rangeInfo: String? = nil) -> Int? { if let publication, let bookPageMap = readerContext.bookPageMap, let spineIndex = readerContext.normalizedSpineIndex(for: location), let entry = bookPageMap.entry(forSpineIndex: spineIndex) { let normalizedLocation = publication.resourceResolver.normalizedLocation( location, relativeToSpineIndex: nil, bookIdentifier: currentBookIdentifier ) ?? location let localPageIndex = resolvedLocalPageIndex( for: normalizedLocation, spineIndex: spineIndex, fallbackEntry: entry, rangeInfo: rangeInfo ) ?? fallbackLocalPageIndex(for: normalizedLocation, pageCount: entry.pageCount) return bookPageMap.absolutePageIndex( spineIndex: spineIndex, localPageIndex: min(max(localPageIndex, 0), max(entry.pageCount - 1, 0)) ).map { $0 + 1 } } if let textBook, let publication { let normalizedLocation = publication.resourceResolver.normalizedLocation( location, relativeToSpineIndex: nil, bookIdentifier: currentBookIdentifier ) ?? location if let pageNumber = pageNumberFromRangeInfo(rangeInfo, in: textBook, matching: normalizedLocation) { return pageNumber } if let anchor = normalizedLocation.rangeAnchor?.start { if let page = textBook.indexTable.pageNumber(for: anchor, in: textBook) { return page + 1 } } return textBook.pageNumber( for: normalizedLocation, resolver: publication.resourceResolver, bookIdentifier: currentBookIdentifier ) } return readingSession?.queueNavigation( to: location, relativeToSpineIndex: nil, bookIdentifier: currentBookIdentifier ) } func resolvedTextLocation(forPageNumber pageNumber: Int) -> RDEPUBLocation? { if let resolvedPage = resolvedRuntimePage(forPageNumber: pageNumber) { let startOffset = resolvedPage.page.pageStartOffset let endOffset = max(startOffset, resolvedPage.page.pageEndOffset) let chapterData = makeRuntimeChapterData(from: resolvedPage) let location = chapterData.location( for: NSRange(location: startOffset, length: max(endOffset - startOffset + 1, 1)), bookIdentifier: currentBookIdentifier ) if let publication { return publication.resourceResolver.normalizedLocation( location, relativeToSpineIndex: nil, bookIdentifier: currentBookIdentifier ) ?? location } return location } guard let textBook, let page = textBook.page(at: pageNumber) else { return nil } let location = textBook.chapterData(forPageNumber: pageNumber)?.location(forPage: page, bookIdentifier: currentBookIdentifier) ?? textBook.location(forPageNumber: pageNumber, bookIdentifier: currentBookIdentifier) guard let location else { return nil } if let publication { return publication.resourceResolver.normalizedLocation( location, relativeToSpineIndex: nil, bookIdentifier: currentBookIdentifier ) ?? location } return location } func currentVisibleRangeInfo() -> String? { guard let pageNumber = currentPageNumber else { return nil } if let resolvedPage = resolvedRuntimePage(forPageNumber: pageNumber) { let startOffset = resolvedPage.page.pageStartOffset let endOffset = max(startOffset + 1, resolvedPage.page.pageEndOffset + 1) return RDEPUBTextOffsetRangeInfo( href: resolvedPage.chapter.href, start: startOffset, end: endOffset ).jsonString() } guard let textBook, let page = textBook.page(at: pageNumber) else { return nil } let startOffset = page.pageStartOffset let endOffset = max(startOffset + 1, page.pageEndOffset + 1) return RDEPUBTextOffsetRangeInfo( href: page.href, start: startOffset, end: endOffset ).jsonString() } func synchronizeTextReadingState(pageNumber: Int, location: RDEPUBLocation) { if let resolvedPage = resolvedRuntimePage(forPageNumber: pageNumber) { readingSession?.updateReadingContext( pageNumber: pageNumber, location: location, spineIndex: resolvedPage.page.spineIndex, chapterIndex: resolvedPage.chapterIndex, bookIdentifier: currentBookIdentifier ) return } guard let textBook, let page = textBook.page(at: pageNumber) else { return } readingSession?.updateReadingContext( pageNumber: pageNumber, location: location, spineIndex: page.spineIndex, chapterIndex: page.chapterIndex, bookIdentifier: currentBookIdentifier ) } func nativeTextSnapshot(from textBook: RDEPUBTextBook) -> RDEPUBNativeTextSnapshot { let chapters = textBook.chapterInfos let pages = textBook.pages.map { EPUBPage( spineIndex: $0.spineIndex, chapterIndex: $0.chapterIndex, pageIndexInChapter: $0.pageIndexInChapter, totalPagesInChapter: $0.totalPagesInChapter, chapterTitle: $0.chapterTitle, fixedSpread: nil ) } return (pages, chapters) } func chapterOffset(for location: RDEPUBLocation, fallbackEntry: RDEPUBBookPageMapEntry, rangeInfo: String? = nil) -> Int { if let decodedRangeInfo = decodedRangeInfo(from: rangeInfo, matching: location) { return decodedRangeInfo.start } if let spineIndex = readerContext.normalizedSpineIndex(for: location), let runtimeChapter = runtime.chapterRuntimeStore.chapterData(for: spineIndex), let cfi = primaryLocationCFI(for: location), let offset = runtimeChapter.chapterOffsetMap.chapterOffset(forCFI: cfi) { return offset } if let rawCFI = primaryLocationCFI(for: location), let cfi = RDEPUBCFICompatibility.parseLossy(rawCFI), let cfiOffset = RDEPUBCFIResolver.resolve(cfi).chapterOffset { return cfiOffset } if let anchor = location.rangeAnchor?.start { return anchor.chapterOffset } if let fragment = location.fragment, let offset = fallbackEntry.fragmentOffsets[fragment] { return offset } return 0 } func fallbackLocalPageIndex(for location: RDEPUBLocation, pageCount: Int) -> Int { guard pageCount > 1 else { return 0 } return min( pageCount - 1, max(0, Int(round(location.navigationProgression * Double(pageCount - 1)))) ) } private func resolvedRuntimePage(forPageNumber pageNumber: Int) -> RDEPUBResolvedPage? { runtime.pageResolver.resolvePage(absolutePageIndex: pageNumber - 1) } private func resolvedLocalPageIndex( for location: RDEPUBLocation, spineIndex: Int, fallbackEntry: RDEPUBBookPageMapEntry, rangeInfo: String? = nil ) -> Int? { let offset = chapterOffset(for: location, fallbackEntry: fallbackEntry, rangeInfo: rangeInfo) if let runtimeChapter = runtime.chapterRuntimeStore.chapterData(for: spineIndex), let pageIndex = runtimeChapter.chapterOffsetMap.pageIndex(forChapterOffset: offset) { return pageIndex } if let summary = readerContext.chapterSummary(forSpineIndex: spineIndex) { return summary.pageRanges.firstIndex { let range = $0.nsRange return offset >= range.location && offset <= max(range.location + range.length - 1, range.location) } } return nil } private func primaryLocationCFI(for location: RDEPUBLocation) -> String? { if let rangeCFI = RDEPUBCFICompatibility.parseRangeLossy(location.rangeCFI) { return rangeCFI.start.rawValue } return location.cfi } private func pageNumberFromRangeInfo( _ rangeInfo: String?, in textBook: RDEPUBTextBook, matching location: RDEPUBLocation ) -> Int? { guard let decodedRangeInfo = decodedRangeInfo(from: rangeInfo, matching: location), let chapterData = chapterData(in: textBook, for: decodedRangeInfo), let page = chapterData.page(containing: decodedRangeInfo.start) else { return nil } return page.absolutePageIndex + 1 } private func decodedRangeInfo( from rangeInfo: String?, matching location: RDEPUBLocation ) -> RDEPUBTextOffsetRangeInfo? { guard let decodedRangeInfo = RDEPUBTextOffsetRangeInfo.decode(from: rangeInfo), normalizedHref(decodedRangeInfo.href) == normalizedHref(location.href) else { return nil } return decodedRangeInfo } private func chapterData( in textBook: RDEPUBTextBook, for rangeInfo: RDEPUBTextOffsetRangeInfo ) -> RDEPUBChapterData? { if let chapterData = textBook.chapterData(for: rangeInfo.href) { return chapterData } let targetHref = normalizedHref(rangeInfo.href) if let chapterData = textBook.chapterData(for: targetHref) { return chapterData } guard let chapter = textBook.chapters.first(where: { normalizedHref($0.href) == targetHref }) else { return nil } return textBook.chapterData(for: chapter.href) } private func normalizedHref(_ href: String) -> String { publication?.resourceResolver.normalizedHref(href) ?? href.components(separatedBy: "#").first ?? href } private func nearestFragmentID(beforeOrAt offset: Int, fragmentOffsets: [String: Int]) -> String? { var bestID: String? var bestOffset = Int.min for (fragmentID, fragmentOffset) in fragmentOffsets where fragmentOffset <= offset && fragmentOffset > bestOffset { bestOffset = fragmentOffset bestID = fragmentID } return bestID } private func makeRuntimeChapterData(from resolvedPage: RDEPUBResolvedPage) -> RDEPUBChapterData { let textChapter = RDEPUBTextChapter( chapterIndex: resolvedPage.chapterIndex, spineIndex: resolvedPage.chapter.spineIndex, href: resolvedPage.chapter.href, title: resolvedPage.chapter.title, attributedContent: resolvedPage.chapter.typesetAttributedString, fragmentOffsets: resolvedPage.chapter.chapterOffsetMap.fragmentOffsets, cfiMap: resolvedPage.chapter.chapterOffsetMap.cfiMap, pageBreakReasons: resolvedPage.chapter.pages.map(\.metadata.breakReason), pages: resolvedPage.chapter.pages ) return RDEPUBChapterData( chapter: textChapter, indexTable: RDEPUBTextIndexTable(chapters: [textChapter]) ) } }