/* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * 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 "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) { if (result.range.location >= 3) { scale = [string substringWithRange:NSMakeRange(result.range.location + 1, result.range.length - 2)].doubleValue; } }]; return scale; } @interface SDAnimatedImage () @property (nonatomic, strong) id coder; @property (nonatomic, assign, readwrite) SDImageFormat animatedImageFormat; @property (atomic, copy) NSArray *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() return [self imageNamed:name inBundle:nil compatibleWithTraitCollection:nil]; #else return [self imageNamed:name inBundle:nil]; #endif } #if __has_include() + (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle compatibleWithTraitCollection:(UITraitCollection *)traitCollection { if (!traitCollection) { traitCollection = UIScreen.mainScreen.traitCollection; } 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]; return [self initWithData:data scale:SDImageScaleFromPath(path)]; } - (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; } data = [data copy]; // avoid mutable data id animatedCoder = nil; for (idcoder in [SDImageCodersManager sharedManager].coders) { if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) { if ([coder canDecodeFromData:data]) { if (!options) { options = @{SDImageCoderDecodeScaleFactor : @(scale)}; } animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:data options:options]; break; } } } if (!animatedCoder) { return nil; } return [self initWithAnimatedCoder:animatedCoder scale:scale]; } - (instancetype)initWithAnimatedCoder:(id)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) { _coder = animatedCoder; NSData *data = [animatedCoder animatedImageData]; SDImageFormat format = [NSData sd_imageFormatForImageData:data]; _animatedImageFormat = format; } return self; } #pragma mark - Preload - (void)preloadAllFrames { if (!self.isAllFramesLoaded) { NSMutableArray *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 (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))]; CGFloat scale = self.scale; if (!animatedImageData) { return self; } id animatedCoder = nil; for (idcoder in [SDImageCodersManager sharedManager].coders) { if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) { if ([coder canDecodeFromData:animatedImageData]) { animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:animatedImageData options:@{SDImageCoderDecodeScaleFactor : @(scale)}]; break; } } } if (!animatedCoder) { return self; } _coder = animatedCoder; SDImageFormat format = [NSData sd_imageFormatForImageData:animatedImageData]; _animatedImageFormat = format; } 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 - SDAnimatedImage - (NSData *)animatedImageData { return [self.coder animatedImageData]; } - (NSUInteger)animatedImageLoopCount { return [self.coder animatedImageLoopCount]; } - (NSUInteger)animatedImageFrameCount { return [self.coder 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.coder 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.coder 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