Files
keyboard/Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.m
2025-10-27 21:55:05 +08:00

450 lines
15 KiB
Objective-C

/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDAnimatedImage.h"
#import "NSImage+Compatibility.h"
#import "SDImageCoder.h"
#import "SDImageCodersManager.h"
#import "SDImageFrame.h"
#import "UIImage+MemoryCacheCost.h"
#import "UIImage+Metadata.h"
#import "UIImage+MultiFormat.h"
#import "SDImageCoderHelper.h"
#import "SDImageAssetManager.h"
#import "objc/runtime.h"
static CGFloat SDImageScaleFromPath(NSString *string) {
if (string.length == 0 || [string hasSuffix:@"/"]) return 1;
NSString *name = string.stringByDeletingPathExtension;
__block CGFloat scale = 1;
NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:@"@[0-9]+\\.?[0-9]*x$" options:NSRegularExpressionAnchorsMatchLines error:nil];
[pattern enumerateMatchesInString:name options:kNilOptions range:NSMakeRange(0, name.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) {
scale = [string substringWithRange:NSMakeRange(result.range.location + 1, result.range.length - 2)].doubleValue;
}];
return scale;
}
@interface SDAnimatedImage ()
@property (nonatomic, strong) id<SDAnimatedImageCoder> animatedCoder;
@property (atomic, copy) NSArray<SDImageFrame *> *loadedAnimatedImageFrames; // Mark as atomic to keep thread-safe
@property (nonatomic, assign, getter=isAllFramesLoaded) BOOL allFramesLoaded;
@end
@implementation SDAnimatedImage
@dynamic scale; // call super
#pragma mark - UIImage override method
+ (instancetype)imageNamed:(NSString *)name {
#if __has_include(<UIKit/UITraitCollection.h>) && !SD_WATCH
return [self imageNamed:name inBundle:nil compatibleWithTraitCollection:nil];
#else
return [self imageNamed:name inBundle:nil];
#endif
}
#if __has_include(<UIKit/UITraitCollection.h>) && !SD_WATCH
+ (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle compatibleWithTraitCollection:(UITraitCollection *)traitCollection {
#if SD_VISION
if (!traitCollection) {
traitCollection = UITraitCollection.currentTraitCollection;
}
#else
if (!traitCollection) {
traitCollection = UIScreen.mainScreen.traitCollection;
}
#endif
CGFloat scale = traitCollection.displayScale;
return [self imageNamed:name inBundle:bundle scale:scale];
}
#else
+ (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle {
return [self imageNamed:name inBundle:bundle scale:0];
}
#endif
// 0 scale means automatically check
+ (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle scale:(CGFloat)scale {
if (!name) {
return nil;
}
if (!bundle) {
bundle = [NSBundle mainBundle];
}
SDImageAssetManager *assetManager = [SDImageAssetManager sharedAssetManager];
SDAnimatedImage *image = (SDAnimatedImage *)[assetManager imageForName:name];
if ([image isKindOfClass:[SDAnimatedImage class]]) {
return image;
}
NSString *path = [assetManager getPathForName:name bundle:bundle preferredScale:&scale];
if (!path) {
return image;
}
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
return image;
}
image = [[self alloc] initWithData:data scale:scale];
if (image) {
[assetManager storeImage:image forName:name];
}
return image;
}
+ (instancetype)imageWithContentsOfFile:(NSString *)path {
return [[self alloc] initWithContentsOfFile:path];
}
+ (instancetype)imageWithData:(NSData *)data {
return [[self alloc] initWithData:data];
}
+ (instancetype)imageWithData:(NSData *)data scale:(CGFloat)scale {
return [[self alloc] initWithData:data scale:scale];
}
- (instancetype)initWithContentsOfFile:(NSString *)path {
NSData *data = [NSData dataWithContentsOfFile:path];
if (!data) {
return nil;
}
CGFloat scale = SDImageScaleFromPath(path);
// path extension may be useful for coder like raw-image
NSString *fileExtensionHint = path.pathExtension; // without dot
if (fileExtensionHint.length == 0) {
// Ignore file extension which is empty
fileExtensionHint = nil;
}
SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:1];
mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint;
return [self initWithData:data scale:scale options:[mutableCoderOptions copy]];
}
- (instancetype)initWithData:(NSData *)data {
return [self initWithData:data scale:1];
}
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale {
return [self initWithData:data scale:scale options:nil];
}
- (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale options:(SDImageCoderOptions *)options {
if (!data || data.length == 0) {
return nil;
}
// Vector image does not supported, guard firstly
SDImageFormat format = [NSData sd_imageFormatForImageData:data];
if (format == SDImageFormatSVG || format == SDImageFormatPDF) {
return nil;
}
id<SDAnimatedImageCoder> animatedCoder = nil;
SDImageCoderMutableOptions *mutableCoderOptions;
if (options != nil) {
mutableCoderOptions = [NSMutableDictionary dictionaryWithDictionary:options];
} else {
mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:1];
}
mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
options = [mutableCoderOptions copy];
for (id<SDImageCoder>coder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) {
if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) {
if ([coder canDecodeFromData:data]) {
animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:data options:options];
break;
}
}
}
if (animatedCoder) {
// Animated Image
return [self initWithAnimatedCoder:animatedCoder scale:scale];
} else {
// Static Image (Before 5.19 this code path return nil)
UIImage *image = [[SDImageCodersManager sharedManager] decodedImageWithData:data options:options];
if (!image) {
return nil;
}
// Vector image does not supported, guard secondly
if (image.sd_isVector) {
return nil;
}
#if SD_MAC
self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:kCGImagePropertyOrientationUp];
#else
self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation];
#endif
// Defines the associated object that holds the format for static images
super.sd_imageFormat = format;
return self;
}
}
- (instancetype)initWithAnimatedCoder:(id<SDAnimatedImageCoder>)animatedCoder scale:(CGFloat)scale {
if (!animatedCoder) {
return nil;
}
UIImage *image = [animatedCoder animatedImageFrameAtIndex:0];
if (!image) {
return nil;
}
#if SD_MAC
self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:kCGImagePropertyOrientationUp];
#else
self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation];
#endif
if (self) {
// Only keep the animated coder if frame count > 1, save RAM usage for non-animated image format (APNG/WebP)
if (animatedCoder.animatedImageFrameCount > 1) {
_animatedCoder = animatedCoder;
}
}
return self;
}
- (SDImageFormat)animatedImageFormat {
return [NSData sd_imageFormatForImageData:self.animatedImageData];
}
#pragma mark - Preload
- (void)preloadAllFrames {
if (!_animatedCoder) {
return;
}
if (!self.isAllFramesLoaded) {
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:self.animatedImageFrameCount];
for (size_t i = 0; i < self.animatedImageFrameCount; i++) {
UIImage *image = [self animatedImageFrameAtIndex:i];
NSTimeInterval duration = [self animatedImageDurationAtIndex:i];
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration]; // through the image should be nonnull, used as nullable for `animatedImageFrameAtIndex:`
[frames addObject:frame];
}
self.loadedAnimatedImageFrames = frames;
self.allFramesLoaded = YES;
}
}
- (void)unloadAllFrames {
if (!_animatedCoder) {
return;
}
if (self.isAllFramesLoaded) {
self.loadedAnimatedImageFrames = nil;
self.allFramesLoaded = NO;
}
}
#pragma mark - NSSecureCoding
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
self = [super initWithCoder:aDecoder];
if (self) {
NSData *animatedImageData = [aDecoder decodeObjectOfClass:[NSData class] forKey:NSStringFromSelector(@selector(animatedImageData))];
if (!animatedImageData) {
return self;
}
CGFloat scale = self.scale;
id<SDAnimatedImageCoder> animatedCoder = nil;
for (id<SDImageCoder>coder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) {
if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) {
if ([coder canDecodeFromData:animatedImageData]) {
animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:animatedImageData options:@{SDImageCoderDecodeScaleFactor : @(scale)}];
break;
}
}
}
if (!animatedCoder) {
return self;
}
if (animatedCoder.animatedImageFrameCount > 1) {
_animatedCoder = animatedCoder;
}
}
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[super encodeWithCoder:aCoder];
NSData *animatedImageData = self.animatedImageData;
if (animatedImageData) {
[aCoder encodeObject:animatedImageData forKey:NSStringFromSelector(@selector(animatedImageData))];
}
}
+ (BOOL)supportsSecureCoding {
return YES;
}
#pragma mark - SDAnimatedImageProvider
- (NSData *)animatedImageData {
return [self.animatedCoder animatedImageData];
}
- (NSUInteger)animatedImageLoopCount {
return [self.animatedCoder animatedImageLoopCount];
}
- (NSUInteger)animatedImageFrameCount {
return [self.animatedCoder animatedImageFrameCount];
}
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
if (index >= self.animatedImageFrameCount) {
return nil;
}
if (self.isAllFramesLoaded) {
SDImageFrame *frame = [self.loadedAnimatedImageFrames objectAtIndex:index];
return frame.image;
}
return [self.animatedCoder animatedImageFrameAtIndex:index];
}
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
if (index >= self.animatedImageFrameCount) {
return 0;
}
if (self.isAllFramesLoaded) {
SDImageFrame *frame = [self.loadedAnimatedImageFrames objectAtIndex:index];
return frame.duration;
}
return [self.animatedCoder animatedImageDurationAtIndex:index];
}
@end
@implementation SDAnimatedImage (MemoryCacheCost)
- (NSUInteger)sd_memoryCost {
NSNumber *value = objc_getAssociatedObject(self, @selector(sd_memoryCost));
if (value != nil) {
return value.unsignedIntegerValue;
}
CGImageRef imageRef = self.CGImage;
if (!imageRef) {
return 0;
}
NSUInteger bytesPerFrame = CGImageGetBytesPerRow(imageRef) * CGImageGetHeight(imageRef);
NSUInteger frameCount = 1;
if (self.isAllFramesLoaded) {
frameCount = self.animatedImageFrameCount;
}
frameCount = frameCount > 0 ? frameCount : 1;
NSUInteger cost = bytesPerFrame * frameCount;
return cost;
}
@end
@implementation SDAnimatedImage (Metadata)
- (BOOL)sd_isAnimated {
return self.animatedImageFrameCount > 1;
}
- (NSUInteger)sd_imageLoopCount {
return self.animatedImageLoopCount;
}
- (void)setSd_imageLoopCount:(NSUInteger)sd_imageLoopCount {
return;
}
- (NSUInteger)sd_imageFrameCount {
NSUInteger frameCount = self.animatedImageFrameCount;
if (frameCount > 1) {
return frameCount;
} else {
return 1;
}
}
- (SDImageFormat)sd_imageFormat {
NSData *animatedImageData = self.animatedImageData;
if (animatedImageData) {
return [NSData sd_imageFormatForImageData:animatedImageData];
} else {
return [super sd_imageFormat];
}
}
- (BOOL)sd_isVector {
return NO;
}
@end
@implementation SDAnimatedImage (MultiFormat)
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
return [self sd_imageWithData:data scale:1];
}
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data scale:(CGFloat)scale {
return [self sd_imageWithData:data scale:scale firstFrameOnly:NO];
}
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data scale:(CGFloat)scale firstFrameOnly:(BOOL)firstFrameOnly {
if (!data) {
return nil;
}
return [[self alloc] initWithData:data scale:scale options:@{SDImageCoderDecodeFirstFrameOnly : @(firstFrameOnly)}];
}
- (nullable NSData *)sd_imageData {
NSData *imageData = self.animatedImageData;
if (imageData) {
return imageData;
} else {
return [self sd_imageDataAsFormat:self.animatedImageFormat];
}
}
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
return [self sd_imageDataAsFormat:imageFormat compressionQuality:1];
}
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality {
return [self sd_imageDataAsFormat:imageFormat compressionQuality:compressionQuality firstFrameOnly:NO];
}
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality firstFrameOnly:(BOOL)firstFrameOnly {
// Protect when user input the imageFormat == self.animatedImageFormat && compressionQuality == 1
// This should be treated as grabbing `self.animatedImageData` as well :)
NSData *imageData;
if (imageFormat == self.animatedImageFormat && compressionQuality == 1) {
imageData = self.animatedImageData;
}
if (imageData) return imageData;
SDImageCoderOptions *options = @{SDImageCoderEncodeCompressionQuality : @(compressionQuality), SDImageCoderEncodeFirstFrameOnly : @(firstFrameOnly)};
NSUInteger frameCount = self.animatedImageFrameCount;
if (frameCount <= 1) {
// Static image
imageData = [SDImageCodersManager.sharedManager encodedDataWithImage:self format:imageFormat options:options];
}
if (imageData) return imageData;
NSUInteger loopCount = self.animatedImageLoopCount;
// Keep animated image encoding, loop each frame.
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:frameCount];
for (size_t i = 0; i < frameCount; i++) {
UIImage *image = [self animatedImageFrameAtIndex:i];
NSTimeInterval duration = [self animatedImageDurationAtIndex:i];
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
[frames addObject:frame];
}
imageData = [SDImageCodersManager.sharedManager encodedDataWithFrames:frames loopCount:loopCount format:imageFormat options:options];
return imageData;
}
@end