kean / Nuke

Image loading system
https://kean.blog/nuke
MIT License
8.06k stars 527 forks source link

Loading image from raw data URL fails #310

Closed dlindenkreuz closed 4 years ago

dlindenkreuz commented 4 years ago

When loading an image from a URL initialized with URL(dataRepresentation: someImage.pngData()!, relativeTo: nil), the request fails with

Task <4BC8E110-08EE-4BD6-8ECF-08AEB79AA8EB>.<17> load failed with error Error Domain=NSURLErrorDomain Code=-1002 "unsupported URL" UserInfo={NSLocalizedDescription=unsupported URL, NSErrorFailingURLStringKey=%89PNG%0D%0A%1A%0A // …and so on

Some context: I would like to cache and process images that already exist in memory, but do not have a local file system or remote URL representation.

This is the case when dragging & dropping an image from Safari, where NSItemProvider.loadObject(ofClass: UIImage.self, ...) gives me a UIImage object that is ready to go and does not need to be downloaded via HTTP anymore.

kean commented 4 years ago

Hmm, I'm not sure this is the correct way to create URLs with data:// scheme. I though I had some tests for this scenario. There is nothing in Nuke that would've prevented data:// scheme to work.

kean commented 4 years ago

Yeah, I think this is how you create a URL with an inline image https://stackoverflow.com/questions/23644193/can-i-create-an-nsurl-that-refers-to-in-memory-nsdata. URL(dataRepresentation: someImage.pngData()!, relativeTo: nil) – this probably won't work. "No overview available" – the documentation is empty 🤦‍♂️

dlindenkreuz commented 4 years ago

For the record: I was able to solve my problem by implementing a custom URLProtocol that skipped the system's URLCache because it does not store cached values synchronously.

A more elegant working solution is to wrap Nuke.DataLoader with a custom DataLoading protocol implementation:

private class NotCancellable: Cancellable {
    func cancel() { /* noop */ }
    static let shared = NotCancellable()
}

class CachingDataLoader: DataLoading {
    private let dataLoader: DataLoader
    static let sharedCache: NSCache<NSURL, CachedURLResponse> = {
        let result = NSCache<NSURL, CachedURLResponse>()
        result.totalCostLimit = 80 * 1024 * 1024 // Megabytes
        return result
    }()

    init(dataLoader: DataLoader = DataLoader()) {
        self.dataLoader = dataLoader
    }

    class func storeResponse(url: URL, data: Data, mimeType: String?) {
        let response = URLResponse(url: url, mimeType: mimeType, expectedContentLength: data.count, textEncodingName: nil)
        let cachedResponse = CachedURLResponse(response: response, data: data, storagePolicy: .allowed)
        sharedCache.setObject(cachedResponse, forKey: url as NSURL, cost: data.count)
    }

    class func removeResponse(url: URL) {
        sharedCache.removeObject(forKey: url as NSURL)
    }

    func loadData(with request: URLRequest, didReceiveData: @escaping (Data, URLResponse) -> Void, completion: @escaping (Error?) -> Void) -> Cancellable {
        if let url = request.url, let cached = Self.sharedCache.object(forKey: url as NSURL) {
            // use cached values
            didReceiveData(cached.data, cached.response)
            completion(nil)
            return NotCancellable.shared
        }
        // call actual data loader implementation
        return dataLoader.loadData(with: request, didReceiveData: didReceiveData, completion: completion)
    }
}

This way, I can generate URLs to files that have not been saved yet (as part of a UIDocument package), pass the raw image data to CachingDataLoader, load the images with Nuke and purge the cache as soon as my document has been saved successfully and the files have been created.

NSCache gives me a thread-safe configurable cache that works synchronously.

Yeah, I think this is how you create a URL with an inline image https://stackoverflow.com/questions/23644193/can-i-create-an-nsurl-that-refers-to-in-memory-nsdata

This would have worked too, but serializing large images as base64 strings is way too expensive.

dlindenkreuz commented 4 years ago

For anyone using custom NSURLProtocol / URLProtocol implementations: registering the protocol via URLProtocol.registerClass will not work as Nuke uses URLSessionConfiguration explicitly which will override system default behaviour.

Register your custom URLProtocols directly via the ImagePipeline configuration instead:

ImagePipeline.shared = ImagePipeline {
    let config = DataLoader.defaultConfiguration
    config.protocolClasses = [SpecialURLProtocol.self]
    $0.dataLoader = DataLoader(configuration: config)
}