Phase 1: 稳定性优先 - 新增 RDEPUBJumpSession 保护机制,防止远距跳转后翻页串章 - 升级页图接管条件,增加 JumpSession 保护区检查 - 窗口扩展改为基于当前权威窗口方向 Phase 2: 补全优先级重排 - 新增 RDEPUBBackgroundPriorityPolicy 策略配置 - 实现 hot/warm/cold zone 优先级排序 - 添加失败重试机制(指数退避,最多3次) Phase 3: 分段覆盖与最终收敛 - 新增 RDEPUBBackgroundCoverageStore 分段存储 - 新增 RDEPUBPageMapReconciliationCoordinator 页图接管仲裁 - 实现 LRU 淘汰和内存警告处理 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
249 lines
11 KiB
Swift
249 lines
11 KiB
Swift
import UIKit
|
||
|
||
// MARK: - 文本渲染引擎
|
||
|
||
/// EPUB 文本渲染引擎枚举
|
||
/// 定义了当前支持的文本渲染方式,未来可扩展为多引擎选择
|
||
public enum RDEPUBTextRenderingEngine: Equatable {
|
||
/// 基于 DTCoreText 的渲染引擎,将 HTML/CSS 转换为 NSAttributedString
|
||
case dtCoreText
|
||
}
|
||
|
||
// MARK: - 阅读字体
|
||
|
||
/// 阅读器内置字体选项。
|
||
/// 第一版优先使用系统授权字体,后续可扩展为 bundle 内置字体注册。
|
||
public enum RDEPUBReaderFontChoice: String, Codable, CaseIterable, Equatable {
|
||
/// 系统默认无衬线字体
|
||
case system
|
||
/// 系统衬线字体
|
||
case serif
|
||
/// 系统圆体字体
|
||
case rounded
|
||
/// 系统等宽字体
|
||
case monospaced
|
||
|
||
public var displayName: String {
|
||
switch self {
|
||
case .system:
|
||
return "系统"
|
||
case .serif:
|
||
return "宋体"
|
||
case .rounded:
|
||
return "圆体"
|
||
case .monospaced:
|
||
return "等宽"
|
||
}
|
||
}
|
||
|
||
public func font(ofSize size: CGFloat) -> UIFont {
|
||
switch self {
|
||
case .system:
|
||
return UIFont.systemFont(ofSize: size)
|
||
case .serif:
|
||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||
.withDesign(.serif) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||
return UIFont(descriptor: descriptor, size: size)
|
||
case .rounded:
|
||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||
.withDesign(.rounded) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||
return UIFont(descriptor: descriptor, size: size)
|
||
case .monospaced:
|
||
return UIFont.monospacedSystemFont(ofSize: size, weight: .regular)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - 阅读器配置
|
||
|
||
/// EPUB 阅读器的完整配置结构体
|
||
/// 作为 EPUBUI 层的入口配置,控制阅读器的外观、行为和功能开关
|
||
/// 调用方在创建 RDEPUBReaderController 前可自定义此配置
|
||
public struct RDEPUBReaderConfiguration: Equatable {
|
||
// MARK: 阅读排版参数
|
||
|
||
/// 正文字号(单位:pt),默认 15
|
||
public var fontSize: CGFloat
|
||
/// 行距倍数,默认 1.6(即行距为字号的 1.6 倍)
|
||
public var lineHeightMultiple: CGFloat
|
||
/// 正文字体,默认系统字体
|
||
public var fontChoice: RDEPUBReaderFontChoice
|
||
/// 栏数,默认单栏
|
||
public var numberOfColumns: Int
|
||
/// 栏间距,默认 20pt
|
||
public var columnGap: CGFloat
|
||
/// 翻页显示模式:.pageCurl(仿真翻页)或 .scroll(滚动阅读)
|
||
public var displayType: RDReaderView.DisplayType
|
||
/// 横屏时是否启用双页显示,默认开启
|
||
public var landscapeDualPageEnabled: Bool
|
||
|
||
// MARK: UI 功能开关
|
||
|
||
/// 是否显示目录入口,默认开启
|
||
public var showsTableOfContents: Bool
|
||
/// 是否允许文本高亮功能,默认开启
|
||
public var allowsHighlights: Bool
|
||
/// 是否显示设置面板入口,默认开启
|
||
public var showsSettingsPanel: Bool
|
||
|
||
// MARK: 内容内边距
|
||
|
||
/// 流式排版(reflowable)内容的内边距,上下 40pt、左右 16pt
|
||
public var reflowableContentInsets: UIEdgeInsets
|
||
/// 固定布局(fixed layout)内容的内边距,默认为零
|
||
public var fixedContentInset: UIEdgeInsets
|
||
|
||
// MARK: 主题与布局
|
||
|
||
/// 当前阅读主题(浅色/深色/护眼等),默认 .light
|
||
public var theme: RDEPUBReaderTheme
|
||
/// 暗色主题下是否柔化正文图片,降低夜间阅读刺眼感
|
||
public var darkImageAdjustmentEnabled: Bool
|
||
/// 暗色主题图片柔化混合比例,0 表示不处理,推荐 0.12 ~ 0.2
|
||
public var darkImageBlendRatio: CGFloat
|
||
/// 固定布局的适配模式:按页适配或按宽度适配
|
||
public var fixedLayoutFit: RDEPUBFixedLayoutFit
|
||
/// 固定布局的跨页模式:自动/单页/双页
|
||
public var fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode
|
||
/// 文本渲染引擎,默认使用 DTCoreText
|
||
public var textRenderingEngine: RDEPUBTextRenderingEngine
|
||
/// 章节按需加载窗口大小(总章节数,包含当前章),默认 3,范围 3...15,偶数自动向上取奇
|
||
public var onDemandChapterWindowSize: Int
|
||
/// 后台元数据解析并发数,默认为 CPU 核心数。
|
||
/// 若 profiling 显示 renderTotalMs ≈ wallClockMs(渲染受限),维持核心数即可;
|
||
/// 若 writeTotalMs 占比显著(I/O 等待),可试探 cpuCount * 1.25~1.5 以填充 I/O 等待间隙。
|
||
public var metadataParsingConcurrency: Int
|
||
|
||
// MARK: JumpSession 策略
|
||
|
||
/// JumpSession 策略配置
|
||
public var jumpSessionPolicy: RDEPUBJumpSessionPolicy
|
||
|
||
// MARK: 安全策略
|
||
|
||
/// 允许直接打开的外部 URL scheme 集合,默认仅允许 https
|
||
public var allowedExternalURLSchemes: Set<String>
|
||
/// 打开外部链接前是否需要用户确认,默认 true
|
||
public var requiresExternalLinkConfirmation: Bool
|
||
/// 是否允许正文和离屏分页 WebView 开启 inspectable,默认 false
|
||
public var allowsInspectableWebViews: Bool
|
||
/// 是否允许输出完整 WebView 消息体日志,默认 false
|
||
public var enablesVerboseWebViewLogging: Bool
|
||
|
||
// MARK: 初始化
|
||
|
||
/// 创建阅读器配置,所有参数均提供合理的默认值
|
||
/// - Parameters:
|
||
/// - fontSize: 正文字号,默认 15pt
|
||
/// - lineHeightMultiple: 行距倍数,默认 1.6
|
||
/// - fontChoice: 正文字体,默认系统字体
|
||
/// - displayType: 翻页模式,默认 .pageCurl
|
||
/// - landscapeDualPageEnabled: 横屏双页,默认 true
|
||
/// - showsTableOfContents: 显示目录入口,默认 true
|
||
/// - allowsHighlights: 允许高亮,默认 true
|
||
/// - showsSettingsPanel: 显示设置面板,默认 true
|
||
/// - reflowableContentInsets: 流式排版内边距
|
||
/// - fixedContentInset: 固定布局内边距
|
||
/// - theme: 阅读主题,默认 .light
|
||
/// - darkImageAdjustmentEnabled: 暗色主题下是否柔化正文图片
|
||
/// - darkImageBlendRatio: 暗色主题图片柔化混合比例
|
||
/// - fixedLayoutFit: 固定布局适配模式
|
||
/// - fixedLayoutSpreadMode: 固定布局跨页模式
|
||
/// - textRenderingEngine: 文本渲染引擎
|
||
/// - onDemandChapterWindowSize: 章节按需加载窗口大小(总章节数 3...15,偶数自动向上取奇)
|
||
/// - metadataParsingConcurrency: 后台元数据解析并发数,默认 CPU 核心数
|
||
/// - allowedExternalURLSchemes: 允许直接打开的外部 URL scheme 集合,默认仅 https
|
||
/// - requiresExternalLinkConfirmation: 打开外部链接前是否需要确认,默认 true
|
||
/// - allowsInspectableWebViews: 是否允许开启 inspectable,默认 false
|
||
/// - enablesVerboseWebViewLogging: 是否允许输出完整 WebView 消息体日志,默认 false
|
||
public init(
|
||
fontSize: CGFloat = 15,
|
||
lineHeightMultiple: CGFloat = 1.6,
|
||
fontChoice: RDEPUBReaderFontChoice = .system,
|
||
numberOfColumns: Int = 1,
|
||
columnGap: CGFloat = 20,
|
||
displayType: RDReaderView.DisplayType = .pageCurl,
|
||
landscapeDualPageEnabled: Bool = true,
|
||
showsTableOfContents: Bool = true,
|
||
allowsHighlights: Bool = true,
|
||
showsSettingsPanel: Bool = true,
|
||
reflowableContentInsets: UIEdgeInsets = UIEdgeInsets(top: 40, left: 16, bottom: 40, right: 16),
|
||
fixedContentInset: UIEdgeInsets = .zero,
|
||
theme: RDEPUBReaderTheme = .light,
|
||
darkImageAdjustmentEnabled: Bool = true,
|
||
darkImageBlendRatio: CGFloat = 0.15,
|
||
fixedLayoutFit: RDEPUBFixedLayoutFit = .page,
|
||
fixedLayoutSpreadMode: RDEPUBFixedLayoutSpreadMode = .automatic,
|
||
textRenderingEngine: RDEPUBTextRenderingEngine = .dtCoreText,
|
||
onDemandChapterWindowSize: Int = 3,
|
||
metadataParsingConcurrency: Int = ProcessInfo.processInfo.activeProcessorCount,
|
||
jumpSessionPolicy: RDEPUBJumpSessionPolicy = .default,
|
||
allowedExternalURLSchemes: Set<String> = ["https"],
|
||
requiresExternalLinkConfirmation: Bool = true,
|
||
allowsInspectableWebViews: Bool = false,
|
||
enablesVerboseWebViewLogging: Bool = false
|
||
) {
|
||
self.fontSize = fontSize
|
||
self.lineHeightMultiple = lineHeightMultiple
|
||
self.fontChoice = fontChoice
|
||
self.numberOfColumns = max(1, numberOfColumns)
|
||
self.columnGap = max(0, columnGap)
|
||
self.displayType = displayType
|
||
self.landscapeDualPageEnabled = landscapeDualPageEnabled
|
||
self.showsTableOfContents = showsTableOfContents
|
||
self.allowsHighlights = allowsHighlights
|
||
self.showsSettingsPanel = showsSettingsPanel
|
||
self.reflowableContentInsets = reflowableContentInsets
|
||
self.fixedContentInset = fixedContentInset
|
||
self.theme = theme
|
||
self.darkImageAdjustmentEnabled = darkImageAdjustmentEnabled
|
||
self.darkImageBlendRatio = max(0, min(0.35, darkImageBlendRatio))
|
||
self.fixedLayoutFit = fixedLayoutFit
|
||
self.fixedLayoutSpreadMode = fixedLayoutSpreadMode
|
||
self.textRenderingEngine = textRenderingEngine
|
||
self.onDemandChapterWindowSize = Self.normalizedChapterWindowSize(onDemandChapterWindowSize)
|
||
self.metadataParsingConcurrency = max(1, metadataParsingConcurrency)
|
||
self.jumpSessionPolicy = jumpSessionPolicy
|
||
self.allowedExternalURLSchemes = allowedExternalURLSchemes
|
||
self.requiresExternalLinkConfirmation = requiresExternalLinkConfirmation
|
||
self.allowsInspectableWebViews = allowsInspectableWebViews
|
||
self.enablesVerboseWebViewLogging = enablesVerboseWebViewLogging
|
||
}
|
||
|
||
/// 默认配置实例,使用所有参数的默认值
|
||
public static let `default` = RDEPUBReaderConfiguration()
|
||
}
|
||
|
||
extension RDEPUBReaderConfiguration {
|
||
static func normalizedChapterWindowSize(_ size: Int) -> Int {
|
||
let clamped = max(3, min(15, size))
|
||
return clamped % 2 == 0 ? clamped + 1 : clamped
|
||
}
|
||
|
||
var chapterWindowRadius: Int {
|
||
onDemandChapterWindowSize / 2
|
||
}
|
||
}
|
||
|
||
// MARK: - 配置转换
|
||
|
||
extension RDEPUBReaderConfiguration {
|
||
/// 将用户可见的配置转换为底层排版引擎所需的 RDEPUBPreferences
|
||
/// - Returns: 排版偏好设置实例,供 Core 层的渲染管道使用
|
||
func makePreferences() -> RDEPUBPreferences {
|
||
RDEPUBPreferences(
|
||
fontSize: fontSize,
|
||
lineHeightMultiple: lineHeightMultiple,
|
||
reflowableContentInsets: reflowableContentInsets,
|
||
fixedContentInset: fixedContentInset,
|
||
numberOfColumns: numberOfColumns,
|
||
columnGap: columnGap,
|
||
themeBackgroundColor: theme.themeBackgroundColorCSS,
|
||
themeTextColor: theme.themeTextColorCSS,
|
||
fixedBackgroundColor: theme.themeBackgroundColorCSS,
|
||
fixedLayoutFit: fixedLayoutFit,
|
||
fixedLayoutSpreadMode: fixedLayoutSpreadMode
|
||
)
|
||
}
|
||
}
|