- 新增 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>
64 lines
2.7 KiB
Swift
64 lines
2.7 KiB
Swift
import Foundation
|
||
|
||
/// 加密 EPUB 资源数据提供者。
|
||
///
|
||
/// 宿主 App 实现该协议以支持“解压目录内单文件加密”的 EPUB:
|
||
/// SDK 读取章节 HTML、图片、内联 CSS 等资源时会优先向 provider 索取解密后的数据,
|
||
/// provider 返回 nil 表示该文件无需解密,SDK 回退为直接读取磁盘明文
|
||
/// (明文书、或加密书内的明文文件均走该兜底路径,因此对明文书零影响)。
|
||
///
|
||
/// 密钥推导与解密算法完全由宿主实现,SDK 不持有任何密钥。
|
||
///
|
||
/// 注意:方法会在排版 / 渲染的后台队列被同步调用,实现必须线程安全,
|
||
/// 且除必要的文件 I/O 与解密计算外不得阻塞(例如不要在其中发起网络请求)。
|
||
public protocol RDEPUBResourceDataProvider: AnyObject {
|
||
|
||
/// 返回指定文件解密后的数据。
|
||
/// - Parameter fileURL: 解压目录内资源文件的绝对路径
|
||
/// - Returns: 解密后的数据;返回 nil 表示该文件不需要解密,由 SDK 直读磁盘
|
||
func resourceData(at fileURL: URL) -> Data?
|
||
}
|
||
|
||
/// 加密资源访问登记表。
|
||
///
|
||
/// DTCoreText 在解析 `<img>` 时由自己内部加载图片文件,拿不到 parser 实例,
|
||
/// 因此这里以弱引用登记所有启用了 provider 的 parser,
|
||
/// 供 `RDEPUBDecryptingImageAttachment` 按文件路径反查所属书籍的 provider。
|
||
enum RDEPUBResourceAccessRegistry {
|
||
|
||
private struct WeakParserBox {
|
||
weak var parser: RDEPUBParser?
|
||
}
|
||
|
||
private static let lock = NSLock()
|
||
|
||
private static var boxes: [WeakParserBox] = []
|
||
|
||
/// 登记启用了 provider 的 parser(重复登记会被去重,弱引用失效项顺带清理)
|
||
static func register(_ parser: RDEPUBParser) {
|
||
lock.lock()
|
||
defer { lock.unlock() }
|
||
boxes.removeAll { $0.parser == nil || $0.parser === parser }
|
||
boxes.append(WeakParserBox(parser: parser))
|
||
#if canImport(DTCoreText)
|
||
RDEPUBDecryptingImageAttachment.registerTagClassIfNeeded()
|
||
#endif
|
||
}
|
||
|
||
/// 按文件路径反查 provider 并返回解密数据;
|
||
/// 仅当文件位于某个已登记 parser 的解压目录内才会命中
|
||
static func providedResourceData(at fileURL: URL) -> Data? {
|
||
let targetPath = fileURL.resolvingSymlinksInPath().standardizedFileURL.path
|
||
lock.lock()
|
||
let parsers = boxes.compactMap(\.parser)
|
||
lock.unlock()
|
||
for parser in parsers {
|
||
guard let rootURL = parser.extractionRootURL else { continue }
|
||
let rootPath = rootURL.resolvingSymlinksInPath().standardizedFileURL.path
|
||
guard targetPath.hasPrefix(rootPath + "/") else { continue }
|
||
return parser.providedResourceData(at: fileURL)
|
||
}
|
||
return nil
|
||
}
|
||
}
|