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 }