damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

KFOptionSetter+.swift (6253B)


      1 //
      2 //  KFOptionSetter+.swift
      3 //  damus
      4 //
      5 //  Created by Oleg Abalonski on 2/15/23.
      6 //
      7 
      8 import UIKit
      9 import Kingfisher
     10 
     11 extension KFOptionSetter {
     12     
     13     func imageContext(_ imageContext: ImageContext, disable_animation: Bool) -> Self {
     14         options.callbackQueue = .dispatch(.global(qos: .background))
     15         options.processingQueue = .dispatch(.global(qos: .background))
     16         options.downloader = CustomImageDownloader.shared
     17         options.processor = CustomImageProcessor(
     18             maxSize: imageContext.maxMebibyteSize(),
     19             downsampleSize: imageContext.downsampleSize()
     20         )
     21         options.cacheSerializer = CustomCacheSerializer(
     22             maxSize: imageContext.maxMebibyteSize(),
     23             downsampleSize: imageContext.downsampleSize()
     24         )
     25         options.loadDiskFileSynchronously = false
     26         options.backgroundDecode = true
     27         options.cacheOriginalImage = true
     28         options.scaleFactor = UIScreen.main.scale
     29         options.onlyLoadFirstFrame = disable_animation
     30         
     31         switch imageContext {
     32             case .pfp:
     33                 options.diskCacheExpiration = .days(60)
     34                 break
     35             case .banner:
     36                 options.diskCacheExpiration = .days(5)
     37                 break
     38             case .note:
     39                 options.diskCacheExpiration = .days(1)
     40                 break
     41         }
     42         
     43         return self
     44     }
     45     
     46     func image_fade(duration: TimeInterval) -> Self {
     47         options.transition = ImageTransition.fade(duration)
     48         options.keepCurrentImageWhileLoading = false
     49         
     50         return self
     51     }
     52     
     53     func onFailure(fallbackUrl: URL?, cacheKey: String?) -> Self {
     54         guard let url = fallbackUrl, let key = cacheKey else { return self }
     55         let imageResource = Kingfisher.ImageResource(downloadURL: url, cacheKey: key)
     56         let source = imageResource.convertToSource()
     57         options.alternativeSources = [source]
     58         
     59         return self
     60     }
     61     
     62     /// This allows you to observe the size of the image, and get a callback when the size changes
     63     /// This is useful for when you need to layout views based on the size of the image
     64     /// - Parameter size_changed: A callback that will be called when the size of the image changes
     65     /// - Returns: The same KFOptionSetter instance
     66     func observe_image_size(size_changed: @escaping (CGSize) -> Void) -> Self {
     67         let modifier = AnyImageModifier { image -> KFCrossPlatformImage in
     68             let image_size = image.size
     69             DispatchQueue.main.async { [size_changed, image_size] in
     70                 size_changed(image_size)
     71             }
     72             return image
     73         }
     74         options.imageModifier = modifier
     75         return self
     76     }
     77 }
     78 
     79 let MAX_FILE_SIZE = 20_971_520 // 20MiB
     80 
     81 enum ImageContext {
     82     case pfp
     83     case banner
     84     case note
     85     
     86     func maxMebibyteSize() -> Int {
     87         switch self {
     88         case .pfp:
     89             return 5_242_880 // 5Mib
     90         case .banner, .note:
     91             return 20_971_520 // 20MiB
     92         }
     93     }
     94     
     95     func downsampleSize() -> CGSize {
     96         switch self {
     97         case .pfp:
     98             return CGSize(width: 200, height: 200)
     99         case .banner:
    100             return CGSize(width: 750, height: 250)
    101         case .note:
    102             return CGSize(width: 500, height: 500)
    103         }
    104     }
    105 }
    106 
    107 struct CustomImageProcessor: ImageProcessor {
    108     
    109     let maxSize: Int
    110     let downsampleSize: CGSize
    111     
    112     let identifier = "com.damus.customimageprocessor"
    113     
    114     func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
    115         
    116         switch item {
    117         case .image:
    118             // This case will never run
    119             return DefaultImageProcessor.default.process(item: item, options: options)
    120         case .data(let data):
    121             
    122             // Handle large image size
    123             if data.count > maxSize {
    124                 return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
    125             }
    126             
    127             // Handle SVG image
    128             if let dataString = String(data: data, encoding: .utf8),
    129                 let svg = SVG(dataString) {
    130                 
    131                     let render = UIGraphicsImageRenderer(size: svg.size)
    132                     let image = render.image { context in
    133                         svg.draw(in: context.cgContext)
    134                     }
    135 
    136                     return image.kf.scaled(to: options.scaleFactor)
    137             }
    138             
    139             return DefaultImageProcessor.default.process(item: item, options: options)
    140         }
    141     }
    142 }
    143 
    144 struct CustomCacheSerializer: CacheSerializer {
    145     
    146     let maxSize: Int
    147     let downsampleSize: CGSize
    148 
    149     func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
    150         return DefaultCacheSerializer.default.data(with: image, original: original)
    151     }
    152 
    153     func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
    154         if data.count > maxSize {
    155             return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
    156         }
    157 
    158         return DefaultCacheSerializer.default.image(with: data, options: options)
    159     }
    160 }
    161 
    162 class CustomSessionDelegate: SessionDelegate {
    163     override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
    164         let contentLength = response.expectedContentLength
    165         
    166         // Content-Length header is optional (-1 when missing)
    167         if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
    168             return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
    169         }
    170         
    171         super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
    172     }
    173 }
    174 
    175 class CustomImageDownloader: ImageDownloader {
    176     
    177     static let shared = CustomImageDownloader(name: "shared")
    178     
    179     override init(name: String) {
    180         super.init(name: name)
    181         sessionDelegate = CustomSessionDelegate()
    182     }
    183 }