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 和语义标记。 /// /// 渲染流程: /// 1. 将 HTML 字符串编码为 Data /// 2. 通过 DTCoreText 解析为富文本(失败则回退到纯文本) /// 3. 注入分页语义标记(${rd-sem-start/end} → 属性字典) /// 4. 提取 fragment 偏移量映射表 /// 5. 规范化阅读属性(字体、行距、颜色统一) 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) RDEPUBTextRendererSupport.applyPaginationSemantics(in: attributedString) let fragmentOffsets = RDEPUBTextRendererSupport.extractFragmentOffsets(from: attributedString) RDEPUBTextRendererSupport.normalizeReadingAttributes(in: attributedString, style: request.style) 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 = RDEPUBTextRendererSupport.makeChapterRenderRequest( href: "", title: "", rawHTML: html, baseURL: baseURL, style: style, resourceResolver: nil ) return try renderChapter(request: request) } /// 回退渲染:当 DTCoreText 不可用时,将 HTML 源码当作纯文本处理 private func fallbackRenderedContent(request: RDEPUBTextChapterRenderRequest) -> RDEPUBRenderedChapterContent { let attributedString = RDEPUBTextRendererSupport.fallbackAttributedString(for: request.context.html, style: request.style) RDEPUBTextRendererSupport.applyPaginationSemantics(in: attributedString) let fragmentOffsets = RDEPUBTextRendererSupport.extractFragmentOffsets(from: attributedString) RDEPUBTextRendererSupport.normalizeReadingAttributes(in: attributedString, style: request.style) return RDEPUBRenderedChapterContent( attributedString: attributedString, fragmentOffsets: fragmentOffsets, resourceDiagnostics: request.context.resourceDiagnostics ) } #if canImport(DTCoreText) /// 使用 DTCoreText 将 HTML Data 解析为富文本。 /// /// 通过 `willFlushCallback` 回调,在每个 DOM 元素最终写入富文本前, /// 对图片附件做尺寸规范化、判断是否为脚注/封面等特殊元素。 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 } RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering( element, style: request.style, maxImageSize: resolvedMaxImageSize(for: request) ) } return builder?.generatedAttributedString() } /// 构建 DTCoreText 的解析选项字典,包括字体、行高、图片尺寸限制等。 /// /// - 注意:行高倍率计算公式为 `(字体行高 + 行间距) / 字体行高`, /// 确保最终行距与用户设置的 style.lineSpacing 一致。 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 { if let pageSize = request.pageSize { let layoutConfig = request.layoutConfig ?? .default let contentRect = layoutConfig.contentRect(fallback: pageSize) let maxWidth = max(round(contentRect.width), 1) let maxHeight = max(round(contentRect.height * layoutConfig.imageMaxHeightRatio), 1) return CGSize(width: maxWidth, height: maxHeight) } let screenBounds = UIScreen.main.bounds.insetBy(dx: 20, dy: 28) return CGSize(width: max(round(screenBounds.width), 1), height: max(round(screenBounds.height * 0.85), 1)) } #endif }