ReadViewSDK/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift
shenlei c65c190b71 feat: EPUB阅读器搜索、注释、CFI模块及大书远距跳转优化
- 实现EPUB阅读器搜索功能及选中注释功能
- 优化CFI模块,修复代码审查发现的11个问题
- 实现大书远距目录跳转与后台补全优化方案
- 优化设置面板与章节运行时联动
- 重构及大量改进优化
2026-06-22 20:26:34 +08:00

136 lines
5.0 KiB
Swift

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
}