diff --git a/ReadViewDemo/Pods/Pods.xcodeproj/xcuserdata/shen.xcuserdatad/xcschemes/xcschememanagement.plist b/ReadViewDemo/Pods/Pods.xcodeproj/xcuserdata/shen.xcuserdatad/xcschemes/xcschememanagement.plist
index 31f7503..4e445b7 100644
--- a/ReadViewDemo/Pods/Pods.xcodeproj/xcuserdata/shen.xcuserdatad/xcschemes/xcschememanagement.plist
+++ b/ReadViewDemo/Pods/Pods.xcodeproj/xcuserdata/shen.xcuserdatad/xcschemes/xcschememanagement.plist
@@ -8,56 +8,78 @@
isShown
+ orderHint
+ 1
DTCoreText.xcscheme
isShown
+ orderHint
+ 0
DTFoundation.xcscheme
isShown
+ orderHint
+ 2
Pods-ReadViewDemo.xcscheme
isShown
+ orderHint
+ 3
RDReaderView-RDReaderViewAssets.xcscheme
isShown
+ orderHint
+ 5
RDReaderView.xcscheme
isShown
+ orderHint
+ 4
SSAlertSwift.xcscheme
isShown
+ orderHint
+ 8
SnapKit-SnapKit_Privacy.xcscheme
isShown
+ orderHint
+ 7
SnapKit.xcscheme
isShown
+ orderHint
+ 6
ZIPFoundation-ZIPFoundation_Privacy.xcscheme
isShown
+ orderHint
+ 10
ZIPFoundation.xcscheme
isShown
+ orderHint
+ 9
SuppressBuildableAutocreation
diff --git a/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj b/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj
index 0d517cc..1a1aebb 100644
--- a/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj
+++ b/ReadViewDemo/ReadViewDemo.xcodeproj/project.pbxproj
@@ -185,10 +185,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
+ inputPaths = (
+ );
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
+ outputPaths = (
+ );
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ReadViewDemo/Pods-ReadViewDemo-frameworks.sh\"\n";
diff --git a/ReadViewDemo/ReadViewDemo.xcworkspace/xcuserdata/shen.xcuserdatad/UserInterfaceState.xcuserstate b/ReadViewDemo/ReadViewDemo.xcworkspace/xcuserdata/shen.xcuserdatad/UserInterfaceState.xcuserstate
index 601a06f..9af9d20 100644
Binary files a/ReadViewDemo/ReadViewDemo.xcworkspace/xcuserdata/shen.xcuserdatad/UserInterfaceState.xcuserstate and b/ReadViewDemo/ReadViewDemo.xcworkspace/xcuserdata/shen.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift
index c8841c9..bcfff2d 100644
--- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift
+++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBDTCoreTextRenderer.swift
@@ -77,18 +77,27 @@ public struct RDEPUBDTCoreTextRenderer: RDEPUBTextRenderer {
options: dtOptions(request: request),
documentAttributes: nil
)
+ builder?.willFlushCallback = { element in
+ guard let element else { return }
+ RDEPUBTextRendererSupport.prepareHTMLElementForReaderRendering(
+ element,
+ style: request.style
+ )
+ }
return builder?.generatedAttributedString()
}
private func dtOptions(request: RDEPUBTextChapterRenderRequest) -> [AnyHashable: Any] {
let style = request.style
+ let screenBounds = UIScreen.main.bounds.insetBy(dx: 20, dy: 28)
var options: [AnyHashable: Any] = [
NSTextSizeMultiplierDocumentOption: 1.0,
DTDefaultFontFamily: style.font.familyName,
DTDefaultFontName: style.font.fontName,
DTDefaultFontSize: style.font.pointSize,
DTDefaultLineHeightMultiplier: max((style.font.lineHeight + style.lineSpacing) / max(style.font.lineHeight, 1), 1),
- DTUseiOS6Attributes: true
+ DTUseiOS6Attributes: true,
+ DTMaxImageSize: NSValue(cgSize: screenBounds.size)
]
if let baseURL = request.context.baseURL {
diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
index 2263fa2..b8027d3 100644
--- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
+++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextBookBuilder.swift
@@ -159,15 +159,43 @@ public final class RDEPUBTextBookBuilder {
lastBuildResourceDiagnostics.append(contentsOf: rendered.resourceDiagnostics)
let plainText = rendered.attributedString.string.trimmingCharacters(in: .whitespacesAndNewlines)
- if shouldSkipChapter(item: item, text: plainText) {
+ if item.href.lowercased().contains("cover") {
+ print("[EPUB][Cover] rendered href=\(item.href) textLength=\(plainText.count) attrLength=\(rendered.attributedString.length) attachments=\(attachmentCount(in: rendered.attributedString))")
+ }
+ if shouldSkipChapter(item: item, content: rendered.attributedString, text: plainText) {
+ if item.href.lowercased().contains("cover") {
+ print("[EPUB][Cover] skipped href=\(item.href)")
+ }
continue
}
let chapterIndex = chapters.count
let content = NSMutableAttributedString(attributedString: rendered.attributedString)
- let layoutFrames = content.length > 0
- ? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)
- : []
+ let layoutFrames: [RDEPUBTextLayoutFrame]
+ if isAttachmentOnlyCoverChapter(item: item, content: content, plainText: plainText) {
+ layoutFrames = [
+ RDEPUBTextLayoutFrame(
+ contentRange: NSRange(location: 0, length: content.length),
+ breakReason: .chapterEnd,
+ blockRange: nil,
+ attachmentRanges: attachmentRanges(in: content),
+ attachmentKinds: [],
+ blockKinds: [],
+ semanticHints: [],
+ attachmentPlacements: [],
+ trailingFragmentID: nil,
+ diagnostics: [
+ "page break: chapterEnd",
+ "cover fallback: single attachment page",
+ "page range: \(NSStringFromRange(NSRange(location: 0, length: content.length)))"
+ ]
+ )
+ ]
+ } else {
+ layoutFrames = content.length > 0
+ ? content.rd_paginatedFrames(size: pageSize, fragmentOffsets: rendered.fragmentOffsets)
+ : []
+ }
let effectiveFrames = layoutFrames.isEmpty && content.length > 0
? [
RDEPUBTextLayoutFrame(
@@ -187,6 +215,9 @@ public final class RDEPUBTextBookBuilder {
)
]
: layoutFrames
+ if item.href.lowercased().contains("cover") {
+ print("[EPUB][Cover] paginated href=\(item.href) pages=\(effectiveFrames.count) firstRange=\(effectiveFrames.first.map { NSStringFromRange($0.contentRange) } ?? "none")")
+ }
let pages = effectiveFrames.enumerated().map { localPageIndex, frame in
let range = frame.contentRange
@@ -258,14 +289,55 @@ public final class RDEPUBTextBookBuilder {
}
}
- private func shouldSkipChapter(item: RDEPUBSpineItem, text: String) -> Bool {
+ private func shouldSkipChapter(item: RDEPUBSpineItem, content: NSAttributedString, text: String) -> Bool {
let lowercasedHref = item.href.lowercased()
- if text.isEmpty && (lowercasedHref.contains("cover") || lowercasedHref.contains("title")) {
+ var hasAttachment = false
+ if content.length > 0 {
+ content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, stop in
+ guard value != nil else { return }
+ hasAttachment = true
+ stop.pointee = true
+ }
+ }
+ if text.isEmpty && !hasAttachment && (lowercasedHref.contains("cover") || lowercasedHref.contains("title")) {
return true
}
return false
}
+ private func attachmentCount(in content: NSAttributedString) -> Int {
+ guard content.length > 0 else { return 0 }
+ var count = 0
+ content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, _ in
+ if value != nil {
+ count += 1
+ }
+ }
+ return count
+ }
+
+ private func attachmentRanges(in content: NSAttributedString) -> [NSRange] {
+ guard content.length > 0 else { return [] }
+ var ranges: [NSRange] = []
+ content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, range, _ in
+ if value != nil {
+ ranges.append(range)
+ }
+ }
+ return ranges
+ }
+
+ private func isAttachmentOnlyCoverChapter(
+ item: RDEPUBSpineItem,
+ content: NSAttributedString,
+ plainText: String
+ ) -> Bool {
+ let lowercasedHref = item.href.lowercased()
+ guard lowercasedHref.contains("cover") else { return false }
+ let trimmed = plainText.trimmingCharacters(in: .whitespacesAndNewlines)
+ return attachmentCount(in: content) > 0 && trimmed.count <= 1
+ }
+
private func uniqueValues(from values: [T]) -> [T] {
values.reduce(into: [T]()) { result, value in
if !result.contains(value) {
diff --git a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift
index e19b7f7..e079eca 100644
--- a/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift
+++ b/Sources/RDReaderView/EPUBTextRendering/RDEPUBTextRendererSupport.swift
@@ -1,9 +1,15 @@
import UIKit
+#if canImport(DTCoreText)
+import DTCoreText
+#endif
+
enum RDEPUBTextRendererSupport {
private static let stylesheetLinkPattern = #"]*rel\s*=\s*["'][^"']*stylesheet[^"']*["'][^>]*href\s*=\s*["']([^"']+)["'][^>]*>"#
private static let imageSourcePattern = #"
]*src\s*=\s*["']([^"']+)["'][^>]*>"#
private static let semanticMarkerPattern = #"\$\{rd-sem-(start|end):([^}]+)\}"#
+ private static var didLogFootnoteAttachment = false
+ private static var didLogCoverAttachment = false
static func injectFragmentMarkers(into html: String) -> String {
guard let regex = try? NSRegularExpression(pattern: #"(<[^>]+\sid="([^"]+)"[^>]*>)"#, options: [.caseInsensitive]) else {
@@ -84,16 +90,18 @@ enum RDEPUBTextRendererSupport {
var blockIndex = 0
attributedString.enumerateAttributes(in: fullRange) { attributes, range, _ in
let sourceFont = attributes[.font] as? UIFont
+ let normalizedFont = normalizedFont(from: sourceFont, baseFont: style.font)
let paragraph = (attributes[.paragraphStyle] as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? paragraphStyle(lineSpacing: style.lineSpacing)
paragraph.lineSpacing = style.lineSpacing
paragraph.paragraphSpacing = max(paragraph.paragraphSpacing, style.lineSpacing / 2)
var updatedAttributes = attributes
- updatedAttributes[.font] = normalizedFont(from: sourceFont, baseFont: style.font)
+ updatedAttributes[.font] = normalizedFont
updatedAttributes[.paragraphStyle] = paragraph
if let textColor = style.textColor {
updatedAttributes[.foregroundColor] = textColor
}
+ normalizeAttachmentDisplayIfNeeded(in: &updatedAttributes, font: normalizedFont)
updatedAttributes[.rdPageBlockRange] = NSStringFromRange(range)
updatedAttributes[.rdPageBlockIndex] = blockIndex
if let attachmentKind = attachmentKind(for: attributes) {
@@ -201,9 +209,59 @@ enum RDEPUBTextRendererSupport {
}
}
+ cleanedHTML = normalizeAttachmentHTMLMarkers(in: cleanedHTML)
return cleanedHTML
}
+#if canImport(DTCoreText)
+ static func prepareHTMLElementForReaderRendering(
+ _ element: DTHTMLElement,
+ style: RDEPUBTextRenderStyle
+ ) {
+ guard let attachment = element.textAttachment else { return }
+
+ let lowercasedClasses = (((attachment.attributes["class"] as? String) ?? (element.attributes["class"] as? String) ?? "")).lowercased()
+ let lowercasedPath = attachment.contentURL?.lastPathComponent.lowercased()
+ ?? ((attachment.attributes["src"] as? String) ?? (element.attributes["src"] as? String) ?? "").lowercased()
+ let pointSize = max(element.fontDescriptor.pointSize, style.font.pointSize)
+
+ if lowercasedClasses.contains("qqreader-footnote") || lowercasedPath == "note.png" {
+ let targetHeight = max(round(pointSize * 0.54), 1)
+ let originalSize = attachment.originalSize
+ let aspectRatio = originalSize.height > 0 ? originalSize.width / originalSize.height : 1
+ let targetWidth = max(round(targetHeight * max(aspectRatio, 0.1)), 1)
+ attachment.displaySize = CGSize(width: targetWidth, height: targetHeight)
+ attachment.verticalAlignment = .center
+ element.displayStyle = .inline
+ if !didLogFootnoteAttachment {
+ didLogFootnoteAttachment = true
+ print("[EPUB][Attachment] footnote classes=\(lowercasedClasses) path=\(lowercasedPath) original=\(string(from: originalSize)) display=\(string(from: attachment.displaySize)) font=\(pointSize)")
+ }
+ return
+ }
+
+ if lowercasedClasses.contains("rd-front-cover-image") || lowercasedPath == "cover.jpg" {
+ let maxSize = UIScreen.main.bounds.insetBy(dx: 20, dy: 28).size
+ let originalSize = attachment.originalSize
+ if originalSize.width > 0, originalSize.height > 0 {
+ let scale = min(maxSize.width / originalSize.width, maxSize.height / originalSize.height)
+ attachment.displaySize = CGSize(
+ width: round(originalSize.width * scale),
+ height: round(originalSize.height * scale)
+ )
+ } else {
+ attachment.displaySize = CGSize(width: round(maxSize.width), height: round(maxSize.height))
+ }
+ attachment.verticalAlignment = .baseline
+ element.displayStyle = .block
+ if !didLogCoverAttachment {
+ didLogCoverAttachment = true
+ print("[EPUB][Attachment] cover classes=\(lowercasedClasses) path=\(lowercasedPath) original=\(string(from: originalSize)) display=\(string(from: attachment.displaySize))")
+ }
+ }
+ }
+#endif
+
private static func makeStyleSheetLayers(
style: RDEPUBTextRenderStyle,
epubCSS: String
@@ -244,10 +302,32 @@ enum RDEPUBTextRendererSupport {
private static func replaceCSS() -> String {
"""
img, svg, video, canvas {
- display: block;
max-width: 100%;
height: auto;
}
+ .frontCover,
+ .bodyPic,
+ .qrbodyPic,
+ figure {
+ text-align: center;
+ text-indent: 0;
+ }
+ .frontCover img,
+ .rd-front-cover-image,
+ .bodyPic img,
+ .qrbodyPic img,
+ figure img {
+ display: block;
+ margin-left: auto;
+ margin-right: auto;
+ }
+ .qqreader-footnote,
+ .s-pic,
+ .h-pic,
+ .g-pic {
+ display: inline;
+ vertical-align: middle;
+ }
h1, h2, h3, h4, h5, h6 {
page-break-after: avoid;
break-after: avoid;
@@ -262,6 +342,168 @@ enum RDEPUBTextRendererSupport {
"""
}
+ private static func normalizeAttachmentHTMLMarkers(in html: String) -> String {
+ var normalized = html
+
+ if let footnoteRegex = try? NSRegularExpression(
+ pattern: #"
]*class\s*=\s*["'][^"']*\bqqreader-footnote\b[^"']*["'][^>]*)>"#,
+ options: [.caseInsensitive]
+ ) {
+ normalized = replaceMatches(
+ using: footnoteRegex,
+ in: normalized
+ ) { tag in
+ mergeHTMLAttributes(
+ into: tag,
+ requiredClass: nil,
+ styleFragments: [
+ "width:1em",
+ "height:1em",
+ "vertical-align:middle",
+ "display:inline-block"
+ ]
+ )
+ }
+ }
+
+ if let coverRegex = try? NSRegularExpression(
+ pattern: #"]*class\s*=\s*["'][^"']*\bfrontCover\b[^"']*["'][^>]*)>\s*(
]*>)\s*
"#,
+ options: [.caseInsensitive]
+ ) {
+ normalized = replaceMatches(
+ using: coverRegex,
+ in: normalized
+ ) { tag in
+ guard let imageTagRegex = try? NSRegularExpression(pattern: #"
]*>"#, options: [.caseInsensitive]),
+ let imageMatch = imageTagRegex.firstMatch(
+ in: tag,
+ options: [],
+ range: NSRange(location: 0, length: (tag as NSString).length)
+ ),
+ let imageRange = Range(imageMatch.range, in: tag) else {
+ return tag
+ }
+
+ let imageTag = String(tag[imageRange])
+ let normalizedImageTag = mergeHTMLAttributes(
+ into: imageTag,
+ requiredClass: "rd-front-cover-image",
+ styleFragments: [
+ "display:block",
+ "width:100%",
+ "height:auto",
+ "margin-left:auto",
+ "margin-right:auto"
+ ]
+ )
+ return tag.replacingCharacters(in: imageRange, with: normalizedImageTag)
+ }
+ }
+
+ return normalized
+ }
+
+ private static func replaceMatches(
+ using regex: NSRegularExpression,
+ in source: String,
+ transform: (String) -> String
+ ) -> String {
+ let nsSource = source as NSString
+ let matches = regex.matches(in: source, options: [], range: NSRange(location: 0, length: nsSource.length))
+ guard !matches.isEmpty else { return source }
+
+ var rewritten = source
+ for match in matches.reversed() {
+ guard let range = Range(match.range, in: rewritten) else { continue }
+ let original = String(rewritten[range])
+ rewritten.replaceSubrange(range, with: transform(original))
+ }
+ return rewritten
+ }
+
+ private static func string(from size: CGSize) -> String {
+ "{\(Int(round(size.width))), \(Int(round(size.height)))}"
+ }
+
+ private static func mergeHTMLAttributes(
+ into tag: String,
+ requiredClass: String?,
+ styleFragments: [String]
+ ) -> String {
+ var rewritten = tag
+ if let requiredClass {
+ if let classRegex = try? NSRegularExpression(pattern: #"class\s*=\s*["']([^"']*)["']"#, options: [.caseInsensitive]),
+ let match = classRegex.firstMatch(in: rewritten, options: [], range: NSRange(location: 0, length: (rewritten as NSString).length)),
+ match.numberOfRanges > 1 {
+ let existingClasses = (rewritten as NSString).substring(with: match.range(at: 1))
+ if !existingClasses.localizedCaseInsensitiveContains(requiredClass) {
+ let replacement = #"class="\#(existingClasses) \#(requiredClass)""#
+ if let range = Range(match.range, in: rewritten) {
+ rewritten.replaceSubrange(range, with: replacement)
+ }
+ }
+ } else if let closing = rewritten.lastIndex(of: ">") {
+ rewritten.insert(contentsOf: #" class="\#(requiredClass)""#, at: closing)
+ }
+ }
+
+ let styleValue = styleFragments.joined(separator: ";") + ";"
+ if let styleRegex = try? NSRegularExpression(pattern: #"style\s*=\s*["']([^"']*)["']"#, options: [.caseInsensitive]),
+ let match = styleRegex.firstMatch(in: rewritten, options: [], range: NSRange(location: 0, length: (rewritten as NSString).length)),
+ match.numberOfRanges > 1 {
+ let existing = (rewritten as NSString).substring(with: match.range(at: 1)).trimmingCharacters(in: .whitespacesAndNewlines)
+ let merged = existing.isEmpty ? styleValue : existing + (existing.hasSuffix(";") ? "" : ";") + styleValue
+ let replacement = #"style="\#(merged)""#
+ if let range = Range(match.range, in: rewritten) {
+ rewritten.replaceSubrange(range, with: replacement)
+ }
+ } else if let closing = rewritten.lastIndex(of: ">") {
+ rewritten.insert(contentsOf: #" style="\#(styleValue)""#, at: closing)
+ }
+
+ return rewritten
+ }
+
+ private static func normalizeAttachmentDisplayIfNeeded(
+ in attributes: inout [NSAttributedString.Key: Any],
+ font: UIFont
+ ) {
+ guard let attachment = attributes[.attachment] else { return }
+
+#if canImport(DTCoreText)
+ if let textAttachment = attachment as? DTTextAttachment {
+ let lowercasedClasses = ((textAttachment.attributes["class"] as? String) ?? "").lowercased()
+ let lowercasedPath = textAttachment.contentURL?.lastPathComponent.lowercased() ?? ""
+ if lowercasedClasses.contains("qqreader-footnote") || lowercasedPath == "note.png" {
+ let targetHeight = max(round(font.pointSize * 0.14), 1)
+ let originalSize = textAttachment.originalSize
+ let aspectRatio = originalSize.height > 0 ? originalSize.width / originalSize.height : 1
+ let targetWidth = max(round(targetHeight * max(aspectRatio, 0.1)), 1)
+ textAttachment.displaySize = CGSize(width: targetWidth, height: round(targetHeight))
+ textAttachment.verticalAlignment = .center
+ } else if lowercasedClasses.contains("rd-front-cover-image") {
+ let originalSize = textAttachment.originalSize
+ let maxWidth = max(UIScreen.main.bounds.width - 48, font.lineHeight * 8)
+ if originalSize.width > 0, originalSize.height > 0 {
+ let scaledHeight = round(originalSize.height * (maxWidth / originalSize.width))
+ textAttachment.displaySize = CGSize(width: round(maxWidth), height: scaledHeight)
+ } else {
+ textAttachment.displaySize = CGSize(width: round(maxWidth), height: round(maxWidth * 1.4))
+ }
+ textAttachment.verticalAlignment = .baseline
+ }
+ attributes[.attachment] = textAttachment
+ return
+ }
+#endif
+
+ if let textAttachment = attachment as? NSTextAttachment, textAttachment.bounds.height <= 0 {
+ let targetHeight = max(round(font.pointSize * 0.86), 1)
+ textAttachment.bounds = CGRect(x: 0, y: 0, width: targetHeight, height: targetHeight)
+ attributes[.attachment] = textAttachment
+ }
+ }
+
private static func darkCSS(style: RDEPUBTextRenderStyle) -> String {
let background = style.backgroundColor?.ss_cssString ?? "rgba(0, 0, 0, 1.000)"
let text = style.textColor?.ss_cssString ?? "rgba(255, 255, 255, 1.000)"
diff --git a/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift b/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift
index c95e027..618e529 100644
--- a/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift
+++ b/Sources/RDReaderView/EPUBUI/RDEPUBTextContentView.swift
@@ -1,6 +1,10 @@
import UIKit
import Foundation
+#if canImport(DTCoreText)
+import DTCoreText
+#endif
+
protocol RDEPUBTextContentViewDelegate: AnyObject {
func textContentView(_ contentView: RDEPUBTextContentView, didChangeSelection selection: RDEPUBSelection?)
func textContentView(_ contentView: RDEPUBTextContentView, didRequestSelectionAction action: RDEPUBAnnotationMenuAction)
@@ -50,6 +54,13 @@ final class RDEPUBTextContentView: UIView {
return view
}()
+ private let coverImageView: UIImageView = {
+ let view = UIImageView()
+ view.contentMode = .scaleAspectFit
+ view.isHidden = true
+ return view
+ }()
+
private let pageNumberLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 13)
@@ -58,6 +69,7 @@ final class RDEPUBTextContentView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
+ addSubview(coverImageView)
addSubview(textView)
addSubview(pageNumberLabel)
textView.delegate = self
@@ -80,6 +92,7 @@ final class RDEPUBTextContentView: UIView {
super.layoutSubviews()
textView.frame = bounds.inset(by: contentInsets)
+ coverImageView.frame = bounds.inset(by: contentInsets)
let labelSize = pageNumberLabel.sizeThatFits(CGSize(width: bounds.width, height: 20))
pageNumberLabel.frame = CGRect(
@@ -105,6 +118,17 @@ final class RDEPUBTextContentView: UIView {
pageNumberLabel.textColor = configuration.theme.contentTextColor
pageNumberLabel.text = "\(pageNumber) / \(totalPages)"
+ if configureCoverIfNeeded(for: page) {
+ textView.attributedText = nil
+ textView.selectedRange = NSRange(location: 0, length: 0)
+ delegate?.textContentView(self, didChangeSelection: nil)
+ setNeedsLayout()
+ return
+ }
+
+ coverImageView.isHidden = true
+ coverImageView.image = nil
+
let displayContent = NSMutableAttributedString(attributedString: page.content)
let fullRange = NSRange(location: 0, length: displayContent.length)
displayContent.addAttribute(
@@ -112,6 +136,7 @@ final class RDEPUBTextContentView: UIView {
value: configuration.theme.contentTextColor,
range: fullRange
)
+ normalizeInlineAttachments(in: displayContent, basePointSize: configuration.fontSize)
applyHighlights(to: displayContent, page: page)
applySearchHighlights(to: displayContent, page: page, searchState: searchState)
textView.tintColor = configuration.theme.toolControlTextColor
@@ -189,6 +214,73 @@ final class RDEPUBTextContentView: UIView {
return lowerBound.. Bool {
+ guard page.pageIndexInChapter == 0,
+ page.href.lowercased().contains("cover"),
+ let image = coverImage(from: page.content) else {
+ return false
+ }
+
+ coverImageView.image = image
+ coverImageView.isHidden = false
+ textView.attributedText = nil
+ return true
+ }
+
+ private func coverImage(from content: NSAttributedString) -> UIImage? {
+ guard content.length > 0 else { return nil }
+ var resolvedImage: UIImage?
+ content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, _, stop in
+ guard let image = image(from: value) else { return }
+ resolvedImage = image
+ stop.pointee = true
+ }
+ return resolvedImage
+ }
+
+ private func image(from attachmentValue: Any?) -> UIImage? {
+#if canImport(DTCoreText)
+ if let attachment = attachmentValue as? DTTextAttachment,
+ let url = attachment.contentURL {
+ return UIImage(contentsOfFile: url.path)
+ }
+#endif
+ if let attachment = attachmentValue as? NSTextAttachment {
+ if let image = attachment.image {
+ return image
+ }
+ if let data = attachment.contents {
+ return UIImage(data: data)
+ }
+ if let fileWrapper = attachment.fileWrapper,
+ let data = fileWrapper.regularFileContents {
+ return UIImage(data: data)
+ }
+ }
+ return nil
+ }
+
+ private func normalizeInlineAttachments(in content: NSMutableAttributedString, basePointSize: CGFloat) {
+ guard content.length > 0 else { return }
+ content.enumerateAttribute(.attachment, in: NSRange(location: 0, length: content.length)) { value, range, _ in
+#if canImport(DTCoreText)
+ if let attachment = value as? DTTextAttachment {
+ let classes = ((attachment.attributes["class"] as? String) ?? "").lowercased()
+ let source = (attachment.contentURL?.lastPathComponent ?? (attachment.attributes["src"] as? String) ?? "").lowercased()
+ guard classes.contains("qqreader-footnote") || source.contains("note.png") else { return }
+
+ let pointSize = max(basePointSize, 1)
+ let targetHeight = max(round(pointSize * 0.14), 1)
+ let aspectRatio = attachment.originalSize.height > 0 ? attachment.originalSize.width / attachment.originalSize.height : 1
+ let targetWidth = max(round(targetHeight * max(aspectRatio, 0.1)), 1)
+ attachment.displaySize = CGSize(width: targetWidth, height: targetHeight)
+ attachment.verticalAlignment = .center
+ content.addAttribute(.attachment, value: attachment, range: range)
+ }
+#endif
+ }
+ }
+
}
extension RDEPUBTextContentView: UITextViewDelegate {