123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449 |
- /*
- * 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
|