Skip to content

Commit

Permalink
Merge pull request onevcat#591 from onevcat/feature/gif-first-image
Browse files Browse the repository at this point in the history
Feature gif first image
  • Loading branch information
onevcat authored Feb 10, 2017
2 parents 5da7886 + 7090147 commit 1da0c5a
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 76 deletions.
2 changes: 2 additions & 0 deletions Demo/Kingfisher-macOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ extension ViewController: NSCollectionViewDataSource {
print("\(indexPath.item + 1): Finished")
})

// Set imageView's `animates` to true if you are loading a GIF.
// item.imageView?.animates = true
return item
}
}
10 changes: 6 additions & 4 deletions Sources/CacheSerializer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ public struct DefaultCacheSerializer: CacheSerializer {
}

public func image(with data: Data, options: KingfisherOptionsInfo?) -> Image? {
let scale = (options ?? KingfisherEmptyOptionsInfo).scaleFactor
let preloadAllGIFData = (options ?? KingfisherEmptyOptionsInfo).preloadAllGIFData

return Kingfisher<Image>.image(data: data, scale: scale, preloadAllGIFData: preloadAllGIFData)
let options = options ?? KingfisherEmptyOptionsInfo
return Kingfisher<Image>.image(
data: data,
scale: options.scaleFactor,
preloadAllGIFData: options.preloadAllGIFData,
onlyFirstFrame: options.onlyLoadFirstFrame)
}
}
131 changes: 61 additions & 70 deletions Sources/Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ private var durationKey: Void?
import UIKit
import MobileCoreServices
private var imageSourceKey: Void?
private var animatedImageDataKey: Void?
#endif
private var animatedImageDataKey: Void?

import ImageIO
import CoreGraphics
Expand All @@ -46,6 +46,15 @@ import CoreImage

