epub问题修改
This commit is contained in:
parent
22adb76332
commit
ea21c6a831
Binary file not shown.
@ -223,6 +223,10 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
self.init(renderer: RDEPUBDTCoreTextRenderer())
|
self.init(renderer: RDEPUBDTCoreTextRenderer())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var isPaginationDebugEnabled: Bool {
|
||||||
|
ProcessInfo.processInfo.arguments.contains("--demo-pagination-debug")
|
||||||
|
}
|
||||||
|
|
||||||
/// 生成最近一次构建的语义摘要,用于 Phase 7 质量检测日志
|
/// 生成最近一次构建的语义摘要,用于 Phase 7 质量检测日志
|
||||||
public func phase7SemanticSummary(title: String? = nil) -> String? {
|
public func phase7SemanticSummary(title: String? = nil) -> String? {
|
||||||
guard !lastBuildPaginationDiagnostics.isEmpty else { return nil }
|
guard !lastBuildPaginationDiagnostics.isEmpty else { return nil }
|
||||||
@ -263,6 +267,9 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
pageSize: CGSize,
|
pageSize: CGSize,
|
||||||
style: RDEPUBTextRenderStyle
|
style: RDEPUBTextRenderStyle
|
||||||
) throws -> RDEPUBTextBook {
|
) throws -> RDEPUBTextBook {
|
||||||
|
if isPaginationDebugEnabled {
|
||||||
|
print("[PaginationDebug] build pageSize=\(NSCoder.string(for: pageSize)) layoutInsets=\(NSCoder.string(for: layoutConfig.edgeInsets))")
|
||||||
|
}
|
||||||
var chapters: [RDEPUBTextChapter] = []
|
var chapters: [RDEPUBTextChapter] = []
|
||||||
var flatPages: [RDEPUBTextPage] = []
|
var flatPages: [RDEPUBTextPage] = []
|
||||||
lastBuildResourceDiagnostics = []
|
lastBuildResourceDiagnostics = []
|
||||||
@ -437,6 +444,18 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isPaginationDebugEnabled,
|
||||||
|
item.href.contains("Chapter_3.xhtml") {
|
||||||
|
print("[PaginationDebug] href=\(item.href) pages=\(pages.count)")
|
||||||
|
for page in pages {
|
||||||
|
let preview = debugPreview(for: page.content, limit: 36)
|
||||||
|
print("[PaginationDebug] absPage=\(page.absolutePageIndex + 1) localPage=\(page.pageIndexInChapter + 1) range=\(NSStringFromRange(page.contentRange)) break=\(page.metadata.breakReason.rawValue) preview=\(preview)")
|
||||||
|
for note in page.metadata.diagnostics.prefix(4) {
|
||||||
|
print("[PaginationDebug] note=\(note)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
chapters.append(
|
chapters.append(
|
||||||
RDEPUBTextChapter(
|
RDEPUBTextChapter(
|
||||||
chapterIndex: chapterIndex,
|
chapterIndex: chapterIndex,
|
||||||
@ -575,11 +594,26 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
return attachmentCount(in: content) > 0 && trimmed.count <= 1
|
return attachmentCount(in: content) > 0 && trimmed.count <= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func debugPreview(for content: NSAttributedString, limit: Int) -> String {
|
||||||
|
let collapsed = content.string
|
||||||
|
.replacingOccurrences(of: "\n", with: " ")
|
||||||
|
.replacingOccurrences(of: "\r", with: " ")
|
||||||
|
.replacingOccurrences(of: "\t", with: " ")
|
||||||
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard !collapsed.isEmpty else { return "<empty>" }
|
||||||
|
if collapsed.count <= limit {
|
||||||
|
return collapsed
|
||||||
|
}
|
||||||
|
let head = collapsed.prefix(limit)
|
||||||
|
return "\(head)…"
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - 尾页规范化
|
// MARK: - 尾页规范化
|
||||||
|
|
||||||
/// 规范化分页结果的尾部页面:
|
/// 规范化分页结果:
|
||||||
/// 1. 丢弃纯空白的尾页
|
/// 1. 移除章节中间误产生的纯空白页
|
||||||
/// 2. 将过短的尾页(≤2 字符)合并到前一页
|
/// 2. 丢弃纯空白的尾页
|
||||||
|
/// 3. 将过短的尾页(≤2 字符)合并到前一页
|
||||||
private func normalizeTrailingFrames(
|
private func normalizeTrailingFrames(
|
||||||
_ frames: [RDEPUBTextLayoutFrame],
|
_ frames: [RDEPUBTextLayoutFrame],
|
||||||
content: NSAttributedString,
|
content: NSAttributedString,
|
||||||
@ -589,9 +623,28 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
|
|
||||||
var normalized = frames
|
var normalized = frames
|
||||||
|
|
||||||
|
// 先移除章节中间的纯空白页。这类页通常只包含换行/占位空白,
|
||||||
|
// 会把相邻正文页硬生生拆开,直接表现为“上一页底部空很多”。
|
||||||
|
var compacted: [RDEPUBTextLayoutFrame] = []
|
||||||
|
compacted.reserveCapacity(normalized.count)
|
||||||
|
for frame in normalized {
|
||||||
|
if shouldDropWhitespaceOnlyFrame(frame, in: content) {
|
||||||
|
let note = "normalized: dropped whitespace-only intermediate page \(NSStringFromRange(frame.contentRange))"
|
||||||
|
if var previous = compacted.popLast() {
|
||||||
|
previous.diagnostics.append(note)
|
||||||
|
compacted.append(previous)
|
||||||
|
} else {
|
||||||
|
print("[EPUB][Pagination] href=\(href) dropped leading/intermediate whitespace frame \(NSStringFromRange(frame.contentRange))")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
compacted.append(frame)
|
||||||
|
}
|
||||||
|
normalized = compacted
|
||||||
|
|
||||||
// 丢弃纯空白的尾页
|
// 丢弃纯空白的尾页
|
||||||
while let lastFrame = normalized.last,
|
while let lastFrame = normalized.last,
|
||||||
shouldDropWhitespaceOnlyTrailingFrame(lastFrame, in: content) {
|
shouldDropWhitespaceOnlyFrame(lastFrame, in: content) {
|
||||||
normalized.removeLast()
|
normalized.removeLast()
|
||||||
let note = "normalized: dropped whitespace-only trailing page \(NSStringFromRange(lastFrame.contentRange))"
|
let note = "normalized: dropped whitespace-only trailing page \(NSStringFromRange(lastFrame.contentRange))"
|
||||||
if var previousFrame = normalized.popLast() {
|
if var previousFrame = normalized.popLast() {
|
||||||
@ -617,7 +670,7 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 判断尾页是否为纯空白(无可显示字符且无附件)
|
/// 判断尾页是否为纯空白(无可显示字符且无附件)
|
||||||
private func shouldDropWhitespaceOnlyTrailingFrame(
|
private func shouldDropWhitespaceOnlyFrame(
|
||||||
_ frame: RDEPUBTextLayoutFrame,
|
_ frame: RDEPUBTextLayoutFrame,
|
||||||
in content: NSAttributedString
|
in content: NSAttributedString
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
@ -737,7 +790,7 @@ public final class RDEPUBTextBookBuilder {
|
|||||||
bookID: bookID,
|
bookID: bookID,
|
||||||
fontSize: style.font.pointSize,
|
fontSize: style.font.pointSize,
|
||||||
lineHeightMultiple: style.lineSpacing,
|
lineHeightMultiple: style.lineSpacing,
|
||||||
contentInsets: .zero,
|
contentInsets: layoutConfig.edgeInsets,
|
||||||
pageSize: pageSize,
|
pageSize: pageSize,
|
||||||
layoutConfigSignature: layoutConfig.cacheSignature
|
layoutConfigSignature: layoutConfig.cacheSignature
|
||||||
)
|
)
|
||||||
|
|||||||
@ -123,7 +123,8 @@ final class ChapterPaginationArchive: NSObject, NSSecureCoding {
|
|||||||
public final class RDEPUBTextBookCache {
|
public final class RDEPUBTextBookCache {
|
||||||
|
|
||||||
/// 缓存模式版本号,变更时旧缓存自动失效
|
/// 缓存模式版本号,变更时旧缓存自动失效
|
||||||
public var schemaVersion: Int = 4
|
// 分页算法调整后需要提升版本,避免继续复用旧页范围缓存。
|
||||||
|
public var schemaVersion: Int = 6
|
||||||
|
|
||||||
/// 串行队列,保证缓存读写的线程安全
|
/// 串行队列,保证缓存读写的线程安全
|
||||||
private let queue = DispatchQueue(label: "com.rdreader.textbookcache", qos: .utility)
|
private let queue = DispatchQueue(label: "com.rdreader.textbookcache", qos: .utility)
|
||||||
|
|||||||
@ -67,23 +67,42 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
var frames: [RDEPUBTextLayoutFrame] = []
|
var frames: [RDEPUBTextLayoutFrame] = []
|
||||||
var location = 0
|
var location = 0
|
||||||
|
let resolvedSize = config.resolvedFrameSize(fallback: pageSize)
|
||||||
|
let usableWidth = resolvedSize.width - config.edgeInsets.left - config.edgeInsets.right
|
||||||
|
let usableHeight = resolvedSize.height - config.edgeInsets.top - config.edgeInsets.bottom
|
||||||
|
|
||||||
|
guard usableWidth > 0, usableHeight > 0 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
while location < attributedString.length {
|
while location < attributedString.length {
|
||||||
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), path, nil)
|
let framePath = CGMutablePath()
|
||||||
let visibleRange = CTFrameGetVisibleStringRange(frame)
|
// 对齐 WXRead WRChapterPageCount:
|
||||||
guard visibleRange.length > 0 else {
|
// CoreText 分页路径使用 bottom inset 作为 y 原点,而不是 UIKit 语义的 top inset。
|
||||||
|
let pageRect = CGRect(
|
||||||
|
x: config.edgeInsets.left,
|
||||||
|
y: config.edgeInsets.bottom,
|
||||||
|
width: usableWidth,
|
||||||
|
height: usableHeight
|
||||||
|
)
|
||||||
|
framePath.addRect(pageRect)
|
||||||
|
let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), framePath, nil)
|
||||||
|
let proposedRange = proposedRangeUsingWXReadPageCount(
|
||||||
|
from: frame,
|
||||||
|
start: location,
|
||||||
|
usableHeight: usableHeight,
|
||||||
|
totalLength: attributedString.length
|
||||||
|
)
|
||||||
|
guard proposedRange.length > 0 else {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
let proposedRange = NSRange(location: location, length: visibleRange.length)
|
|
||||||
|
|
||||||
// 行级 avoidPageBreakInside 处理(WXRead 方案:从最后一行向前扫描)
|
// 行级 avoidPageBreakInside 处理(WXRead 方案:从最后一行向前扫描)
|
||||||
let avoidAdjusted = trimmedRangeForAvoidPageBreakInside(from: frame, proposed: proposedRange)
|
let avoidAdjusted = trimmedRangeForAvoidPageBreakInside(from: frame, proposed: proposedRange)
|
||||||
let lineAdjusted = trimmedRangeForKeepWithNext(from: frame, proposed: avoidAdjusted)
|
|
||||||
let lineRanges = lineRanges(from: frame)
|
let lineRanges = lineRanges(from: frame)
|
||||||
|
|
||||||
let adjusted = adjustedRange(
|
let adjusted = adjustedRange(
|
||||||
from: lineAdjusted,
|
from: avoidAdjusted,
|
||||||
totalLength: attributedString.length,
|
totalLength: attributedString.length,
|
||||||
lineRanges: lineRanges
|
lineRanges: lineRanges
|
||||||
)
|
)
|
||||||
@ -108,7 +127,7 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
let nextLocation = adjusted.range.location + adjusted.range.length
|
let nextLocation = adjusted.range.location + adjusted.range.length
|
||||||
guard nextLocation > location else {
|
guard nextLocation > location else {
|
||||||
location += max(visibleRange.length, 1)
|
location += max(proposedRange.length, 1)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
location = nextLocation
|
location = nextLocation
|
||||||
@ -155,7 +174,12 @@ struct RDEPUBTextLayouter {
|
|||||||
totalLength: attributedString.length,
|
totalLength: attributedString.length,
|
||||||
lineRanges: lineRanges
|
lineRanges: lineRanges
|
||||||
)
|
)
|
||||||
let verifiedRange = verifiedDisplayRange(for: adjusted.range)
|
let verifiedRange: NSRange
|
||||||
|
if adjusted.breakReason == .attachmentBoundary {
|
||||||
|
verifiedRange = verifiedDisplayRange(for: adjusted.range)
|
||||||
|
} else {
|
||||||
|
verifiedRange = adjusted.range
|
||||||
|
}
|
||||||
let trailingFragmentID = nearestTrailingFragmentID(
|
let trailingFragmentID = nearestTrailingFragmentID(
|
||||||
endingAt: verifiedRange.location + verifiedRange.length,
|
endingAt: verifiedRange.location + verifiedRange.length,
|
||||||
fragmentOffsets: fragmentOffsets
|
fragmentOffsets: fragmentOffsets
|
||||||
@ -191,27 +215,28 @@ struct RDEPUBTextLayouter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func verifiedDisplayRange(for range: NSRange) -> NSRange {
|
private func verifiedDisplayRange(for range: NSRange) -> NSRange {
|
||||||
guard range.length > 0,
|
guard let clampedRange = clampedRange(range),
|
||||||
!attachmentRanges(in: range).isEmpty else {
|
clampedRange.length > 0,
|
||||||
|
!attachmentRanges(in: clampedRange).isEmpty else {
|
||||||
return range
|
return range
|
||||||
}
|
}
|
||||||
|
|
||||||
let pageContent = attributedString.attributedSubstring(from: range)
|
let pageContent = attributedString.attributedSubstring(from: clampedRange)
|
||||||
guard let layouter = DTCoreTextLayouter(attributedString: pageContent) else {
|
guard let layouter = DTCoreTextLayouter(attributedString: pageContent) else {
|
||||||
return range
|
return clampedRange
|
||||||
}
|
}
|
||||||
|
|
||||||
layouter.shouldCacheLayoutFrames = false
|
layouter.shouldCacheLayoutFrames = false
|
||||||
guard let layoutFrame = layouter.layoutFrame(with: dtLayoutRect, range: NSRange(location: 0, length: 0)) else {
|
guard let layoutFrame = layouter.layoutFrame(with: dtLayoutRect, range: NSRange(location: 0, length: 0)) else {
|
||||||
return range
|
return clampedRange
|
||||||
}
|
}
|
||||||
|
|
||||||
let visibleRange = layoutFrame.visibleStringRange()
|
let visibleRange = layoutFrame.visibleStringRange()
|
||||||
guard visibleRange.length > 0, visibleRange.length < range.length else {
|
guard visibleRange.length > 0, visibleRange.length < clampedRange.length else {
|
||||||
return range
|
return clampedRange
|
||||||
}
|
}
|
||||||
|
|
||||||
return NSRange(location: range.location, length: visibleRange.length)
|
return NSRange(location: clampedRange.location, length: visibleRange.length)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@ -281,10 +306,6 @@ struct RDEPUBTextLayouter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最小分页长度:原始范围的 55%
|
|
||||||
let minLength = max(Int(Double(proposedRange.length) * 0.55), 1)
|
|
||||||
let minimumEnd = proposedRange.location + minLength
|
|
||||||
|
|
||||||
let currentBlockRange = blockRange(at: max(proposedRange.location, pageEnd - 1))
|
let currentBlockRange = blockRange(at: max(proposedRange.location, pageEnd - 1))
|
||||||
let currentAttachmentRanges = attachmentRanges(in: proposedRange)
|
let currentAttachmentRanges = attachmentRanges(in: proposedRange)
|
||||||
let currentAttachmentKinds = attachmentKinds(in: proposedRange)
|
let currentAttachmentKinds = attachmentKinds(in: proposedRange)
|
||||||
@ -292,38 +313,12 @@ struct RDEPUBTextLayouter {
|
|||||||
let currentSemanticHints = proposedSemanticHints
|
let currentSemanticHints = proposedSemanticHints
|
||||||
let currentAttachmentPlacements = proposedAttachmentPlacements
|
let currentAttachmentPlacements = proposedAttachmentPlacements
|
||||||
|
|
||||||
// 1. 语义边界(pageBreakBefore/After)
|
// 对齐 WXRead:默认按 CTFrame 已经容纳的行数分页,仅保留 pageRelate 这种
|
||||||
if let semanticBoundary = preferredSemanticBoundary(
|
// 微信读书特有的跨页关联规则;其他 keepWithNext/attachmentBoundary/通用语义边界
|
||||||
in: proposedRange,
|
// 先不参与截断,避免提前裁短页尾内容。
|
||||||
minimumEnd: minimumEnd
|
|
||||||
) {
|
|
||||||
let adjustedRange = NSRange(location: proposedRange.location, length: semanticBoundary.location - proposedRange.location)
|
|
||||||
return (
|
|
||||||
range: adjustedRange,
|
|
||||||
breakReason: .semanticBoundary,
|
|
||||||
blockRange: currentBlockRange,
|
|
||||||
attachmentRanges: currentAttachmentRanges,
|
|
||||||
attachmentKinds: currentAttachmentKinds,
|
|
||||||
blockKinds: currentBlockKinds,
|
|
||||||
semanticHints: currentSemanticHints,
|
|
||||||
attachmentPlacements: currentAttachmentPlacements,
|
|
||||||
diagnostics: diagnostics(
|
|
||||||
reason: .semanticBoundary,
|
|
||||||
range: adjustedRange,
|
|
||||||
attachmentRanges: currentAttachmentRanges,
|
|
||||||
blockRange: currentBlockRange,
|
|
||||||
blockKinds: currentBlockKinds,
|
|
||||||
semanticHints: currentSemanticHints,
|
|
||||||
attachmentPlacements: currentAttachmentPlacements,
|
|
||||||
trigger: semanticBoundary.trigger
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. pageRelate 跨页关联边界
|
|
||||||
if let pageRelateBoundary = preferredPageRelateBoundary(
|
if let pageRelateBoundary = preferredPageRelateBoundary(
|
||||||
after: proposedRange,
|
after: proposedRange,
|
||||||
minimumEnd: minimumEnd,
|
minimumEnd: proposedRange.location + 1,
|
||||||
lineRanges: lineRanges
|
lineRanges: lineRanges
|
||||||
) {
|
) {
|
||||||
let adjustedRange = NSRange(location: proposedRange.location, length: pageRelateBoundary - proposedRange.location)
|
let adjustedRange = NSRange(location: proposedRange.location, length: pageRelateBoundary - proposedRange.location)
|
||||||
@ -349,33 +344,6 @@ struct RDEPUBTextLayouter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 附件边界(块级附件整体移动)
|
|
||||||
if let attachmentBoundary = preferredAttachmentBoundary(
|
|
||||||
in: proposedRange,
|
|
||||||
minimumEnd: minimumEnd
|
|
||||||
) {
|
|
||||||
let adjustedRange = NSRange(location: proposedRange.location, length: attachmentBoundary - proposedRange.location)
|
|
||||||
return (
|
|
||||||
range: adjustedRange,
|
|
||||||
breakReason: .attachmentBoundary,
|
|
||||||
blockRange: currentBlockRange,
|
|
||||||
attachmentRanges: currentAttachmentRanges,
|
|
||||||
attachmentKinds: currentAttachmentKinds,
|
|
||||||
blockKinds: currentBlockKinds,
|
|
||||||
semanticHints: currentSemanticHints,
|
|
||||||
attachmentPlacements: currentAttachmentPlacements,
|
|
||||||
diagnostics: diagnostics(
|
|
||||||
reason: .attachmentBoundary,
|
|
||||||
range: adjustedRange,
|
|
||||||
attachmentRanges: currentAttachmentRanges,
|
|
||||||
blockRange: currentBlockRange,
|
|
||||||
blockKinds: currentBlockKinds,
|
|
||||||
semanticHints: currentSemanticHints,
|
|
||||||
attachmentPlacements: currentAttachmentPlacements
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 帧限制(默认分页)
|
// 4. 帧限制(默认分页)
|
||||||
return (
|
return (
|
||||||
range: proposedRange,
|
range: proposedRange,
|
||||||
@ -407,8 +375,11 @@ struct RDEPUBTextLayouter {
|
|||||||
in range: NSRange,
|
in range: NSRange,
|
||||||
minimumEnd: Int
|
minimumEnd: Int
|
||||||
) -> (location: Int, trigger: String)? {
|
) -> (location: Int, trigger: String)? {
|
||||||
|
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var boundary: (location: Int, trigger: String)?
|
var boundary: (location: Int, trigger: String)?
|
||||||
attributedString.enumerateAttribute(.rdPageSemanticHints, in: range) { value, attributeRange, stop in
|
attributedString.enumerateAttribute(.rdPageSemanticHints, in: safeRange) { value, attributeRange, stop in
|
||||||
guard let rawValue = value as? String else { return }
|
guard let rawValue = value as? String else { return }
|
||||||
let hints = rawValue
|
let hints = rawValue
|
||||||
.split(separator: ",")
|
.split(separator: ",")
|
||||||
@ -416,7 +387,7 @@ struct RDEPUBTextLayouter {
|
|||||||
guard !hints.isEmpty else { return }
|
guard !hints.isEmpty else { return }
|
||||||
|
|
||||||
if hints.contains(.pageBreakBefore),
|
if hints.contains(.pageBreakBefore),
|
||||||
attributeRange.location > range.location,
|
attributeRange.location > safeRange.location,
|
||||||
attributeRange.location >= minimumEnd {
|
attributeRange.location >= minimumEnd {
|
||||||
boundary = (attributeRange.location, RDEPUBTextSemanticHint.pageBreakBefore.rawValue)
|
boundary = (attributeRange.location, RDEPUBTextSemanticHint.pageBreakBefore.rawValue)
|
||||||
stop.pointee = true
|
stop.pointee = true
|
||||||
@ -426,7 +397,7 @@ struct RDEPUBTextLayouter {
|
|||||||
let attributeEnd = attributeRange.location + attributeRange.length
|
let attributeEnd = attributeRange.location + attributeRange.length
|
||||||
if hints.contains(.pageBreakAfter),
|
if hints.contains(.pageBreakAfter),
|
||||||
attributeEnd > minimumEnd,
|
attributeEnd > minimumEnd,
|
||||||
attributeEnd < range.location + range.length {
|
attributeEnd < safeRange.location + safeRange.length {
|
||||||
boundary = (attributeEnd, RDEPUBTextSemanticHint.pageBreakAfter.rawValue)
|
boundary = (attributeEnd, RDEPUBTextSemanticHint.pageBreakAfter.rawValue)
|
||||||
stop.pointee = true
|
stop.pointee = true
|
||||||
return
|
return
|
||||||
@ -438,8 +409,11 @@ struct RDEPUBTextLayouter {
|
|||||||
/// 查找附件边界:只有块级附件(.attachment 或 .centered)才触发分页,
|
/// 查找附件边界:只有块级附件(.attachment 或 .centered)才触发分页,
|
||||||
/// 行内脚注图标等不应导致整段移动。
|
/// 行内脚注图标等不应导致整段移动。
|
||||||
private func preferredAttachmentBoundary(in range: NSRange, minimumEnd: Int) -> Int? {
|
private func preferredAttachmentBoundary(in range: NSRange, minimumEnd: Int) -> Int? {
|
||||||
|
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
var boundary: Int?
|
var boundary: Int?
|
||||||
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { value, attributeRange, stop in
|
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, attributeRange, stop in
|
||||||
guard value != nil else { return }
|
guard value != nil else { return }
|
||||||
|
|
||||||
let location = attributeRange.location
|
let location = attributeRange.location
|
||||||
@ -448,11 +422,21 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
// 对标 WXRead:只有块级附件才应将整个块推到下一页。
|
// 对标 WXRead:只有块级附件才应将整个块推到下一页。
|
||||||
// 行内脚注图标等行内附件不应导致整段移动。
|
// 行内脚注图标等行内附件不应导致整段移动。
|
||||||
let isBlockLevelAttachment = blockKind == .attachment || placement == .centered
|
// 只有真正的块级附件才需要整块挪页。
|
||||||
|
// 行内/基线对齐的附件(例如脚注 note.png)不能触发附件边界分页。
|
||||||
|
let isBlockLevelAttachment: Bool
|
||||||
|
switch placement {
|
||||||
|
case .centered:
|
||||||
|
isBlockLevelAttachment = true
|
||||||
|
case .inline, .baseline:
|
||||||
|
isBlockLevelAttachment = false
|
||||||
|
case nil:
|
||||||
|
isBlockLevelAttachment = blockKind == .attachment
|
||||||
|
}
|
||||||
guard isBlockLevelAttachment else { return }
|
guard isBlockLevelAttachment else { return }
|
||||||
|
|
||||||
let boundaryRange = blockRange(at: location) ?? paragraphRange(containing: location)
|
let boundaryRange = blockRange(at: location) ?? paragraphRange(containing: location)
|
||||||
if boundaryRange.location > range.location, boundaryRange.location >= minimumEnd {
|
if boundaryRange.location > safeRange.location, boundaryRange.location >= minimumEnd {
|
||||||
boundary = boundaryRange.location
|
boundary = boundaryRange.location
|
||||||
stop.pointee = true
|
stop.pointee = true
|
||||||
}
|
}
|
||||||
@ -523,8 +507,11 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
/// 获取指定范围内的所有附件字符范围
|
/// 获取指定范围内的所有附件字符范围
|
||||||
private func attachmentRanges(in range: NSRange) -> [NSRange] {
|
private func attachmentRanges(in range: NSRange) -> [NSRange] {
|
||||||
|
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
var results: [NSRange] = []
|
var results: [NSRange] = []
|
||||||
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { value, attributeRange, _ in
|
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, attributeRange, _ in
|
||||||
guard value != nil else { return }
|
guard value != nil else { return }
|
||||||
results.append(attributeRange)
|
results.append(attributeRange)
|
||||||
}
|
}
|
||||||
@ -543,8 +530,11 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
/// 获取指定范围内的附件类型列表(去重)
|
/// 获取指定范围内的附件类型列表(去重)
|
||||||
private func attachmentKinds(in range: NSRange) -> [RDEPUBTextAttachmentKind] {
|
private func attachmentKinds(in range: NSRange) -> [RDEPUBTextAttachmentKind] {
|
||||||
|
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
var kinds: [RDEPUBTextAttachmentKind] = []
|
var kinds: [RDEPUBTextAttachmentKind] = []
|
||||||
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { value, _, _ in
|
attributedString.enumerateAttribute(.rdPageAttachmentKind, in: safeRange) { value, _, _ in
|
||||||
guard let rawValue = value as? String,
|
guard let rawValue = value as? String,
|
||||||
let kind = RDEPUBTextAttachmentKind(rawValue: rawValue),
|
let kind = RDEPUBTextAttachmentKind(rawValue: rawValue),
|
||||||
!kinds.contains(kind) else {
|
!kinds.contains(kind) else {
|
||||||
@ -557,8 +547,11 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
/// 获取指定范围内的块级元素类型列表(去重)
|
/// 获取指定范围内的块级元素类型列表(去重)
|
||||||
private func blockKinds(in range: NSRange) -> [RDEPUBTextBlockKind] {
|
private func blockKinds(in range: NSRange) -> [RDEPUBTextBlockKind] {
|
||||||
|
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
var kinds: [RDEPUBTextBlockKind] = []
|
var kinds: [RDEPUBTextBlockKind] = []
|
||||||
attributedString.enumerateAttribute(.rdPageBlockKind, in: range) { value, _, _ in
|
attributedString.enumerateAttribute(.rdPageBlockKind, in: safeRange) { value, _, _ in
|
||||||
guard let rawValue = value as? String,
|
guard let rawValue = value as? String,
|
||||||
let kind = RDEPUBTextBlockKind(rawValue: rawValue),
|
let kind = RDEPUBTextBlockKind(rawValue: rawValue),
|
||||||
!kinds.contains(kind) else {
|
!kinds.contains(kind) else {
|
||||||
@ -571,8 +564,11 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
/// 获取指定范围内的语义提示列表(去重)
|
/// 获取指定范围内的语义提示列表(去重)
|
||||||
private func semanticHints(in range: NSRange) -> [RDEPUBTextSemanticHint] {
|
private func semanticHints(in range: NSRange) -> [RDEPUBTextSemanticHint] {
|
||||||
|
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
var hints: [RDEPUBTextSemanticHint] = []
|
var hints: [RDEPUBTextSemanticHint] = []
|
||||||
attributedString.enumerateAttribute(.rdPageSemanticHints, in: range) { value, _, _ in
|
attributedString.enumerateAttribute(.rdPageSemanticHints, in: safeRange) { value, _, _ in
|
||||||
guard let rawValue = value as? String else { return }
|
guard let rawValue = value as? String else { return }
|
||||||
for hint in rawValue.split(separator: ",").compactMap({ RDEPUBTextSemanticHint(rawValue: String($0)) }) where !hints.contains(hint) {
|
for hint in rawValue.split(separator: ",").compactMap({ RDEPUBTextSemanticHint(rawValue: String($0)) }) where !hints.contains(hint) {
|
||||||
hints.append(hint)
|
hints.append(hint)
|
||||||
@ -583,8 +579,11 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
/// 获取指定范围内的附件布局方式列表(去重)
|
/// 获取指定范围内的附件布局方式列表(去重)
|
||||||
private func attachmentPlacements(in range: NSRange) -> [RDEPUBTextAttachmentPlacement] {
|
private func attachmentPlacements(in range: NSRange) -> [RDEPUBTextAttachmentPlacement] {
|
||||||
|
guard let safeRange = clampedRange(range), safeRange.length > 0 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
var placements: [RDEPUBTextAttachmentPlacement] = []
|
var placements: [RDEPUBTextAttachmentPlacement] = []
|
||||||
attributedString.enumerateAttribute(.rdPageAttachmentPlacement, in: range) { value, _, _ in
|
attributedString.enumerateAttribute(.rdPageAttachmentPlacement, in: safeRange) { value, _, _ in
|
||||||
guard let rawValue = value as? String,
|
guard let rawValue = value as? String,
|
||||||
let placement = RDEPUBTextAttachmentPlacement(rawValue: rawValue),
|
let placement = RDEPUBTextAttachmentPlacement(rawValue: rawValue),
|
||||||
!placements.contains(placement) else {
|
!placements.contains(placement) else {
|
||||||
@ -595,6 +594,22 @@ struct RDEPUBTextLayouter {
|
|||||||
return placements
|
return placements
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 将范围裁剪到 attributedString 的合法边界内,避免属性枚举与子串提取越界。
|
||||||
|
private func clampedRange(_ range: NSRange) -> NSRange? {
|
||||||
|
guard range.location >= 0, range.length >= 0 else { return nil }
|
||||||
|
guard attributedString.length > 0 else {
|
||||||
|
return range.location == 0 ? NSRange(location: 0, length: 0) : nil
|
||||||
|
}
|
||||||
|
guard range.location < attributedString.length else { return nil }
|
||||||
|
|
||||||
|
let maxLength = attributedString.length - range.location
|
||||||
|
return NSRange(location: range.location, length: min(range.length, maxLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clampedProbeRange(for lineRange: NSRange) -> NSRange? {
|
||||||
|
clampedRange(NSRange(location: lineRange.location, length: max(lineRange.length, 1)))
|
||||||
|
}
|
||||||
|
|
||||||
/// 查找指定位置之前最近的 fragment ID(用于阅读位置恢复)
|
/// 查找指定位置之前最近的 fragment ID(用于阅读位置恢复)
|
||||||
private func nearestTrailingFragmentID(
|
private func nearestTrailingFragmentID(
|
||||||
endingAt location: Int,
|
endingAt location: Int,
|
||||||
@ -770,10 +785,15 @@ struct RDEPUBTextLayouter {
|
|||||||
|
|
||||||
/// 检查某行是否落在 avoidPageBreakInside 保护块内
|
/// 检查某行是否落在 avoidPageBreakInside 保护块内
|
||||||
private func lineIsInAvoidPageBreakInsideBlock(_ lineRange: NSRange) -> Bool {
|
private func lineIsInAvoidPageBreakInsideBlock(_ lineRange: NSRange) -> Bool {
|
||||||
|
guard let probeRange = clampedProbeRange(for: lineRange) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
var found = false
|
var found = false
|
||||||
let probeRange = NSRange(location: lineRange.location, length: max(lineRange.length, 1))
|
attributedString.enumerateAttributes(in: probeRange) { attributes, _, stop in
|
||||||
attributedString.enumerateAttribute(.rdPageSemanticHints, in: probeRange) { value, _, stop in
|
guard shouldTreatAvoidHintAsBlockProtection(attributes) else {
|
||||||
guard let rawValue = value as? String else { return }
|
return
|
||||||
|
}
|
||||||
|
guard let rawValue = attributes[.rdPageSemanticHints] as? String else { return }
|
||||||
let hints = rawValue
|
let hints = rawValue
|
||||||
.split(separator: ",")
|
.split(separator: ",")
|
||||||
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
|
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
|
||||||
@ -785,10 +805,37 @@ struct RDEPUBTextLayouter {
|
|||||||
return found
|
return found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 只有真正的块级内容才应触发 avoidPageBreakInside。
|
||||||
|
/// 像脚注 note.png 这类行内图片会被标记为 `img`,但不该把整行推到下一页。
|
||||||
|
private func shouldTreatAvoidHintAsBlockProtection(_ attributes: [NSAttributedString.Key: Any]) -> Bool {
|
||||||
|
guard let rawValue = attributes[.rdPageSemanticHints] as? String else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let hints = rawValue
|
||||||
|
.split(separator: ",")
|
||||||
|
.compactMap { RDEPUBTextSemanticHint(rawValue: String($0)) }
|
||||||
|
guard hints.contains(.avoidPageBreakInside) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
let placement = (attributes[.rdPageAttachmentPlacement] as? String)
|
||||||
|
.flatMap(RDEPUBTextAttachmentPlacement.init(rawValue:))
|
||||||
|
let blockKind = (attributes[.rdPageBlockKind] as? String)
|
||||||
|
.flatMap(RDEPUBTextBlockKind.init(rawValue:))
|
||||||
|
|
||||||
|
if blockKind == .attachment, placement != .centered {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
/// 检查某行是否落在 keepWithNext 保护块内
|
/// 检查某行是否落在 keepWithNext 保护块内
|
||||||
private func lineIsInKeepWithNextBlock(_ lineRange: NSRange) -> Bool {
|
private func lineIsInKeepWithNextBlock(_ lineRange: NSRange) -> Bool {
|
||||||
|
guard let probeRange = clampedProbeRange(for: lineRange) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
var found = false
|
var found = false
|
||||||
let probeRange = NSRange(location: lineRange.location, length: max(lineRange.length, 1))
|
|
||||||
attributedString.enumerateAttribute(.rdPageSemanticHints, in: probeRange) { value, _, stop in
|
attributedString.enumerateAttribute(.rdPageSemanticHints, in: probeRange) { value, _, stop in
|
||||||
guard let rawValue = value as? String else { return }
|
guard let rawValue = value as? String else { return }
|
||||||
let hints = rawValue
|
let hints = rawValue
|
||||||
@ -811,6 +858,47 @@ struct RDEPUBTextLayouter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 逐句对齐 WXRead `WRChapterPageCount.recalculatePageRangesForAttributedString` 的分页循环:
|
||||||
|
/// 1. 取出 CTFrame 当前页的所有行
|
||||||
|
/// 2. 读取每行 origin
|
||||||
|
/// 3. 用 `lineY - ascent > usableHeight` 判断是否越过可用高度
|
||||||
|
/// 4. 累计能容纳行的字符数得到本页范围
|
||||||
|
private func proposedRangeUsingWXReadPageCount(
|
||||||
|
from frame: CTFrame,
|
||||||
|
start location: Int,
|
||||||
|
usableHeight: CGFloat,
|
||||||
|
totalLength: Int
|
||||||
|
) -> NSRange {
|
||||||
|
let lines = CTFrameGetLines(frame) as! [CTLine]
|
||||||
|
guard !lines.isEmpty else {
|
||||||
|
return NSRange(location: location, length: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var origins = [CGPoint](repeating: .zero, count: lines.count)
|
||||||
|
CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)
|
||||||
|
|
||||||
|
var pageCharCount = 0
|
||||||
|
for (index, line) in lines.enumerated() {
|
||||||
|
let lineRange = CTLineGetStringRange(line)
|
||||||
|
let lineY = origins[index].y
|
||||||
|
var ascent: CGFloat = 0
|
||||||
|
var descent: CGFloat = 0
|
||||||
|
CTLineGetTypographicBounds(line, &ascent, &descent, nil)
|
||||||
|
|
||||||
|
if lineY - ascent > usableHeight {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
pageCharCount += lineRange.length
|
||||||
|
}
|
||||||
|
|
||||||
|
if pageCharCount == 0 {
|
||||||
|
pageCharCount = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return NSRange(location: location, length: min(pageCharCount, totalLength - location))
|
||||||
|
}
|
||||||
|
|
||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
/// 获取 DTCoreTextLayoutFrame 中所有行的字符范围
|
/// 获取 DTCoreTextLayoutFrame 中所有行的字符范围
|
||||||
private func lineRanges(from layoutFrame: DTCoreTextLayoutFrame) -> [NSRange] {
|
private func lineRanges(from layoutFrame: DTCoreTextLayoutFrame) -> [NSRange] {
|
||||||
|
|||||||
@ -13,18 +13,12 @@ final class RDEPUBPageInteractionController {
|
|||||||
|
|
||||||
var snapshot: RDEPUBPageLayoutSnapshot?
|
var snapshot: RDEPUBPageLayoutSnapshot?
|
||||||
private var dtLayoutFrame: DTCoreTextLayoutFrame?
|
private var dtLayoutFrame: DTCoreTextLayoutFrame?
|
||||||
private var contentOffsetBase: Int = 0
|
|
||||||
|
|
||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
func configure(layoutFrame: DTCoreTextLayoutFrame?, page: RDEPUBTextPage?, contentOffsetBase: Int = 0) {
|
func configure(layoutFrame: DTCoreTextLayoutFrame?, page: RDEPUBTextPage?) {
|
||||||
dtLayoutFrame = layoutFrame
|
dtLayoutFrame = layoutFrame
|
||||||
self.contentOffsetBase = contentOffsetBase
|
|
||||||
if let layoutFrame, let page {
|
if let layoutFrame, let page {
|
||||||
snapshot = RDEPUBPageLayoutSnapshot.build(
|
snapshot = RDEPUBPageLayoutSnapshot.build(from: layoutFrame, page: page)
|
||||||
from: layoutFrame,
|
|
||||||
page: page,
|
|
||||||
contentOffsetBase: contentOffsetBase
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
snapshot = nil
|
snapshot = nil
|
||||||
}
|
}
|
||||||
@ -53,11 +47,7 @@ final class RDEPUBPageInteractionController {
|
|||||||
)
|
)
|
||||||
let idx = dtLine.stringIndex(forPosition: relativePoint)
|
let idx = dtLine.stringIndex(forPosition: relativePoint)
|
||||||
guard idx != NSNotFound, idx >= 0 else { return nil }
|
guard idx != NSNotFound, idx >= 0 else { return nil }
|
||||||
return normalizedIndex(
|
return normalizedIndex(idx, lineRange: line.stringRange, pageRange: snapshot.pageContentRange)
|
||||||
absoluteIndex(for: idx),
|
|
||||||
lineRange: line.stringRange,
|
|
||||||
pageRange: snapshot.pageContentRange
|
|
||||||
)
|
|
||||||
#else
|
#else
|
||||||
return nil
|
return nil
|
||||||
#endif
|
#endif
|
||||||
@ -85,9 +75,9 @@ final class RDEPUBPageInteractionController {
|
|||||||
|
|
||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
guard let dtLine = dtLineContaining(range: line.stringRange) else { continue }
|
guard let dtLine = dtLineContaining(range: line.stringRange) else { continue }
|
||||||
let startX = dtLine.offset(forStringIndex: localIndex(for: overlap.location))
|
let startX = dtLine.offset(forStringIndex: overlap.location)
|
||||||
let endIdx = overlap.location + overlap.length
|
let endIdx = overlap.location + overlap.length
|
||||||
let endX = dtLine.offset(forStringIndex: localIndex(for: endIdx))
|
let endX = dtLine.offset(forStringIndex: endIdx)
|
||||||
#else
|
#else
|
||||||
let startX: CGFloat = 0
|
let startX: CGFloat = 0
|
||||||
let endX: CGFloat = line.frame.width
|
let endX: CGFloat = line.frame.width
|
||||||
@ -145,7 +135,7 @@ final class RDEPUBPageInteractionController {
|
|||||||
|
|
||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
guard let dtLine = dtLineContaining(range: line.stringRange) else { return nil }
|
guard let dtLine = dtLineContaining(range: line.stringRange) else { return nil }
|
||||||
let offsetX = dtLine.offset(forStringIndex: localIndex(for: index))
|
let offsetX = dtLine.offset(forStringIndex: index)
|
||||||
#else
|
#else
|
||||||
let offsetX: CGFloat = 0
|
let offsetX: CGFloat = 0
|
||||||
#endif
|
#endif
|
||||||
@ -197,18 +187,10 @@ final class RDEPUBPageInteractionController {
|
|||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
private func dtLineContaining(range: NSRange) -> DTCoreTextLayoutLine? {
|
private func dtLineContaining(range: NSRange) -> DTCoreTextLayoutLine? {
|
||||||
guard let dtLayoutFrame else { return nil }
|
guard let dtLayoutFrame else { return nil }
|
||||||
return dtLayoutFrame.lineContaining(UInt(max(localIndex(for: range.location), 0)))
|
return dtLayoutFrame.lineContaining(UInt(range.location))
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private func localIndex(for absoluteIndex: Int) -> Int {
|
|
||||||
absoluteIndex - contentOffsetBase
|
|
||||||
}
|
|
||||||
|
|
||||||
private func absoluteIndex(for localIndex: Int) -> Int {
|
|
||||||
localIndex + contentOffsetBase
|
|
||||||
}
|
|
||||||
|
|
||||||
private func mergeAdjacentRects(_ rects: [CGRect]) -> [CGRect] {
|
private func mergeAdjacentRects(_ rects: [CGRect]) -> [CGRect] {
|
||||||
guard rects.count > 1 else { return rects }
|
guard rects.count > 1 else { return rects }
|
||||||
|
|
||||||
|
|||||||
@ -130,8 +130,7 @@ struct RDEPUBPageLayoutSnapshot {
|
|||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
static func build(
|
static func build(
|
||||||
from layoutFrame: DTCoreTextLayoutFrame,
|
from layoutFrame: DTCoreTextLayoutFrame,
|
||||||
page: RDEPUBTextPage,
|
page: RDEPUBTextPage
|
||||||
contentOffsetBase: Int
|
|
||||||
) -> RDEPUBPageLayoutSnapshot? {
|
) -> RDEPUBPageLayoutSnapshot? {
|
||||||
guard let dtLines = layoutFrame.lines as? [DTCoreTextLayoutLine], !dtLines.isEmpty else {
|
guard let dtLines = layoutFrame.lines as? [DTCoreTextLayoutLine], !dtLines.isEmpty else {
|
||||||
return nil
|
return nil
|
||||||
@ -140,7 +139,7 @@ struct RDEPUBPageLayoutSnapshot {
|
|||||||
var lines: [RDEPUBPageLine] = []
|
var lines: [RDEPUBPageLine] = []
|
||||||
var runs: [RDEPUBPageRun] = []
|
var runs: [RDEPUBPageRun] = []
|
||||||
var attachments: [RDEPUBPageAttachment] = []
|
var attachments: [RDEPUBPageAttachment] = []
|
||||||
let pageOffset = contentOffsetBase
|
let pageOffset = page.pageStartOffset
|
||||||
|
|
||||||
for dtLine in dtLines {
|
for dtLine in dtLines {
|
||||||
let lineRange = offset(dtLine.stringRange(), by: pageOffset)
|
let lineRange = offset(dtLine.stringRange(), by: pageOffset)
|
||||||
|
|||||||
@ -1267,6 +1267,9 @@ public final class RDEPUBReaderController: UIViewController {
|
|||||||
private func currentTextPageSize() -> CGSize {
|
private func currentTextPageSize() -> CGSize {
|
||||||
let pageNum = readerView.currentPage >= 0 ? readerView.currentPage : nil
|
let pageNum = readerView.currentPage >= 0 ? readerView.currentPage : nil
|
||||||
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
|
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
|
||||||
|
if ProcessInfo.processInfo.arguments.contains("--demo-pagination-debug") {
|
||||||
|
print("[PaginationDebug] currentTextPageSize currentPage=\(readerView.currentPage) resolved=\(NSCoder.string(for: resolvedSize)) readerBounds=\(NSCoder.string(for: readerView.bounds)) viewBounds=\(NSCoder.string(for: view.bounds)) safe=\(NSCoder.string(for: view.safeAreaInsets))")
|
||||||
|
}
|
||||||
if resolvedSize.width > 0, resolvedSize.height > 0 {
|
if resolvedSize.width > 0, resolvedSize.height > 0 {
|
||||||
return resolvedSize
|
return resolvedSize
|
||||||
}
|
}
|
||||||
|
|||||||
@ -114,7 +114,6 @@ final class RDEPUBTextContentView: UIView {
|
|||||||
|
|
||||||
private var coreTextDisplayContent: NSAttributedString?
|
private var coreTextDisplayContent: NSAttributedString?
|
||||||
private var coreTextDisplayRange: NSRange?
|
private var coreTextDisplayRange: NSRange?
|
||||||
private var coreTextDisplayOffsetBase: Int = 0
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private let interactionController = RDEPUBPageInteractionController()
|
private let interactionController = RDEPUBPageInteractionController()
|
||||||
@ -266,10 +265,7 @@ final class RDEPUBTextContentView: UIView {
|
|||||||
)
|
)
|
||||||
|
|
||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
let usesChapterContextDisplay = shouldUseChapterContextDisplay(for: page)
|
let displayContent = normalizedPageContent(from: page)
|
||||||
let displayContent = usesChapterContextDisplay
|
|
||||||
? NSMutableAttributedString(attributedString: page.chapterContent)
|
|
||||||
: normalizedPageContent(from: page)
|
|
||||||
let fullRange = NSRange(location: 0, length: displayContent.length)
|
let fullRange = NSRange(location: 0, length: displayContent.length)
|
||||||
displayContent.addAttribute(
|
displayContent.addAttribute(
|
||||||
.foregroundColor,
|
.foregroundColor,
|
||||||
@ -279,8 +275,7 @@ final class RDEPUBTextContentView: UIView {
|
|||||||
coreTextContentView.isHidden = false
|
coreTextContentView.isHidden = false
|
||||||
coreTextContentView.backgroundColor = .clear
|
coreTextContentView.backgroundColor = .clear
|
||||||
coreTextDisplayContent = displayContent
|
coreTextDisplayContent = displayContent
|
||||||
coreTextDisplayRange = usesChapterContextDisplay ? page.contentRange : NSRange(location: 0, length: displayContent.length)
|
coreTextDisplayRange = NSRange(location: 0, length: displayContent.length)
|
||||||
coreTextDisplayOffsetBase = usesChapterContextDisplay ? 0 : page.pageStartOffset
|
|
||||||
textView.isHidden = true
|
textView.isHidden = true
|
||||||
textView.isUserInteractionEnabled = false
|
textView.isUserInteractionEnabled = false
|
||||||
textView.attributedText = nil
|
textView.attributedText = nil
|
||||||
@ -688,10 +683,6 @@ final class RDEPUBTextContentView: UIView {
|
|||||||
return !CharacterSet.newlines.contains(previousScalar)
|
return !CharacterSet.newlines.contains(previousScalar)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func shouldUseChapterContextDisplay(for page: RDEPUBTextPage) -> Bool {
|
|
||||||
page.metadata.attachmentRanges.isEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
#if canImport(DTCoreText)
|
#if canImport(DTCoreText)
|
||||||
private func updateCoreTextLayoutFrameIfNeeded() {
|
private func updateCoreTextLayoutFrameIfNeeded() {
|
||||||
guard !coreTextContentView.isHidden,
|
guard !coreTextContentView.isHidden,
|
||||||
@ -712,11 +703,7 @@ final class RDEPUBTextContentView: UIView {
|
|||||||
layouter.shouldCacheLayoutFrames = false
|
layouter.shouldCacheLayoutFrames = false
|
||||||
let layoutFrame = layouter.layoutFrame(with: coreTextContentView.bounds, range: displayRange)
|
let layoutFrame = layouter.layoutFrame(with: coreTextContentView.bounds, range: displayRange)
|
||||||
coreTextContentView.layoutFrame = layoutFrame
|
coreTextContentView.layoutFrame = layoutFrame
|
||||||
interactionController.configure(
|
interactionController.configure(layoutFrame: layoutFrame, page: page)
|
||||||
layoutFrame: layoutFrame,
|
|
||||||
page: page,
|
|
||||||
contentOffsetBase: coreTextDisplayOffsetBase
|
|
||||||
)
|
|
||||||
overlayView.updateSnapshot(interactionController.snapshot)
|
overlayView.updateSnapshot(interactionController.snapshot)
|
||||||
backgroundOverlayView.updateSnapshot(interactionController.snapshot)
|
backgroundOverlayView.updateSnapshot(interactionController.snapshot)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user