Use the context arg to pass the SDAnimatedImage class to create the instance by image cache and downloader.

Also enhance view category to support firstFrameOnly, scaleFactor and preload
This commit is contained in:
DreamPiggy 2018-01-24 00:44:44 +08:00
parent 87bbcdc46f
commit f94dd00c52
28 changed files with 365 additions and 116 deletions

View File

@ -27,7 +27,7 @@
- (NSUInteger)animatedImageLoopCount;
/**
Returns the frame image from a specified index.
This method may be called on background thread. And the index maybe randomly if one image was set to different imageViews, keep it re-entrant.
@note The index maybe randomly if one image was set to different imageViews, keep it re-entrant. (It's not recommend to store the images into array because it's memory consuming)
@param index Frame index (zero based).
@return Frame's image
@ -35,12 +35,22 @@
- (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index;
/**
Returns the frames's duration from a specified index.
@note The index maybe randomly if one image was set to different imageViews, keep it re-entrant. (It's recommend to store the durations into array because it's not memory-consuming)
@param index Frame index (zero based).
@return Frame's duration
*/
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index;
@optional
/**
Preload all frame image to memory. Then directly return the frame for index without decoding.
This method may be called on background thread.
@note If the image is shared by lots of imageViews, preload all frames will reduce the CPU cost because the decoder may not need to keep re-entrant for randomly index access.
*/
- (void)preloadAllFrames;
@end
@interface SDAnimatedImage : UIImage <SDAnimatedImage>
@ -63,4 +73,13 @@
*/
@property (nonatomic, copy, readonly, nullable) NSData *animatedImageData;
/**
Preload all frame image to memory. Then directly return the frame for index without decoding.
The preloaded animated image frames will be removed when receiving memory warning.
@note If the image is shared by lots of imageViews, preload all frames will reduce the CPU cost because the decoder may not need to keep re-entrant for randomly index access.
@note Once preload the frames into memory, there is no huge differenec on performance between UIImage's `animatedImageWithImages:duration:` for UIKit. But UIImage's animation have some issue such like blanking or frame resetting. It's recommend to use only if need.
*/
- (void)preloadAllFrames;
@end

View File

