damus

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

KFOptionSetter+.swift (5488B)


      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 
     63 let MAX_FILE_SIZE = 20_971_520 // 20MiB
     64 
     65 enum ImageContext {
     66     case pfp
     67     case banner
     68     case note
     69     
     70     func maxMebibyteSize() -> Int {
     71         switch self {
     72         case .pfp:
     73             return 5_242_880 // 5Mib
     74         case .banner, .note:
     75             return 20_971_520 // 20MiB
     76         }
     77     }
     78     
     79     func downsampleSize() -> CGSize {
     80         switch self {
     81         case .pfp:
     82             return CGSize(width: 200, height: 200)
     83         case .banner:
     84             return CGSize(width: 750, height: 250)
     85         case .note:
     86             return CGSize(width: 500, height: 500)
     87         }
     88     }
     89 }
     90 
     91 struct CustomImageProcessor: ImageProcessor {
     92     
     93     let maxSize: Int
     94     let downsampleSize: CGSize
     95     
     96     let identifier = "com.damus.customimageprocessor"
     97     
     98     func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? {
     99         
    100         switch item {
    101         case .image:
    102             // This case will never run
    103             return DefaultImageProcessor.default.process(item: item, options: options)
    104         case .data(let data):
    105             
    106             // Handle large image size
    107             if data.count > maxSize {
    108                 return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
    109             }
    110             
    111             // Handle SVG image
    112             if let dataString = String(data: data, encoding: .utf8),
    113                 let svg = SVG(dataString) {
    114                 
    115                     let render = UIGraphicsImageRenderer(size: svg.size)
    116                     let image = render.image { context in
    117                         svg.draw(in: context.cgContext)
    118                     }
    119 
    120                     return image.kf.scaled(to: options.scaleFactor)
    121             }
    122             
    123             return DefaultImageProcessor.default.process(item: item, options: options)
    124         }
    125     }
    126 }
    127 
    128 struct CustomCacheSerializer: CacheSerializer {
    129     
    130     let maxSize: Int
    131     let downsampleSize: CGSize
    132 
    133     func data(with image: Kingfisher.KFCrossPlatformImage, original: Data?) -> Data? {
    134         return DefaultCacheSerializer.default.data(with: image, original: original)
    135     }
    136 
    137     func image(with data: Data, options: Kingfisher.KingfisherParsedOptionsInfo) -> Kingfisher.KFCrossPlatformImage? {
    138         if data.count > maxSize {
    139             return KingfisherWrapper.downsampledImage(data: data, to: downsampleSize, scale: options.scaleFactor)
    140         }
    141 
    142         return DefaultCacheSerializer.default.image(with: data, options: options)
    143     }
    144 }
    145 
    146 class CustomSessionDelegate: SessionDelegate {
    147     override func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
    148         let contentLength = response.expectedContentLength
    149         
    150         // Content-Length header is optional (-1 when missing)
    151         if (contentLength != -1 && contentLength > MAX_FILE_SIZE) {
    152             return super.urlSession(session, dataTask: dataTask, didReceive: URLResponse(), completionHandler: completionHandler)
    153         }
    154         
    155         super.urlSession(session, dataTask: dataTask, didReceive: response, completionHandler: completionHandler)
    156     }
    157 }
    158 
    159 class CustomImageDownloader: ImageDownloader {
    160     
    161     static let shared = CustomImageDownloader(name: "shared")
    162     
    163     override init(name: String) {
    164         super.init(name: name)
    165         sessionDelegate = CustomSessionDelegate()
    166     }
    167 }