From 50197df41bd0b4e4c551ff9a944238677451a62c Mon Sep 17 00:00:00 2001 From: Yang Chao Date: Sat, 13 Jan 2024 13:59:22 +0800 Subject: [PATCH 1/6] Cache decoded frames in animated webp to boost performance --- Sources/Image+WebP.swift | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Sources/Image+WebP.swift b/Sources/Image+WebP.swift index 776a9a1..76ae123 100644 --- a/Sources/Image+WebP.swift +++ b/Sources/Image+WebP.swift @@ -103,6 +103,7 @@ class WebPFrameSource: ImageFrameSource { let data: Data? private let decoder: WebPDecoderRef private var decoderLock: UnsafeMutablePointer + private var frameCache = NSCache() var frameCount: Int { get { @@ -115,9 +116,14 @@ class WebPFrameSource: ImageFrameSource { defer { os_unfair_lock_unlock(decoderLock) } - guard let image = WebPDecoderCopyImageAtIndex(decoder, Int32(index)) else { - return nil + var image = frameCache.object(forKey: index as NSNumber) + if image == nil { + image = WebPDecoderCopyImageAtIndex(decoder, Int32(index)) + if image != nil { + frameCache.setObject(image!, forKey: index as NSNumber) + } } + guard let image = image else { return nil } if let maxSize = maxSize, maxSize != .zero, CGFloat(image.width) > maxSize.width || CGFloat(image.height) > maxSize.height { let scale = min(maxSize.width / CGFloat(image.width), maxSize.height / CGFloat(image.height)) let destWidth = Int(CGFloat(image.width) * scale) From f282c015c85c4e61633a97cacb01df78c19dc30a Mon Sep 17 00:00:00 2001 From: Yang Chao Date: Sat, 13 Jan 2024 14:02:08 +0800 Subject: [PATCH 2/6] Support serialization for animated images with variable durations --- Sources/KingfisherWebP-ObjC/CGImage+WebP.m | 25 +++++++++++++++---- .../include/CGImage+WebP.h | 1 + 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/Sources/KingfisherWebP-ObjC/CGImage+WebP.m b/Sources/KingfisherWebP-ObjC/CGImage+WebP.m index 379e7ae..ca7bfac 100644 --- a/Sources/KingfisherWebP-ObjC/CGImage+WebP.m +++ b/Sources/KingfisherWebP-ObjC/CGImage+WebP.m @@ -317,6 +317,7 @@ CFDataRef WebPDataCreateWithImage(CGImageRef image, bool isLossy, float quality) const CFStringRef kWebPAnimatedImageDuration = CFSTR("kWebPAnimatedImageDuration"); const CFStringRef kWebPAnimatedImageLoopCount = CFSTR("kWebPAnimatedImageLoopCount"); const CFStringRef kWebPAnimatedImageFrames = CFSTR("kWebPAnimatedImageFrames"); +const CFStringRef kWebPAnimatedImageFrameDurations = CFSTR("kWebPAnimatedImageFrameDurations"); uint32_t WebPImageFrameCountGetFromData(CFDataRef webpData) { WebPData webp_data; @@ -412,11 +413,16 @@ CFDataRef WebPDataCreateWithAnimatedImageInfo(CFDictionaryRef imageInfo, bool is CFNumberRef loopCount = CFDictionaryGetValue(imageInfo, kWebPAnimatedImageLoopCount); CFNumberRef durationRef = CFDictionaryGetValue(imageInfo, kWebPAnimatedImageDuration); CFArrayRef imageFrames = CFDictionaryGetValue(imageInfo, kWebPAnimatedImageFrames); + CFArrayRef frameDurations = CFDictionaryGetValue(imageInfo, kWebPAnimatedImageFrameDurations); if (!imageFrames || CFArrayGetCount(imageFrames) < 1) { return NULL; } + if (frameDurations && CFArrayGetCount(frameDurations) != CFArrayGetCount(imageFrames)) { + return NULL; + } + WebPAnimEncoderOptions enc_options; WebPAnimEncoderOptionsInit(&enc_options); if (loopCount) { @@ -429,13 +435,14 @@ CFDataRef WebPDataCreateWithAnimatedImageInfo(CFDictionaryRef imageInfo, bool is return NULL; } - int frameDurationInMilliSec = 100; - if (durationRef) { + int defaultDurationInMilliSec = 100; + if (durationRef && !frameDurations) { double totalDurationInSec; CFNumberGetValue(durationRef, kCFNumberDoubleType, &totalDurationInSec); - frameDurationInMilliSec = (int)(totalDurationInSec * 1000 / CFArrayGetCount(imageFrames)); + defaultDurationInMilliSec = (int)(totalDurationInSec * 1000 / CFArrayGetCount(imageFrames)); } + int timestamp = 0; for (CFIndex i = 0; i < CFArrayGetCount(imageFrames); i ++) { WebPPicture frame; WebPPictureInit(&frame); @@ -447,11 +454,19 @@ CFDataRef WebPDataCreateWithAnimatedImageInfo(CFDictionaryRef imageInfo, bool is } else { WebPConfigLosslessPreset(&config, 0); } - WebPAnimEncoderAdd(enc, &frame, (int)(frameDurationInMilliSec * i), &config); + WebPAnimEncoderAdd(enc, &frame, timestamp, &config); + if (frameDurations) { + CFNumberRef frameDuration = CFArrayGetValueAtIndex(frameDurations, i); + double durationInSec = 0.1; + CFNumberGetValue(frameDuration, kCFNumberDoubleType, &durationInSec); + timestamp += (int)(durationInSec * 1000); + } else { + timestamp += defaultDurationInMilliSec; + } } WebPPictureFree(&frame); } - WebPAnimEncoderAdd(enc, NULL, (int)(frameDurationInMilliSec * CFArrayGetCount(imageFrames)), NULL); + WebPAnimEncoderAdd(enc, NULL, timestamp, NULL); WebPData webp_data; WebPDataInit(&webp_data); diff --git a/Sources/KingfisherWebP-ObjC/include/CGImage+WebP.h b/Sources/KingfisherWebP-ObjC/include/CGImage+WebP.h index 2404e9b..88e1415 100644 --- a/Sources/KingfisherWebP-ObjC/include/CGImage+WebP.h +++ b/Sources/KingfisherWebP-ObjC/include/CGImage+WebP.h @@ -19,6 +19,7 @@ CFDataRef __nullable WebPDataCreateWithImage(CGImageRef image, bool isLossy, flo CG_EXTERN const CFStringRef kWebPAnimatedImageDuration; CG_EXTERN const CFStringRef kWebPAnimatedImageLoopCount; CG_EXTERN const CFStringRef kWebPAnimatedImageFrames; // CFArrayRef of CGImageRef +CG_EXTERN const CFStringRef kWebPAnimatedImageFrameDurations; // CFArrayRef of CFNumberRef uint32_t WebPImageFrameCountGetFromData(CFDataRef webpData); CFDictionaryRef __nullable WebPAnimatedImageInfoCreateWithData(CFDataRef webpData); From 19c6f660c3e9cb185fce6f587f7a78a004a4ac7f Mon Sep 17 00:00:00 2001 From: Yang Chao Date: Sat, 13 Jan 2024 15:39:31 +0800 Subject: [PATCH 3/6] Ensure the original data is used when encoding image with frame source --- Sources/WebPSerializer.swift | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Sources/WebPSerializer.swift b/Sources/WebPSerializer.swift index 750bc4d..f54b4d9 100644 --- a/Sources/WebPSerializer.swift +++ b/Sources/WebPSerializer.swift @@ -25,14 +25,19 @@ public struct WebPSerializer: CacheSerializer { private init() {} public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { - if let original = original, originalDataUsed { - return original - } else if let original = original, !original.isWebPFormat { + if originalDataUsed { + if let original = original { + return original + } + if let frameData = image.kf.frameSource?.data { + return frameData + } + } + if let original = original, !original.isWebPFormat { return DefaultCacheSerializer.default.data(with: image, original: original) - } else { - let qualityInWebp = min(max(0, compressionQuality), 1) * 100 - return image.kf.normalized.kf.webpRepresentation(isLossy: isLossy, quality: Float(qualityInWebp)) } + let qualityInWebp = min(max(0, compressionQuality), 1) * 100 + return image.kf.normalized.kf.webpRepresentation(isLossy: isLossy, quality: Float(qualityInWebp)) } public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { From 3e08ad6a5ca2898e466cacf80f293360e91c828f Mon Sep 17 00:00:00 2001 From: Yang Chao Date: Sat, 13 Jan 2024 15:41:05 +0800 Subject: [PATCH 4/6] Support encoding images with frame source --- Sources/Image+WebP.swift | 29 ++++++++++++---- Tests/KingfisherWebPTests.swift | 59 +++++++++++++++++++++++++++++++-- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/Sources/Image+WebP.swift b/Sources/Image+WebP.swift index 76ae123..0f6cf0f 100644 --- a/Sources/Image+WebP.swift +++ b/Sources/Image+WebP.swift @@ -14,6 +14,10 @@ import Foundation import KingfisherWebP_ObjC #endif +#if canImport(AppKit) +import AppKit +#endif + // MARK: - Image Representation extension KingfisherWrapper where Base: KFCrossPlatformImage { /// isLossy (0=lossy , 1=lossless (default)). @@ -37,16 +41,27 @@ extension KingfisherWrapper where Base: KFCrossPlatformImage { /// isLossy (0=lossy , 1=lossless (default)). /// Note that the default values are isLossy= false and quality=75.0f private func animatedWebPRepresentation(isLossy: Bool = false, quality: Float = 75.0) -> Data? { - #if os(macOS) - return nil - #else - guard let images = base.images?.compactMap({ $0.cgImage }) else { + let imageInfo: [CFString: Any] + if let frameSource = frameSource { + let frameCount = frameSource.frameCount + imageInfo = [ + kWebPAnimatedImageFrames: (0.. TimeInterval { return 0 } + func frame(at index: Int, maxSize: CGSize?) -> CGImage? { + KFCrossPlatformImage(data: .init(fileName: "cover.png"))?.kfCGImage + } + } + + let source = RandomFrameSource() + let image = KingfisherWrapper.animatedImage(source: source, options: .init())! + let encoded2 = s.data(with: image, original: nil) + XCTAssertEqual(encoded2, source.data, "Original data should be used") + } + func testEncodingPerformance() { let s = WebPSerializer.default let images = fileNames.compactMap { fileName -> KFCrossPlatformImage? in From f9b3dc9ce93a58cc34206ca6dc80c08aee7930b8 Mon Sep 17 00:00:00 2001 From: Yang Chao Date: Sat, 13 Jan 2024 15:42:24 +0800 Subject: [PATCH 5/6] fix build failure on iOS --- Tests/KingfisherWebPTests.swift | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Tests/KingfisherWebPTests.swift b/Tests/KingfisherWebPTests.swift index 1ccedea..f6d556e 100644 --- a/Tests/KingfisherWebPTests.swift +++ b/Tests/KingfisherWebPTests.swift @@ -192,8 +192,13 @@ class KingfisherWebPTests: XCTestCase { XCTAssertEqual(originalFrameSource.frameCount, encodedFrameSource.frameCount) (0.. Date: Thu, 22 Feb 2024 23:42:10 +0800 Subject: [PATCH 6/6] update dependencies --- KingfisherWebP.podspec | 2 +- KingfisherWebP.xcodeproj/project.pbxproj | 4 ++-- Package.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/KingfisherWebP.podspec b/KingfisherWebP.podspec index 899a959..9af50a5 100644 --- a/KingfisherWebP.podspec +++ b/KingfisherWebP.podspec @@ -36,6 +36,6 @@ KingfisherWebP is an extension of the popular library [Kingfisher](https://githu 'USER_HEADER_SEARCH_PATHS' => '$(inherited) $(SRCROOT)/libwebp/src' } - s.dependency 'Kingfisher', '~> 7.9' + s.dependency 'Kingfisher', '~> 7.11' s.dependency 'libwebp', '>= 1.1.0' end diff --git a/KingfisherWebP.xcodeproj/project.pbxproj b/KingfisherWebP.xcodeproj/project.pbxproj index f45d703..b54f015 100644 --- a/KingfisherWebP.xcodeproj/project.pbxproj +++ b/KingfisherWebP.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 53; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -587,7 +587,7 @@ repositoryURL = "https://github.com/onevcat/Kingfisher.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 7.8.0; + minimumVersion = 7.11.0; }; }; 38E23F732591BBB000EBE21D /* XCRemoteSwiftPackageReference "libwebp-Xcode" */ = { diff --git a/Package.swift b/Package.swift index 25eaf2e..e400117 100644 --- a/Package.swift +++ b/Package.swift @@ -10,7 +10,7 @@ let package = Package( .library(name: "KingfisherWebP", targets: ["KingfisherWebP"]) ], dependencies: [ - .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.8.1"), + .package(url: "https://github.com/onevcat/Kingfisher.git", from: "7.11.0"), .package(url: "https://github.com/SDWebImage/libwebp-Xcode", from: "1.1.0") ], targets: [