Selaa lähdekoodia

Merge pull request #2094 from yeatse/feature/animated-image

Introduce a custom image source provider to enable third-party image processors to utilize AnimatedImageView.
Wei Wang 2 vuotta sitten
vanhempi
commit
1b0f606e63

+ 60 - 4
Sources/Image/GIFAnimatedImage.swift

@@ -74,13 +74,13 @@ public class GIFAnimatedImage {
     let images: [KFCrossPlatformImage]
     let duration: TimeInterval
     
-    init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) {
-        let frameCount = CGImageSourceGetCount(imageSource)
+    init?(from frameSource: ImageFrameSource, options: ImageCreatingOptions) {
+        let frameCount = frameSource.frameCount
         var images = [KFCrossPlatformImage]()
         var gifDuration = 0.0
         
         for i in 0 ..< frameCount {
-            guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else {
+            guard let imageRef = frameSource.frame(at: i) else {
                 return nil
             }
             
@@ -88,7 +88,7 @@ public class GIFAnimatedImage {
                 gifDuration = .infinity
             } else {
                 // Get current animated GIF frame duration
-                gifDuration += GIFAnimatedImage.getFrameDuration(from: imageSource, at: i)
+                gifDuration += frameSource.duration(at: i)
             }
             images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil))
             if options.onlyFirstFrame { break }
@@ -97,6 +97,11 @@ public class GIFAnimatedImage {
         self.duration = gifDuration
     }
     
+    convenience init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) {
+        let frameSource = CGImageFrameSource(data: nil, imageSource: imageSource, options: info)
+        self.init(from: frameSource, options: options)
+    }
+    
     /// Calculates frame duration for a gif frame out of the kCGImagePropertyGIFDictionary dictionary.
     public static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval {
         let defaultFrameDuration = 0.1
@@ -119,3 +124,54 @@ public class GIFAnimatedImage {
         return getFrameDuration(from: gifInfo)
     }
 }
+
+/// Represents a frame source for animated image
+public protocol ImageFrameSource {
+    /// Source data associated with this frame source.
+    var data: Data? { get }
+    
+    /// Count of total frames in this frame source.
+    var frameCount: Int { get }
+    
+    /// Retrieves the frame at a specific index. The result image is expected to be
+    /// no larger than `maxSize`. If the index is invalid, implementors should return `nil`.
+    func frame(at index: Int, maxSize: CGSize?) -> CGImage?
+    
+    /// Retrieves the duration at a specific index. If the index is invalid, implementors should return `0.0`.
+    func duration(at index: Int) -> TimeInterval
+}
+
+public extension ImageFrameSource {
+    /// Retrieves the frame at a specific index. If the index is invalid, implementors should return `nil`.
+    func frame(at index: Int) -> CGImage? {
+        return frame(at: index, maxSize: nil)
+    }
+}
+
+struct CGImageFrameSource: ImageFrameSource {
+    let data: Data?
+    let imageSource: CGImageSource
+    let options: [String: Any]?
+    
+    var frameCount: Int {
+        return CGImageSourceGetCount(imageSource)
+    }
+
+    func frame(at index: Int, maxSize: CGSize?) -> CGImage? {
+        var options = self.options as? [CFString: Any]
+        if let maxSize = maxSize, maxSize != .zero {
+            options = (options ?? [:]).merging([
+                kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
+                kCGImageSourceCreateThumbnailWithTransform: true,
+                kCGImageSourceShouldCacheImmediately: true,
+                kCGImageSourceThumbnailMaxPixelSize: max(maxSize.width, maxSize.height)
+            ], uniquingKeysWith: { $1 })
+        }
+        return CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?)
+    }
+
+    func duration(at index: Int) -> TimeInterval {
+        return GIFAnimatedImage.getFrameDuration(from: imageSource, at: index)
+    }
+}
+

+ 49 - 12
Sources/Image/Image.swift

@@ -91,7 +91,15 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
     var size: CGSize { return base.size }
     
     /// The image source reference of current image.
