From 4ae33983e0fa254ab999467ac36f2a2a73569c93 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 25 May 2021 15:38:04 +0800 Subject: [PATCH 1/3] Added `sd_imageFrameCount` convenient API for UIAinmatedImage/NSBitmapImageRep Fix one issue when input UIAnimatedImage contains only 1 image --- SDWebImage/Core/SDImageCoderHelper.m | 10 +++----- SDWebImage/Core/UIImage+Metadata.h | 17 +++++++++++--- SDWebImage/Core/UIImage+Metadata.m | 34 ++++++++++++++++++++++++++++ Tests/Tests/SDAnimatedImageTest.m | 3 +++ Tests/Tests/SDCategoriesTests.m | 1 + Tests/Tests/SDImageCoderTests.m | 2 +- 6 files changed, 56 insertions(+), 11 deletions(-) diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 4244a924..56a645a9 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -135,7 +135,6 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over avgDuration = 0.1; // if it's a animated image but no duration, set it to default 100ms (this do not have that 10ms limit like GIF or WebP to allow custom coder provide the limit) } - __block NSUInteger index = 0; __block NSUInteger repeatCount = 1; __block UIImage *previousImage = animatedImages.firstObject; [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) { @@ -149,15 +148,12 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; [frames addObject:frame]; repeatCount = 1; - index++; } previousImage = image; - // last one - if (idx == frameCount - 1) { - SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; - [frames addObject:frame]; - } }]; + // last one + SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; + [frames addObject:frame]; #else diff --git a/SDWebImage/Core/UIImage+Metadata.h b/SDWebImage/Core/UIImage+Metadata.h index 8328c261..6a278e2e 100644 --- a/SDWebImage/Core/UIImage+Metadata.h +++ b/SDWebImage/Core/UIImage+Metadata.h @@ -20,12 +20,23 @@ * For animated image format, 0 means infinite looping. * Note that because of the limitations of categories this property can get out of sync if you create another instance with CGImage or other methods. * AppKit: - * NSImage currently only support animated via GIF imageRep unlike UIImage. - * The getter of this property will get the loop count from GIF imageRep - * The setter of this property will set the loop count from GIF imageRep + * NSImage currently only support animated via `NSBitmapImageRep`(GIF) or `SDAnimatedImageRep`(APNG/GIF/WebP) unlike UIImage. + * The getter of this property will get the loop count from animated imageRep + * The setter of this property will set the loop count from animated imageRep */ @property (nonatomic, assign) NSUInteger sd_imageLoopCount; +/** + * UIKit: + * Returns the `images`'s count by unapply the patch for the different frame durations. Which matches the real visible frame count when displaying on UIImageView. + * See more in `SDImageCoderHelper.animatedImageWithFrames`. + * Returns 1 for static image. + * AppKit: + * Returns the underlaying `NSBitmapImageRep` or `SDAnimatedImageRep` frame count. + * Returns 1 for static image. + */ +@property (nonatomic, assign, readonly) NSUInteger sd_imageFrameCount; + /** * UIKit: * Check the `images` array property. diff --git a/SDWebImage/Core/UIImage+Metadata.m b/SDWebImage/Core/UIImage+Metadata.m index 09724236..0db5ae73 100644 --- a/SDWebImage/Core/UIImage+Metadata.m +++ b/SDWebImage/Core/UIImage+Metadata.m @@ -29,6 +29,27 @@ objc_setAssociatedObject(self, @selector(sd_imageLoopCount), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +- (NSUInteger)sd_imageFrameCount { + NSArray *animatedImages = self.images; + if (!animatedImages || animatedImages.count <= 1) { + return 1; + } + __block NSUInteger frameCount = 1; + __block UIImage *previousImage = animatedImages.firstObject; + [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) { + // ignore first + if (idx == 0) { + return; + } + if (![image isEqual:previousImage]) { + frameCount++; + } + previousImage = image; + }]; + + return frameCount; +} + - (BOOL)sd_isAnimated { return (self.images != nil); } @@ -87,6 +108,19 @@ } } +- (NSUInteger)sd_imageFrameCount { + NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); + NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; + NSBitmapImageRep *bitmapImageRep; + if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { + bitmapImageRep = (NSBitmapImageRep *)imageRep; + } + if (bitmapImageRep) { + return [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; + } + return 1; +} + - (BOOL)sd_isAnimated { BOOL isAnimated = NO; NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); diff --git a/Tests/Tests/SDAnimatedImageTest.m b/Tests/Tests/SDAnimatedImageTest.m index 7d93b32a..6d3cd854 100644 --- a/Tests/Tests/SDAnimatedImageTest.m +++ b/Tests/Tests/SDAnimatedImageTest.m @@ -770,7 +770,10 @@ static BOOL _isCalled; [[SDImageCodersManager sharedManager] addCoder:[SDImageAWebPCoder sharedCoder]]; UIImage *image = [UIImage sd_imageWithData:[NSData dataWithContentsOfFile:[self testMemotyCostImagePath]]]; NSUInteger cost = [image sd_memoryCost]; +#if SD_UIKIT expect(image.images.count).equal(5333); +#endif + expect(image.sd_imageFrameCount).equal(16); expect(image.scale).equal(1); expect(cost).equal(16 * image.size.width * image.size.height * 4); [[SDImageCodersManager sharedManager] removeCoder:[SDImageAWebPCoder sharedCoder]]; diff --git a/Tests/Tests/SDCategoriesTests.m b/Tests/Tests/SDCategoriesTests.m index ee5aaf56..d03ba431 100644 --- a/Tests/Tests/SDCategoriesTests.m +++ b/Tests/Tests/SDCategoriesTests.m @@ -59,6 +59,7 @@ image = [UIImage sd_imageWithGIFData:data]; expect(image).notTo.beNil(); expect(image.sd_isAnimated).beTruthy(); + expect(image.sd_imageFrameCount).equal(5); } #pragma mark - Helper diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 6184efff..eae64697 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -435,7 +435,7 @@ withLocalImageURL:(NSURL *)imageUrl } } -- (NSArray *)thumbnailImagesFromImageSource:(CGImageSourceRef)source API_AVAILABLE(ios(11.0), tvos(11.0), macos(13.0)) { +- (NSArray *)thumbnailImagesFromImageSource:(CGImageSourceRef)source API_AVAILABLE(ios(11.0), tvos(11.0), macos(10.13)) { NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil); NSDictionary *fileProperties = properties[(__bridge NSString *)kCGImagePropertyFileContentsDictionary]; NSArray *imagesProperties = fileProperties[(__bridge NSString *)kCGImagePropertyImages]; From 7f078d21ba4cc5c3bfa83a6c1800d34e770acec5 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 25 May 2021 16:05:42 +0800 Subject: [PATCH 2/3] Adopt SDAnimatedImage for `sd_imageFrameCount` --- SDWebImage/Core/SDAnimatedImage.m | 4 ++++ SDWebImage/Core/UIImage+MemoryCacheCost.m | 2 +- Tests/Tests/SDImageCoderTests.m | 8 ++------ 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/SDWebImage/Core/SDAnimatedImage.m b/SDWebImage/Core/SDAnimatedImage.m index 6f3916ce..f98c4929 100644 --- a/SDWebImage/Core/SDAnimatedImage.m +++ b/SDWebImage/Core/SDAnimatedImage.m @@ -314,6 +314,10 @@ static CGFloat SDImageScaleFromPath(NSString *string) { return; } +- (NSUInteger)sd_imageFrameCount { + return self.animatedImageFrameCount; +} + - (SDImageFormat)sd_imageFormat { return self.animatedImageFormat; } diff --git a/SDWebImage/Core/UIImage+MemoryCacheCost.m b/SDWebImage/Core/UIImage+MemoryCacheCost.m index 4cf80bf8..b9365009 100644 --- a/SDWebImage/Core/UIImage+MemoryCacheCost.m +++ b/SDWebImage/Core/UIImage+MemoryCacheCost.m @@ -21,7 +21,7 @@ FOUNDATION_STATIC_INLINE NSUInteger SDMemoryCacheCostForImage(UIImage *image) { frameCount = 1; #elif SD_UIKIT || SD_WATCH // Filter the same frame in `_UIAnimatedImage`. - frameCount = image.images.count > 0 ? [NSSet setWithArray:image.images].count : 1; + frameCount = image.images.count > 1 ? [NSSet setWithArray:image.images].count : 1; #endif NSUInteger cost = bytesPerFrame * frameCount; return cost; diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index eae64697..7ae1f717 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -410,9 +410,7 @@ withLocalImageURL:(NSURL *)imageUrl UIImage *outputImage = [coder decodedImageWithData:outputImageData options:nil]; expect(outputImage.size).to.equal(inputImage.size); expect(outputImage.scale).to.equal(inputImage.scale); -#if SD_UIKIT - expect(outputImage.images.count).to.equal(inputImage.images.count); -#endif + expect(outputImage.sd_imageLoopCount).to.equal(inputImage.sd_imageLoopCount); // check max pixel size encoding with scratch CGFloat maxWidth = 50; @@ -429,9 +427,7 @@ withLocalImageURL:(NSURL *)imageUrl // Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1 expect(ABS(outputMaxImage.size.width - maxPixelSize.width)).beLessThanOrEqualTo(1); expect(ABS(outputMaxImage.size.height - maxPixelSize.height)).beLessThanOrEqualTo(1); -#if SD_UIKIT - expect(outputMaxImage.images.count).to.equal(inputImage.images.count); -#endif + expect(outputMaxImage.sd_imageLoopCount).to.equal(inputImage.sd_imageLoopCount); } } From a77e5f561eea327e71a99693eef3fbb3a05e6dd8 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Tue, 25 May 2021 16:08:53 +0800 Subject: [PATCH 3/3] Avoid extra calculation for sd_imageFrameCount --- SDWebImage/Core/UIImage+Metadata.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/SDWebImage/Core/UIImage+Metadata.m b/SDWebImage/Core/UIImage+Metadata.m index 0db5ae73..b8f4fd82 100644 --- a/SDWebImage/Core/UIImage+Metadata.m +++ b/SDWebImage/Core/UIImage+Metadata.m @@ -34,6 +34,10 @@ if (!animatedImages || animatedImages.count <= 1) { return 1; } + NSNumber *value = objc_getAssociatedObject(self, @selector(sd_imageFrameCount)); + if ([value isKindOfClass:[NSNumber class]]) { + return [value unsignedIntegerValue]; + } __block NSUInteger frameCount = 1; __block UIImage *previousImage = animatedImages.firstObject; [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) { @@ -46,6 +50,7 @@ } previousImage = image; }]; + objc_setAssociatedObject(self, @selector(sd_imageFrameCount), @(frameCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC); return frameCount; }