Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[*.swift]
indent_style = space
indent_size = 4
tab_width = 4
end_of_line = crlf
insert_final_newline = false
max_line_length = 120
trim_trailing_whitespace = true
2 changes: 2 additions & 0 deletions Nuke.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@
0CDB92801DAF9BB900002905 /* CHANGELOG.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = "<group>"; };
0CDB92821DAF9BC600002905 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
0CDB92831DAF9BCB00002905 /* Package.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Package.swift; sourceTree = "<group>"; };
207CFF4E2F92C1F8007289D8 /* .editorconfig */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = .editorconfig; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
Expand Down Expand Up @@ -275,6 +276,7 @@
0C9174861BAE99EE004A7905 = {
isa = PBXGroup;
children = (
207CFF4E2F92C1F8007289D8 /* .editorconfig */,
0C096C7B1BAE9ADD007FE380 /* Sources */,
0C7C06551BCA87EC00089D7F /* Tests */,
0C9174911BAE99EE004A7905 /* Products */,
Expand Down
6 changes: 3 additions & 3 deletions Sources/Nuke/Decoding/ImageDecoderRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public final class ImageDecoderRegistry: @unchecked Sendable {
/// A shared registry.
public static let shared = ImageDecoderRegistry()

private var matches = [(ImageDecodingContext) -> (any ImageDecoding)?]()
private var matches = [(ImageDecodingContext) -> (any BaseImageDecoding)?]()
private let lock = NSLock()

/// Initializes a custom registry.
Expand All @@ -18,7 +18,7 @@ public final class ImageDecoderRegistry: @unchecked Sendable {
}

/// Returns a decoder that matches the given context.
public func decoder(for context: ImageDecodingContext) -> (any ImageDecoding)? {
public func decoder(for context: ImageDecodingContext) -> (any BaseImageDecoding)? {
for match in matches.reversed() {
if let decoder = match(context) {
return decoder
Expand All @@ -34,7 +34,7 @@ public final class ImageDecoderRegistry: @unchecked Sendable {
/// The decoder is created once and is used for the entire decoding session,
/// including progressively decoded images. If the decoder doesn't support
/// progressive decoding, return `nil` when `isCompleted` is `false`.
public func register(_ match: @escaping (ImageDecodingContext) -> (any ImageDecoding)?) {
public func register(_ match: @escaping (ImageDecodingContext) -> (any BaseImageDecoding)?) {
lock.withLock { matches.append(match) }
}

Expand Down
45 changes: 44 additions & 1 deletion Sources/Nuke/Decoding/ImageDecoding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import Foundation

public protocol BaseImageDecoding: Sendable {}

/// An image decoder.
///
/// A decoder is a one-shot object created for a single image decoding session.
///
/// - note: If you need additional information in the decoder, you can pass
/// anything that you might need from the ``ImageDecodingContext``.
public protocol ImageDecoding: Sendable {
public protocol ImageDecoding: BaseImageDecoding {
/// Returns `true` if you want the decoding to be performed on the decoding
/// queue (see ``ImagePipeline/Configuration-swift.struct/imageDecodingQueue``). If `false`, the decoding will be
/// performed synchronously on the pipeline operation queue. By default, `true`.
Expand All @@ -36,6 +38,24 @@ extension ImageDecoding {
public func decodePartiallyDownloadedData(_ data: Data) -> ImageContainer? { nil }
}

public protocol AsyncImageDecoding: BaseImageDecoding {
/// Produces an image from the given image data.
func decode(_ data: Data) async throws -> ImageContainer

/// Produces an image from the given partially downloaded image data.
/// This method might be called multiple times during a single decoding
/// session. When the image download is complete, ``decode(_:)`` method is called.
///
/// - returns: nil by default.
func decodePartiallyDownloadedData(_ data: Data) async -> ImageContainer?
}

extension AsyncImageDecoding {
/// The default implementation which simply returns `nil` (no progressive
/// decoding available).
public func decodePartiallyDownloadedData(_ data: Data) async -> ImageContainer? { nil }
}

public enum ImageDecodingError: Error, CustomStringConvertible, Sendable {
case unknown

Expand All @@ -62,3 +82,26 @@ extension ImageDecoding {
return ImageResponse(container: container, request: context.request, urlResponse: context.urlResponse, cacheType: context.cacheType)
}
}

extension AsyncImageDecoding {
func decode(_ context: ImageDecodingContext) async throws -> ImageResponse {
// autoreleasepool does not support async-await
let container: ImageContainer = try await {
if context.isCompleted {
return try await decode(context.data)
} else {
if let preview = await decodePartiallyDownloadedData(context.data) {
return preview
}
throw ImageDecodingError.unknown
}
}()

#if !os(macOS)
if container.userInfo[.isThumbnailKey] == nil && !container.isPreview {
ImageDecompression.setDecompressionNeeded(true, for: container.image)
}
#endif
return ImageResponse(container: container, request: context.request, urlResponse: context.urlResponse, cacheType: context.cacheType)
}
}
11 changes: 11 additions & 0 deletions Sources/Nuke/Internal/Log.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@ func signpost<T>(_ name: StaticString, _ work: () throws -> T) rethrows -> T {
return result
}

func signpost<T>(_ name: StaticString, _ work: () async throws -> T) async rethrows -> T {
guard ImagePipeline.Configuration.isSignpostLoggingEnabled else { return try await work() }

let log = log.value
let signpostId = OSSignpostID(log: log)
os_signpost(.begin, log: log, name: name, signpostID: signpostId)
let result = try await work()
os_signpost(.end, log: log, name: name, signpostID: signpostId)
return result
}

private let log = Mutex(value: OSLog(subsystem: "com.github.kean.Nuke.ImagePipeline", category: "Image Loading"))

enum Formatter {
Expand Down
16 changes: 12 additions & 4 deletions Sources/Nuke/Pipeline/ImagePipeline+Cache.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ extension ImagePipeline.Cache {
/// - request: The request. Make sure to remove the processors if you want
/// to retrieve an original image (if it's stored).
/// - caches: `[.all]`, by default.
public func cachedImage(for request: ImageRequest, caches: Caches = [.all]) -> ImageContainer? {
public func cachedImage(for request: ImageRequest, caches: Caches = [.all]) async -> ImageContainer? {
if caches.contains(.memory) {
if let image = cachedImageFromMemoryCache(for: request) {
return image
}
}
if caches.contains(.disk) {
if let data = cachedData(for: request),
let image = decodeImageData(data, for: request) {
let image = await decodeImageData(data, for: request) {
return image
}
}
Expand Down Expand Up @@ -222,12 +222,20 @@ extension ImagePipeline.Cache {

// MARK: Private

private func decodeImageData(_ data: Data, for request: ImageRequest) -> ImageContainer? {
private func decodeImageData(_ data: Data, for request: ImageRequest) async -> ImageContainer? {
let context = ImageDecodingContext(request: request, data: data, cacheType: .disk)
guard let decoder = pipeline.delegate.imageDecoder(for: context, pipeline: pipeline) else {
return nil
}
return (try? decoder.decode(context))?.container

switch decoder {
case let decoder as ImageDecoding:
return (try? decoder.decode(context))?.container
case let decoder as AsyncImageDecoding:
return (try? await decoder.decode(context))?.container
default:
fatalError("Invalid BaseImageDecoding Implementation")
}
}

private func encodeImage(_ image: ImageContainer, for request: ImageRequest) -> Data? {
Expand Down
2 changes: 1 addition & 1 deletion Sources/Nuke/Pipeline/ImagePipeline+Configuration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ extension ImagePipeline {

/// Default implementation uses shared ``ImageDecoderRegistry`` to create
/// a decoder that matches the context.
public var makeImageDecoder: @Sendable (ImageDecodingContext) -> (any ImageDecoding)? = {
public var makeImageDecoder: @Sendable (ImageDecodingContext) -> (any BaseImageDecoding)? = {
ImageDecoderRegistry.shared.decoder(for: $0)
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Nuke/Pipeline/ImagePipeline+Delegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ extension ImagePipeline {
// MARK: Misc

/// Returns image decoder for the given context.
func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)?
func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any BaseImageDecoding)?

/// Returns image encoder for the given context.
func imageEncoder(for context: ImageEncodingContext, pipeline: ImagePipeline) -> any ImageEncoding
Expand Down Expand Up @@ -126,7 +126,7 @@ extension ImagePipeline.Delegate {
pipeline.configuration.dataCache
}

public func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any ImageDecoding)? {
public func imageDecoder(for context: ImageDecodingContext, pipeline: ImagePipeline) -> (any BaseImageDecoding)? {
pipeline.configuration.makeImageDecoder(context)
}

Expand Down
2 changes: 1 addition & 1 deletion Sources/Nuke/Pipeline/ImagePipeline+Error.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ extension ImagePipeline {
/// By default, the pipeline uses ``ImageDecoders/Default`` as a catch-all.
case decoderNotRegistered(context: ImageDecodingContext)
/// Decoder failed to produce a final image.
case decodingFailed(decoder: any ImageDecoding, context: ImageDecodingContext, error: Swift.Error)
case decodingFailed(decoder: any BaseImageDecoding, context: ImageDecodingContext, error: Swift.Error)
/// Processor failed to produce a final image.
case processingFailed(processor: any ImageProcessing, context: ImageProcessingContext, error: Swift.Error)
/// Load image method was called with no image request or no URL.
Expand Down
67 changes: 55 additions & 12 deletions Sources/Nuke/Tasks/AsyncPipelineTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,63 @@ extension AsyncPipelineTask: ImageTaskSubscribers {
}

extension AsyncPipelineTask {
/// Decodes the data on the dedicated queue and calls the completion
/// on the pipeline's internal queue.
func decode(_ context: ImageDecodingContext, decoder: any ImageDecoding, _ completion: @escaping @ImagePipelineActor (Result<ImageResponse, ImagePipeline.Error>) -> Void) {
@Sendable func decode() -> Result<ImageResponse, ImagePipeline.Error> {
signpost(context.isCompleted ? "DecodeImageData" : "DecodeProgressiveImageData") {
Result { try decoder.decode(context) }
.mapError { .decodingFailed(decoder: decoder, context: context, error: $0) }
}
@Sendable nonisolated func decode(
_ context: ImageDecodingContext,
using decoder: any ImageDecoding
) -> Result<ImageResponse, ImagePipeline.Error> {
signpost(context.isCompleted ? "DecodeImageData" : "DecodeProgressiveImageData") {
Result { try decoder.decode(context) }
.mapError { .decodingFailed(decoder: decoder, context: context, error: $0) }
}
guard decoder.isAsynchronous else {
return completion(decode())
}

@Sendable nonisolated func decode(
_ context: ImageDecodingContext,
using decoder: any AsyncImageDecoding
) async -> Result<ImageResponse, ImagePipeline.Error> {
await signpost(context.isCompleted ? "DecodeImageData" : "DecodeProgressiveImageData") {
do {
return .success(try await decoder.decode(context))
} catch {
let mappedError = ImagePipeline.Error.decodingFailed(
decoder: decoder,
context: context,
error: error
)

return .failure(mappedError)
}
}
operation = pipeline.configuration.imageDecodingQueue.add {
completion(await performInBackground(decode))
}

/// Decodes the data on the dedicated queue and calls the completion
/// on the pipeline's internal queue.
func decode(
_ context: ImageDecodingContext,
decoder: any BaseImageDecoding,
_ completion: @escaping @ImagePipelineActor (Result<ImageResponse, ImagePipeline.Error>) -> Void
) {
switch decoder {
case let decoder as ImageDecoding where decoder.isAsynchronous:
return completion(decode(context, using: decoder))

case let decoder as ImageDecoding:
operation = pipeline.configuration.imageDecodingQueue.add {
let imageContainer = await performInBackground {
self.decode(context, using: decoder)
}

completion(imageContainer)
}

case let decoder as AsyncImageDecoding:
operation = pipeline.configuration.imageDecodingQueue.add {
let imageContainer = await self.decode(context, using: decoder)
completion(imageContainer)
}

default:
fatalError("Invalid BaseImageDecoding Implementation")
}
}
}
4 changes: 2 additions & 2 deletions Sources/Nuke/Tasks/TaskFetchOriginalImage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import Foundation

/// Receives data from ``TaskLoadImageData`` and decodes it as it arrives.
final class TaskFetchOriginalImage: AsyncPipelineTask<ImageResponse>, @unchecked Sendable {
private var decoder: (any ImageDecoding)?
private var decoder: (any BaseImageDecoding)?
private var lastPreviewTime: CFAbsoluteTime?

override func start() {
Expand Down Expand Up @@ -76,7 +76,7 @@ final class TaskFetchOriginalImage: AsyncPipelineTask<ImageResponse>, @unchecked
}

// Lazily creates decoding for task
private func getDecoder(for context: ImageDecodingContext) -> (any ImageDecoding)? {
private func getDecoder(for context: ImageDecodingContext) -> (any BaseImageDecoding)? {
// Return the existing processor in case it has already been created.
if let decoder {
return decoder
Expand Down