feat(epub): align text rendering with WXRead
This commit is contained in:
@@ -88,6 +88,7 @@ enum RDEPUBTextRendererSupport {
|
||||
static func normalizeReadingAttributes(in attributedString: NSMutableAttributedString, style: RDEPUBTextRenderStyle) {
|
||||
let fullRange = NSRange(location: 0, length: attributedString.length)
|
||||
var blockIndex = 0
|
||||
let sourceText = attributedString.string as NSString
|
||||
attributedString.enumerateAttributes(in: fullRange) { attributes, range, _ in
|
||||
let sourceFont = attributes[.font] as? UIFont
|
||||
let normalizedFont = normalizedFont(from: sourceFont, baseFont: style.font)
|
||||
@@ -102,7 +103,13 @@ enum RDEPUBTextRendererSupport {
|
||||
updatedAttributes[.foregroundColor] = textColor
|
||||
}
|
||||
normalizeAttachmentDisplayIfNeeded(in: &updatedAttributes, font: normalizedFont)
|
||||
updatedAttributes[.rdPageBlockRange] = NSStringFromRange(range)
|
||||
let semanticBlockRange = (attributes[.rdPageBlockRange] as? String)
|
||||
.flatMap(NSRangeFromString)
|
||||
.flatMap { $0.length > 0 ? $0 : nil }
|
||||
let paragraphRange = sourceText.length > 0
|
||||
? sourceText.paragraphRange(for: NSRange(location: min(range.location, max(sourceText.length - 1, 0)), length: 0))
|
||||
: range
|
||||
updatedAttributes[.rdPageBlockRange] = NSStringFromRange(semanticBlockRange ?? paragraphRange)
|
||||
updatedAttributes[.rdPageBlockIndex] = blockIndex
|
||||
if let attachmentKind = attachmentKind(for: attributes) {
|
||||
updatedAttributes[.rdPageAttachmentKind] = attachmentKind.rawValue
|
||||
@@ -219,45 +226,16 @@ enum RDEPUBTextRendererSupport {
|
||||
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
|
||||
let maxSize = CGSize(
|
||||
width: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).width),
|
||||
height: round(UIScreen.main.bounds.insetBy(dx: 20, dy: 28).height * 0.85)
|
||||
)
|
||||
normalizeAttachmentLayoutForWXRead(attachment, fontPointSize: pointSize, maxImageSize: maxSize)
|
||||
if isFootnoteAttachment(attachment) {
|
||||
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
|
||||
} else if isCoverAttachment(attachment) {
|
||||
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
|
||||
@@ -472,26 +450,7 @@ enum RDEPUBTextRendererSupport {
|
||||
|
||||
#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
|
||||
}
|
||||
normalizeAttachmentLayoutForWXRead(textAttachment, fontPointSize: font.pointSize)
|
||||
attributes[.attachment] = textAttachment
|
||||
return
|
||||
}
|
||||
@@ -887,7 +846,7 @@ enum RDEPUBTextRendererSupport {
|
||||
if rawTag.contains("avoidpagebreakinside") ||
|
||||
rawTag.contains("break-inside: avoid") ||
|
||||
rawTag.contains("page-break-inside: avoid") ||
|
||||
["blockquote", "pre", "code", "table", "ul", "ol"].contains(tagName) {
|
||||
["blockquote", "pre", "code", "table", "ul", "ol", "figure", "img"].contains(tagName) {
|
||||
hints.append(.avoidPageBreakInside)
|
||||
}
|
||||
if rawTag.contains("pagebreakbefore") ||
|
||||
@@ -977,6 +936,7 @@ enum RDEPUBTextRendererSupport {
|
||||
in attributedString: NSMutableAttributedString
|
||||
) {
|
||||
var attributes: [NSAttributedString.Key: Any] = [:]
|
||||
attributes[.rdPageBlockRange] = NSStringFromRange(range)
|
||||
if let blockKind = semantics.blockKind {
|
||||
attributes[.rdPageBlockKind] = blockKind.rawValue
|
||||
}
|
||||
@@ -1025,4 +985,85 @@ enum RDEPUBTextRendererSupport {
|
||||
var hints: [RDEPUBTextSemanticHint]
|
||||
var attachmentPlacement: RDEPUBTextAttachmentPlacement?
|
||||
}
|
||||
|
||||
#if canImport(DTCoreText)
|
||||
static func normalizeAttachmentLayoutForWXRead(
|
||||
_ attachment: DTTextAttachment,
|
||||
fontPointSize: CGFloat,
|
||||
maxImageSize: CGSize? = nil
|
||||
) {
|
||||
let pointSize = max(fontPointSize, 1)
|
||||
let originalSize = attachment.originalSize
|
||||
|
||||
if isFootnoteAttachment(attachment) {
|
||||
let targetWidth = max(round(pointSize), 1)
|
||||
let aspectRatio = originalSize.height > 0 ? originalSize.width / originalSize.height : 1
|
||||
let targetHeight = max(round(targetWidth / max(aspectRatio, 0.1)), 1)
|
||||
attachment.displaySize = CGSize(width: targetWidth, height: targetHeight)
|
||||
attachment.verticalAlignment = .baseline
|
||||
|
||||
if !didLogFootnoteAttachment {
|
||||
didLogFootnoteAttachment = true
|
||||
print("[EPUB][Attachment] footnote original=\(string(from: originalSize)) display=\(string(from: attachment.displaySize)) font=\(pointSize)")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if isCoverAttachment(attachment) {
|
||||
let maxSize = maxImageSize ?? CGSize(width: round(UIScreen.main.bounds.width - 40), height: round(UIScreen.main.bounds.height - 56))
|
||||
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 = maxSize
|
||||
}
|
||||
attachment.verticalAlignment = .baseline
|
||||
|
||||
if !didLogCoverAttachment {
|
||||
didLogCoverAttachment = true
|
||||
print("[EPUB][Attachment] cover original=\(string(from: originalSize)) display=\(string(from: attachment.displaySize))")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var resolvedSize = attachment.displaySize
|
||||
if resolvedSize.width <= 0 || resolvedSize.height <= 0 {
|
||||
resolvedSize = originalSize
|
||||
}
|
||||
if resolvedSize.width <= 0 || resolvedSize.height <= 0 {
|
||||
resolvedSize = CGSize(width: pointSize, height: pointSize)
|
||||
}
|
||||
|
||||
if let maxImageSize,
|
||||
resolvedSize.width > 0,
|
||||
resolvedSize.height > 0,
|
||||
(resolvedSize.width > maxImageSize.width || resolvedSize.height > maxImageSize.height) {
|
||||
let scale = min(maxImageSize.width / resolvedSize.width, maxImageSize.height / resolvedSize.height)
|
||||
resolvedSize = CGSize(
|
||||
width: round(resolvedSize.width * scale),
|
||||
height: round(resolvedSize.height * scale)
|
||||
)
|
||||
}
|
||||
|
||||
attachment.displaySize = CGSize(width: round(resolvedSize.width), height: round(resolvedSize.height))
|
||||
attachment.verticalAlignment = .center
|
||||
}
|
||||
|
||||
private static func isFootnoteAttachment(_ attachment: DTTextAttachment) -> Bool {
|
||||
let lowercasedClasses = ((attachment.attributes["class"] as? String) ?? "").lowercased()
|
||||
let lowercasedPath = attachment.contentURL?.lastPathComponent.lowercased()
|
||||
?? ((attachment.attributes["src"] as? String) ?? "").lowercased()
|
||||
return lowercasedClasses.contains("qqreader-footnote") || lowercasedPath == "note.png"
|
||||
}
|
||||
|
||||
private static func isCoverAttachment(_ attachment: DTTextAttachment) -> Bool {
|
||||
let lowercasedClasses = ((attachment.attributes["class"] as? String) ?? "").lowercased()
|
||||
let lowercasedPath = attachment.contentURL?.lastPathComponent.lowercased()
|
||||
?? ((attachment.attributes["src"] as? String) ?? "").lowercased()
|
||||
return lowercasedClasses.contains("rd-front-cover-image") || lowercasedPath == "cover.jpg"
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user