Compare commits

..

No commits in common. "ea21c6a831c31b6f47761e3a4bc5f821c24a8005" and "0e7d952fe5057c4e64e766604b6b3b404146e7ef" have entirely different histories.

14 changed files with 163 additions and 583 deletions

View File

@ -21,7 +21,7 @@
| 文档 | 说明 | | 文档 | 说明 |
|------|------| |------|------|
| [ReflowableEPUB_WXReadRenderer_Design.md](FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md) | 基于读书的 CoreText 渲染架构,设计 Reflowable EPUB 的增强文本渲染方案CSS 分层、类型器升级) | | [ReflowableEPUB_WXReadRenderer_Design.md](FeatureSolution/ReflowableEPUB_WXReadRenderer_Design.md) | 基于读书反编译的 CoreText 渲染架构,设计 Reflowable EPUB 的增强文本渲染方案CSS 分层、类型器升级) |
## 目录约定 ## 目录约定

View File

@ -185,14 +185,10 @@
inputFileListPaths = ( inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist",
); );
inputPaths = (
);
name = "[CP] Embed Pods Frameworks"; name = "[CP] Embed Pods Frameworks";
outputFileListPaths = ( outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist", "${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist",
); );
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n"; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n";

View File

@ -104,8 +104,7 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
guard let element else { return } guard let element else { return }
RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering( RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering(
element, element,
style: request.style, style: request.style
maxImageSize: resolvedMaxImageSize(for: request)
) )
} }
return builder?.generatedAttributedString() return builder?.generatedAttributedString()
@ -117,7 +116,7 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
/// style.lineSpacing /// style.lineSpacing
private func dtOptions(request: RDEPUBTextChapterRenderRequest) -> [AnyHashable: Any] { private func dtOptions(request: RDEPUBTextChapterRenderRequest) -> [AnyHashable: Any] {
let style = request.style let style = request.style
let maxImageSize = resolvedMaxImageSize(for: request) let screenBounds = UIScreen.main.bounds.insetBy(dx: 20, dy: 28)
var options: [AnyHashable: Any] = [ var options: [AnyHashable: Any] = [
NSTextSizeMultiplierDocumentOption: 1.0, NSTextSizeMultiplierDocumentOption: 1.0,
DTDefaultFontFamily: style.font.familyName, DTDefaultFontFamily: style.font.familyName,
@ -125,7 +124,7 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
DTDefaultFontSize: style.font.pointSize, DTDefaultFontSize: style.font.pointSize,
DTDefaultLineHeightMultiplier: max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1), DTDefaultLineHeightMultiplier: max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1),
DTUseiOS6Attributes: true, DTUseiOS6Attributes: true,
DTMaxImageSize: NSValue(cgSize: maxImageSize) DTMaxImageSize: NSValue(cgSize: screenBounds.size)
] ]
if let baseURL = request.context.baseURL { if let baseURL = request.context.baseURL {
@ -137,18 +136,5 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
return options return options
} }
private func resolvedMaxImageSize(for request: RDEPUBTextChapterRenderRequest) -> CGSize {
if let pageSize = request.pageSize {
let layoutConfig = request.layoutConfig ?? .default
let contentRect = layoutConfig.contentRect(fallback: pageSize)
let maxWidth = max(round(contentRect.width), 1)
let maxHeight = max(round(contentRect.height * layoutConfig.imageMaxHeightRatio), 1)
return CGSize(width: maxWidth, height: maxHeight)
}
let screenBounds = UIScreen.main.bounds.insetBy(dx: 20, dy: 28)
return CGSize(width: max(round(screenBounds.width), 1), height: max(round(screenBounds.height * 0.85), 1))
}
#endif #endif
} }

View File

