Files
ReadViewSDK/Sources/RDReaderView/EPUBUI/Settings/RDEPUBReaderConfiguration.swift
T
shenandshen d20196ee34 feat: configurable chapter window & parallel metadata parsing with benchmark
1. Configurable chapter window size (onDemandChapterWindowSize: 3-15)
   - Parameterized window radius in RDEPUBChapterRuntimeStore
   - Updated RDEPUBChapterWindowCoordinator to use configurable radius
   - RDEPUBChapterWindowSnapshot.from() accepts chapter array instead of fixed prev/next
   - Even numbers round up to odd (4→5), min 3, max 15

2. Configurable metadata parsing concurrency (metadataParsingConcurrency)
   - Default equals CPU core count
   - Parallel execution via OperationQueue in paginateMetadataOnly
   - Each worker creates independent builder instance
   - NSLock protects result aggregation

3. Per-chapter and total wall-clock timing instrumentation
   - Separated render vs I/O timing per chapter
   - Summary log with wallClockMs, renderTotalMs, writeTotalMs, avgRenderMs
   - Timing stored in RDEPUBReaderContext for test access

4. UI automation test infrastructure
   - Added --demo-window-size, --demo-concurrency, --demo-clear-cache launch args
   - DemoReaderState exposes windowSize, parseMs, parseConcurrency
   - ConfigurableWindowTests: 5 test cases for window size 3/5/15
   - ConcurrentParsingTests: 4 test cases for concurrency 2/4
   - MetadataParseBenchmarkTests: serial vs parallel benchmark

5. Bug fixes
   - Fixed page snap-back during background parsing (isUserInteracting check)
   - Reduced BookPageMap refresh frequency from 16 to 32 chapters
   - Moved waitForReadingInteractionToSettle outside operation loop

6. Design doc: dual-layer PageMap (estimated + precise mixed)
2026-06-03 23:38:11 +08:00

219 lines
9.2 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: 初始化
/// 创建阅读器配置,所有参数均提供合理的默认值
/// - 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 核心数
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
) {
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)
}
/// 默认配置实例,使用所有参数的默认值
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
)
}
}