import UIKit #if canImport(DTCoreText) import DTCoreText #endif enum RDEPUBDarkImageAdjuster { private static let imageCache: NSCache = { let cache = NSCache() 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, 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) content.enumerateAttribute(.attachment, in: fullRange) { 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 }