@ -11,6 +11,7 @@
#import "UIImage+WebCache.h"
#import "SDWebImageCoder.h"
#import "SDWebImageCodersManager.h"
#import "SDWebImageFrame.h"
static CGFloat SDImageScaleFromPath(NSString *string) {
if (string.length == 0 || [string hasSuffix:@"/"]) return 1;
@ -32,8 +33,11 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
@property (nonatomic, strong) id<SDWebImageAnimatedCoder> coder;
@property (nonatomic, assign, readwrite) NSUInteger animatedImageLoopCount;
@property (nonatomic, assign, readwrite) NSUInteger animatedImageFrameCount;
@property (nonatomic, copy, readwrite) NSData *animatedImageData;
@property (nonatomic, assign, readwrite) SDImageFormat animatedImageFormat;
@property (nonatomic, assign) BOOL animatedImageLoopCountCheck;
@property (atomic, copy) NSArray<SDWebImageFrame *> *preloadAnimatedImageFrames;
@property (nonatomic, assign) BOOL animatedImageFramesPreloaded;
@property (nonatomic, assign) BOOL animatedImageLoopCountChecked;
@property (nonatomic, assign) BOOL animatedImageFrameCountChecked;
#if SD_MAC
@ -44,6 +48,21 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
@implementation SDAnimatedImage
#pragma mark - Dealloc & Memory warning
- (void)dealloc {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
- (void)didReceiveMemoryWarning:(NSNotification *)notification {
if (self.animatedImageFramesPreloaded) {
self.preloadAnimatedImageFrames = nil;
self.animatedImageFramesPreloaded = NO;
}
}
#pragma mark - UIImage override method
+ (instancetype)imageWithContentsOfFile:(NSString *)path {
return [[self alloc] initWithContentsOfFile:path];
@ -70,14 +89,8 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
if (!data || data.length == 0) {
return nil;
}
if (scale <= 0) {
#if SD_WATCH
scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
scale = [UIScreen mainScreen].scale;
#endif
}
for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedInstance].coders) {
data = [data copy]; // avoid mutable data
for (id<SDWebImageCoder>coder in [SDWebImageCodersManager sharedManager].coders) {
if ([coder conformsToProtocol:@protocol(SDWebImageAnimatedCoder)]) {
if ([coder canDecodeFromData:data]) {
id<SDWebImageAnimatedCoder> animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:data];
@ -98,19 +111,44 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
if (!image) {
return nil;
}
if (scale <= 0) {
scale = 1;
}
#if SD_MAC
self = [super initWithCGImage:image.CGImage size:NSZeroSize];
self = [super initWithCGImage:image.CGImage scale:scale];
#else
self = [super initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
#endif
if (!self) {
return nil;
if (self) {
#if SD_MAC
_scale = scale;
#endif
_animatedImageData = data;
SDImageFormat format = [NSData sd_imageFormatForImageData:data];
_animatedImageFormat = format;
}
SDImageFormat format = [NSData sd_imageFormatForImageData:data];
self.animatedImageFormat = format;
return self;
}
#pragma mark - Preload
- (void)preloadAllFrames {
if (!self.animatedImageFramesPreloaded) {
NSMutableArray<SDWebImageFrame *> *frames = [NSMutableArray arrayWithCapacity:self.animatedImageFrameCount];
for (size_t i = 0; i < self.animatedImageFrameCount; i++) {
UIImage *image = [self animatedImageFrameAtIndex:i];
NSTimeInterval duration = [self animatedImageDurationAtIndex:i];
SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:image duration:duration]; // through the image should be nonnull, used as nullable for `animatedImageFrameAtIndex:`
[frames addObject:frame];
}
self.preloadAnimatedImageFrames = frames;
self.animatedImageFramesPreloaded = YES;
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
}
#pragma mark - NSSecureCoding
- (instancetype)initWithCoder:(NSCoder *)aDecoder {
NSNumber *scale = [aDecoder decodeObjectOfClass:[NSNumber class] forKey:NSStringFromSelector(@selector(scale))];
@ -137,8 +175,8 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
#pragma mark - SDAnimatedImage
- (NSUInteger)animatedImageLoopCount {
if (!self.animatedImageLoopCountCheck) {
self.animatedImageLoopCountCheck = YES;
if (!self.animatedImageLoopCountChecked) {
self.animatedImageLoopCountChecked = YES;
_animatedImageLoopCount = [self.coder animatedImageLoopCount];
}
return _animatedImageLoopCount;
@ -153,15 +191,25 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
}
- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
if (index >= self.animatedImageFrameCount) {
return nil;
}
if (self.animatedImageFramesPreloaded) {
SDWebImageFrame *frame = [self.preloadAnimatedImageFrames objectAtIndex:index];
return frame.image;
}
return [self.coder animatedImageFrameAtIndex:index];
}
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
if (index >= self.animatedImageFrameCount) {
return 0;
}
if (self.animatedImageFramesPreloaded) {
SDWebImageFrame *frame = [self.preloadAnimatedImageFrames objectAtIndex:index];
return frame.duration;
}
return [self.coder animatedImageDurationAtIndex:index];
}
- (NSData *)animatedImageData {
return self.coder.animatedImageData;
}
@end

View File

@ -11,6 +11,7 @@
#if SD_UIKIT || SD_MAC
#import "UIView+WebCache.h"
#import "SDAnimatedImage.h"
@implementation SDAnimatedImageView (WebCache)
@ -43,13 +44,16 @@
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
Class animatedImageClass = [SDAnimatedImage class];
SDWebImageContext *context = @{SDWebImageContextAnimatedImageClass : animatedImageClass};
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
completed:completedBlock
context:context];
}
@end

View File

@ -31,7 +31,7 @@
*/
@property (nonatomic, assign, readonly) NSUInteger currentLoopCount;
/**
YES to choose `animationRepeatCount` property instead of image's loop count for animtion loop count. Default is NO.
YES to choose `animationRepeatCount` property instead of image's loop count for animation loop count. Default is NO.
*/
@property (nonatomic, assign) BOOL shouldCustomLoopCount;
/**
@ -40,6 +40,11 @@
This class override UIImageView's `animationRepeatCount` property on iOS, use this property as well.
*/
@property (nonatomic, assign) NSInteger animationRepeatCount;
/**
Returns a Boolean value indicating whether the animation is running.
This class override UIImageView's `animating` property on iOS, use this property as well.
*/
@property (nonatomic, readonly, getter=isAnimating) BOOL animating;
/**
Provide a max buffer size by bytes. This is used to adjust frame buffer count and can be useful when the decoding cost is expensive (such as Animated WebP software decoding). Default is 0.
`0` means automatically adjust by calculating current memory usage.

View File

@ -19,7 +19,7 @@ static CVReturn renderCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *
#endif
static NSUInteger SDDeviceTotalMemory() {
return [[NSProcessInfo processInfo] physicalMemory];
return (NSUInteger)[[NSProcessInfo processInfo] physicalMemory];
}
static NSUInteger SDDeviceFreeMemory() {
@ -226,6 +226,7 @@ dispatch_semaphore_signal(self->_lock);
self.bufferMiss = NO;
self.shouldAnimate = NO;
self.maxBufferCount = 0;
self.layer.contentsScale = 1;
[_frameBuffer removeAllObjects];
_frameBuffer = nil;
[_fetchQueue cancelAllOperations];
@ -276,6 +277,7 @@ dispatch_semaphore_signal(self->_lock);
[self startAnimating];
}
self.layer.contentsScale = image.scale;
[self.layer setNeedsDisplay];
}
}

View File

@ -40,6 +40,14 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
* Use this flag to transform them anyway.
*/
SDImageCacheTransformAnimatedImage = 1 << 2,
/**
* By default, we decode the animated image. This flag can force decode the first frame only and produece the static image.
*/
SDImageCacheDecodeFirstFrameOnly = 1 << 3,
/**
* By default, for `SDAnimatedImage`, we decode the animated image frame during rendering to reduce memory usage. This flag actually trigger `preloadAllAnimatedImageFrames = YES` after image load from disk cache
*/
SDImageCachePreloadAllFrames = 1 << 4
};
typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);

View File

@ -9,9 +9,11 @@
#import "SDImageCache.h"
#import <CommonCrypto/CommonDigest.h>
#import "NSImage+Additions.h"
#import "UIImage+WebCache.h"
#import "SDWebImageCodersManager.h"
#import "SDWebImageTransformer.h"
#import "SDWebImageCoderHelper.h"
#import "SDAnimatedImage.h"
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
@ -476,11 +478,42 @@ FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
}
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
return [self diskImageForKey:key data:data options:0 context:nil];
}
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context {
if (data) {
UIImage *image = [[SDWebImageCodersManager sharedManager] decodedImageWithData:data options:nil];
image = [self scaledImageForKey:key image:image];
if (self.config.shouldDecompressImages) {
image = [SDWebImageCoderHelper decodedImageWithImage:image];
UIImage *image;
BOOL decodeFirstFrame = options & SDImageCacheDecodeFirstFrameOnly;
if (!decodeFirstFrame) {
// check whether we should use `SDAnimatedImage`
if ([context valueForKey:SDWebImageContextAnimatedImageClass]) {
Class animatedImageClass = [context valueForKey:SDWebImageContextAnimatedImageClass];
if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
CGFloat scale = SDImageScaleForKey(key);
image = [[animatedImageClass alloc] initWithData:data scale:scale];
if (options & SDImageCachePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
[((id<SDAnimatedImage>)image) preloadAllFrames];
}
}
}
}
if (!image) {
image = [[SDWebImageCodersManager sharedManager] decodedImageWithData:data options:@{SDWebImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame)}];
image = [self scaledImageForKey:key image:image];
}
BOOL shouldDecode = YES;
if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
if (shouldDecode) {
if (self.config.shouldDecompressImages) {
image = [SDWebImageCoderHelper decodedImageWithImage:image];
}
}
return image;
} else {
@ -542,7 +575,7 @@ FOUNDATION_STATIC_INLINE NSUInteger SDCacheCostForImage(UIImage *image) {
cacheKey = SDTransformedKeyForKey(key, transformerKey);
}
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:cacheKey data:diskData];
diskImage = [self diskImageForKey:cacheKey data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:cacheKey cost:cost];

