import UIKit #if canImport(DTCoreText) import DTCoreText #endif public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer { public init() {} public static var isAvailable: Bool { #if canImport(DTCoreText) return true #else return false #endif } 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 } public func renderChapter( html: String, baseURL: URL?, style: RDEPUBTextRenderStyle ) throws -> RDEPUBRenderedChapterContent { let request = RDEPUBTextTypesetterPipeline().makeRequest( from: RDEPUBTypesettingInput( href: "", spineIndex: nil, title: "", rawHTML: html, baseURL: baseURL, style: style, resourceResolver: nil ) ).request return try renderChapter(request: request) } 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) 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() } 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 }