import UIKit #if canImport(DTCoreText) import DTCoreText #endif struct RDEPUBPageLine { let stringRange: NSRange let frame: CGRect let baselineOrigin: CGPoint let ascent: CGFloat let descent: CGFloat let leading: CGFloat } struct RDEPUBPageRun { let stringRange: NSRange let frame: CGRect let isAttachment: Bool } struct RDEPUBPageAttachment { let stringRange: NSRange let frame: CGRect let displaySize: CGSize let placement: RDEPUBTextAttachmentPlacement? let kind: RDEPUBTextAttachmentKind? } struct RDEPUBPageLayoutSnapshot { let page: RDEPUBTextPage let lines: [RDEPUBPageLine] let runs: [RDEPUBPageRun] let attachments: [RDEPUBPageAttachment] let pageContentRange: NSRange #if canImport(DTCoreText) let layoutFrame: DTCoreTextLayoutFrame #endif func line(containing absoluteIndex: Int) -> RDEPUBPageLine? { lines.first { NSLocationInRange(absoluteIndex, $0.stringRange) } } func run(containing absoluteIndex: Int) -> RDEPUBPageRun? { runs.first { NSLocationInRange(absoluteIndex, $0.stringRange) } } func runs(intersecting range: NSRange) -> [RDEPUBPageRun] { runs.filter { NSIntersectionRange($0.stringRange, range).length > 0 } } func attachment(at point: CGPoint, hitSlop: CGFloat = 6) -> RDEPUBPageAttachment? { attachments.first { $0.frame.insetBy(dx: -hitSlop, dy: -hitSlop).contains(point) } } func rects(containing point: CGPoint, in decorations: [RDEPUBTextOverlayDecoration]) -> [RDEPUBTextOverlayDecoration] { decorations.filter { decoration in decoration.rects.contains { $0.insetBy(dx: -4, dy: -4).contains(point) } } } #if canImport(DTCoreText) static func build( from layoutFrame: DTCoreTextLayoutFrame, page: RDEPUBTextPage ) -> RDEPUBPageLayoutSnapshot? { guard let dtLines = layoutFrame.lines as? [DTCoreTextLayoutLine], !dtLines.isEmpty else { return nil } var lines: [RDEPUBPageLine] = [] var runs: [RDEPUBPageRun] = [] var attachments: [RDEPUBPageAttachment] = [] for dtLine in dtLines { let lineRange = dtLine.stringRange() let line = RDEPUBPageLine( stringRange: lineRange, frame: dtLine.frame, baselineOrigin: dtLine.baselineOrigin, ascent: dtLine.ascent, descent: dtLine.descent, leading: dtLine.leading ) lines.append(line) if let glyphRuns = dtLine.glyphRuns as? [DTCoreTextGlyphRun] { for run in glyphRuns { let runRange = run.stringRange() let isAttachment = run.attachment != nil runs.append( RDEPUBPageRun( stringRange: runRange, frame: run.frame, isAttachment: isAttachment ) ) guard isAttachment else { continue } let metadata = attachmentMetadata( for: runRange, on: page ) attachments.append( RDEPUBPageAttachment( stringRange: runRange, frame: run.frame, displaySize: run.frame.size, placement: metadata.placement, kind: metadata.kind ) ) } } } let visibleRange = layoutFrame.visibleStringRange() return RDEPUBPageLayoutSnapshot( page: page, lines: lines, runs: runs, attachments: attachments, pageContentRange: visibleRange, layoutFrame: layoutFrame ) } private static func attachmentMetadata( for range: NSRange, on page: RDEPUBTextPage ) -> (placement: RDEPUBTextAttachmentPlacement?, kind: RDEPUBTextAttachmentKind?) { guard let attachmentIndex = page.metadata.attachmentRanges.firstIndex(where: { NSIntersectionRange($0, range).length > 0 }) else { return (nil, nil) } let placement = page.metadata.attachmentPlacements.indices.contains(attachmentIndex) ? page.metadata.attachmentPlacements[attachmentIndex] : nil let kind = page.metadata.attachmentKinds.indices.contains(attachmentIndex) ? page.metadata.attachmentKinds[attachmentIndex] : nil return (placement, kind) } #endif }