View File

@ -19,9 +19,9 @@ typedef NSDictionary<SDWebImageCoderOption, id> SDWebImageCoderOptions;
*/
FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderDecodeFirstFrameOnly;
/**
A double value between 0.0-1.0 indicating the encode quality to produce the image data. If not provide, use 1.0. (NSNumber)
A double value between 0.0-1.0 indicating the encode compression quality to produce the image data. If not provide, use 1.0. (NSNumber)
*/
FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderEncodeQuality;
FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderEncodeCompressionQuality;
/**
This is the image coder protocol to provide custom image decoding/encoding.
@ -44,7 +44,7 @@ FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderEncodeQual
Decode the image data to image.
@param data The image data to be decoded
@param optionsDict A dictionary containing any decoding options. Pass {SDWebImageCoderDecodeFirstFrameOnlyKey: @(YES)} to decode the first frame only.
@param options A dictionary containing any decoding options. Pass @{SDWebImageCoderDecodeFirstFrameOnly: @(YES)} to decode the first frame only.
@return The decoded image from data
*/
- (nullable UIImage *)decodedImageWithData:(nullable NSData *)data
@ -65,6 +65,7 @@ FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderEncodeQual
@param image The image to be encoded
@param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible
@param options A dictionary containing any encoding options. Pass @{SDWebImageCoderEncodeCompressionQuality: @(1)} to specify compression quality.
@return The encoded image data
*/
- (nullable NSData *)encodedDataWithImage:(nullable UIImage *)image
@ -121,12 +122,4 @@ FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderEncodeQual
*/
- (nullable instancetype)initWithAnimatedImageData:(nullable NSData *)data;
/**
Return the current animated image data. This is used for image instance archive or image information retrieval
You can return back the desired data(may be not the same instance provide for init method, but have the equal data)
@return The animated image data
*/
- (nullable NSData *)animatedImageData;
@end

