ReadViewSDK/Sources/RDReaderView/EPUBCore/RDEPUBResourceResolver.swift
shenlei d5a7755702 feat: 支持加密 EPUB、试读墙、工具栏定制与朝向锁定
- 新增 RDEPUBResourceDataProvider 解密钩子协议 + 访问登记表,收敛章节 HTML/
  图片/内联 CSS/脚注/封面/图片查看器/正文取图等读取点,scheme handler 对加密书
  禁用流式分支;新增 RDEPUBDecryptingImageAttachment 解密 DTCoreText 图片附件;
  RDEPUBReaderDependencies.live(resourceDataProvider:) 便捷注入。明文书零影响。
- 试读墙:configuration.trialPolicy + delegate epubReaderTrialWallView/
  DidReachTrialWall,UI 由宿主提供(RDEPUBReaderController+Trial)。
- 工具栏定制:RDEPUBReaderTop/BottomToolViewProtocol 协议 + dependencies 工厂注入,
  内置栏已 conform,configureTopToolView 改协议类型。
- 朝向锁定:RDEPUBMetadata.orientation(OPF rendition:orientation 主,
  iBooks display-options 兜底)+ RDEPUBReaderController+Orientation 重写支持朝向、
  打开后主动转向。

注:RDEPUBReaderController+LocationResolution / PaginationCoordinator 为本次改动前
即存在的工作区修改,一并纳入。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 12:25:17 +09:00

176 lines
6.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
public final class RDEPUBResourceResolver {
private let parser: RDEPUBParser
public init(parser: RDEPUBParser) {
self.parser = parser
}
public var opfDirectoryURL: URL? {
parser.opfDirectoryURL
}
public func fileURL(forRelativePath relativePath: String) -> URL? {
parser.fileURL(forRelativePath: relativePath)
}
public func resourceURL(forRelativePath relativePath: String) -> URL? {
parser.resourceURL(forRelativePath: relativePath)
}
public func fileURL(forResourceURL resourceURL: URL) -> URL? {
parser.fileURL(forResourceURL: resourceURL)
}
///
public var hasResourceDataProvider: Bool {
parser.resourceDataProvider != nil
}
/// provider provider provider nil
public func providedResourceData(at fileURL: URL) -> Data? {
parser.providedResourceData(at: fileURL)
}
/// provider
public func resourceData(at fileURL: URL) -> Data? {
parser.resourceData(at: fileURL)
}
public func normalizedHref(_ href: String, relativeToSpineIndex spineIndex: Int? = nil) -> String? {
guard let opfDirectoryURL else {
return href.components(separatedBy: "#").first
}
let pathPart = href.components(separatedBy: "#").first ?? href
if pathPart.isEmpty {
guard let spineIndex, parser.spine.indices.contains(spineIndex) else {
return nil
}
return parser.spine[spineIndex].href.components(separatedBy: "#").first
}
let baseURL: URL
if let spineIndex, parser.spine.indices.contains(spineIndex) {
baseURL = opfDirectoryURL
.appendingPathComponent(parser.spine[spineIndex].href)
.deletingLastPathComponent()
} else {
baseURL = opfDirectoryURL
}
guard let resolvedURL = URL(string: pathPart, relativeTo: baseURL)?.standardizedFileURL else {
return pathPart
}
let opfPath = opfDirectoryURL.standardizedFileURL.path + "/"
let resolvedPath = resolvedURL.path
if resolvedPath.hasPrefix(opfPath) {
return String(resolvedPath.dropFirst(opfPath.count))
}
return pathPart
}
public func normalizedHref(_ href: String, relativeToHref baseHref: String) -> String? {
guard let opfDirectoryURL else {
return href.components(separatedBy: "#").first
}
let pathPart = href.components(separatedBy: "#").first ?? href
let basePath = baseHref.components(separatedBy: "#").first ?? baseHref
let baseURL = opfDirectoryURL
.appendingPathComponent(basePath)
.deletingLastPathComponent()
guard let resolvedURL = URL(string: pathPart, relativeTo: baseURL)?.standardizedFileURL else {
return pathPart
}
let opfPath = opfDirectoryURL.standardizedFileURL.path + "/"
if resolvedURL.path.hasPrefix(opfPath) {
return String(resolvedURL.path.dropFirst(opfPath.count))
}
return pathPart
}
public func fileURL(forReference href: String, relativeToHref baseHref: String) -> URL? {
guard let normalizedHref = normalizedHref(href, relativeToHref: baseHref) else {
return nil
}
return fileURL(forRelativePath: normalizedHref)
}
public func normalizedLocation(
_ location: RDEPUBLocation,
relativeToSpineIndex spineIndex: Int? = nil,
bookIdentifier: String?
) -> RDEPUBLocation? {
let rawHref = location.href
let hrefParts = rawHref.split(separator: "#", maxSplits: 1, omittingEmptySubsequences: false)
let pathPart = hrefParts.first.map(String.init) ?? rawHref
let fragment = location.fragment ?? (hrefParts.count > 1 ? String(hrefParts[1]) : nil)
let normalizedTargetHref: String
if pathPart.isEmpty {
guard let spineIndex, parser.spine.indices.contains(spineIndex) else {
return nil
}
normalizedTargetHref = parser.spine[spineIndex].href.components(separatedBy: "#").first ?? parser.spine[spineIndex].href
} else {
guard let href = normalizedHref(pathPart, relativeToSpineIndex: spineIndex) else {
return nil
}
normalizedTargetHref = href
}
return RDEPUBLocation(
bookIdentifier: bookIdentifier,
href: normalizedTargetHref,
progression: location.progression,
lastProgression: location.lastProgression,
fragment: fragment,
rangeAnchor: location.rangeAnchor,
cfi: location.cfi,
lastCFI: location.lastCFI,
rangeCFI: location.rangeCFI
)
}
public func spineIndex(forNormalizedHref normalizedHref: String) -> Int? {
parser.spine.firstIndex { item in
self.normalizedHref(item.href) == normalizedHref
}
}
public func spineIndex(for location: RDEPUBLocation) -> Int? {
guard let normalizedHref = normalizedHref(location.href) else {
return nil
}
return spineIndex(forNormalizedHref: normalizedHref)
}
public func href(forSpineIndex spineIndex: Int) -> String? {
guard parser.spine.indices.contains(spineIndex) else {
return nil
}
return parser.spine[spineIndex].href
}
public func title(forSpineIndex spineIndex: Int) -> String? {
guard parser.spine.indices.contains(spineIndex) else {
return nil
}
return parser.spine[spineIndex].title
}
public func manifestItem(forHref href: String) -> RDEPUBManifestItem? {
let targetHref = normalizedHref(href) ?? href.components(separatedBy: "#").first ?? href
return parser.manifest.values.first {
normalizedHref($0.href) == targetHref
}
}
}