123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- //
- // AnimatedImage.swift
- // Kingfisher
- //
- // Created by onevcat on 2018/09/26.
- //
- // Copyright (c) 2019 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.
- import Foundation
- import ImageIO
- /// Represents a set of image creating options used in Kingfisher.
- public struct ImageCreatingOptions {
- /// The target scale of image needs to be created.
- public let scale: CGFloat
- /// The expected animation duration if an animated image being created.
- public let duration: TimeInterval
- /// For an animated image, whether or not all frames should be loaded before displaying.
- public let preloadAll: Bool
- /// For an animated image, whether or not only the first image should be
- /// loaded as a static image. It is useful for preview purpose of an animated image.
- public let onlyFirstFrame: Bool
-
- /// Creates an `ImageCreatingOptions` object.
- ///
- /// - Parameters:
- /// - scale: The target scale of image needs to be created. Default is `1.0`.
- /// - duration: The expected animation duration if an animated image being created.
- /// A value less or equal to `0.0` means the animated image duration will
- /// be determined by the frame data. Default is `0.0`.
- /// - preloadAll: For an animated image, whether or not all frames should be loaded before displaying.
- /// Default is `false`.
- /// - onlyFirstFrame: For an animated image, whether or not only the first image should be
- /// loaded as a static image. It is useful for preview purpose of an animated image.
- /// Default is `false`.
- public init(
- scale: CGFloat = 1.0,
- duration: TimeInterval = 0.0,
- preloadAll: Bool = false,
- onlyFirstFrame: Bool = false)
- {
- self.scale = scale
- self.duration = duration
- self.preloadAll = preloadAll
- self.onlyFirstFrame = onlyFirstFrame
- }
- }
- /// Represents the decoding for a GIF image. This class extracts frames from an `imageSource`, then
- /// hold the images for later use.
- public class GIFAnimatedImage {
- let images: [KFCrossPlatformImage]
- let duration: TimeInterval
-
- 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 = frameSource.frame(at: i) else {
- return nil
- }
-
- if frameCount == 1 {
- gifDuration = .infinity
- } else {
- // Get current animated GIF frame duration
- gifDuration += frameSource.duration(at: i)
- }
- images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil))
- if options.onlyFirstFrame { break }
- }
- self.images = images
- 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
- guard let gifInfo = gifInfo else { return defaultFrameDuration }
-
- let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber
- let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber
- let duration = unclampedDelayTime ?? delayTime
-
- guard let frameDuration = duration else { return defaultFrameDuration }
- return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration
- }
- /// Calculates frame duration at a specific index for a gif from an `imageSource`.
- public static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval {
- guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil)
- as? [String: Any] else { return 0.0 }
- let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any]
- 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)
- }
- }
|