View File

@ -9,4 +9,4 @@
#import "SDWebImageCoder.h"
SDWebImageCoderOption const SDWebImageCoderDecodeFirstFrameOnly = @"decodeFirstFrameOnly";
SDWebImageCoderOption const SDWebImageCoderEncodeQuality = @"encodeQuality";
SDWebImageCoderOption const SDWebImageCoderEncodeCompressionQuality = @"compressionQuality";

View File

@ -23,7 +23,7 @@ static const size_t kBitsPerComponent = 8;
* Suggested value for iPad2 and iPhone 4: 120.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
*/
static const CGFloat kDestImageSizeMB = 120.f;
static const CGFloat kDestImageSizeMB = 60.f;
/*
* Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
@ -31,7 +31,7 @@ static const CGFloat kDestImageSizeMB = 120.f;
* Suggested value for iPad2 and iPhone 4: 40.
* Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
*/
static const CGFloat kSourceImageTileSizeMB = 40.f;
static const CGFloat kSourceImageTileSizeMB = 20.f;
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;

View File

@ -83,6 +83,7 @@
#define NS_OPTIONS(_type, _name) enum _name : _type _name; enum _name : _type
#endif
FOUNDATION_EXPORT CGFloat SDImageScaleForKey(NSString * _Nullable key);
FOUNDATION_EXPORT UIImage * _Nullable SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image);
FOUNDATION_EXPORT NSString *const _Nonnull SDWebImageErrorDomain;

View File

@ -18,6 +18,34 @@
#error SDWebImage need ARC for dispatch object
#endif
inline CGFloat SDImageScaleForKey(NSString * _Nullable key) {
CGFloat scale = 1;
if (!key) {
return scale;
}
#if SD_WATCH
if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)])
#elif SD_UIKIT
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)])
#elif SD_MAC
if ([[NSScreen mainScreen] respondsToSelector:@selector(backingScaleFactor)])
#endif
{
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
}
return scale;
}
inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
if (!image) {
return nil;
@ -36,40 +64,18 @@ inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullabl
animatedImage.sd_imageLoopCount = image.sd_imageLoopCount;
}
return animatedImage;
} else {
#endif
#if SD_WATCH
if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)]) {
#elif SD_UIKIT
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
#elif SD_MAC
if ([[NSScreen mainScreen] respondsToSelector:@selector(backingScaleFactor)]) {
#endif
CGFloat scale = 1;
if (key.length >= 8) {
NSRange range = [key rangeOfString:@"@2x."];
if (range.location != NSNotFound) {
scale = 2.0;
}
range = [key rangeOfString:@"@3x."];
if (range.location != NSNotFound) {
scale = 3.0;
}
}
if (scale > 1) {
#if SD_UIKIT || SD_WATCH
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
#else
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale];
#endif
image = scaledImage;
}
}
return image;
#if SD_UIKIT || SD_WATCH
}
#endif
CGFloat scale = SDImageScaleForKey(key);
if (scale > 1) {
#if SD_UIKIT || SD_WATCH
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
#else
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale];
#endif
image = scaledImage;
}
return image;
}
NSString *const SDWebImageErrorDomain = @"SDWebImageErrorDomain";