@ -223,10 +223,6 @@ 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 }
@ -267,9 +263,6 @@ 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 = []
@ -300,9 +293,7 @@ public final class RDEPUBTextBookBuilder {
baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(), baseURL: parser.fileURL(forRelativePath: item.href)?.deletingLastPathComponent(),
style: style, style: style,
resourceResolver: publication.resourceResolver, resourceResolver: publication.resourceResolver,
contentLanguageCode: publication.metadata.language, contentLanguageCode: publication.metadata.language
pageSize: pageSize,
layoutConfig: layoutConfig
) )
// HTML NSAttributedString // HTML NSAttributedString
@ -444,18 +435,6 @@ 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,
@ -594,26 +573,11 @@ 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,
@ -623,28 +587,9 @@ 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,
shouldDropWhitespaceOnlyFrame(lastFrame, in: content) { shouldDropWhitespaceOnlyTrailingFrame(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() {
@ -670,7 +615,7 @@ public final class RDEPUBTextBookBuilder {
} }
/// ///
private func shouldDropWhitespaceOnlyFrame( private func shouldDropWhitespaceOnlyTrailingFrame(
_ frame: RDEPUBTextLayoutFrame, _ frame: RDEPUBTextLayoutFrame,
in content: NSAttributedString in content: NSAttributedString
) -> Bool { ) -> Bool {
@ -790,7 +735,7 @@ public final class RDEPUBTextBookBuilder {
bookID: bookID, bookID: bookID,
fontSize: style.font.pointSize, fontSize: style.font.pointSize,
lineHeightMultiple: style.lineSpacing, lineHeightMultiple: style.lineSpacing,
contentInsets: layoutConfig.edgeInsets, contentInsets: .zero,
pageSize: pageSize, pageSize: pageSize,
layoutConfigSignature: layoutConfig.cacheSignature layoutConfigSignature: layoutConfig.cacheSignature
) )

View File

@ -123,8 +123,7 @@ final class ChapterPaginationArchive: NSObject, NSSecureCoding {
public final class RDEPUBTextBookCache { public final class RDEPUBTextBookCache {
/// ///
// public var schemaVersion: Int = 1
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)

View File

@ -67,42 +67,23 @@ 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 framePath = CGMutablePath() let frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(location, 0), path, nil)
// WXRead WRChapterPageCount let visibleRange = CTFrameGetVisibleStringRange(frame)
// CoreText 使 bottom inset y UIKit top inset guard visibleRange.length > 0 else {
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: avoidAdjusted, from: lineAdjusted,
totalLength: attributedString.length, totalLength: attributedString.length,
lineRanges: lineRanges lineRanges: lineRanges
) )
@ -127,7 +108,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(proposedRange.length, 1) location += max(visibleRange.length, 1)
continue continue
} }
location = nextLocation location = nextLocation
@ -174,36 +155,27 @@ struct RDEPUBTextLayouter {
totalLength: attributedString.length, totalLength: attributedString.length,
lineRanges: lineRanges lineRanges: lineRanges
) )
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: adjusted.range.location + adjusted.range.length,
fragmentOffsets: fragmentOffsets fragmentOffsets: fragmentOffsets
) )
let diagnostics = verifiedRange == adjusted.range
? adjusted.diagnostics
: adjusted.diagnostics + ["verified-display-range \(NSStringFromRange(adjusted.range)) -> \(NSStringFromRange(verifiedRange))"]
frames.append( frames.append(
RDEPUBTextLayoutFrame( RDEPUBTextLayoutFrame(
contentRange: verifiedRange, contentRange: adjusted.range,
breakReason: adjusted.breakReason, breakReason: adjusted.breakReason,
blockRange: blockRange(at: max(verifiedRange.location, verifiedRange.location + verifiedRange.length - 1)), blockRange: adjusted.blockRange,
attachmentRanges: attachmentRanges(in: verifiedRange), attachmentRanges: adjusted.attachmentRanges,
attachmentKinds: attachmentKinds(in: verifiedRange), attachmentKinds: adjusted.attachmentKinds,
blockKinds: blockKinds(in: verifiedRange), blockKinds: adjusted.blockKinds,
semanticHints: semanticHints(in: verifiedRange), semanticHints: adjusted.semanticHints,
attachmentPlacements: attachmentPlacements(in: verifiedRange), attachmentPlacements: adjusted.attachmentPlacements,
trailingFragmentID: trailingFragmentID, trailingFragmentID: trailingFragmentID,
diagnostics: diagnostics diagnostics: adjusted.diagnostics
) )
) )
let nextLocation = verifiedRange.location + verifiedRange.length let nextLocation = adjusted.range.location + adjusted.range.length
guard nextLocation > location else { guard nextLocation > location else {
location += max(visibleRange.length, 1) location += max(visibleRange.length, 1)
continue continue
@ -213,31 +185,6 @@ struct RDEPUBTextLayouter {
return frames return frames
} }
private func verifiedDisplayRange(for range: NSRange) -> NSRange {
guard let clampedRange = clampedRange(range),
clampedRange.length > 0,
!attachmentRanges(in: clampedRange).isEmpty else {
return range
}
let pageContent = attributedString.attributedSubstring(from: clampedRange)
guard let layouter = DTCoreTextLayouter(attributedString: pageContent) else {
return clampedRange
}
layouter.shouldCacheLayoutFrames = false
guard let layoutFrame = layouter.layoutFrame(with: dtLayoutRect, range: NSRange(location: 0, length: 0)) else {
return clampedRange
}
let visibleRange = layoutFrame.visibleStringRange()
guard visibleRange.length > 0, visibleRange.length < clampedRange.length else {
return clampedRange
}
return NSRange(location: clampedRange.location, length: visibleRange.length)
}
#endif #endif
private static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath { private static func makeLayoutPath(pageSize: CGSize, config: RDEPUBTextLayoutConfig) -> CGPath {
@ -306,6 +253,10 @@ 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)
@ -313,12 +264,38 @@ struct RDEPUBTextLayouter {
let currentSemanticHints = proposedSemanticHints let currentSemanticHints = proposedSemanticHints
let currentAttachmentPlacements = proposedAttachmentPlacements let currentAttachmentPlacements = proposedAttachmentPlacements
// WXRead CTFrame pageRelate // 1. pageBreakBefore/After
// keepWithNext/attachmentBoundary/ if let semanticBoundary = preferredSemanticBoundary(
// 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: proposedRange.location + 1, minimumEnd: minimumEnd,
lineRanges: lineRanges lineRanges: lineRanges
) { ) {
let adjustedRange = NSRange(location: proposedRange.location, length: pageRelateBoundary - proposedRange.location) let adjustedRange = NSRange(location: proposedRange.location, length: pageRelateBoundary - proposedRange.location)
@ -344,6 +321,33 @@ 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,
@ -375,11 +379,8 @@ 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: safeRange) { value, attributeRange, stop in attributedString.enumerateAttribute(.rdPageSemanticHints, in: range) { 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: ",")
@ -387,7 +388,7 @@ struct RDEPUBTextLayouter {
guard !hints.isEmpty else { return } guard !hints.isEmpty else { return }
if hints.contains(.pageBreakBefore), if hints.contains(.pageBreakBefore),
attributeRange.location > safeRange.location, attributeRange.location > range.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
@ -397,7 +398,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 < safeRange.location + safeRange.length { attributeEnd < range.location + range.length {
boundary = (attributeEnd, RDEPUBTextSemanticHint.pageBreakAfter.rawValue) boundary = (attributeEnd, RDEPUBTextSemanticHint.pageBreakAfter.rawValue)
stop.pointee = true stop.pointee = true
return return
@ -409,11 +410,8 @@ 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: safeRange) { value, attributeRange, stop in attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { value, attributeRange, stop in
guard value != nil else { return } guard value != nil else { return }
let location = attributeRange.location let location = attributeRange.location
@ -422,21 +420,11 @@ 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 > safeRange.location, boundaryRange.location >= minimumEnd { if boundaryRange.location > range.location, boundaryRange.location >= minimumEnd {
boundary = boundaryRange.location boundary = boundaryRange.location
stop.pointee = true stop.pointee = true
} }
@ -507,11 +495,8 @@ 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: safeRange) { value, attributeRange, _ in attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { value, attributeRange, _ in
guard value != nil else { return } guard value != nil else { return }
results.append(attributeRange) results.append(attributeRange)
} }
@ -530,11 +515,8 @@ 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: safeRange) { value, _, _ in attributedString.enumerateAttribute(.rdPageAttachmentKind, in: range) { 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 {
@ -547,11 +529,8 @@ 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: safeRange) { value, _, _ in attributedString.enumerateAttribute(.rdPageBlockKind, in: range) { 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 {
@ -564,11 +543,8 @@ 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: safeRange) { value, _, _ in attributedString.enumerateAttribute(.rdPageSemanticHints, in: range) { 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)
@ -579,11 +555,8 @@ 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: safeRange) { value, _, _ in attributedString.enumerateAttribute(.rdPageAttachmentPlacement, in: range) { 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 {
@ -594,22 +567,6 @@ 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,
@ -785,15 +742,10 @@ 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
attributedString.enumerateAttributes(in: probeRange) { attributes, _, stop in let probeRange = NSRange(location: lineRange.location, length: max(lineRange.length, 1))
guard shouldTreatAvoidHintAsBlockProtection(attributes) else { attributedString.enumerateAttribute(.rdPageSemanticHints, in: probeRange) { value, _, stop in
return guard let rawValue = value as? String else { 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)) }
@ -805,37 +757,10 @@ 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
@ -858,47 +783,6 @@ 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] {

View File

@ -286,21 +286,10 @@ public struct RDEPUBTextChapterContext: Equatable {
public struct RDEPUBTextChapterRenderRequest { public struct RDEPUBTextChapterRenderRequest {
public var context: RDEPUBTextChapterContext public var context: RDEPUBTextChapterContext
public var style: RDEPUBTextRenderStyle public var style: RDEPUBTextRenderStyle
/// page size
public var pageSize: CGSize?
/// WXRead
public var layoutConfig: RDEPUBTextLayoutConfig?
public init( public init(context: RDEPUBTextChapterContext, style: RDEPUBTextRenderStyle) {
context: RDEPUBTextChapterContext,
style: RDEPUBTextRenderStyle,
pageSize: CGSize? = nil,
layoutConfig: RDEPUBTextLayoutConfig? = nil
) {
self.context = context self.context = context
self.style = style self.style = style
self.pageSize = pageSize
self.layoutConfig = layoutConfig
} }
} }

View File

@ -197,9 +197,7 @@ enum RDEPUBTextRendererSupport {
baseURL: URL?, baseURL: URL?,
style: RDEPUBTextRenderStyle, style: RDEPUBTextRenderStyle,
resourceResolver: RDEPUBResourceResolver?, resourceResolver: RDEPUBResourceResolver?,
contentLanguageCode: String? = nil, contentLanguageCode: String? = nil
pageSize: CGSize? = nil,
layoutConfig: RDEPUBTextLayoutConfig? = nil
) -> RDEPUBTextChapterRenderRequest { ) -> RDEPUBTextChapterRenderRequest {
let normalizedHTML = injectPaginationSemanticMarkers(into: normalizeHTML(rawHTML)) let normalizedHTML = injectPaginationSemanticMarkers(into: normalizeHTML(rawHTML))
let stylesheetHrefReplacements = inlineLinkedStyleSheets( let stylesheetHrefReplacements = inlineLinkedStyleSheets(
@ -257,12 +255,7 @@ enum RDEPUBTextRendererSupport {
stylesheet: RDEPUBTextStyleSheetPackage(layers: layers), stylesheet: RDEPUBTextStyleSheetPackage(layers: layers),
resourceDiagnostics: resourceDiagnostics resourceDiagnostics: resourceDiagnostics
) )
return RDEPUBTextChapterRenderRequest( return RDEPUBTextChapterRenderRequest(context: context, style: style)
context: context,
style: style,
pageSize: pageSize,
layoutConfig: layoutConfig
)
} }
/// HTML CR HTML /// HTML CR HTML
@ -292,20 +285,15 @@ enum RDEPUBTextRendererSupport {
#if canImport(DTCoreText) #if canImport(DTCoreText)
static func prepareHTMLElementForReaderRendering( static func prepareHTMLElementForReaderRendering(
_ element: DTHTMLElement, _ element: DTHTMLElement,
style: RDEPUBTextRenderStyle, style: RDEPUBTextRenderStyle
maxImageSize: CGSize? = nil
) { ) {
guard let attachment = element.textAttachment else { return } guard let attachment = element.textAttachment else { return }
let pointSize = max(element.fontDescriptor.pointSize, style.font.pointSize) let pointSize = max(element.fontDescriptor.pointSize, style.font.pointSize)
let fallbackSize = CGSize( let maxSize = CGSize(
width: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).width), width: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).width),
height: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).height * 0.85) height: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).height * 0.85)
) )
normalizeAttachmentLayoutForWXRead( normalizeAttachmentLayoutForWXRead(attachment, fontPointSize: pointSize, maxImageSize: maxSize)
attachment,
fontPointSize: pointSize,
maxImageSize: maxImageSize ?? fallbackSize
)
if isFootnoteAttachment(attachment) { if isFootnoteAttachment(attachment) {
element.displayStyle = .inline element.displayStyle = .inline
} else if isCoverAttachment(attachment) { } else if isCoverAttachment(attachment) {
@ -402,38 +390,6 @@ enum RDEPUBTextRendererSupport {
private static func normalizeAttachmentHTMLMarkers(in html: String) -> String { private static func normalizeAttachmentHTMLMarkers(in html: String) -> String {
var normalized = html var normalized = html
if let bodyPicContainerRegex = try? NSRegularExpression(
pattern: #"<div\b([^>]*class\s*=\s*["'][^"']*\b(?:qrbodyPic|bodyPic)\b[^"']*["'][^>]*)>([\s\S]*?)</div>"#,
options: [.caseInsensitive]
) {
normalized = replaceMatches(
using: bodyPicContainerRegex,
in: normalized
) { tag in
guard let imageTagRegex = try? NSRegularExpression(pattern: #"<img\b[^>]*>"#, options: [.caseInsensitive]) else {
return tag
}
return replaceMatches(
using: imageTagRegex,
in: tag
) { imageTag in
mergeHTMLAttributes(
into: imageTag,
requiredClass: "bodyPic",
styleFragments: [
"wr-vertical-center-style:2",
"max-width:100%",
"height:auto",
"display:block",
"margin-left:auto",
"margin-right:auto"
]
)
}
}
}
if let footnoteRegex = try? NSRegularExpression( if let footnoteRegex = try? NSRegularExpression(
pattern: #"<img\b([^>]*class\s*=\s*["'][^"']*\bqqreader-footnote\b[^"']*["'][^>]*)>"#, pattern: #"<img\b([^>]*class\s*=\s*["'][^"']*\bqqreader-footnote\b[^"']*["'][^>]*)>"#,
options: [.caseInsensitive] options: [.caseInsensitive]

View File

@ -139,10 +139,9 @@ struct RDEPUBPageLayoutSnapshot {
var lines: [RDEPUBPageLine] = [] var lines: [RDEPUBPageLine] = []
var runs: [RDEPUBPageRun] = [] var runs: [RDEPUBPageRun] = []
var attachments: [RDEPUBPageAttachment] = [] var attachments: [RDEPUBPageAttachment] = []
let pageOffset = page.pageStartOffset
for dtLine in dtLines { for dtLine in dtLines {
let lineRange = offset(dtLine.stringRange(), by: pageOffset) let lineRange = dtLine.stringRange()
let line = RDEPUBPageLine( let line = RDEPUBPageLine(
stringRange: lineRange, stringRange: lineRange,
frame: dtLine.frame, frame: dtLine.frame,
@ -155,7 +154,7 @@ struct RDEPUBPageLayoutSnapshot {
if let glyphRuns = dtLine.glyphRuns as? [DTCoreTextGlyphRun] { if let glyphRuns = dtLine.glyphRuns as? [DTCoreTextGlyphRun] {
for run in glyphRuns { for run in glyphRuns {
let runRange = offset(run.stringRange(), by: pageOffset) let runRange = run.stringRange()
let isAttachment = run.attachment != nil let isAttachment = run.attachment != nil
runs.append( runs.append(
RDEPUBPageRun( RDEPUBPageRun(
@ -183,7 +182,7 @@ struct RDEPUBPageLayoutSnapshot {
} }
} }
let visibleRange = offset(layoutFrame.visibleStringRange(), by: pageOffset) let visibleRange = layoutFrame.visibleStringRange()
return RDEPUBPageLayoutSnapshot( return RDEPUBPageLayoutSnapshot(
page: page, page: page,
@ -211,12 +210,5 @@ struct RDEPUBPageLayoutSnapshot {
: nil : nil
return (placement, kind) return (placement, kind)
} }
private static func offset(_ range: NSRange, by offset: Int) -> NSRange {
guard range.location != NSNotFound else {
return range
}
return NSRange(location: range.location + offset, length: range.length)
}
#endif #endif
} }

View File

@ -165,8 +165,6 @@ public final class RDEPUBReaderController: UIViewController {
private var currentBrightness: CGFloat private var currentBrightness: CGFloat
private var didStartInitialLoad = false private var didStartInitialLoad = false
private var isRepaginating = false private var isRepaginating = false
private var lastTextPaginationPageSize: CGSize?
private var isReconcilingTextPaginationSize = false
private var paginationToken = UUID() private var paginationToken = UUID()
private var paginator: RDEPUBPaginator? private var paginator: RDEPUBPaginator?
private var searchState: RDEPUBSearchState? private var searchState: RDEPUBSearchState?
@ -697,7 +695,6 @@ public final class RDEPUBReaderController: UIViewController {
if publication.readingProfile == .textReflowable { if publication.readingProfile == .textReflowable {
let renderer = resolvedTextRenderer() let renderer = resolvedTextRenderer()
let pageSize = currentTextPageSize() let pageSize = currentTextPageSize()
lastTextPaginationPageSize = pageSize
let layoutConfig = currentTextLayoutConfig(pageSize: pageSize) let layoutConfig = currentTextLayoutConfig(pageSize: pageSize)
let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache, layoutConfig: layoutConfig) let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache, layoutConfig: layoutConfig)
let renderStyle = currentTextRenderStyle() let renderStyle = currentTextRenderStyle()
@ -1162,10 +1159,6 @@ public final class RDEPUBReaderController: UIViewController {
navigationController.popViewController(animated: true) navigationController.popViewController(animated: true)
return return
} }
if let navigationController, navigationController.presentingViewController != nil {
navigationController.dismiss(animated: true)
return
}
dismiss(animated: true) dismiss(animated: true)
} }
@ -1265,15 +1258,12 @@ public final class RDEPUBReaderController: UIViewController {
} }
private func currentTextPageSize() -> CGSize { private func currentTextPageSize() -> CGSize {
let pageNum = readerView.currentPage >= 0 ? readerView.currentPage : nil let viewportSize = currentLayoutContext().viewportSize
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum) let insets = configuration.reflowableContentInsets
if ProcessInfo.processInfo.arguments.contains("--demo-pagination-debug") { return CGSize(
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))") width: max(viewportSize.width - insets.left - insets.right, 1),
} height: max(viewportSize.height - insets.top - insets.bottom, 1)
if resolvedSize.width > 0, resolvedSize.height > 0 { )
return resolvedSize
}
return currentLayoutContext().viewportSize
} }
private func currentTextRenderStyle() -> RDEPUBTextRenderStyle { private func currentTextRenderStyle() -> RDEPUBTextRenderStyle {
@ -1288,10 +1278,15 @@ public final class RDEPUBReaderController: UIViewController {
} }
private func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig { private func currentTextLayoutConfig(pageSize: CGSize) -> RDEPUBTextLayoutConfig {
let viewportSize = currentLayoutContext().viewportSize
let derivedFrameSize = CGSize(
width: max(viewportSize.width, pageSize.width),
height: max(viewportSize.height, pageSize.height)
)
return RDEPUBTextLayoutConfig( return RDEPUBTextLayoutConfig(
frameWidth: max(pageSize.width, 1), frameWidth: derivedFrameSize.width,
frameHeight: max(pageSize.height, 1), frameHeight: derivedFrameSize.height,
edgeInsets: configuration.reflowableContentInsets, edgeInsets: .zero,
numberOfColumns: configuration.numberOfColumns, numberOfColumns: configuration.numberOfColumns,
columnGap: configuration.columnGap, columnGap: configuration.columnGap,
avoidOrphans: true, avoidOrphans: true,
@ -1839,7 +1834,6 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
public func pageNum(readerView: RDReaderView, pageNum: Int) { public func pageNum(readerView: RDReaderView, pageNum: Int) {
updateCurrentSelection(nil) updateCurrentSelection(nil)
reconcileTextPaginationSizeIfNeeded(for: pageNum)
let totalPages = pageCountOfReaderView(readerView: readerView) let totalPages = pageCountOfReaderView(readerView: readerView)
if totalPages > 0, pageNum == totalPages - 1 { if totalPages > 0, pageNum == totalPages - 1 {
@ -1865,36 +1859,6 @@ extension RDEPUBReaderController: RDReaderDataSource, RDReaderDelegate {
_ = isLandscape _ = isLandscape
pendingPresentationRestoreLocation = currentVisibleLocation() ?? persistenceLocation() pendingPresentationRestoreLocation = currentVisibleLocation() ?? persistenceLocation()
} }
private func reconcileTextPaginationSizeIfNeeded(for pageNum: Int) {
guard textBook != nil,
!isRepaginating,
!isReconcilingTextPaginationSize,
pageNum >= 0,
let lastTextPaginationPageSize else {
return
}
let resolvedSize = readerView.resolvedSinglePageSize(pageNum: pageNum)
guard resolvedSize.width > 0,
resolvedSize.height > 0 else {
return
}
let sizeChanged = abs(resolvedSize.width - lastTextPaginationPageSize.width) > 0.5
|| abs(resolvedSize.height - lastTextPaginationPageSize.height) > 0.5
guard sizeChanged else {
return
}
isReconcilingTextPaginationSize = true
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.isReconcilingTextPaginationSize = false
guard self.textBook != nil, !self.isRepaginating else { return }
self.repaginatePreservingCurrentLocation()
}
}
} }
// MARK: - Web EPUB /Web // MARK: - Web EPUB /Web

View File

@ -256,7 +256,7 @@ final class RDEPUBTextContentView: UIView {
coverImageView.isHidden = true coverImageView.isHidden = true
coverImageView.image = nil coverImageView.image = nil
let selectionContent = normalizedPageContent(from: page) let selectionContent = NSMutableAttributedString(attributedString: page.content)
let selectionRange = NSRange(location: 0, length: selectionContent.length) let selectionRange = NSRange(location: 0, length: selectionContent.length)
selectionContent.addAttribute( selectionContent.addAttribute(
.foregroundColor, .foregroundColor,
@ -265,7 +265,7 @@ final class RDEPUBTextContentView: UIView {
) )
#if canImport(DTCoreText) #if canImport(DTCoreText)
let displayContent = normalizedPageContent(from: page) let displayContent = NSMutableAttributedString(attributedString: page.chapterContent)
let fullRange = NSRange(location: 0, length: displayContent.length) let fullRange = NSRange(location: 0, length: displayContent.length)
displayContent.addAttribute( displayContent.addAttribute(
.foregroundColor, .foregroundColor,
@ -275,7 +275,7 @@ final class RDEPUBTextContentView: UIView {
coreTextContentView.isHidden = false coreTextContentView.isHidden = false
coreTextContentView.backgroundColor = .clear coreTextContentView.backgroundColor = .clear
coreTextDisplayContent = displayContent coreTextDisplayContent = displayContent
coreTextDisplayRange = NSRange(location: 0, length: displayContent.length) coreTextDisplayRange = page.contentRange
textView.isHidden = true textView.isHidden = true
textView.isUserInteractionEnabled = false textView.isUserInteractionEnabled = false
textView.attributedText = nil textView.attributedText = nil
@ -646,43 +646,6 @@ final class RDEPUBTextContentView: UIView {
return proxy return proxy
} }
private func normalizedPageContent(from page: RDEPUBTextPage) -> NSMutableAttributedString {
let content = NSMutableAttributedString(attributedString: page.content)
guard shouldNormalizeContinuationParagraph(for: page) else {
return content
}
let text = content.string as NSString
let firstParagraphRange = text.paragraphRange(for: NSRange(location: 0, length: 0))
guard firstParagraphRange.length > 0 else {
return content
}
content.enumerateAttribute(.paragraphStyle, in: firstParagraphRange) { value, range, _ in
guard let style = value as? NSParagraphStyle else { return }
let mutableStyle = (style.mutableCopy() as? NSMutableParagraphStyle) ?? NSMutableParagraphStyle()
mutableStyle.firstLineHeadIndent = mutableStyle.headIndent
mutableStyle.paragraphSpacingBefore = 0
content.addAttribute(.paragraphStyle, value: mutableStyle.copy() as Any, range: range)
}
return content
}
private func shouldNormalizeContinuationParagraph(for page: RDEPUBTextPage) -> Bool {
let pageStart = page.pageStartOffset
guard pageStart > 0, pageStart < page.chapterContent.length else {
return false
}
let chapterText = page.chapterContent.string as NSString
guard let previousScalar = UnicodeScalar(chapterText.character(at: pageStart - 1)) else {
return false
}
return !CharacterSet.newlines.contains(previousScalar)
}
#if canImport(DTCoreText) #if canImport(DTCoreText)
private func updateCoreTextLayoutFrameIfNeeded() { private func updateCoreTextLayoutFrameIfNeeded() {
guard !coreTextContentView.isHidden, guard !coreTextContentView.isHidden,

View File

@ -123,7 +123,7 @@ public final class RDURLReaderController: UIViewController {
layoutConfig: RDEPUBTextLayoutConfig( layoutConfig: RDEPUBTextLayoutConfig(
frameWidth: pageSize.width, frameWidth: pageSize.width,
frameHeight: pageSize.height, frameHeight: pageSize.height,
edgeInsets: epubConfiguration.reflowableContentInsets, edgeInsets: .zero,
numberOfColumns: 1, numberOfColumns: 1,
columnGap: 20, columnGap: 20,
avoidOrphans: true, avoidOrphans: true,
@ -179,11 +179,15 @@ public final class RDURLReaderController: UIViewController {
print("[ReadViewDemo] automation \(prefix) -> page \(page) href \(href) progression \(progression)") print("[ReadViewDemo] automation \(prefix) -> page \(page) href \(href) progression \(progression)")
} }
/// ///
/// 使 viewport layoutConfig.edgeInsets ///
///
private func currentTextPageSize() -> CGSize { private func currentTextPageSize() -> CGSize {
UIScreen.main.bounds.size let viewportSize = UIScreen.main.bounds.size
let insets = epubConfiguration.reflowableContentInsets
return CGSize(
width: max(viewportSize.width - insets.left - insets.right, 1),
height: max(viewportSize.height - insets.top - insets.bottom, 1)
)
} }
/// ///

View File

@ -309,10 +309,6 @@ public class RDReaderView: UIView {
private var topToolView: UIView? private var topToolView: UIView?
/// ///
private var bottomToolView: UIView? private var bottomToolView: UIView?
///
private var topToolViewHeightConstraint: NSLayoutConstraint?
///
private var bottomToolViewHeightConstraint: NSLayoutConstraint?
/// ///
private var isShowToolView: Bool = false private var isShowToolView: Bool = false
/// ///
@ -368,7 +364,6 @@ public class RDReaderView: UIView {
super.layoutSubviews() super.layoutSubviews()
guard bounds.width > 0, bounds.height > 0 else { return } guard bounds.width > 0, bounds.height > 0 else { return }
preloadHostView.frame = bounds preloadHostView.frame = bounds
updateToolViewHeightConstraintsIfNeeded()
let nowLandscape = isLandscape let nowLandscape = isLandscape
if let prev = previousIsLandscape, prev != nowLandscape { if let prev = previousIsLandscape, prev != nowLandscape {
previousIsLandscape = nowLandscape previousIsLandscape = nowLandscape
@ -629,29 +624,17 @@ public class RDReaderView: UIView {
/// ///
/// ///
private func pageViewForDisplay(pageNum: Int) -> UIView { private func pageViewForDisplay(pageNum: Int) -> UIView {
let reusableView = detachedReusablePageView(for: pageNum) if let cached = pageCurlCachedViews[pageNum] {
let view = dataSource?.pageContentView(readerView: self, pageNum: pageNum, containerView: reusableView) return cached
?? reusableView }
?? UIView()
pageCurlCachedViews[pageNum] = view
return view
}
/// page view
/// view pageCurl UIView
/// child controller
private func detachedReusablePageView(for pageNum: Int) -> UIView? {
if let preloaded = preloadedPageViews.removeValue(forKey: pageNum) { if let preloaded = preloadedPageViews.removeValue(forKey: pageNum) {
preloaded.removeFromSuperview() preloaded.removeFromSuperview()
pageCurlCachedViews[pageNum] = preloaded
return preloaded return preloaded
} }
let view = dataSource?.pageContentView(readerView: self, pageNum: pageNum, containerView: nil) ?? UIView()
guard let cached = pageCurlCachedViews[pageNum], cached.superview == nil else { pageCurlCachedViews[pageNum] = view
return nil return view
}
pageCurlCachedViews.removeValue(forKey: pageNum)
return cached
} }
/// ///
@ -722,7 +705,7 @@ public class RDReaderView: UIView {
preloadHostView.frame = bounds preloadHostView.frame = bounds
for targetPage in targets { for targetPage in targets {
let existing = detachedReusablePageView(for: targetPage) let existing = preloadedPageViews[targetPage] ?? pageCurlCachedViews.removeValue(forKey: targetPage)
let contentView = dataSource?.pageContentView(readerView: self, pageNum: targetPage, containerView: existing) ?? existing ?? UIView() let contentView = dataSource?.pageContentView(readerView: self, pageNum: targetPage, containerView: existing) ?? existing ?? UIView()
preloadedPageViews[targetPage] = contentView preloadedPageViews[targetPage] = contentView
if contentView.superview !== preloadHostView { if contentView.superview !== preloadHostView {
@ -854,7 +837,13 @@ public class RDReaderView: UIView {
isShowToolView = !isShowToolView isShowToolView = !isShowToolView
if isShowToolView { if isShowToolView {
if let topToolView = topToolView { if let topToolView = topToolView {
installToolViewIfNeeded(topToolView, position: .top) addSubview(topToolView)
topToolView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
topToolView.leadingAnchor.constraint(equalTo: leadingAnchor),
topToolView.trailingAnchor.constraint(equalTo: trailingAnchor),
topToolView.topAnchor.constraint(equalTo: topAnchor)
])
layoutIfNeeded() layoutIfNeeded()
topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height) topToolView.transform = CGAffineTransform(translationX: 0, y: -topToolView.bounds.height)
UIView.animate(withDuration: toolViewAnimationDuration) { UIView.animate(withDuration: toolViewAnimationDuration) {
@ -863,7 +852,13 @@ public class RDReaderView: UIView {
} }
if let bottomToolView = bottomToolView { if let bottomToolView = bottomToolView {
installToolViewIfNeeded(bottomToolView, position: .bottom) addSubview(bottomToolView)
bottomToolView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
bottomToolView.leadingAnchor.constraint(equalTo: leadingAnchor),
bottomToolView.trailingAnchor.constraint(equalTo: trailingAnchor),
bottomToolView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
layoutIfNeeded() layoutIfNeeded()
bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height) bottomToolView.transform = CGAffineTransform(translationX: 0, y: bottomToolView.bounds.height)
UIView.animate(withDuration: toolViewAnimationDuration) { UIView.animate(withDuration: toolViewAnimationDuration) {
@ -905,58 +900,6 @@ public class RDReaderView: UIView {
return hitView === toolView || hitView.isDescendant(of: toolView) return hitView === toolView || hitView.isDescendant(of: toolView)
} }
private enum ToolViewPosition {
case top
case bottom
}
private func installToolViewIfNeeded(_ toolView: UIView, position: ToolViewPosition) {
guard toolView.superview !== self else { return }
toolView.removeFromSuperview()
addSubview(toolView)
toolView.translatesAutoresizingMaskIntoConstraints = false
let heightConstraint: NSLayoutConstraint
switch position {
case .top:
topToolViewHeightConstraint?.isActive = false
heightConstraint = toolView.heightAnchor.constraint(equalToConstant: resolvedToolViewHeight(for: .top))
topToolViewHeightConstraint = heightConstraint
NSLayoutConstraint.activate([
toolView.leadingAnchor.constraint(equalTo: leadingAnchor),
toolView.trailingAnchor.constraint(equalTo: trailingAnchor),
toolView.topAnchor.constraint(equalTo: topAnchor),
heightConstraint
])
case .bottom:
bottomToolViewHeightConstraint?.isActive = false
heightConstraint = toolView.heightAnchor.constraint(equalToConstant: resolvedToolViewHeight(for: .bottom))
bottomToolViewHeightConstraint = heightConstraint
NSLayoutConstraint.activate([
toolView.leadingAnchor.constraint(equalTo: leadingAnchor),
toolView.trailingAnchor.constraint(equalTo: trailingAnchor),
toolView.bottomAnchor.constraint(equalTo: bottomAnchor),
heightConstraint
])
}
}
private func updateToolViewHeightConstraintsIfNeeded() {
topToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .top)
bottomToolViewHeightConstraint?.constant = resolvedToolViewHeight(for: .bottom)
}
private func resolvedToolViewHeight(for position: ToolViewPosition) -> CGFloat {
let contentHeight: CGFloat = 52
switch position {
case .top:
return safeAreaInsets.top + contentHeight
case .bottom:
return safeAreaInsets.bottom + contentHeight
}
}
/// 仿// /// 仿//
/// PageViewController CollectionView /// PageViewController CollectionView
/// - Parameter displayType: /// - Parameter displayType:
@ -1322,47 +1265,6 @@ extension RDReaderView {
return cell?.containerView return cell?.containerView
} }
} }
///
/// 使退
public func resolvedSinglePageSize(pageNum: Int? = nil) -> CGSize {
let targetPage = pageNum ?? (currentPage >= 0 ? currentPage : nil)
if let targetPage,
let contentView = pageContentView(pageNum: targetPage),
contentView.bounds.width > 0,
contentView.bounds.height > 0 {
return contentView.bounds.size
}
if currentDisplayType == .pageCurl {
if let childViewController = pageViewController.viewControllers?.first as? RDReaderPageChildViewController,
childViewController.view.bounds.width > 0,
childViewController.view.bounds.height > 0 {
return childViewController.view.bounds.size
}
if pageViewController.view.bounds.width > 0, pageViewController.view.bounds.height > 0 {
return pageViewController.view.bounds.size
}
return bounds.size
}
let containerBounds = collectionView.bounds.size == .zero ? bounds.size : collectionView.bounds.size
guard containerBounds.width > 0, containerBounds.height > 0 else {
return bounds.size
}
switch currentDisplayType {
case .horizontalScroll:
return CGSize(
width: containerBounds.width / CGFloat(max(pagesPerScreen, 1)),
height: containerBounds.height
)
case .verticalScroll:
return containerBounds
case .pageCurl:
return bounds.size
}
}
} }
private var cellViewKey: Int8 = 0 private var cellViewKey: Int8 = 0