import UIKit #if canImport(DTCoreText) import DTCoreText /// Redistributes the leftover space at the bottom of a page into the gaps /// between lines so the last line sits flush with the content bottom edge /// (vertical justification), keeping page character ranges untouched. /// /// Mutating each line's `baselineOrigin` is sufficient: DTCoreText derives /// line frames, glyph-run frames and attachment positions from it lazily, /// so drawing, selection, highlights and hit-testing all stay consistent. enum RDEPUBTextPageVerticalJustifier { /// Leftover larger than this many typical line advances is kept as /// whitespace instead of being stretched: it usually comes from a whole /// block (image, table) pushed to the next page, and stretching would /// make the line spacing visibly sparse. static let maxStretchLineAdvanceRatio: CGFloat = 1.5 static func justify( _ layoutFrame: DTCoreTextLayoutFrame, contentHeight: CGFloat, isChapterLastPage: Bool, pixelScale: CGFloat ) { guard !isChapterLastPage, contentHeight > 0, let lines = layoutFrame.lines as? [DTCoreTextLayoutLine], lines.count >= 2, let firstLine = lines.first, let lastLine = lines.last else { return } let leftover = contentHeight - lastLine.frame.maxY guard leftover > 0.5 else { return } let gapCount = CGFloat(lines.count - 1) let typicalAdvance = (lastLine.baselineOrigin.y - firstLine.baselineOrigin.y) / gapCount guard typicalAdvance > 0, leftover <= typicalAdvance * maxStretchLineAdvanceRatio else { return } let scale = max(pixelScale, 1) for (index, line) in lines.enumerated() where index > 0 { // Round each cumulative shift down to the pixel grid so glyphs // stay sharp and the last line never overshoots the bottom edge. let shift = floor(leftover * CGFloat(index) / gapCount * scale) / scale var origin = line.baselineOrigin origin.y += shift line.baselineOrigin = origin } } } #endif