View File

@ -16,6 +16,7 @@ typedef NSDictionary<SDWebImageContextOption, id> SDWebImageContext;
A Dispatch group to maintain setImageBlock and completionBlock. This is used for custom setImageBlock advanced usage, such like perform background task but need to guarantee the completion block is called after setImageBlock. (dispatch_group_t)
*/
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextSetImageGroup;
/**
A SDWebImageManager instance to control the image download and cache process using in UIImageView+WebCache category and likes. If not provided, use the shared manager (SDWebImageManager)
*/
@ -25,3 +26,9 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustom
A id<SDWebImageTransformer> instance which conforms SDWebImageTransformer protocol. It's used for image transform after the image load finished and store the transformed image to cache. If you provide one, it will ignore the `transformer` in manager and use provided one instead. (id<SDWebImageTransformer>)
*/
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomTransformer;
/**
A Class object which the instance is a `UIImage/NSImage` subclass and adopt `SDAnimatedImage` protocol. And call `initWithData:scale:` to create the instance. If the instance create failed, fallback to normal `UIImage/NSImage`.
This can be used to improve animated images rendering performance (especially memory usage on big animated images) with `SDAnimatedImageView` (Class).
*/
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextAnimatedImageClass;

View File

@ -11,3 +11,4 @@
SDWebImageContextOption const SDWebImageContextSetImageGroup = @"setImageGroup";
SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager";
SDWebImageContextOption const SDWebImageContextCustomTransformer = @"customTransformer";
SDWebImageContextOption const SDWebImageContextAnimatedImageClass = @"animatedImageClass";

View File

