- PaginationCoordinator: 新增 textReflowable 快速进入路径,支持增量章节构建 与 staged 合并策略,避免整书一次性构建的主线程阻塞 - TextBookBuilder: 重构为支持单章构建与增量合并模式 - ChapterPageCounter: 优化分页计数逻辑,支持章节级分页缓存 - PageFrameFactory: 新增 CoreText 页面帧工厂,提升排版性能 - ReaderContext/ReaderRuntime: 适配增量分页状态管理 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
148 lines
6.1 KiB
Swift
148 lines
6.1 KiB
Swift
import UIKit
|
||
|
||
#if canImport(DTCoreText)
|
||
import DTCoreText
|
||
#endif
|
||
|
||
/// DTCoreText 渲染器实现:将 EPUB 章节 HTML 转换为富文本(NSAttributedString)。
|
||
///
|
||
/// 这是 `RDEPUBTextRenderer` 协议的默认实现,位于渲染链路的第二步:
|
||
/// 章节 HTML → DTCoreText 渲染 → NSAttributedString → 提取 fragment 偏移量 → 统一字体/行距
|
||
///
|
||
/// DTCoreText 库负责将 HTML 解析为带有排版属性的富文本,
|
||
/// 渲染过程中会通过 `willFlushCallback` 回调对每个 DOM 元素做布局规范化(图片尺寸等)。
|
||
/// 当 DTCoreText 不可用时(条件编译失败),回退到纯文本渲染。
|
||
public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
|
||
public init() {}
|
||
|
||
/// DTCoreText 库是否在当前编译环境中可用
|
||
public static var isAvailable: Bool {
|
||
#if canImport(DTCoreText)
|
||
return true
|
||
#else
|
||
return false
|
||
#endif
|
||
}
|
||
|
||
/// 渲染单个章节:HTML → NSAttributedString,同时提取 fragment 和语义标记。
|
||
public func renderChapter(
|
||
request: RDEPUBTextChapterRenderRequest
|
||
) throws -> RDEPUBRenderedChapterContent {
|
||
#if canImport(DTCoreText)
|
||
let chapterContext = request.context
|
||
guard let data = chapterContext.html.data(using: .utf8) else {
|
||
throw RDEPUBTextRenderingError.htmlEncodingFailed
|
||
}
|
||
|
||
guard let rendered = makeAttributedString(from: data, request: request) else {
|
||
return fallbackRenderedContent(request: request)
|
||
}
|
||
|
||
let attributedString = NSMutableAttributedString(attributedString: rendered)
|
||
RDEPUBSemanticMarkerInjector.applyPaginationSemantics(in: attributedString)
|
||
let fragmentOffsets = RDEPUBFragmentMarkerInjector.extractFragmentOffsets(from: attributedString)
|
||
RDEPUBTextRendererSupport.normalizeReadingAttributes(
|
||
in: attributedString,
|
||
style: request.style,
|
||
layoutConfig: request.layoutConfig ?? .default
|
||
)
|
||
return RDEPUBRenderedChapterContent(
|
||
attributedString: attributedString,
|
||
fragmentOffsets: fragmentOffsets,
|
||
resourceDiagnostics: chapterContext.resourceDiagnostics
|
||
)
|
||
#else
|
||
return fallbackRenderedContent(request: request)
|
||
#endif
|
||
}
|
||
|
||
/// 便捷方法:直接传入 HTML 字符串进行渲染(不含上下文信息)
|
||
public func renderChapter(
|
||
html: String,
|
||
baseURL: URL?,
|
||
style: RDEPUBTextRenderStyle
|
||
) throws -> RDEPUBRenderedChapterContent {
|
||
let request = RDEPUBTextTypesetterPipeline().makeRequest(
|
||
from: RDEPUBTypesettingInput(
|
||
href: "",
|
||
title: "",
|
||
rawHTML: html,
|
||
baseURL: baseURL,
|
||
style: style,
|
||
resourceResolver: nil
|
||
)
|
||
).request
|
||
return try renderChapter(request: request)
|
||
}
|
||
|
||
/// 回退渲染:当 DTCoreText 不可用时,将 HTML 源码当作纯文本处理
|
||
private func fallbackRenderedContent(request: RDEPUBTextChapterRenderRequest) -> RDEPUBRenderedChapterContent {
|
||
let attributedString = RDEPUBTextRendererSupport.fallbackAttributedString(for: request.context.html, style: request.style)
|
||
RDEPUBSemanticMarkerInjector.applyPaginationSemantics(in: attributedString)
|
||
let fragmentOffsets = RDEPUBFragmentMarkerInjector.extractFragmentOffsets(from: attributedString)
|
||
RDEPUBTextRendererSupport.normalizeReadingAttributes(
|
||
in: attributedString,
|
||
style: request.style,
|
||
layoutConfig: request.layoutConfig ?? .default
|
||
)
|
||
return RDEPUBRenderedChapterContent(
|
||
attributedString: attributedString,
|
||
fragmentOffsets: fragmentOffsets,
|
||
resourceDiagnostics: request.context.resourceDiagnostics
|
||
)
|
||
}
|
||
|
||
#if canImport(DTCoreText)
|
||
/// 使用 DTCoreText 将 HTML Data 解析为富文本。
|
||
private func makeAttributedString(from data: Data, request: RDEPUBTextChapterRenderRequest) -> NSAttributedString? {
|
||
let builder = DTHTMLAttributedStringBuilder(
|
||
html: data,
|
||
options: dtOptions(request: request),
|
||
documentAttributes: nil
|
||
)
|
||
builder?.willFlushCallback = { element in
|
||
guard let element else { return }
|
||
RDEPUBAttachmentNormalizer.prepareHTMLElementForReaderRendering(
|
||
element,
|
||
style: request.style,
|
||
maxImageSize: resolvedMaxImageSize(for: request)
|
||
)
|
||
}
|
||
return builder?.generatedAttributedString()
|
||
}
|
||
|
||
/// 构建 DTCoreText 的解析选项字典,包括字体、行高、图片尺寸限制等。
|
||
private func dtOptions(request: RDEPUBTextChapterRenderRequest) -> [AnyHashable: Any] {
|
||
let style = request.style
|
||
let maxImageSize = resolvedMaxImageSize(for: request)
|
||
var options: [AnyHashable: Any] = [
|
||
NSTextSizeMultiplierDocumentOption: 1.0,
|
||
DTDefaultFontFamily: style.font.familyName,
|
||
DTDefaultFontName: style.font.fontName,
|
||
DTDefaultFontSize: style.font.pointSize,
|
||
DTDefaultLineHeightMultiplier: max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1),
|
||
DTUseiOS6Attributes: true,
|
||
DTMaxImageSize: NSValue(cgSize: maxImageSize)
|
||
]
|
||
|
||
if let baseURL = request.context.baseURL {
|
||
options[NSBaseURLDocumentOption] = baseURL
|
||
}
|
||
if let textColor = style.textColor {
|
||
options[DTDefaultTextColor] = textColor
|
||
}
|
||
|
||
return options
|
||
}
|
||
|
||
private func resolvedMaxImageSize(for request: RDEPUBTextChapterRenderRequest) -> CGSize {
|
||
let layoutConfig = request.layoutConfig ?? .default
|
||
let fallbackPageSize = request.pageSize ?? layoutConfig.fallbackViewportSize
|
||
let contentRect = layoutConfig.contentRect(fallback: fallbackPageSize)
|
||
let maxWidth = max(round(contentRect.width), 1)
|
||
let maxHeight = max(round(contentRect.height * layoutConfig.imageMaxHeightRatio), 1)
|
||
return CGSize(width: maxWidth, height: maxHeight)
|
||
}
|
||
#endif
|
||
}
|