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 }