@ -61,6 +61,16 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
* Scale down the image
*/
SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
/**
* By default, we decode the animated image. This flag can force decode the first frame only and produece the static image.
*/
SDWebImageDownloaderDecodeFirstFrameOnly = 1 << 9,
/**
* By default, for `SDAnimatedImage`, we decode the animated image frame during rendering to reduce memory usage. This flag actually trigger `preloadAllAnimatedImageFrames = YES` after image load from network
*/
SDWebImageDownloaderPreloadAllFrames = 1 << 10
};
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {

View File

@ -9,8 +9,10 @@
#import "SDWebImageDownloaderOperation.h"
#import "SDWebImageManager.h"
#import "NSImage+Additions.h"
#import "UIImage+WebCache.h"
#import "SDWebImageCodersManager.h"
#import "SDWebImageCoderHelper.h"
#import "SDAnimatedImage.h"
#define LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#define UNLOCK(lock) dispatch_semaphore_signal(lock);
@ -428,13 +430,33 @@ didReceiveResponse:(NSURLResponse *)response
} else {
// decode the image in coder queue
dispatch_async(self.coderQueue, ^{
UIImage *image = [[SDWebImageCodersManager sharedManager] decodedImageWithData:imageData options:nil];
BOOL decodeFirstFrame = self.options & SDWebImageDownloaderDecodeFirstFrameOnly;
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
image = [self scaledImageForKey:key image:image];
UIImage *image;
if (!decodeFirstFrame) {
// check whether we should use `SDAnimatedImage`
if ([self.context valueForKey:SDWebImageContextAnimatedImageClass]) {
Class animatedImageClass = [self.context valueForKey:SDWebImageContextAnimatedImageClass];
if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) {
CGFloat scale = SDImageScaleForKey(key);
image = [[animatedImageClass alloc] initWithData:imageData scale:scale];
if (self.options & SDWebImageDownloaderPreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) {
[((id<SDAnimatedImage>)image) preloadAllFrames];
}
}
}
}
if (!image) {
image = [[SDWebImageCodersManager sharedManager] decodedImageWithData:imageData options:@{SDWebImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame)}];
image = [self scaledImageForKey:key image:image];
}
BOOL shouldDecode = YES;
// Do not force decoding animated GIFs and WebPs
if (image.images) {
if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) {
// `SDAnimatedImage` do not decode
shouldDecode = NO;
} else if (image.sd_isAnimated) {
// animated image do not decode
shouldDecode = NO;
}
if (shouldDecode) {

View File

@ -70,7 +70,7 @@
return ([NSData sd_imageFormatForImageData:data] == SDImageFormatGIF);
}
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable NSDictionary<SDWebImageCoderOption,id> *)optionsDict {
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDWebImageCoderOptions *)options {
if (!data) {
return nil;
}
@ -90,7 +90,7 @@
UIImage *animatedImage;
BOOL decodeFirstFrame = [optionsDict[SDWebImageCoderDecodeFirstFrameOnly] boolValue];
BOOL decodeFirstFrame = [options[SDWebImageCoderDecodeFirstFrameOnly] boolValue];
if (decodeFirstFrame || count <= 1) {
animatedImage = [[UIImage alloc] initWithData:data];
} else {
@ -191,14 +191,21 @@
// Handle failure.
return nil;
}
NSMutableDictionary *properties = [NSMutableDictionary dictionary];
double compressionQuality = 1;
if ([options valueForKey:SDWebImageCoderEncodeCompressionQuality]) {
compressionQuality = [[options valueForKey:SDWebImageCoderEncodeCompressionQuality] doubleValue];
}
[properties setValue:@(compressionQuality) forKey:(__bridge_transfer NSString *)kCGImageDestinationLossyCompressionQuality];
if (frames.count == 0) {
// for static single GIF images
CGImageDestinationAddImage(imageDestination, image.CGImage, nil);
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
} else {
// for animated GIF images
NSUInteger loopCount = image.sd_imageLoopCount;
NSDictionary *gifProperties = @{(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary: @{(__bridge_transfer NSString *)kCGImagePropertyGIFLoopCount : @(loopCount)}};
CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)gifProperties);
NSDictionary *gifProperties = @{(__bridge_transfer NSString *)kCGImagePropertyGIFLoopCount : @(loopCount)};
[properties setValue:gifProperties forKey:(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary];
CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)properties);
for (size_t i = 0; i < frames.count; i++) {
SDWebImageFrame *frame = frames[i];
@ -268,11 +275,6 @@
return YES;
}
- (NSData *)animatedImageData
{
return _imageData;
}
- (NSUInteger)animatedImageLoopCount
{
return _loopCount;

View File

@ -229,6 +229,11 @@
NSInteger exifOrientation = [SDWebImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
[properties setValue:@(exifOrientation) forKey:(__bridge_transfer NSString *)kCGImagePropertyOrientation];
#endif
double compressionQuality = 1;
if ([options valueForKey:SDWebImageCoderEncodeCompressionQuality]) {
compressionQuality = [[options valueForKey:SDWebImageCoderEncodeCompressionQuality] doubleValue];
}
[properties setValue:@(compressionQuality) forKey:(__bridge_transfer NSString *)kCGImageDestinationLossyCompressionQuality];
// Add your image to the destination.
CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);

View File

@ -112,10 +112,22 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
* By default, when the cache missed, the image is download from the network. This flag can prevent network to load from cache only.
*/
SDWebImageFromCacheOnly = 1 << 15,
/**
* By default, when you use `SDWebImageTransition` to do some view transition after the image load finished, this transition is only applied for image download from the network. This mask can force to apply view transition for memory and disk cache as well.
*/
SDWebImageForceTransition = 1 << 16,
/**
* By default, we decode the animated image. This flag can force decode the first frame only and produece the static image.
*/
SDWebImageDecodeFirstFrameOnly = 1 << 17,
/**
* By default, for `SDAnimatedImage`, we decode the animated image frame during rendering to reduce memory usage. However, you can specify to preload all frames into memory to reduce CPU usage when the animated image is shared by lots of imageViews.
* This will actually trigger `preloadAllAnimatedImageFrames` in the background queue(Disk Cache & Download only).
*/
SDWebImagePreloadAllFrames = 1 << 18
};
typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL);

View File

@ -8,6 +8,8 @@
#import "SDWebImageManager.h"
#import "NSImage+Additions.h"
#import "UIImage+WebCache.h"
#import "SDAnimatedImage.h"
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@ -153,6 +155,8 @@
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageTransformAnimatedImage) cacheOptions |= SDImageCacheTransformAnimatedImage;
if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
// Image transformer
id<SDWebImageTransformer> transformer;
@ -195,6 +199,8 @@
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
if (options & SDWebImageDecodeFirstFrameOnly) downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) downloaderOptions |= SDWebImageDownloaderPreloadAllFrames;
if (cachedImage && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
@ -244,7 +250,7 @@
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
// We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage && ![downloadedImage conformsToProtocol:@protocol(SDAnimatedImage)]) {
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}

View File

