added iOS source code
[wl-app.git] / iOS / Pods / Kingfisher / Sources / ImageDownloader.swift
diff --git a/iOS/Pods/Kingfisher/Sources/ImageDownloader.swift b/iOS/Pods/Kingfisher/Sources/ImageDownloader.swift
new file mode 100755 (executable)
index 0000000..7ee7578
--- /dev/null
@@ -0,0 +1,637 @@
+//
+//  ImageDownloader.swift
+//  Kingfisher
+//
+//  Created by Wei Wang on 15/4/6.
+//
+//  Copyright (c) 2018 Wei Wang <onevcat@gmail.com>
+//
+//  Permission is hereby granted, free of charge, to any person obtaining a copy
+//  of this software and associated documentation files (the "Software"), to deal
+//  in the Software without restriction, including without limitation the rights
+//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+//  copies of the Software, and to permit persons to whom the Software is
+//  furnished to do so, subject to the following conditions:
+//
+//  The above copyright notice and this permission notice shall be included in
+//  all copies or substantial portions of the Software.
+//
+//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+//  THE SOFTWARE.
+
+#if os(macOS)
+import AppKit
+#else
+import UIKit
+#endif
+
+/// Progress update block of downloader.
+public typealias ImageDownloaderProgressBlock = DownloadProgressBlock
+
+/// Completion block of downloader.
+public typealias ImageDownloaderCompletionHandler = ((_ image: Image?, _ error: NSError?, _ url: URL?, _ originalData: Data?) -> Void)
+
+/// Download task.
+public struct RetrieveImageDownloadTask {
+    let internalTask: URLSessionDataTask
+    
+    /// Downloader by which this task is intialized.
+    public private(set) weak var ownerDownloader: ImageDownloader?
+
+    
+    /// Cancel this download task. It will trigger the completion handler with an NSURLErrorCancelled error.
+    /// If you want to cancel all downloading tasks, call `cancelAll()` of `ImageDownloader` instance.
+    public func cancel() {
+        ownerDownloader?.cancel(self)
+    }
+    
+    /// The original request URL of this download task.
+    public var url: URL? {
+        return internalTask.originalRequest?.url
+    }
+    
+    /// The relative priority of this download task. 
+    /// It represents the `priority` property of the internal `NSURLSessionTask` of this download task.
+    /// The value for it is between 0.0~1.0. Default priority is value of 0.5.
+    /// See documentation on `priority` of `NSURLSessionTask` for more about it.
+    public var priority: Float {
+        get {
+            return internalTask.priority
+        }
+        set {
+            internalTask.priority = newValue
+        }
+    }
+}
+
+///The code of errors which `ImageDownloader` might encountered.
+public enum KingfisherError: Int {
+    
+    /// badData: The downloaded data is not an image or the data is corrupted.
+    case badData = 10000
+    
+    /// notModified: The remote server responsed a 304 code. No image data downloaded.
+    case notModified = 10001
+    
+    /// The HTTP status code in response is not valid. If an invalid
+    /// code error received, you could check the value under `KingfisherErrorStatusCodeKey` 
+    /// in `userInfo` to see the code.
+    case invalidStatusCode = 10002
+    
+    /// notCached: The image rquested is not in cache but .onlyFromCache is activated.
+    case notCached = 10003
+    
+    /// The URL is invalid.
+    case invalidURL = 20000
+    
+    /// The downloading task is cancelled before started.
+    case downloadCancelledBeforeStarting = 30000
+}
+
+/// Key will be used in the `userInfo` of `.invalidStatusCode`
+public let KingfisherErrorStatusCodeKey = "statusCode"
+
+/// Protocol of `ImageDownloader`.
+public protocol ImageDownloaderDelegate: class {
+    /**
+    Called when the `ImageDownloader` object successfully downloaded an image from specified URL.
+    
+    - parameter downloader: The `ImageDownloader` object finishes the downloading.
+    - parameter image:      Downloaded image.
+    - parameter url:        URL of the original request URL.
+    - parameter response:   The response object of the downloading process.
+    */
+    func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?)
+    
+    /**
+    Called when the `ImageDownloader` object starts to download an image from specified URL.
+     
+    - parameter downloader: The `ImageDownloader` object starts the downloading.
+    - parameter url:        URL of the original request.
+    - parameter response:   The request object of the downloading process.
+    */
+    func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?)
+    
+    /**
+    Check if a received HTTP status code is valid or not. 
+    By default, a status code between 200 to 400 (excluded) is considered as valid.
+    If an invalid code is received, the downloader will raise an .invalidStatusCode error.
+    It has a `userInfo` which includes this statusCode and localizedString error message.
+     
+    - parameter code: The received HTTP status code.
+    - parameter downloader: The `ImageDownloader` object asking for validate status code.
+     
+    - returns: Whether this HTTP status code is valid or not.
+     
+    - Note: If the default 200 to 400 valid code does not suit your need, 
+            you can implement this method to change that behavior.
+    */
+    func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool
+    
+    /**
+     Called when the `ImageDownloader` object successfully downloaded image data from specified URL.
+     
+     - parameter downloader: The `ImageDownloader` object finishes data downloading.
+     - parameter data:       Downloaded data.
+     - parameter url:        URL of the original request URL.
+     
+     - returns: The data from which Kingfisher should use to create an image.
+     
+     - Note: This callback can be used to preprocess raw image data
+             before creation of UIImage instance (i.e. decrypting or verification).
+     */
+    func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data?
+}
+
+extension ImageDownloaderDelegate {
+    public func imageDownloader(_ downloader: ImageDownloader, didDownload image: Image, for url: URL, with response: URLResponse?) {}
+    
+    public func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) {}
+    public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool {
+        return (200..<400).contains(code)
+    }
+    public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? {
+        return data
+    }
+}
+
+/// Protocol indicates that an authentication challenge could be handled.
+public protocol AuthenticationChallengeResponsable: class {
+    /**
+     Called when an session level authentication challenge is received.
+     This method provide a chance to handle and response to the authentication challenge before downloading could start.
+     
+     - parameter downloader:        The downloader which receives this challenge.
+     - parameter challenge:         An object that contains the request for authentication.
+     - parameter completionHandler: A handler that your delegate method must call.
+     
+     - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionDelegate`.
+     */
+    func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
+
+    /**
+     Called when an session level authentication challenge is received.
+     This method provide a chance to handle and response to the authentication challenge before downloading could start.
+     
+     - parameter downloader:        The downloader which receives this challenge.
+     - parameter task:              The task whose request requires authentication.
+     - parameter challenge:         An object that contains the request for authentication.
+     - parameter completionHandler: A handler that your delegate method must call.
+     
+     - Note: This method is a forward from `URLSessionTaskDelegate.urlSession(:task:didReceiveChallenge:completionHandler:)`. Please refer to the document of it in `URLSessionTaskDelegate`.
+     */
+    func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
+}
+
+extension AuthenticationChallengeResponsable {
+    
+    func downloader(_ downloader: ImageDownloader, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+    
+        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
+            if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) {
+                let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
+                completionHandler(.useCredential, credential)
+                return
+            }
+        }
+        
+        completionHandler(.performDefaultHandling, nil)
+    }
+    
+    func downloader(_ downloader: ImageDownloader, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        
+        completionHandler(.performDefaultHandling, nil)
+    }
+
+}
+
+/// `ImageDownloader` represents a downloading manager for requesting the image with a URL from server.
+open class ImageDownloader {
+    
+    class ImageFetchLoad {
+        var contents = [(callback: CallbackPair, options: KingfisherOptionsInfo)]()
+        var responseData = NSMutableData()
+
+        var downloadTaskCount = 0
+        var downloadTask: RetrieveImageDownloadTask?
+        var cancelSemaphore: DispatchSemaphore?
+    }
+    
+    // MARK: - Public property
+    /// The duration before the download is timeout. Default is 15 seconds.
+    open var downloadTimeout: TimeInterval = 15.0
+    
+    /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this set will be ignored. 
+    /// You can use this set to specify the self-signed site. It only will be used if you don't specify the `authenticationChallengeResponder`. 
+    /// If `authenticationChallengeResponder` is set, this property will be ignored and the implemention of `authenticationChallengeResponder` will be used instead.
+    open var trustedHosts: Set<String>?
+    
+    /// Use this to set supply a configuration for the downloader. By default, NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. 
+    /// You could change the configuration before a downloaing task starts. A configuration without persistent storage for caches is requsted for downloader working correctly.
+    open var sessionConfiguration = URLSessionConfiguration.ephemeral {
+        didSet {
+            session?.invalidateAndCancel()
+            session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: OperationQueue.main)
+        }
+    }
+    
+    /// Whether the download requests should use pipeling or not. Default is false.
+    open var requestsUsePipelining = false
+    
+    fileprivate let sessionHandler: ImageDownloaderSessionHandler
+    fileprivate var session: URLSession?
+    
+    /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more.
+    open weak var delegate: ImageDownloaderDelegate?
+    
+    /// A responder for authentication challenge. 
+    /// Downloader will forward the received authentication challenge for the downloading session to this responder.
+    open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable?
+    
+    // MARK: - Internal property
+    let barrierQueue: DispatchQueue
+    let processQueue: DispatchQueue
+    let cancelQueue: DispatchQueue
+    
+    typealias CallbackPair = (progressBlock: ImageDownloaderProgressBlock?, completionHandler: ImageDownloaderCompletionHandler?)
+    
+    var fetchLoads = [URL: ImageFetchLoad]()
+    
+    // MARK: - Public method
+    /// The default downloader.
+    public static let `default` = ImageDownloader(name: "default")
+    
+    /**
+    Init a downloader with name.
+    
+    - parameter name: The name for the downloader. It should not be empty.
+    
+    - returns: The downloader object.
+    */
+    public init(name: String) {
+        if name.isEmpty {
+            fatalError("[Kingfisher] You should specify a name for the downloader. A downloader with empty name is not permitted.")
+        }
+        
+        barrierQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Barrier.\(name)", attributes: .concurrent)
+        processQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process.\(name)", attributes: .concurrent)
+        cancelQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Cancel.\(name)")
+        
+        sessionHandler = ImageDownloaderSessionHandler()
+
+        // Provide a default implement for challenge responder.
+        authenticationChallengeResponder = sessionHandler
+        session = URLSession(configuration: sessionConfiguration, delegate: sessionHandler, delegateQueue: .main)
+    }
+    
+    deinit {
+        session?.invalidateAndCancel()
+    }
+    
+    func fetchLoad(for url: URL) -> ImageFetchLoad? {
+        var fetchLoad: ImageFetchLoad?
+        barrierQueue.sync(flags: .barrier) { fetchLoad = fetchLoads[url] }
+        return fetchLoad
+    }
+    
+    /**
+     Download an image with a URL and option.
+     
+     - parameter url:               Target URL.
+     - parameter retrieveImageTask: The task to cooporate with cache. Pass `nil` if you are not trying to use downloader and cache.
+     - parameter options:           The options could control download behavior. See `KingfisherOptionsInfo`.
+     - parameter progressBlock:     Called when the download progress updated.
+     - parameter completionHandler: Called when the download progress finishes.
+     
+     - returns: A downloading task. You could call `cancel` on it to stop the downloading process.
+     */
+    @discardableResult
+    open func downloadImage(with url: URL,
+                       retrieveImageTask: RetrieveImageTask? = nil,
+                       options: KingfisherOptionsInfo? = nil,
+                       progressBlock: ImageDownloaderProgressBlock? = nil,
+                       completionHandler: ImageDownloaderCompletionHandler? = nil) -> RetrieveImageDownloadTask?
+    {
+        if let retrieveImageTask = retrieveImageTask, retrieveImageTask.cancelledBeforeDownloadStarting {
+            completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
+            return nil
+        }
+        
+        let timeout = self.downloadTimeout == 0.0 ? 15.0 : self.downloadTimeout
+        
+        // We need to set the URL as the load key. So before setup progress, we need to ask the `requestModifier` for a final URL.
+        var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: timeout)
+        request.httpShouldUsePipelining = requestsUsePipelining
+
+        if let modifier = options?.modifier {
+            guard let r = modifier.modified(for: request) else {
+                completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.downloadCancelledBeforeStarting.rawValue, userInfo: nil), nil, nil)
+                return nil
+            }
+            request = r
+        }
+        
+        // There is a possiblility that request modifier changed the url to `nil` or empty.
+        guard let url = request.url, !url.absoluteString.isEmpty else {
+            completionHandler?(nil, NSError(domain: KingfisherErrorDomain, code: KingfisherError.invalidURL.rawValue, userInfo: nil), nil, nil)
+            return nil
+        }
+        
+        var downloadTask: RetrieveImageDownloadTask?
+        setup(progressBlock: progressBlock, with: completionHandler, for: url, options: options) {(session, fetchLoad) -> Void in
+            if fetchLoad.downloadTask == nil {
+                let dataTask = session.dataTask(with: request)
+                
+                fetchLoad.downloadTask = RetrieveImageDownloadTask(internalTask: dataTask, ownerDownloader: self)
+                
+                dataTask.priority = options?.downloadPriority ?? URLSessionTask.defaultPriority
+                dataTask.resume()
+                self.delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request)
+                
+                // Hold self while the task is executing.
+                self.sessionHandler.downloadHolder = self
+            }
+            
+            fetchLoad.downloadTaskCount += 1
+            downloadTask = fetchLoad.downloadTask
+            
+            retrieveImageTask?.downloadTask = downloadTask
+        }
+        return downloadTask
+    }
+    
+}
+
+// MARK: - Download method
+extension ImageDownloader {
+    
+    // A single key may have multiple callbacks. Only download once.
+    func setup(progressBlock: ImageDownloaderProgressBlock?, with completionHandler: ImageDownloaderCompletionHandler?, for url: URL, options: KingfisherOptionsInfo?, started: @escaping ((URLSession, ImageFetchLoad) -> Void)) {
+
+        func prepareFetchLoad() {
+            barrierQueue.sync(flags: .barrier) {
+                let loadObjectForURL = fetchLoads[url] ?? ImageFetchLoad()
+                let callbackPair = (progressBlock: progressBlock, completionHandler: completionHandler)
+                
+                loadObjectForURL.contents.append((callbackPair, options ?? KingfisherEmptyOptionsInfo))
+                
+                fetchLoads[url] = loadObjectForURL
+                
+                if let session = session {
+                    started(session, loadObjectForURL)
+                }
+            }
+        }
+        
+        if let fetchLoad = fetchLoad(for: url), fetchLoad.downloadTaskCount == 0 {
+            if fetchLoad.cancelSemaphore == nil {
+                fetchLoad.cancelSemaphore = DispatchSemaphore(value: 0)
+            }
+            cancelQueue.async {
+                _ = fetchLoad.cancelSemaphore?.wait(timeout: .distantFuture)
+                fetchLoad.cancelSemaphore = nil
+                prepareFetchLoad()
+            }
+        } else {
+            prepareFetchLoad()
+        }
+    }
+    
+    private func cancelTaskImpl(_ task: RetrieveImageDownloadTask, fetchLoad: ImageFetchLoad? = nil, ignoreTaskCount: Bool = false) {
+        
+        func getFetchLoad(from task: RetrieveImageDownloadTask) -> ImageFetchLoad? {
+            guard let URL = task.internalTask.originalRequest?.url,
+                  let imageFetchLoad = self.fetchLoads[URL] else
+            {
+                return nil
+            }
+            return imageFetchLoad
+        }
+        
+        guard let imageFetchLoad = fetchLoad ?? getFetchLoad(from: task) else {
+            return
+        }
+
+        imageFetchLoad.downloadTaskCount -= 1
+        if ignoreTaskCount || imageFetchLoad.downloadTaskCount == 0 {
+            task.internalTask.cancel()
+        }
+    }
+    
+    func cancel(_ task: RetrieveImageDownloadTask) {
+        barrierQueue.sync(flags: .barrier) { cancelTaskImpl(task) }
+    }
+    
+    /// Cancel all downloading tasks. It will trigger the completion handlers for all not-yet-finished
+    /// downloading tasks with an NSURLErrorCancelled error.
+    ///
+    /// If you need to only cancel a certain task, call `cancel()` on the `RetrieveImageDownloadTask`
+    /// returned by the downloading methods.
+    public func cancelAll() {
+        barrierQueue.sync(flags: .barrier) {
+            fetchLoads.forEach { v in
+                let fetchLoad = v.value
+                guard let task = fetchLoad.downloadTask else { return }
+                cancelTaskImpl(task, fetchLoad: fetchLoad, ignoreTaskCount: true)
+            }
+        }
+    }
+}
+
+// MARK: - NSURLSessionDataDelegate
+
+/// Delegate class for `NSURLSessionTaskDelegate`.
+/// The session object will hold its delegate until it gets invalidated.
+/// If we use `ImageDownloader` as the session delegate, it will not be released.
+/// So we need an additional handler to break the retain cycle.
+// See https://github.com/onevcat/Kingfisher/issues/235
+final class ImageDownloaderSessionHandler: NSObject, URLSessionDataDelegate, AuthenticationChallengeResponsable {
+    
+    // The holder will keep downloader not released while a data task is being executed.
+    // It will be set when the task started, and reset when the task finished.
+    var downloadHolder: ImageDownloader?
+    
+    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
+        
+        guard let downloader = downloadHolder else {
+            completionHandler(.cancel)
+            return
+        }
+        
+        if let statusCode = (response as? HTTPURLResponse)?.statusCode,
+           let url = dataTask.originalRequest?.url,
+            !(downloader.delegate ?? downloader).isValidStatusCode(statusCode, for: downloader)
+        {
+            let error = NSError(domain: KingfisherErrorDomain,
+                                code: KingfisherError.invalidStatusCode.rawValue,
+                                userInfo: [KingfisherErrorStatusCodeKey: statusCode, NSLocalizedDescriptionKey: HTTPURLResponse.localizedString(forStatusCode: statusCode)])
+            callCompletionHandlerFailure(error: error, url: url)
+        }
+        
+        completionHandler(.allow)
+    }
+    
+    func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
+
+        guard let downloader = downloadHolder else {
+            return
+        }
+
+        if let url = dataTask.originalRequest?.url, let fetchLoad = downloader.fetchLoad(for: url) {
+            fetchLoad.responseData.append(data)
+            
+            if let expectedLength = dataTask.response?.expectedContentLength {
+                for content in fetchLoad.contents {
+                    DispatchQueue.main.async {
+                        content.callback.progressBlock?(Int64(fetchLoad.responseData.length), expectedLength)
+                    }
+                }
+            }
+        }
+    }
+    
+    func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
+        
+        guard let url = task.originalRequest?.url else {
+            return
+        }
+        
+        guard error == nil else {
+            callCompletionHandlerFailure(error: error!, url: url)
+            return
+        }
+        
+        processImage(for: task, url: url)
+    }
+    
+    /**
+    This method is exposed since the compiler requests. Do not call it.
+    */
+    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        guard let downloader = downloadHolder else {
+            return
+        }
+        
+        downloader.authenticationChallengeResponder?.downloader(downloader, didReceive: challenge, completionHandler: completionHandler)
+    }
+    
+    func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+        guard let downloader = downloadHolder else {
+            return
+        }
+        
+        downloader.authenticationChallengeResponder?.downloader(downloader, task: task, didReceive: challenge, completionHandler: completionHandler)
+    }
+    
+    private func cleanFetchLoad(for url: URL) {
+        guard let downloader = downloadHolder else {
+            return
+        }
+
+        downloader.barrierQueue.sync(flags: .barrier) {
+            downloader.fetchLoads.removeValue(forKey: url)
+            if downloader.fetchLoads.isEmpty {
+                downloadHolder = nil
+            }
+        }
+    }
+    
+    private func callCompletionHandlerFailure(error: Error, url: URL) {
+        guard let downloader = downloadHolder, let fetchLoad = downloader.fetchLoad(for: url) else {
+            return
+        }
+        
+        // We need to clean the fetch load first, before actually calling completion handler.
+        cleanFetchLoad(for: url)
+        
+        var leftSignal: Int
+        repeat {
+            leftSignal = fetchLoad.cancelSemaphore?.signal() ?? 0
+        } while leftSignal != 0
+        
+        for content in fetchLoad.contents {
+            content.options.callbackDispatchQueue.safeAsync {
+                content.callback.completionHandler?(nil, error as NSError, url, nil)
+            }
+        }
+    }
+    
+    private func processImage(for task: URLSessionTask, url: URL) {
+
+        guard let downloader = downloadHolder else {
+            return
+        }
+        
+        // We are on main queue when receiving this.
+        downloader.processQueue.async {
+            
+            guard let fetchLoad = downloader.fetchLoad(for: url) else {
+                return
+            }
+            
+            self.cleanFetchLoad(for: url)
+            
+            let data: Data?
+            let fetchedData = fetchLoad.responseData as Data
+            
+            if let delegate = downloader.delegate {
+                data = delegate.imageDownloader(downloader, didDownload: fetchedData, for: url)
+            } else {
+                data = fetchedData
+            }
+            
+            // Cache the processed images. So we do not need to re-process the image if using the same processor.
+            // Key is the identifier of processor.
+            var imageCache: [String: Image] = [:]
+            for content in fetchLoad.contents {
+                
+                let options = content.options
+                let completionHandler = content.callback.completionHandler
+                let callbackQueue = options.callbackDispatchQueue
+                
+                let processor = options.processor
+                var image = imageCache[processor.identifier]
+                if let data = data, image == nil {
+                    image = processor.process(item: .data(data), options: options)
+                    // Add the processed image to cache. 
+                    // If `image` is nil, nothing will happen (since the key is not existing before).
+                    imageCache[processor.identifier] = image
+                }
+                
+                if let image = image {
+
+                    downloader.delegate?.imageDownloader(downloader, didDownload: image, for: url, with: task.response)
+
+                    let imageModifier = options.imageModifier
+                    let finalImage = imageModifier.modify(image)
+
+                    if options.backgroundDecode {
+                        let decodedImage = finalImage.kf.decoded
+                        callbackQueue.safeAsync { completionHandler?(decodedImage, nil, url, data) }
+                    } else {
+                        callbackQueue.safeAsync { completionHandler?(finalImage, nil, url, data) }
+                    }
+                    
+                } else {
+                    if let res = task.response as? HTTPURLResponse , res.statusCode == 304 {
+                        let notModified = NSError(domain: KingfisherErrorDomain, code: KingfisherError.notModified.rawValue, userInfo: nil)
+                        completionHandler?(nil, notModified, url, nil)
+                        continue
+                    }
+                    
+                    let badData = NSError(domain: KingfisherErrorDomain, code: KingfisherError.badData.rawValue, userInfo: nil)
+                    callbackQueue.safeAsync { completionHandler?(nil, badData, url, nil) }
+                }
+            }
+        }
+    }
+}
+
+// Placeholder. For retrieving extension methods of ImageDownloaderDelegate
+extension ImageDownloader: ImageDownloaderDelegate {}
+