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 }