From 3e2669d8a81beb405e309e0d1189ec28d3c8f072 Mon Sep 17 00:00:00 2001 From: shen <> Date: Sat, 23 May 2026 15:50:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=9B=AE=E5=89=8D=E9=98=85=E8=AF=BB=E7=A8=B3?= =?UTF-8?q?=E5=AE=9A=E7=89=88=E6=9C=AC=EF=BC=8C=E9=9C=80=E8=A6=81=E4=BF=AE?= =?UTF-8?q?=E6=94=B9=E5=9B=BE=E7=89=87=E9=98=85=E8=AF=BB=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../xcschemes/xcschememanagement.plist | 22 ++ .../ReadViewDemo.xcodeproj/project.pbxproj | 4 + .../UserInterfaceState.xcuserstate | Bin 25055 -> 50620 bytes .../RDEPUBDTCoreTextRenderer.swift | 11 +- .../RDEPUBTextBookBuilder.swift | 84 +++++- .../RDEPUBTextRendererSupport.swift | 246 +++++++++++++++++- .../EPUBUI/RDEPUBTextContentView.swift | 92 +++++++ 7 files changed, 450 insertions(+), 9 deletions(-) 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 601a06f5f8844cc793669e009596372a89f77b8c..9af9d20c8d666de826c57aafcb3a2e78f3374d52 100644 GIT binary patch literal 50620 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 GO!$9CIA4|k delta 14241 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 [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 {