@ -96,7 +96,7 @@ dispatch_semaphore_signal(self->_lock);
return ([NSData sd_imageFormatForImageData:data] == SDImageFormatWebP);
}
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable NSDictionary<NSString *,id> *)optionsDict {
- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDWebImageCoderOptions *)options {
if (!data) {
return nil;
}
@ -112,7 +112,7 @@ dispatch_semaphore_signal(self->_lock);
uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS);
BOOL hasAnimation = flags & ANIMATION_FLAG;
BOOL decodeFirstFrame = [[optionsDict valueForKey:SDWebImageCoderDecodeFirstFrameOnly] boolValue];
BOOL decodeFirstFrame = [[options valueForKey:SDWebImageCoderDecodeFirstFrameOnly] boolValue];
if (!hasAnimation) {
// for static single webp image
UIImage *staticImage = [self sd_rawWebpImageWithData:webpData];
@ -391,10 +391,14 @@ dispatch_semaphore_signal(self->_lock);
NSData *data;
double compressionQuality = 1;
if ([options valueForKey:SDWebImageCoderEncodeCompressionQuality]) {
compressionQuality = [[options valueForKey:SDWebImageCoderEncodeCompressionQuality] doubleValue];
}
NSArray<SDWebImageFrame *> *frames = [SDWebImageCoderHelper framesFromAnimatedImage:image];
if (frames.count == 0) {
// for static single webp image
data = [self sd_encodedWebpDataWithImage:image];
data = [self sd_encodedWebpDataWithImage:image quality:compressionQuality];
} else {
// for animated webp image
WebPMux *mux = WebPMuxNew();
@ -403,7 +407,7 @@ dispatch_semaphore_signal(self->_lock);
}
for (size_t i = 0; i < frames.count; i++) {
SDWebImageFrame *currentFrame = frames[i];
NSData *webpData = [self sd_encodedWebpDataWithImage:currentFrame.image];
NSData *webpData = [self sd_encodedWebpDataWithImage:currentFrame.image quality:compressionQuality];
int duration = currentFrame.duration * 1000;
WebPMuxFrameInfo frame = { .bitstream.bytes = webpData.bytes,
.bitstream.size = webpData.length,
@ -440,7 +444,7 @@ dispatch_semaphore_signal(self->_lock);
return data;
}
- (nullable NSData *)sd_encodedWebpDataWithImage:(nullable UIImage *)image {
- (nullable NSData *)sd_encodedWebpDataWithImage:(nullable UIImage *)image quality:(double)quality {
if (!image) {
return nil;
}
@ -466,8 +470,8 @@ dispatch_semaphore_signal(self->_lock);
uint8_t *rgba = (uint8_t *)CFDataGetBytePtr(dataRef);
uint8_t *data = NULL;
float quality = 100.0;
size_t size = WebPEncodeRGBA(rgba, (int)width, (int)height, (int)bytesPerRow, quality, &data);
float qualityFactor = quality * 100; // WebP quality is 0-100
size_t size = WebPEncodeRGBA(rgba, (int)width, (int)height, (int)bytesPerRow, qualityFactor, &data);
CFRelease(dataRef);
rgba = NULL;
@ -582,11 +586,6 @@ static void FreeImageData(void *info, const void *data, size_t size) {
return YES;
}
- (NSData *)animatedImageData
{
return _imageData;
}
- (NSUInteger)animatedImageLoopCount {
return _loopCount;
}

View File

@ -12,9 +12,21 @@
@interface UIImage (GIF)
/**
* Creates an animated UIImage from an NSData.
* For static GIF, will create an UIImage with `images` array set to nil. For animated GIF, will create an UIImage with valid `images` array.
Creates an animated UIImage from an NSData.
For Static GIF, will create an UIImage with `images` array set to nil. For Animated GIF, will create an UIImage with valid `images` array.
@param data The GIF data
@return The created image
*/
+ (nullable UIImage *)sd_animatedGIFWithData:(nullable NSData *)data;
/**
Creates an animated UIImage from an NSData.
@param data The GIF data
@param firstFrameOnly Even if the image data is Animated GIF format, decode the first frame only
@return The created image
*/
+ (nullable UIImage *)sd_animatedGIFWithData:(nullable NSData *)data firstFrameOnly:(BOOL)firstFrameOnly;
@end

View File

@ -12,11 +12,16 @@
@implementation UIImage (GIF)
+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
+ (nullable UIImage *)sd_animatedGIFWithData:(nullable NSData *)data {
return [self sd_animatedGIFWithData:data firstFrameOnly:NO];
}
+ (nullable UIImage *)sd_animatedGIFWithData:(nullable NSData *)data firstFrameOnly:(BOOL)firstFrameOnly {
if (!data) {
return nil;
}
return [[SDWebImageGIFCoder sharedCoder] decodedImageWithData:data options:nil];
SDWebImageCoderOptions *options = @{SDWebImageCoderDecodeFirstFrameOnly : @(firstFrameOnly)};
return [[SDWebImageGIFCoder sharedCoder] decodedImageWithData:data options:options];
}
@end

View File

@ -10,15 +10,26 @@
#import "NSData+ImageContentType.h"
@interface UIImage (MultiFormat)
#pragma mark - Decode
/**
Create and decode a image with the specify image data
If the image data is animated image format, create an animated image if possible
@param data The image data
@return The created image
*/
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data;
/**
Create and decode a image with the specify image data
@param data The image data
@param firstFrameOnly Even if the image data is animated image format, decode the first frame only
@return The created image
*/
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data firstFrameOnly:(BOOL)firstFrameOnly;
#pragma mark - Encode
/**
Encode the current image to the data, the image format is unspecified
@ -34,4 +45,13 @@
*/
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat;
/**
Encode the current image to data with the specify image format
@param imageFormat The specify image format
@param compressionQuality The quality of the resulting image data. Value between 0.0-1.0. Some coders may not support compression quality.
@return The encoded data. If can't encode, return nil
*/
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality;
@end

View File

@ -12,7 +12,15 @@
@implementation UIImage (MultiFormat)
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
return [[SDWebImageCodersManager sharedManager] decodedImageWithData:data options:nil];
return [self sd_imageWithData:data firstFrameOnly:NO];
}
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data firstFrameOnly:(BOOL)firstFrameOnly {
if (!data) {
return nil;
}
SDWebImageCoderOptions *options = @{SDWebImageCoderDecodeFirstFrameOnly : @(firstFrameOnly)};
return [[SDWebImageCodersManager sharedManager] decodedImageWithData:data options:options];
}
- (nullable NSData *)sd_imageData {
@ -20,12 +28,12 @@
}
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
NSData *imageData = nil;
if (self) {
imageData = [[SDWebImageCodersManager sharedManager] encodedDataWithImage:self format:imageFormat options:nil];
}
return imageData;
return [self sd_imageDataAsFormat:imageFormat compressionQuality:1];
}
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality {
SDWebImageCoderOptions *options = @{SDWebImageCoderEncodeCompressionQuality : @(compressionQuality)};
return [[SDWebImageCodersManager sharedManager] encodedDataWithImage:self format:imageFormat options:options];
}
@end

View File

@ -12,8 +12,24 @@
@interface UIImage (WebP)
/**
Create a image from the WebP data.
This may create animated image if the data is Animated WebP.
@param data The WebP data
@return The created image
*/
+ (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data;
/**
Create a image from the WebP data.
@param data The WebP data
@param firstFrameOnly Even if the image data is Animated WebP format, decode the first frame only
@return The created image
*/
+ (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data firstFrameOnly:(BOOL)firstFrameOnly;
@end
#endif

View File

@ -14,10 +14,15 @@
@implementation UIImage (WebP)
+ (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data {
return [self sd_imageWithWebPData:data firstFrameOnly:NO];
}
+ (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data firstFrameOnly:(BOOL)firstFrameOnly {
if (!data) {
return nil;
}
return [[SDWebImageWebPCoder sharedCoder] decodedImageWithData:data options:nil];
SDWebImageCoderOptions *options = @{SDWebImageCoderDecodeFirstFrameOnly : @(firstFrameOnly)};
return [[SDWebImageWebPCoder sharedCoder] decodedImageWithData:data options:options];
}
@end

View File

@ -157,7 +157,7 @@
expect([coder canEncodeToFormat:inputImageFormat]).to.beTruthy();
// 4 - encode from UIImage to NSData using the inputImageFormat and check it
NSData *outputImageData = [coder encodedDataWithImage:inputImage format:inputImageFormat];
NSData *outputImageData = [coder encodedDataWithImage:inputImage format:inputImageFormat options:nil];
expect(outputImageData).toNot.beNil();
UIImage *outputImage = [coder decodedImageWithData:outputImageData options:nil];
expect(outputImage.size).to.equal(inputImage.size);