feat(epub): align wxread css and overlay pagination behavior

This commit is contained in:
shen
2026-05-25 22:15:24 +08:00
parent c0aac56083
commit 23182e8b5a
31 changed files with 2585 additions and 1691 deletions
@@ -679,8 +679,9 @@ public final class RDEPUBReaderController: UIViewController {
if publication.readingProfile == .textReflowable {
let renderer = resolvedTextRenderer()
let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache)
let pageSize = currentTextPageSize()
let layoutConfig = currentTextLayoutConfig(pageSize: pageSize)
let builder = RDEPUBTextBookBuilder(renderer: renderer, cache: textBookCache, layoutConfig: layoutConfig)
let renderStyle = currentTextRenderStyle()
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
@@ -913,7 +914,7 @@ public final class RDEPUBReaderController: UIViewController {
let restoreLocation = currentVisibleLocation() ?? persistenceLocation()
let pageSize = currentTextPageSize()
let style = currentTextRenderStyle()
let builder = RDPlainTextBookBuilder()
let builder = RDPlainTextBookBuilder(layoutConfig: currentTextLayoutConfig(pageSize: pageSize))
if let newBook = try? builder.build(textFileURL: textFileURL, pageSize: pageSize, style: style) {
applyTextBook(newBook, restoreLocation: restoreLocation)
}
@@ -1231,6 +1232,26 @@ public final class RDEPUBReaderController: UIViewController {
)
}
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(
frameWidth: derivedFrameSize.width,
frameHeight: derivedFrameSize.height,
edgeInsets: .zero,
numberOfColumns: 1,
columnGap: 20,
avoidOrphans: true,
avoidWidows: true,
avoidPageBreakInsideEnabled: true,
hyphenation: true,
imageMaxHeightRatio: 0.85
)
}
private func resolvedTextRenderer() -> RDEPUBTextRenderer {
RDEPUBDTCoreTextRenderer()
}
@@ -445,12 +445,12 @@ final class RDEPUBTextContentView: UIView {
case .highlight:
content.addAttribute(
.backgroundColor,
value: UIColor(hexString: highlight.color, alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45),
value: UIColor(rdHexString: highlight.color, alpha: 0.45) ?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45),
range: relativeRange
)
case .underline:
content.addAttribute(.underlineStyle, value: NSUnderlineStyle.single.rawValue, range: relativeRange)
if let color = UIColor(hexString: highlight.color, alpha: 1) {
if let color = UIColor(rdHexString: highlight.color, alpha: 1) {
content.addAttribute(.underlineColor, value: color, range: relativeRange)
}
}
@@ -545,7 +545,7 @@ final class RDEPUBTextContentView: UIView {
let rects = interactionController.selectionRects(for: absoluteRange)
guard !rects.isEmpty else { continue }
let color = UIColor(hexString: highlight.color, alpha: 0.45)
let color = UIColor(rdHexString: highlight.color, alpha: 0.45)
?? UIColor(red: 248 / 255, green: 225 / 255, blue: 108 / 255, alpha: 0.45)
let decoration = RDEPUBTextOverlayDecoration(
kind: highlight.style == .underline ? .underline : .highlight,
@@ -712,17 +712,3 @@ extension RDEPUBTextContentView: UITextViewDelegate {
delegate?.textContentView(self, didChangeSelection: selection)
}
}
private extension UIColor {
convenience init?(hexString: String, alpha: CGFloat) {
var value = hexString.trimmingCharacters(in: .whitespacesAndNewlines)
value = value.replacingOccurrences(of: "#", with: "")
guard value.count == 6, let hex = Int(value, radix: 16) else { return nil }
self.init(
red: CGFloat((hex >> 16) & 0xFF) / 255,
green: CGFloat((hex >> 8) & 0xFF) / 255,
blue: CGFloat(hex & 0xFF) / 255,
alpha: alpha
)
}
}
@@ -28,6 +28,7 @@ final class RDEPUBWebContentView: UIView {
weak var delegate: RDEPUBWebContentViewDelegate?
private let epubWebView = RDEPUBWebView()
private let decorationOverlayView = RDEPUBWebDecorationOverlayView()
private let pageNumberLabel: UILabel = {
let label = UILabel()
label.font = UIFont.systemFont(ofSize: 13)
@@ -40,8 +41,12 @@ final class RDEPUBWebContentView: UIView {
layer.masksToBounds = true
addSubview(epubWebView)
addSubview(decorationOverlayView)
addSubview(pageNumberLabel)
epubWebView.delegate = self
epubWebView.onDecorationsResolved = { [weak self] decorations in
self?.decorationOverlayView.applyDecorations(decorations)
}
}
required init?(coder: NSCoder) {
@@ -51,6 +56,7 @@ final class RDEPUBWebContentView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
epubWebView.frame = bounds
decorationOverlayView.frame = bounds
let labelSize = pageNumberLabel.sizeThatFits(CGSize(width: bounds.width, height: 20))
pageNumberLabel.frame = CGRect(
@@ -71,13 +77,18 @@ final class RDEPUBWebContentView: UIView {
backgroundColor = theme.contentBackgroundColor
pageNumberLabel.textColor = theme.contentTextColor
pageNumberLabel.text = "\(pageNumber) / \(totalPages)"
decorationOverlayView.applyDecorations([])
epubWebView.load(publication: publication, request: request)
setNeedsLayout()
}
func releaseResources() {
decorationOverlayView.applyDecorations([])
epubWebView.reset()
epubWebView.delegate = self
epubWebView.onDecorationsResolved = { [weak self] decorations in
self?.decorationOverlayView.applyDecorations(decorations)
}
}
}
@@ -105,6 +116,5 @@ extension RDEPUBWebContentView: RDEPUBWebViewDelegate {
func epubWebView(_ webView: RDEPUBWebView, didLogJavaScriptError message: String) {
delegate?.epubWebContentView(self, didLogJavaScriptError: message)
}
func epubWebViewDidFinishRendering(_ webView: RDEPUBWebView) {}
}
@@ -0,0 +1,50 @@
import UIKit
/// WebView
/// WXRead JS rect CGContext
final class RDEPUBWebDecorationOverlayView: UIView {
private var decorations: [RDEPUBTextOverlayDecoration] = []
private let verticalAdjustment: CGFloat = -1
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .clear
isOpaque = false
isUserInteractionEnabled = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func applyDecorations(_ decorations: [RDEPUBTextOverlayDecoration]) {
self.decorations = decorations.filter { !$0.rects.isEmpty }
setNeedsDisplay()
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
for decoration in decorations {
switch decoration.kind {
case .underline:
context.setStrokeColor(decoration.color.cgColor)
context.setLineWidth(2)
for underlineRect in decoration.rects {
let y = underlineRect.maxY - 1
context.move(to: CGPoint(x: underlineRect.minX, y: y))
context.addLine(to: CGPoint(x: underlineRect.maxX, y: y))
context.strokePath()
}
default:
context.setFillColor(decoration.color.cgColor)
for selectionRect in decoration.rects {
let adjustedRect = selectionRect.offsetBy(dx: 0, dy: verticalAdjustment)
let path = UIBezierPath(roundedRect: adjustedRect.insetBy(dx: -1, dy: -1), cornerRadius: 4)
context.addPath(path.cgPath)
context.fillPath()
}
}
}
}
}
@@ -119,7 +119,20 @@ public final class RDURLReaderController: UIViewController {
let bookTitle = bookURL.deletingPathExtension().lastPathComponent
let pageSize = currentTextPageSize()
let renderStyle = currentTextRenderStyle()
let builder = RDPlainTextBookBuilder()
let builder = RDPlainTextBookBuilder(
layoutConfig: RDEPUBTextLayoutConfig(
frameWidth: pageSize.width,
frameHeight: pageSize.height,
edgeInsets: .zero,
numberOfColumns: 1,
columnGap: 20,
avoidOrphans: true,
avoidWidows: true,
avoidPageBreakInsideEnabled: true,
hyphenation: true,
imageMaxHeightRatio: 0.85
)
)
if let textBook = try? builder.build(textFileURL: bookURL, pageSize: pageSize, style: renderStyle) {
controller = RDEPUBReaderController(
textBook: textBook,
@@ -0,0 +1,15 @@
import UIKit
extension UIColor {
convenience init?(rdHexString: String, alpha: CGFloat) {
var value = rdHexString.trimmingCharacters(in: .whitespacesAndNewlines)
value = value.replacingOccurrences(of: "#", with: "")
guard value.count == 6, let hex = Int(value, radix: 16) else { return nil }
self.init(
red: CGFloat((hex >> 16) & 0xFF) / 255,
green: CGFloat((hex >> 8) & 0xFF) / 255,
blue: CGFloat(hex & 0xFF) / 255,
alpha: alpha
)
}
}