import UIKit /// 原生文本渲染路径的批注覆盖层,负责绘制用户高亮、搜索命中高亮和当前选区装饰。 final class RDEPUBTextAnnotationOverlay: RDEPUBSelectionOverlayView { private let normalSearchColor = UIColor(red: 0.21, green: 0.48, blue: 0.95, alpha: 0.16) private let activeSearchColor = UIColor(red: 0.14, green: 0.42, blue: 0.95, alpha: 0.34) /// 将用户高亮批注以富文本属性形式应用到页面内容上 /// - Parameters: /// - highlights: 高亮批注数组 /// - content: 待修改的富文本 /// - page: 目标文本页 /// - contentBaseOffset: 内容在全局偏移中的起始位置 func applyHighlights( _ highlights: [RDEPUBHighlight], to content: NSMutableAttributedString, page: RDEPUBTextPage, contentBaseOffset: Int ) { let pageRange = absoluteOffsetRange(for: page) let pageStart = pageRange.lowerBound let pageEndExclusive = pageRange.upperBound for highlight in highlights where highlight.location.href == page.href { guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue } let overlapStart = max(range.location, pageStart) let overlapEnd = min(range.location + range.length, pageEndExclusive) guard overlapStart < overlapEnd else { continue } let relativeRange = NSRange( location: overlapStart - contentBaseOffset, length: overlapEnd - overlapStart ) switch highlight.style { case .highlight: content.addAttribute( .backgroundColor, value: UIColor(rdHexString: highlight.color, alpha: 0.35) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.35), range: relativeRange ) case .underline: content.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: relativeRange) if let color = UIColor(rdHexString: highlight.color, alpha: 1) { content.addAttribute(.underlineColor, value: color, range: relativeRange) } } } } /// 将搜索命中高亮以富文本背景色形式应用到页面内容上 /// - Parameters: /// - content: 待修改的富文本 /// - page: 目标文本页 /// - searchState: 当前搜索状态 /// - contentBaseOffset: 内容在全局偏移中的起始位置 func applySearchHighlights( to content: NSMutableAttributedString, page: RDEPUBTextPage, searchState: RDEPUBSearchState?, contentBaseOffset: Int ) { guard let searchState else { return } let pageRange = absoluteOffsetRange(for: page) let pageStart = pageRange.lowerBound let pageEndExclusive = pageRange.upperBound for match in searchState.matches { guard let matchStart = match.rangeLocation else { continue } let matchEnd = matchStart + match.rangeLength let overlapStart = max(matchStart, pageStart) let overlapEnd = min(matchEnd, pageEndExclusive) guard overlapStart < overlapEnd else { continue } let relativeRange = NSRange(location: Int(overlapStart - contentBaseOffset), length: Int(overlapEnd - overlapStart)) let color = match == searchState.currentMatch ? activeSearchColor : normalSearchColor content.addAttribute(.backgroundColor, value: color, range: relativeRange) } } /// 构建页面的背景和前景装饰数组(搜索高亮为背景,下划线批注为前景) /// - Parameters: /// - page: 目标文本页 /// - highlights: 高亮批注数组 /// - searchState: 当前搜索状态 /// - interactionController: 用于计算选区矩形的交互控制器 /// - Returns: 分离的背景和前景装饰数组 func buildDecorations( page: RDEPUBTextPage, highlights: [RDEPUBHighlight], searchState: RDEPUBSearchState?, interactionController: RDEPUBPageInteractionController ) -> (background: [RDEPUBTextOverlayDecoration], foreground: [RDEPUBTextOverlayDecoration]) { var background: [RDEPUBTextOverlayDecoration] = [] var foreground: [RDEPUBTextOverlayDecoration] = [] let pageRange = absoluteOffsetRange(for: page) let pageStart = pageRange.lowerBound let pageEndExclusive = pageRange.upperBound if let searchState { for match in searchState.matches { guard let matchStart = match.rangeLocation else { continue } let matchEnd = matchStart + match.rangeLength let overlapStart = max(matchStart, pageStart) let overlapEnd = min(matchEnd, pageEndExclusive) guard overlapStart < overlapEnd else { continue } let absoluteRange = NSRange(location: overlapStart, length: overlapEnd - overlapStart) let rects = interactionController.selectionRects(for: absoluteRange) guard !rects.isEmpty else { continue } let isActive = match == searchState.currentMatch let kind: RDEPUBTextOverlayDecoration.Kind = isActive ? .activeSearch : .search let color = isActive ? activeSearchColor : normalSearchColor background.append(RDEPUBTextOverlayDecoration(kind: kind, absoluteRange: absoluteRange, rects: rects, color: color)) } } for highlight in highlights where highlight.location.href == page.href { guard let range = RDEPUBTextOffsetRangeInfo.decode(from: highlight.rangeInfo)?.nsRange else { continue } let overlapStart = max(range.location, pageStart) let overlapEnd = min(range.location + range.length, pageEndExclusive) guard overlapStart < overlapEnd else { continue } let absoluteRange = NSRange(location: overlapStart, length: overlapEnd - overlapStart) let rects = interactionController.selectionRects(for: absoluteRange) guard !rects.isEmpty else { continue } let color = UIColor(rdHexString: highlight.color, alpha: 0.35) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.35) let decoration = RDEPUBTextOverlayDecoration( kind: highlight.style == .underline ? .underline : .highlight, absoluteRange: absoluteRange, rects: rects, color: color ) if decoration.kind == .underline { foreground.append(decoration) } else { background.append(decoration) } } return (background, foreground) } private func absoluteOffsetRange(for page: RDEPUBTextPage) -> Range { let lowerBound = page.pageStartOffset let upperBound = page.pageEndOffset + 1 return lowerBound..