- Rename source module from RDReaderView to RDEpubReaderView - Move all source files from Sources/RDReaderView/ to Sources/RDEpubReaderView/ - Update podspec: RDReaderView.podspec -> RDEpubReaderView.podspec - Update Podfile, demo project, and CocoaPods config for new pod name - Delete old RDReaderView pod support files from ReadViewDemo/Pods - Add new RDEpubReaderView pod support files - Update documentation (API ref, architecture, UML, conventions, etc.) - Add FixedLayoutRotationTests - Update .gitignore: exclude .DS_Store, manual unpack backups, _ssoft-output
108 lines
4.4 KiB
Swift
108 lines
4.4 KiB
Swift
import UIKit
|
|
|
|
#if canImport(DTCoreText)
|
|
import DTCoreText
|
|
#endif
|
|
|
|
enum RDEPUBDarkImageAdjuster {
|
|
|
|
private static let imageCache: NSCache<NSString, UIImage> = {
|
|
let cache = NSCache<NSString, UIImage>()
|
|
cache.countLimit = 100
|
|
cache.totalCostLimit = 52_428_800 // 50 MB
|
|
return cache
|
|
}()
|
|
|
|
private static func imageCost(of image: UIImage) -> Int {
|
|
let scale = image.scale
|
|
let width = Int(image.size.width * scale)
|
|
let height = Int(image.size.height * scale)
|
|
// 4 bytes per pixel (RGBA)
|
|
return width * height * 4
|
|
}
|
|
|
|
#if canImport(DTCoreText)
|
|
|
|
static func adjustIfNeeded(
|
|
_ content: NSMutableAttributedString,
|
|
in range: NSRange? = nil,
|
|
configuration: RDEPUBReaderConfiguration
|
|
) -> NSMutableAttributedString {
|
|
guard configuration.darkImageAdjustmentEnabled,
|
|
configuration.darkImageBlendRatio > 0,
|
|
configuration.theme.contentBackgroundColor.rd_isDarkBackground else {
|
|
return content
|
|
}
|
|
|
|
let fullRange = NSRange(location: 0, length: content.length)
|
|
let targetRange = range.map { NSIntersectionRange($0, fullRange) } ?? fullRange
|
|
content.enumerateAttribute(.attachment, in: targetRange) { value, range, _ in
|
|
guard let attachment = value as? DTImageTextAttachment,
|
|
!isCoverAttachment(attachment),
|
|
let image = attachment.image,
|
|
shouldAdjust(image) else { return }
|
|
|
|
let adjustedAttachment = DTImageTextAttachment()
|
|
adjustedAttachment.image = adjustedImage(
|
|
image,
|
|
backgroundColor: configuration.theme.contentBackgroundColor,
|
|
blendRatio: configuration.darkImageBlendRatio,
|
|
cacheKey: cacheKey(for: attachment, image: image, configuration: configuration)
|
|
)
|
|
adjustedAttachment.originalSize = attachment.originalSize
|
|
adjustedAttachment.displaySize = attachment.displaySize
|
|
adjustedAttachment.verticalAlignment = attachment.verticalAlignment
|
|
adjustedAttachment.contentURL = attachment.contentURL
|
|
adjustedAttachment.hyperLinkURL = attachment.hyperLinkURL
|
|
adjustedAttachment.hyperLinkGUID = attachment.hyperLinkGUID
|
|
adjustedAttachment.attributes = attachment.attributes
|
|
content.addAttribute(.attachment, value: adjustedAttachment, range: range)
|
|
}
|
|
return content
|
|
}
|
|
|
|
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.contains("cover")
|
|
}
|
|
|
|
private static func shouldAdjust(_ image: UIImage) -> Bool {
|
|
image.size.width >= 80 && image.size.height >= 80
|
|
}
|
|
|
|
private static func cacheKey(
|
|
for attachment: DTImageTextAttachment,
|
|
image: UIImage,
|
|
configuration: RDEPUBReaderConfiguration
|
|
) -> NSString {
|
|
let source = attachment.contentURL?.absoluteString
|
|
?? "\(Unmanaged.passUnretained(image).toOpaque())"
|
|
return "\(source)|\(image.size.width)x\(image.size.height)|\(configuration.theme.contentBackgroundColor.rd_cssString)|\(configuration.darkImageBlendRatio)" as NSString
|
|
}
|
|
|
|
private static func adjustedImage(
|
|
_ image: UIImage,
|
|
backgroundColor: UIColor,
|
|
blendRatio: CGFloat,
|
|
cacheKey: NSString
|
|
) -> UIImage {
|
|
if let cached = imageCache.object(forKey: cacheKey) { return cached }
|
|
let format = UIGraphicsImageRendererFormat()
|
|
format.scale = image.scale
|
|
format.opaque = false
|
|
let renderer = UIGraphicsImageRenderer(size: image.size, format: format)
|
|
let adjusted = renderer.image { context in
|
|
image.draw(in: CGRect(origin: .zero, size: image.size))
|
|
backgroundColor.withAlphaComponent(max(0, min(0.35, blendRatio))).setFill()
|
|
context.cgContext.setBlendMode(.sourceAtop)
|
|
context.fill(CGRect(origin: .zero, size: image.size))
|
|
}
|
|
imageCache.setObject(adjusted, forKey: cacheKey, cost: imageCost(of: adjusted))
|
|
return adjusted
|
|
}
|
|
|
|
#endif
|
|
}
|