290 lines
9.6 KiB
Objective-C
290 lines
9.6 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 "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<SDAnimatedImageCoder> coder;
|
|
@property (nonatomic, assign, readwrite) SDImageFormat animatedImageFormat;
|
|
@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>)
|
|
return [self imageNamed:name inBundle:nil compatibleWithTraitCollection:nil];
|
|
#else
|
|
return [self imageNamed:name inBundle:nil];
|
|
#endif
|
|
}
|
|
|
|
#if __has_include(<UIKit/UITraitCollection.h>)
|
|
+ (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<SDAnimatedImageCoder> animatedCoder = nil;
|
|
for (id<SDImageCoder>coder 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<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) {
|
|
_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<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 (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<SDAnimatedImageCoder> animatedCoder = nil;
|
|
for (id<SDImageCoder>coder 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
|