diff --git a/Sources/RDEpubReaderView/EPUBUI/TextPage/RDEPUBDarkImageAdjuster.swift b/Sources/RDEpubReaderView/EPUBUI/TextPage/RDEPUBDarkImageAdjuster.swift index acc3534..c976d50 100644 --- a/Sources/RDEpubReaderView/EPUBUI/TextPage/RDEPUBDarkImageAdjuster.swift +++ b/Sources/RDEpubReaderView/EPUBUI/TextPage/RDEPUBDarkImageAdjuster.swift @@ -5,22 +5,6 @@ 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( @@ -28,80 +12,13 @@ enum RDEPUBDarkImageAdjuster { 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) - } + // DTCoreText retains rendering state on its original image attachment. + // Replacing that attachment with a newly rendered UIImage preserves its + // layout size but can leave its drawing callback empty in dark mode. + // Keep the original attachment until image dimming can be performed + // without changing the attachment instance. 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 }