-    public private(set) var imageSource: CGImageSource? {
+    public var imageSource: CGImageSource? {
+        get {
+            guard let frameSource = frameSource as? CGImageFrameSource else { return nil }
+            return frameSource.imageSource
+        }
+    }
+    
+    /// The custom frame source of current image.
+    public private(set) var frameSource: ImageFrameSource? {
         get { return getAssociatedObject(base, &imageSourceKey) }
         set { setRetainedAssociatedObject(base, &imageSourceKey, newValue) }
     }
@@ -274,29 +282,51 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
         guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else {
             return nil
         }
-        
+        let frameSource = CGImageFrameSource(data: data, imageSource: imageSource, options: info)
         #if os(macOS)
-        guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else {
+        let baseImage = KFCrossPlatformImage(data: data)
+        #else
+        let baseImage = KFCrossPlatformImage(data: data, scale: options.scale)
+        #endif
+        return animatedImage(source: frameSource, options: options, baseImage: baseImage)
+    }
+    
+    /// Creates an animated image from a given frame source.
+    ///
+    /// - Parameters:
+    ///   - source: The frame source to create animated image from.
+    ///   - options: Options to use when creating the animated image.
+    ///   - baseImage: An optional image object to be used as the key frame of the animated image. If `nil`, the first
+    ///                frame of the `source` will be used.
+    /// - Returns: An `Image` object represents the animated image. It is in form of an array of image frames with a
+    ///           certain duration. `nil` if anything wrong when creating animated image.
+    public static func animatedImage(source: ImageFrameSource, options: ImageCreatingOptions, baseImage: KFCrossPlatformImage? = nil) -> KFCrossPlatformImage? {
+        #if os(macOS)
+        guard let animatedImage = GIFAnimatedImage(from: source, options: options) else {
             return nil
         }
         var image: KFCrossPlatformImage?
         if options.onlyFirstFrame {
             image = animatedImage.images.first
         } else {
-            image = KFCrossPlatformImage(data: data)
+            if let baseImage = baseImage {
+                image = baseImage
+            } else {
+                image = animatedImage.images.first
+            }
             var kf = image?.kf
             kf?.images = animatedImage.images
             kf?.duration = animatedImage.duration
         }
-        image?.kf.animatedImageData = data
-        image?.kf.imageFrameCount = Int(CGImageSourceGetCount(imageSource))
+        image?.kf.animatedImageData = source.data
+        image?.kf.imageFrameCount = source.frameCount
         return image
         #else
         
         var image: KFCrossPlatformImage?
         if options.preloadAll || options.onlyFirstFrame {
             // Use `images` image if you want to preload all animated data
-            guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else {
+            guard let animatedImage = GIFAnimatedImage(from: source, options: options) else {
                 return nil
             }
             if options.onlyFirstFrame {
@@ -305,15 +335,22 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
                 let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration
                 image = .animatedImage(with: animatedImage.images, duration: duration)
             }
-            image?.kf.animatedImageData = data
+            image?.kf.animatedImageData = source.data
         } else {
-            image = KFCrossPlatformImage(data: data, scale: options.scale)
+            if let baseImage = baseImage {
+                image = baseImage
+            } else {
+                guard let firstFrame = source.frame(at: 0) else {
+                    return nil
+                }
+                image = KFCrossPlatformImage(cgImage: firstFrame, scale: options.scale, orientation: .up)
+            }
             var kf = image?.kf
-            kf?.imageSource = imageSource
-            kf?.animatedImageData = data
+            kf?.frameSource = source
+            kf?.animatedImageData = source.data
         }
         
-        image?.kf.imageFrameCount = Int(CGImageSourceGetCount(imageSource))
+        image?.kf.imageFrameCount = source.frameCount
         return image
         #endif
     }

+ 2 - 2
Sources/Image/ImageDrawing.swift

@@ -514,7 +514,7 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
     public func decoded(scale: CGFloat) -> KFCrossPlatformImage {
         // Prevent animated image (GIF) losing it's images
         #if os(iOS)
-        if imageSource != nil { return base }
+        if frameSource != nil { return base }
         #else
         if images != nil { return base }
         #endif
@@ -543,7 +543,7 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage {
     public func decoded(on context: CGContext) -> KFCrossPlatformImage {
         // Prevent animated image (GIF) losing it's images
         #if os(iOS)
-        if imageSource != nil { return base }
+        if frameSource != nil { return base }
         #else
         if images != nil { return base }
         #endif

+ 39 - 20
Sources/Views/AnimatedImageView.swift

@@ -264,10 +264,10 @@ open class AnimatedImageView: UIImageView {
     // Reset the animator.
     private func reset() {
         animator = nil
-        if let image = image, let imageSource = image.kf.imageSource {
+        if let image = image, let frameSource = image.kf.frameSource {
             let targetSize = bounds.scaled(UIScreen.main.scale).size
             let animator = Animator(
-                imageSource: imageSource,
+                frameSource: frameSource,
                 contentMode: contentMode,
                 size: targetSize,
                 imageSize: image.kf.size,
@@ -385,7 +385,7 @@ extension AnimatedImageView {
         /// The maximum count of image frames that needs preload.
         public let maxFrameCount: Int
 
-        private let imageSource: CGImageSource
+        private let frameSource: ImageFrameSource
         private let maxRepeatCount: RepeatCount
 
         private let maxTimeStep: TimeInterval = 1.0
@@ -467,7 +467,37 @@ extension AnimatedImageView {
         ///   - count: Count of frames needed to be preloaded.
         ///   - repeatCount: The repeat count should this animator uses.
         ///   - preloadQueue: Dispatch queue used for preloading images.
-        init(imageSource source: CGImageSource,
+        convenience init(imageSource source: CGImageSource,
+                         contentMode mode: UIView.ContentMode,
+                         size: CGSize,
+                         imageSize: CGSize,
+                         imageScale: CGFloat,
+                         framePreloadCount count: Int,
+                         repeatCount: RepeatCount,
+                         preloadQueue: DispatchQueue) {
+            let frameSource = CGImageFrameSource(data: nil, imageSource: source, options: nil)
+            self.init(frameSource: frameSource,
+                      contentMode: mode,
+                      size: size,
+                      imageSize: imageSize,
+                      imageScale: imageScale,
+                      framePreloadCount: count,
+                      repeatCount: repeatCount,
+                      preloadQueue: preloadQueue)
+        }
+        
+        /// Creates an animator with a custom image frame source.
+        ///
+        /// - Parameters:
+        ///   - frameSource: The reference of animated image.
+        ///   - mode: Content mode of the `AnimatedImageView`.
+        ///   - size: Size of the `AnimatedImageView`.
+        ///   - imageSize: Size of the `KingfisherWrapper`.
+        ///   - imageScale: Scale of the `KingfisherWrapper`.
+        ///   - count: Count of frames needed to be preloaded.
+        ///   - repeatCount: The repeat count should this animator uses.
+        ///   - preloadQueue: Dispatch queue used for preloading images.
+        init(frameSource source: ImageFrameSource,
              contentMode mode: UIView.ContentMode,
              size: CGSize,
              imageSize: CGSize,
@@ -475,7 +505,7 @@ extension AnimatedImageView {
              framePreloadCount count: Int,
              repeatCount: RepeatCount,
              preloadQueue: DispatchQueue) {
-            self.imageSource = source
+            self.frameSource = source
             self.contentMode = mode
             self.size = size
             self.imageSize = imageSize
@@ -504,7 +534,7 @@ extension AnimatedImageView {
         }
 
         func prepareFramesAsynchronously() {
-            frameCount = Int(CGImageSourceGetCount(imageSource))
+            frameCount = frameSource.frameCount
             animatedFrames.reserveCapacity(frameCount)
             preloadQueue.async { [weak self] in
                 self?.setupAnimatedFrames()
@@ -529,7 +559,7 @@ extension AnimatedImageView {
             var duration: TimeInterval = 0
 
             (0..<frameCount).forEach { index in
-                let frameDuration = GIFAnimatedImage.getFrameDuration(from: imageSource, at: index)
+                let frameDuration = frameSource.duration(at: index)
                 duration += min(frameDuration, maxTimeStep)
                 animatedFrames.append(AnimatedFrame(image: nil, duration: frameDuration))
 
@@ -546,19 +576,8 @@ extension AnimatedImageView {
 
         private func loadFrame(at index: Int) -> UIImage? {
             let resize = needsPrescaling && size != .zero
-            let options: [CFString: Any]?
-            if resize {
-                options = [
-                    kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
-                    kCGImageSourceCreateThumbnailWithTransform: true,
-                    kCGImageSourceShouldCacheImmediately: true,
-                    kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height)
-                ]
-            } else {
-                options = nil
-            }
-
-            guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?) else {
+            let maxSize = resize ? size : nil
+            guard let cgImage = frameSource.frame(at: index, maxSize: maxSize) else {
                 return nil
             }