// MARK: - Image Properties
extension Kingfisher where Base: Image {
fileprivate(set) var animatedImageData: Data? {
get {
return objc_getAssociatedObject(base, &animatedImageDataKey) as? Data
}
set {
objc_setAssociatedObject(base, &animatedImageDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

#if os(macOS)
var cgImage: CGImage? {
return base.cgImage(forProposedRect: nil, context: nil, hints: nil)
Expand Down Expand Up @@ -85,15 +94,15 @@ extension Kingfisher where Base: Image {
}

var scale: CGFloat {
return base.scale
return base.scale
}

var images: [Image]? {
return base.images
return base.images
}

var duration: TimeInterval {
return base.duration
return base.duration
}

fileprivate(set) var imageSource: ImageSource? {
Expand All @@ -105,15 +114,6 @@ extension Kingfisher where Base: Image {
}
}

fileprivate(set) var animatedImageData: Data? {
get {
return objc_getAssociatedObject(base, &animatedImageDataKey) as? Data
}
set {
objc_setAssociatedObject(base, &animatedImageDataKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}
}

var size: CGSize {
return base.size
}
Expand Down Expand Up @@ -200,44 +200,13 @@ extension Kingfisher where Base: Image {

// MARK: - GIF
public func gifRepresentation() -> Data? {
#if os(macOS)
return gifRepresentation(duration: 0.0, repeatCount: 0)
#else
return animatedImageData
#endif
return animatedImageData
}

#if os(macOS)
func gifRepresentation(duration: TimeInterval, repeatCount: Int) -> Data? {
guard let images = images else {
return nil
}

let frameCount = images.count
let gifDuration = duration <= 0.0 ? duration / Double(frameCount) : duration / Double(frameCount)

let frameProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFDelayTime as String: gifDuration]]
let imageProperties = [kCGImagePropertyGIFDictionary as String: [kCGImagePropertyGIFLoopCount as String: repeatCount]]

let data = NSMutableData()

guard let destination = CGImageDestinationCreateWithData(data, kUTTypeGIF, frameCount, nil) else {
return nil
}
CGImageDestinationSetProperties(destination, imageProperties as CFDictionary)

for image in images {
CGImageDestinationAddImage(destination, image.kf.cgImage!, frameProperties as CFDictionary)
}

return CGImageDestinationFinalize(destination) ? data.copy() as? Data : nil
}
#endif
}

// MARK: - Create images from data
extension Kingfisher where Base: Image {
static func animated(with data: Data, scale: CGFloat = 1.0, duration: TimeInterval = 0.0, preloadAll: Bool) -> Image? {
static func animated(with data: Data, scale: CGFloat = 1.0, duration: TimeInterval = 0.0, preloadAll: Bool, onlyFirstFrame: Bool = false) -> Image? {

func decode(from imageSource: CGImageSource, for options: NSDictionary) -> ([Image], TimeInterval)? {

Expand All @@ -262,7 +231,7 @@ extension Kingfisher where Base: Image {
guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, options) else {
return nil
}

if frameCount == 1 {
// Single frame
gifDuration = Double.infinity
Expand All @@ -277,6 +246,8 @@ extension Kingfisher where Base: Image {
}

images.append(Kingfisher<Image>.image(cgImage: imageRef, scale: scale, refImage: nil))

if onlyFirstFrame { break }
}

return (images, gifDuration)
Expand All @@ -292,45 +263,65 @@ extension Kingfisher where Base: Image {
guard let (images, gifDuration) = decode(from: imageSource, for: options) else {
return nil
}
let image = Image(data: data)
image?.kf.images = images
image?.kf.duration = gifDuration

let image: Image?
if onlyFirstFrame {
image = images.first
} else {
image = Image(data: data)
image?.kf.images = images
image?.kf.duration = gifDuration
}
image?.kf.animatedImageData = data
return image
#else

if preloadAll {
guard let (images, gifDuration) = decode(from: imageSource, for: options) else {
return nil
}
let image = Kingfisher<Image>.animated(with: images, forDuration: duration <= 0.0 ? gifDuration : duration)
image?.kf.animatedImageData = data
return image
let image: Image?
if preloadAll || onlyFirstFrame {
guard let (images, gifDuration) = decode(from: imageSource, for: options) else { return nil }
image = onlyFirstFrame ? images.first : Kingfisher<Image>.animated(with: images, forDuration: duration <= 0.0 ? gifDuration : duration)
} else {
let image = Image(data: data)
image?.kf.animatedImageData = data
image = Image(data: data)
image?.kf.imageSource = ImageSource(ref: imageSource)
return image
}
image?.kf.animatedImageData = data
return image
#endif
}

static func image(data: Data, scale: CGFloat, preloadAllGIFData: Bool) -> Image? {
static func image(data: Data, scale: CGFloat, preloadAllGIFData: Bool, onlyFirstFrame: Bool) -> Image? {
var image: Image?

#if os(macOS)
switch data.kf.imageFormat {
case .JPEG: image = Image(data: data)
case .PNG: image = Image(data: data)
case .GIF: image = Kingfisher<Image>.animated(with: data, scale: scale, duration: 0.0, preloadAll: preloadAllGIFData)
case .unknown: image = Image(data: data)
case .JPEG:
image = Image(data: data)
case .PNG:
image = Image(data: data)
case .GIF:
image = Kingfisher<Image>.animated(
with: data,
scale: scale,
duration: 0.0,
preloadAll: preloadAllGIFData,
onlyFirstFrame: onlyFirstFrame)
case .unknown:
image = Image(data: data)
}
#else
switch data.kf.imageFormat {
case .JPEG: image = Image(data: data, scale: scale)
case .PNG: image = Image(data: data, scale: scale)
case .GIF: image = Kingfisher<Image>.animated(with: data, scale: scale, duration: 0.0, preloadAll: preloadAllGIFData)
case .unknown: image = Image(data: data, scale: scale)
case .JPEG:
image = Image(data: data, scale: scale)
case .PNG:
image = Image(data: data, scale: scale)
case .GIF:
image = Kingfisher<Image>.animated(
with: data,
scale: scale,
duration: 0.0,
preloadAll: preloadAllGIFData,
onlyFirstFrame: onlyFirstFrame)
case .unknown:
image = Image(data: data, scale: scale)
}
#endif

Expand Down
6 changes: 5 additions & 1 deletion Sources/ImageProcessor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ public struct DefaultImageProcessor: ImageProcessor {
case .image(let image):
return image
case .data(let data):
return Kingfisher<Image>.image(data: data, scale: options.scaleFactor, preloadAllGIFData: options.preloadAllGIFData)
return Kingfisher<Image>.image(
data: data,
scale: options.scaleFactor,
preloadAllGIFData: options.preloadAllGIFData,
onlyFirstFrame: options.onlyLoadFirstFrame)
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions Sources/KingfisherOptionsInfo.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ public enum KingfisherOptionsInfoItem {
/// By setting this option, the placeholder image parameter of imageview extension method
/// will be ignored and the current image will be kept while loading or downloading the new image.
case keepCurrentImageWhileLoading

/// If set, Kingfisher will only load the first frame from a GIF file as a single image.
/// Loading a lot of GIFs may take too much memory. It will be useful when you want to display a
/// static preview of the first frame from a GIF image.
/// This option will be ignored if the target image is not GIF.
case onlyLoadFirstFrame
}

precedencegroup ItemComparisonPrecedence {
Expand Down Expand Up @@ -141,6 +147,7 @@ func <== (lhs: KingfisherOptionsInfoItem, rhs: KingfisherOptionsInfoItem) -> Boo
case (.processor(_), .processor(_)): return true
case (.cacheSerializer(_), .cacheSerializer(_)): return true
case (.keepCurrentImageWhileLoading, .keepCurrentImageWhileLoading): return true
case (.onlyLoadFirstFrame, .onlyLoadFirstFrame): return true
default: return false
}
}
Expand Down Expand Up @@ -282,4 +289,8 @@ public extension Collection where Iterator.Element == KingfisherOptionsInfoItem
public var keepCurrentImageWhileLoading: Bool {
return contains { $0 <== .keepCurrentImageWhileLoading }
}

public var onlyLoadFirstFrame: Bool {
return contains { $0 <== .onlyLoadFirstFrame }
}
}
10 changes: 10 additions & 0 deletions Tests/KingfisherTests/ImageExtensionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,14 @@ class ImageExtensionTests: XCTestCase {
XCTAssertEqual(image.kf.duration, image.kf.duration)
XCTAssertEqual(image.kf.images!.count, image.kf.images!.count)
}

func testLoadOnlyFirstFrame() {
let image = Kingfisher<Image>.animated(with: testImageGIFData,
scale: 1.0,
duration: 0.0,
preloadAll: true,
onlyFirstFrame: true)!
XCTAssertNotNil(image, "The image should be initiated.")
XCTAssertNil(image.kf.images, "The image should be nil")
}
}
49 changes: 49 additions & 0 deletions Tests/KingfisherTests/ImageViewExtensionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -548,4 +548,53 @@ class ImageViewExtensionTests: XCTestCase {
imageView.kf.setImage(with: url, placeholder: nil, options: [.keepCurrentImageWhileLoading])
XCTAssertEqual(testImage, imageView.image)
}

func testSetGIFImageOnlyFirstFrameThenFullFrames() {
let expectation = self.expectation(description: "wait for downloading image")

let URLString = testKeys[0]

_ = stubRequest("GET", URLString).andReturn(200)?.withBody(NSData(data: testImageGIFData))
let url = URL(string: URLString)!

func loadFullGIFImage() {
var progressBlockIsCalled = false
ImageCache.default.clearMemoryCache()

imageView.kf.setImage(with: url, placeholder: nil, options: [], progressBlock: { (receivedSize, totalSize) -> () in
progressBlockIsCalled = true
XCTAssertTrue(Thread.isMainThread)
}) { (image, error, cacheType, imageURL) -> () in

XCTAssertFalse(progressBlockIsCalled, "progressBlock should not be called since the image is cached.")
XCTAssertNotNil(image, "Downloaded image should exist.")
XCTAssertNotNil(image!.kf.images, "images should exist since we load full GIF.")
XCTAssertEqual(image!.kf.images?.count, 8, "There are 8 frames in total.")

XCTAssert(cacheType == .disk, "We should find it cached in disk")
XCTAssertTrue(Thread.isMainThread)

expectation.fulfill()
}
}

var progressBlockIsCalled = false
imageView.kf.setImage(with: url, placeholder: nil, options: [.onlyLoadFirstFrame], progressBlock: { (receivedSize, totalSize) -> () in
progressBlockIsCalled = true
XCTAssertTrue(Thread.isMainThread)
}) { (image, error, cacheType, imageURL) -> () in
XCTAssertTrue(progressBlockIsCalled, "progressBlock should be called at least once.")
XCTAssertNotNil(image, "Downloaded image should exist.")
XCTAssertNil(image!.kf.images, "images should not exist since we set only load first frame.")

XCTAssert(cacheType == .none, "The cache type should be none here. This image was just downloaded.")
XCTAssertTrue(Thread.isMainThread)

loadFullGIFImage()
}


waitForExpectations(timeout: 5, handler: nil)

}
}
5 changes: 4 additions & 1 deletion Tests/KingfisherTests/KingfisherOptionsInfoTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class KingfisherOptionsInfoTests: XCTestCase {
XCTAssertEqual(options.callbackDispatchQueue.label, DispatchQueue.main.label)
XCTAssertEqual(options.scaleFactor, 1.0)
XCTAssertFalse(options.keepCurrentImageWhileLoading)
XCTAssertFalse(options.onlyLoadFirstFrame)
}


Expand Down Expand Up @@ -89,7 +90,8 @@ class KingfisherOptionsInfoTests: XCTestCase {
KingfisherOptionsInfoItem.scaleFactor(2.0),
.requestModifier(testModifier),
.processor(processor),
.keepCurrentImageWhileLoading
.keepCurrentImageWhileLoading,
.onlyLoadFirstFrame
]

XCTAssertTrue(options.targetCache === cache)
Expand All @@ -113,6 +115,7 @@ class KingfisherOptionsInfoTests: XCTestCase {
XCTAssertTrue(options.modifier is TestModifier)
XCTAssertEqual(options.processor.identifier, processor.identifier)
XCTAssertTrue(options.keepCurrentImageWhileLoading)
XCTAssertTrue(options.onlyLoadFirstFrame)
}
}

Expand Down

0 comments on commit 1da0c5a

Please sign in to comment.