From 4563e714d7ccc87b1c3fa0321026b8a155248c01 Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 2 Feb 2018 14:15:29 +0800 Subject: [PATCH] Support progressive decoding for animated image. A little refactory to make coder protocol more readable --- SDWebImage/NSImage+Additions.h | 3 +- SDWebImage/NSImage+Additions.m | 28 ++-- SDWebImage/SDAnimatedImage.h | 30 +++- SDWebImage/SDAnimatedImage.m | 73 +++++---- SDWebImage/SDAnimatedImageView.m | 163 ++++++++++++++++----- SDWebImage/SDWebImageCoder.h | 27 +++- SDWebImage/SDWebImageCoder.m | 2 +- SDWebImage/SDWebImageCoderHelper.m | 11 +- SDWebImage/SDWebImageCompat.h | 3 - SDWebImage/SDWebImageCompat.m | 62 -------- SDWebImage/SDWebImageDefine.h | 24 ++- SDWebImage/SDWebImageDefine.m | 91 ++++++++++++ SDWebImage/SDWebImageDownloaderOperation.m | 63 +++++--- SDWebImage/SDWebImageGIFCoder.h | 2 +- SDWebImage/SDWebImageGIFCoder.m | 90 ++++++++++-- SDWebImage/SDWebImageImageIOCoder.m | 23 +-- SDWebImage/SDWebImageWebPCoder.m | 39 ++--- SDWebImage/UIImage+WebCache.h | 5 + SDWebImage/UIImage+WebCache.m | 51 ++++--- 19 files changed, 541 insertions(+), 249 deletions(-) diff --git a/SDWebImage/NSImage+Additions.h b/SDWebImage/NSImage+Additions.h index 3a78ce91..a5264ed8 100644 --- a/SDWebImage/NSImage+Additions.h +++ b/SDWebImage/NSImage+Additions.h @@ -17,8 +17,7 @@ @property (nonatomic, readonly, nullable) CGImageRef CGImage; @property (nonatomic, readonly, nullable) NSArray *images; @property (nonatomic, readonly) CGFloat scale; - -- (nonnull instancetype)initWithCGImage:(nonnull CGImageRef)cgImage scale:(CGFloat)scale; +@property (nonatomic, readonly, nullable) NSBitmapImageRep *bitmapImageRep; @end diff --git a/SDWebImage/NSImage+Additions.m b/SDWebImage/NSImage+Additions.m index 466a9444..65a66621 100644 --- a/SDWebImage/NSImage+Additions.m +++ b/SDWebImage/NSImage+Additions.m @@ -14,7 +14,7 @@ - (CGImageRef)CGImage { NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); - CGImageRef cgImage = [self CGImageForProposedRect:&imageRect context:NULL hints:nil]; + CGImageRef cgImage = [self CGImageForProposedRect:&imageRect context:nil hints:nil]; return cgImage; } @@ -24,28 +24,22 @@ - (CGFloat)scale { CGFloat scale = 1; - NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); - NSImageRep *rep = [self bestRepresentationForRect:imageRect context:NULL hints:nil]; - NSInteger pixelsWide = rep.pixelsWide; - CGFloat width = rep.size.width; + CGFloat width = self.size.width; if (width > 0) { - scale = pixelsWide / width; + // Use CGImage to get pixel width, NSImageRep.pixelsWide always double on Retina screen + NSUInteger pixelWidth = CGImageGetWidth(self.CGImage); + scale = pixelWidth / width; } return scale; } -- (instancetype)initWithCGImage:(CGImageRef)cgImage scale:(CGFloat)scale { - NSSize size; - if (cgImage && scale > 0) { - NSInteger pixelsWide = CGImageGetWidth(cgImage); - NSInteger pixelsHigh = CGImageGetHeight(cgImage); - CGFloat width = pixelsWide / scale; - CGFloat height = pixelsHigh / scale; - size = NSMakeSize(width, height); - } else { - size = NSZeroSize; +- (NSBitmapImageRep *)bitmapImageRep { + NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); + NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; + if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { + return (NSBitmapImageRep *)imageRep; } - return [self initWithCGImage:cgImage size:size]; + return nil; } @end diff --git a/SDWebImage/SDAnimatedImage.h b/SDWebImage/SDAnimatedImage.h index 47bee11c..d5f75c69 100644 --- a/SDWebImage/SDAnimatedImage.h +++ b/SDWebImage/SDAnimatedImage.h @@ -9,9 +9,18 @@ #import "SDWebImageCompat.h" #import "NSData+ImageContentType.h" +@protocol SDWebImageAnimatedCoder; @protocol SDAnimatedImage @required +/** + The original animated image data for current image. If current image is not an animated format, return nil. + We may use this method to grab back the original image data if need, such as NSCoding or compare. + + @return The animated image data + */ +- (nullable NSData *)animatedImageData; + /** Total animated frame count. It the frame count is less than 1, then the methods below will be ignored. @@ -42,6 +51,7 @@ */ - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index; +// These methods are for SDAnimatedImage class only but not for SDWebImageAnimatedCoder. @optional /** Preload all frame image to memory. Then directly return the frame for index without decoding. @@ -51,6 +61,16 @@ */ - (void)preloadAllFrames; +/** + Initializes the image with an animated coder. You can use the coder to decode the image frame later. + @note Normally we use `initWithData:scale:` to create custom animated image class. So you can implement your custom class without our built-in coder. + + @param animatedCoder An animated coder which conform `SDWebImageAnimatedCoder` protocol + @param scale The scale factor to assume when interpreting the image data. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. (For `NSImage`, `scale` property can be calculated from `size`) + @return An initialized object + */ +- (nullable instancetype)initWithAnimatedCoder:(nonnull id)animatedCoder scale:(CGFloat)scale; + @end @interface SDAnimatedImage : UIImage @@ -63,9 +83,10 @@ - (nullable instancetype)initWithContentsOfFile:(nonnull NSString *)path; - (nullable instancetype)initWithData:(nonnull NSData *)data; - (nullable instancetype)initWithData:(nonnull NSData *)data scale:(CGFloat)scale; +- (nullable instancetype)initWithAnimatedCoder:(nonnull id)animatedCoder scale:(CGFloat)scale; /** - Current animated image format + Current animated image format. */ @property (nonatomic, assign, readonly) SDImageFormat animatedImageFormat; /** @@ -73,6 +94,13 @@ */ @property (nonatomic, copy, readonly, nullable) NSData *animatedImageData; +#if SD_MAC +/** + For AppKit, `NSImage` can contains multiple image representations with different scales. However, this class does not do that from the design. We processs the scale like UIKit and store it as a extra information for correctlly rendering in `SDAnimatedImageView`. + */ +@property (nonatomic, readonly) CGFloat scale; +#endif + /** 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. diff --git a/SDWebImage/SDAnimatedImage.m b/SDWebImage/SDAnimatedImage.m index f69e6380..6a0511a4 100644 --- a/SDWebImage/SDAnimatedImage.m +++ b/SDWebImage/SDAnimatedImage.m @@ -31,18 +31,9 @@ static CGFloat SDImageScaleFromPath(NSString *string) { @interface SDAnimatedImage () @property (nonatomic, strong) id 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 (atomic, copy) NSArray *preloadAnimatedImageFrames; @property (nonatomic, assign) BOOL animatedImageFramesPreloaded; -@property (nonatomic, assign) BOOL animatedImageLoopCountChecked; -@property (nonatomic, assign) BOOL animatedImageFrameCountChecked; - -#if SD_MAC -@property (nonatomic, assign) CGFloat scale; -#endif @end @@ -89,41 +80,64 @@ static CGFloat SDImageScaleFromPath(NSString *string) { if (!data || data.length == 0) { return nil; } + if (scale <= 0) { + scale = 1; + } data = [data copy]; // avoid mutable data + id animatedCoder = nil; for (idcoder in [SDWebImageCodersManager sharedManager].coders) { if ([coder conformsToProtocol:@protocol(SDWebImageAnimatedCoder)]) { if ([coder canDecodeFromData:data]) { - id animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:data]; - if (!animatedCoder) { - // check next coder - continue; - } else { - self.coder = animatedCoder; - break; - } + animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:data]; + break; } } } - if (!self.coder) { + if (!animatedCoder) { return nil; } - UIImage *image = [self.coder animatedImageFrameAtIndex:0]; + UIImage *image = [animatedCoder animatedImageFrameAtIndex:0]; if (!image) { return nil; } +#if SD_MAC + self = [super initWithCGImage:image.CGImage size:NSZeroSize]; +#else + self = [super initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; +#endif + if (self) { + _coder = animatedCoder; +#if SD_MAC + _scale = scale; +#endif + SDImageFormat format = [NSData sd_imageFormatForImageData:data]; + _animatedImageFormat = format; + } + return self; +} + +- (instancetype)initWithAnimatedCoder:(id)animatedCoder scale:(CGFloat)scale { + if (!animatedCoder) { + return nil; + } if (scale <= 0) { scale = 1; } + UIImage *image = [animatedCoder animatedImageFrameAtIndex:0]; + if (!image) { + return nil; + } #if SD_MAC - self = [super initWithCGImage:image.CGImage scale:scale]; + self = [super initWithCGImage:image.CGImage size:NSZeroSize]; #else self = [super initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; #endif if (self) { + _coder = animatedCoder; #if SD_MAC _scale = scale; #endif - _animatedImageData = data; + NSData *data = [animatedCoder animatedImageData]; SDImageFormat format = [NSData sd_imageFormatForImageData:data]; _animatedImageFormat = format; } @@ -174,20 +188,17 @@ static CGFloat SDImageScaleFromPath(NSString *string) { } #pragma mark - SDAnimatedImage + +- (NSData *)animatedImageData { + return [self.coder animatedImageData]; +} + - (NSUInteger)animatedImageLoopCount { - if (!self.animatedImageLoopCountChecked) { - self.animatedImageLoopCountChecked = YES; - _animatedImageLoopCount = [self.coder animatedImageLoopCount]; - } - return _animatedImageLoopCount; + return [self.coder animatedImageLoopCount]; } - (NSUInteger)animatedImageFrameCount { - if (!self.animatedImageFrameCountChecked) { - self.animatedImageFrameCountChecked = YES; - _animatedImageFrameCount = [self.coder animatedImageFrameCount]; - } - return _animatedImageFrameCount; + return [self.coder animatedImageFrameCount]; } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { diff --git a/SDWebImage/SDAnimatedImageView.m b/SDWebImage/SDAnimatedImageView.m index f50e5431..348569e3 100644 --- a/SDWebImage/SDAnimatedImageView.m +++ b/SDWebImage/SDAnimatedImageView.m @@ -131,9 +131,11 @@ dispatch_semaphore_signal(self->_lock); @property (nonatomic, assign) NSTimeInterval currentTime; @property (nonatomic, assign) BOOL bufferMiss; @property (nonatomic, assign) BOOL shouldAnimate; +@property (nonatomic, assign) BOOL isProgressive; @property (nonatomic, assign) NSUInteger maxBufferCount; @property (nonatomic, strong) NSOperationQueue *fetchQueue; @property (nonatomic, strong) dispatch_semaphore_t lock; +@property (nonatomic, assign) CGFloat animatedImageScale; #if SD_MAC @property (nonatomic, assign) CVDisplayLinkRef displayLink; #else @@ -202,9 +204,11 @@ dispatch_semaphore_signal(self->_lock); { #if SD_MAC self.wantsLayer = YES; + // Default value from `NSImageView` + self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawOnSetNeedsDisplay; self.imageScaling = NSImageScaleProportionallyDown; + self.imageAlignment = NSImageAlignCenter; #endif - self.maxBufferCount = 0; self.runLoopMode = [[self class] defaultRunLoopMode]; self.lock = dispatch_semaphore_create(1); #if SD_UIKIT @@ -213,27 +217,41 @@ dispatch_semaphore_signal(self->_lock); } -- (void)resetAnimated +- (void)resetAnimatedImage { + self.animatedImage = nil; + self.totalFrameCount = 0; + self.totalLoopCount = 0; + self.currentFrame = nil; + self.currentFrameIndex = 0; + self.currentLoopCount = 0; + self.currentTime = 0; + self.bufferMiss = NO; + self.shouldAnimate = NO; + self.isProgressive = NO; + self.maxBufferCount = 0; + self.animatedImageScale = 1; + [_fetchQueue cancelAllOperations]; + _fetchQueue = nil; LOCK({ - self.animatedImage = nil; - self.totalFrameCount = 0; - self.totalLoopCount = 0; - self.currentFrame = 0; - self.currentFrameIndex = 0; - self.currentLoopCount = 0; - self.currentTime = 0; - self.bufferMiss = NO; - self.shouldAnimate = NO; - self.maxBufferCount = 0; - self.layer.contentsScale = 1; [_frameBuffer removeAllObjects]; _frameBuffer = nil; - [_fetchQueue cancelAllOperations]; - _fetchQueue = nil; }); } +- (void)resetProgressiveImage +{ + self.animatedImage = nil; + self.totalFrameCount = 0; + self.totalLoopCount = 0; + // preserve current state + self.shouldAnimate = NO; + self.isProgressive = YES; + self.maxBufferCount = 0; + self.animatedImageScale = 1; + // preserve buffer cache +} + #pragma mark - Accessors #pragma mark Public @@ -242,33 +260,63 @@ dispatch_semaphore_signal(self->_lock); if (self.image == image) { return; } - [self stopAnimating]; - // Reset all value - [self resetAnimated]; + // Check Progressive coding + self.isProgressive = NO; + if ([image conformsToProtocol:@protocol(SDAnimatedImage)] && image.sd_isIncremental) { + NSData *currentData = [((UIImage *)image) animatedImageData]; + if (currentData) { + NSData *previousData; + if ([self.image conformsToProtocol:@protocol(SDAnimatedImage)]) { + previousData = [((UIImage *)self.image) animatedImageData]; + } + // Check whether to use progressive coding + if (!previousData) { + // If previous data is nil + self.isProgressive = YES; + } else if ([currentData isEqualToData:previousData]) { + // If current data is equal to previous data + self.isProgressive = YES; + } + } + } + + if (self.isProgressive) { + // Reset all value, but keep current state + [self resetProgressiveImage]; + } else { + // Stop animating + [self stopAnimating]; + // Reset all value + [self resetAnimatedImage]; + } + + // We need call super method to keep function. This will impliedly call `setNeedsDisplay`. But we have no way to avoid this when using animated image. So we call `setNeedsDisplay` again at the end. super.image = image; if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) { NSUInteger animatedImageFrameCount = ((UIImage *)image).animatedImageFrameCount; + // Check the frame count if (animatedImageFrameCount <= 1) { return; } self.animatedImage = (UIImage *)image; self.totalFrameCount = animatedImageFrameCount; + // Get the current frame and loop count. self.totalLoopCount = self.animatedImage.animatedImageLoopCount; + // Get the scale + self.animatedImageScale = image.scale; + if (!self.isProgressive) { + self.currentFrame = image; + LOCK({ + self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame; + }); + } + // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`). super.highlighted = NO; // UIImageView seems to bypass some accessors when calculating its intrinsic content size, so this ensures its intrinsic content size comes from the animated image. [self invalidateIntrinsicContentSize]; - // Get the first frame - self.currentFrame = [self.animatedImage animatedImageFrameAtIndex:0]; - LOCK({ - if (self.currentFrame) { - self.frameBuffer[@(0)] = self.currentFrame; - self.bufferMiss = NO; - } else { - self.bufferMiss = YES; - } - }); + // Calculate max buffer size [self calculateMaxBufferCount]; // Update should animate @@ -277,7 +325,6 @@ dispatch_semaphore_signal(self->_lock); [self startAnimating]; } - self.layer.contentsScale = image.scale; [self.layer setNeedsDisplay]; } } @@ -360,6 +407,7 @@ dispatch_semaphore_signal(self->_lock); } #else [_displayLink invalidate]; + _displayLink = nil; [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } @@ -546,9 +594,9 @@ dispatch_semaphore_signal(self->_lock); { if (self.animatedImage) { #if SD_MAC - CVDisplayLinkStop(self.displayLink); + CVDisplayLinkStop(_displayLink); #else - self.displayLink.paused = YES; + _displayLink.paused = YES; #endif } else { #if SD_UIKIT @@ -627,8 +675,9 @@ dispatch_semaphore_signal(self->_lock); #if SD_UIKIT NSTimeInterval duration = displayLink.duration * displayLink.frameInterval; #endif + NSUInteger totalFrameCount = self.totalFrameCount; NSUInteger currentFrameIndex = self.currentFrameIndex; - NSUInteger nextFrameIndex = (currentFrameIndex + 1) % self.totalFrameCount; + NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount; // Check if we have the frame buffer firstly to improve performance if (!self.bufferMiss) { @@ -652,12 +701,17 @@ dispatch_semaphore_signal(self->_lock); LOCK({ currentFrame = self.frameBuffer[@(currentFrameIndex)]; }); + BOOL bufferFull = NO; if (currentFrame) { LOCK({ // Remove the frame buffer if need if (self.frameBuffer.count > self.maxBufferCount) { self.frameBuffer[@(currentFrameIndex)] = nil; } + // Check whether we can stop fetch + if (self.frameBuffer.count == totalFrameCount) { + bufferFull = YES; + } }); self.currentFrame = currentFrame; self.currentFrameIndex = nextFrameIndex; @@ -667,8 +721,19 @@ dispatch_semaphore_signal(self->_lock); self.bufferMiss = YES; } - // Update the loop count - if (nextFrameIndex == 0) { + // Update the loop count when last frame rendered + if (nextFrameIndex == 0 && !self.bufferMiss) { + // Progressive image reach the current last frame index. Keep the state and stop animating. Wait for later restart + if (self.isProgressive) { + // Recovery the current frame index and removed frame buffer (See above) + self.currentFrameIndex = currentFrameIndex; + LOCK({ + self.frameBuffer[@(currentFrameIndex)] = self.currentFrame; + }); + [self stopAnimating]; + return; + } + // Update the loop count self.currentLoopCount++; // if reached the max loop count, stop animating, 0 means loop indefinitely NSUInteger maxLoopCount = self.shouldCustomLoopCount ? self.animationRepeatCount : self.totalLoopCount; @@ -678,13 +743,22 @@ dispatch_semaphore_signal(self->_lock); } } - // Check if we should prefetch next frame - if (self.fetchQueue.operationCount == 0 && self.frameBuffer.count < self.totalFrameCount) { + // Check if we should prefetch next frame or current frame + NSUInteger fetchFrameIndex; + if (self.bufferMiss) { + // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame + fetchFrameIndex = currentFrameIndex; + } else { + // Or, most cases, the decode speed is faster than render speed, we fetch next frame + fetchFrameIndex = nextFrameIndex; + } + if (!bufferFull && self.fetchQueue.operationCount == 0) { // Prefetch next frame in background queue + UIImage *animatedImage = self.animatedImage; NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ - UIImage *nextFrame = [self.animatedImage animatedImageFrameAtIndex:nextFrameIndex]; + UIImage *fetchFrame = [animatedImage animatedImageFrameAtIndex:fetchFrameIndex]; LOCK({ - self.frameBuffer[@(nextFrameIndex)] = nextFrame; + self.frameBuffer[@(fetchFrameIndex)] = fetchFrame; }); }]; [self.fetchQueue addOperation:operation]; @@ -701,12 +775,15 @@ dispatch_semaphore_signal(self->_lock); #pragma mark - CALayerDelegate (Informal) #pragma mark Providing the Layer's Content +#if SD_UIKIT - (void)displayLayer:(CALayer *)layer { if (_currentFrame) { + layer.contentsScale = self.animatedImageScale; layer.contents = (__bridge id)_currentFrame.CGImage; } } +#endif #if SD_MAC - (BOOL)wantsUpdateLayer @@ -717,7 +794,10 @@ dispatch_semaphore_signal(self->_lock); - (void)updateLayer { if (_currentFrame) { + self.layer.contentsScale = self.animatedImageScale; self.layer.contents = (__bridge id)_currentFrame.CGImage; + } else { + [super updateLayer]; } } #endif @@ -732,13 +812,18 @@ dispatch_semaphore_signal(self->_lock); if (self.maxBufferSize > 0) { max = self.maxBufferSize; } else { - // calculate based on current memory, these factors are by experience + // Calculate based on current memory, these factors are by experience NSUInteger total = SDDeviceTotalMemory(); NSUInteger free = SDDeviceFreeMemory(); max = MIN(total * 0.2, free * 0.6); } NSUInteger maxBufferCount = (double)max / (double)bytes; + if (!maxBufferCount) { + // At least 1 frame + maxBufferCount = 1; + } + self.maxBufferCount = maxBufferCount; } diff --git a/SDWebImage/SDWebImageCoder.h b/SDWebImage/SDWebImageCoder.h index ee6fdd34..0dcaf874 100644 --- a/SDWebImage/SDWebImageCoder.h +++ b/SDWebImage/SDWebImageCoder.h @@ -26,6 +26,7 @@ FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderEncodeComp /** This is the image coder protocol to provide custom image decoding/encoding. These methods are all required to implement. + You do not need to specify image scale during decoding because we may scale image later. @note Pay attention that these methods are not called from main queue. */ @protocol SDWebImageCoder @@ -89,7 +90,7 @@ FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderEncodeComp @param data The image data so we can look at it @return YES if this coder can decode the data, NO otherwise */ -- (BOOL)canIncrementallyDecodeFromData:(nullable NSData *)data; +- (BOOL)canIncrementalDecodeFromData:(nullable NSData *)data; /** Because incremental decoding need to keep the decoded context, we will alloc a new instance with the same class for each download operation to avoid conflicts @@ -97,25 +98,37 @@ FOUNDATION_EXPORT SDWebImageCoderOption _Nonnull const SDWebImageCoderEncodeComp @return A new instance to do incremental decoding for the specify image format */ -- (nonnull instancetype)initIncrementally; +- (nonnull instancetype)initIncremental; /** - Incremental decode the image data to image. - + Update the incremental decoding when new image data available + @param data The image data has been downloaded so far @param finished Whether the download has finished - @return The decoded image from data */ -- (nullable UIImage *)incrementallyDecodedImageWithData:(nullable NSData *)data finished:(BOOL)finished; +- (void)updateIncrementalData:(nullable NSData *)data finished:(BOOL)finished; + +/** + Incremental decode the current image data to image. + + @param options A dictionary containing any decoding options. + @return The decoded image from current data + */ +- (nullable UIImage *)incrementalDecodedImageWithOptions:(nullable SDWebImageCoderOptions *)options; @end + +/** + This is the animated image coder protocol for custom animated image class like `SDAnimatedImage`. Through it inherit from `SDWebImageCoder`. We currentlly only use the method `canDecodeFromData:` to detect the proper coder for specify animated image format. + */ @protocol SDWebImageAnimatedCoder @required /** Because animated image coder should keep the original data, we will alloc a new instance with the same class for the specify animated image data - The init method should return nil if it can't decode the specify animated image data + The init method should return nil if it can't decode the specify animated image data to produce any frame. + After the instance created, we may call methods in `SDAnimatedImage` to produce animated image frame. @param data The animated image data to be decode @return A new instance to do animated decoding for specify image data diff --git a/SDWebImage/SDWebImageCoder.m b/SDWebImage/SDWebImageCoder.m index 8c8ec4b4..e5d82070 100644 --- a/SDWebImage/SDWebImageCoder.m +++ b/SDWebImage/SDWebImageCoder.m @@ -9,4 +9,4 @@ #import "SDWebImageCoder.h" SDWebImageCoderOption const SDWebImageCoderDecodeFirstFrameOnly = @"decodeFirstFrameOnly"; -SDWebImageCoderOption const SDWebImageCoderEncodeCompressionQuality = @"compressionQuality"; +SDWebImageCoderOption const SDWebImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; diff --git a/SDWebImage/SDWebImageCoderHelper.m b/SDWebImage/SDWebImageCoderHelper.m index 901fc39a..29f6b739 100644 --- a/SDWebImage/SDWebImageCoderHelper.m +++ b/SDWebImage/SDWebImageCoderHelper.m @@ -197,17 +197,20 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over static CGColorSpaceRef colorSpace; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ -#if SD_MAC +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability" BOOL shouldUseSRGB = NO; -#else - BOOL shouldUseSRGB = NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_8_x_Max; +#if SD_UIKIT + NSProcessInfo *processInfo = [NSProcessInfo processInfo]; + shouldUseSRGB = processInfo.operatingSystemVersion.majorVersion >= 9; #endif if (shouldUseSRGB) { - // This is what iOS device used colorspace, combined with right bitmapInfo, even without decode, can still avoid extra CA::Render::copy_image(which marked `Color Copied Images` from Instruments) + // This is what iOS/tvOS device used colorspace, combined with right bitmapInfo, even without decode, can still avoid extra CA::Render::copy_image(which marked `Color Copied Images` from Instruments) colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); } else { colorSpace = CGColorSpaceCreateDeviceRGB(); } +#pragma clang diagnostic pop }); return colorSpace; } diff --git a/SDWebImage/SDWebImageCompat.h b/SDWebImage/SDWebImageCompat.h index 50ef17c2..3e745113 100644 --- a/SDWebImage/SDWebImageCompat.h +++ b/SDWebImage/SDWebImageCompat.h @@ -83,9 +83,6 @@ #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; #ifndef dispatch_queue_async_safe diff --git a/SDWebImage/SDWebImageCompat.m b/SDWebImage/SDWebImageCompat.m index e54c274d..2997cfb6 100644 --- a/SDWebImage/SDWebImageCompat.m +++ b/SDWebImage/SDWebImageCompat.m @@ -7,8 +7,6 @@ */ #import "SDWebImageCompat.h" -#import "UIImage+WebCache.h" -#import "NSImage+Additions.h" #if !__has_feature(objc_arc) #error SDWebImage is ARC only. Either turn on ARC for the project or use -fobjc-arc flag @@ -18,64 +16,4 @@ #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; - } - -#if SD_UIKIT || SD_WATCH - if ((image.images).count > 0) { - NSMutableArray *scaledImages = [NSMutableArray array]; - - for (UIImage *tempImage in image.images) { - [scaledImages addObject:SDScaledImageForKey(key, tempImage)]; - } - - UIImage *animatedImage = [UIImage animatedImageWithImages:scaledImages duration:image.duration]; - if (animatedImage) { - animatedImage.sd_imageLoopCount = image.sd_imageLoopCount; - } - return animatedImage; - } -#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"; diff --git a/SDWebImage/SDWebImageDefine.h b/SDWebImage/SDWebImageDefine.h index 23a4086c..060df431 100644 --- a/SDWebImage/SDWebImageDefine.h +++ b/SDWebImage/SDWebImageDefine.h @@ -6,12 +6,34 @@ * file that was distributed with this source code. */ -#import +#import "SDWebImageCompat.h" typedef void(^SDWebImageNoParamsBlock)(void); typedef NSString * SDWebImageContextOption NS_STRING_ENUM; typedef NSDictionary SDWebImageContext; +#pragma mark - Image scale + +/** + Return the image scale from the specify key, supports file name and url key + + @param key The image cache key + @return The scale factor for image + */ +FOUNDATION_EXPORT CGFloat SDImageScaleForKey(NSString * _Nullable key); + +/** + Scale the image with the scale factor from the specify key. If no need to scale, return the original image + This only works for `UIImage`(UIKit) or `NSImage`(AppKit). + + @param key The image cache key + @param image The image + @return The scaled image + */ +FOUNDATION_EXPORT UIImage * _Nullable SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image); + +#pragma mark - Context option + /** 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) */ diff --git a/SDWebImage/SDWebImageDefine.m b/SDWebImage/SDWebImageDefine.m index 41be5f9d..edb7414b 100644 --- a/SDWebImage/SDWebImageDefine.m +++ b/SDWebImage/SDWebImageDefine.m @@ -7,6 +7,97 @@ */ #import "SDWebImageDefine.h" +#import "UIImage+WebCache.h" +#import "NSImage+Additions.h" + +#pragma mark - Image scale + +static inline NSArray * _Nonnull SDImageScaleFactors() { + return @[@2, @3]; +} + +inline CGFloat SDImageScaleForKey(NSString * _Nullable key) { + CGFloat scale = 1; + if (!key) { + return scale; + } + // Check if target OS support 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 + { + // a@2x.png -> 8 + if (key.length >= 8) { + // Fast check + BOOL isURL = [key hasPrefix:@"http://"] || [key hasPrefix:@"https://"]; + for (NSNumber *scaleFactor in SDImageScaleFactors()) { + // @2x. for file name and normal url + NSString *fileScale = [NSString stringWithFormat:@"@%@x.", scaleFactor]; + if ([key containsString:fileScale]) { + scale = scaleFactor.doubleValue; + return scale; + } + if (isURL) { + // %402x. for url encode + NSString *urlScale = [NSString stringWithFormat:@"%%40%@x.", scaleFactor]; + if ([key containsString:urlScale]) { + scale = scaleFactor.doubleValue; + return scale; + } + } + } + } + } + return scale; +} + +inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) { + if (!image) { + return nil; + } + + CGFloat scale = SDImageScaleForKey(key); + if (scale > 1) { + UIImage *scaledImage; + if (image.sd_isAnimated) { + UIImage *animatedImage; +#if SD_UIKIT || SD_WATCH + // `UIAnimatedImage` images share the same size and scale. + NSMutableArray *scaledImages = [NSMutableArray array]; + + for (UIImage *tempImage in image.images) { + UIImage *tempScaledImage = [[UIImage alloc] initWithCGImage:tempImage.CGImage scale:scale orientation:tempImage.imageOrientation]; + [scaledImages addObject:tempScaledImage]; + } + + animatedImage = [UIImage animatedImageWithImages:scaledImages duration:image.duration]; + animatedImage.sd_imageLoopCount = image.sd_imageLoopCount; +#else + // Animated GIF for `NSImage` need to grab `NSBitmapImageRep` + NSSize size = NSMakeSize(image.size.width / scale, image.size.height / scale); + animatedImage = [[NSImage alloc] initWithSize:size]; + NSBitmapImageRep *bitmapImageRep = image.bitmapImageRep; + [animatedImage addRepresentation:bitmapImageRep]; +#endif + scaledImage = animatedImage; + } else { +#if SD_UIKIT || SD_WATCH + scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; +#else + scaledImage = [[NSImage alloc] initWithCGImage:image.CGImage size:NSZeroSize]; +#endif + } + + return scaledImage; + } + return image; +} + +#pragma mark - Context option SDWebImageContextOption const SDWebImageContextSetImageGroup = @"setImageGroup"; SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager"; diff --git a/SDWebImage/SDWebImageDownloaderOperation.m b/SDWebImage/SDWebImageDownloaderOperation.m index be454c43..38a58bb2 100644 --- a/SDWebImage/SDWebImageDownloaderOperation.m +++ b/SDWebImage/SDWebImageDownloaderOperation.m @@ -45,6 +45,7 @@ typedef NSMutableDictionary SDCallbacksDictionary; @property (assign, nonatomic, getter = isFinished) BOOL finished; @property (strong, nonatomic, nullable) NSMutableData *imageData; @property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse` +@property (copy, nonatomic, nullable) NSString *cacheKey; @property (assign, nonatomic, readwrite) NSInteger expectedSize; @property (strong, nonatomic, nullable, readwrite) NSURLResponse *response; @@ -350,22 +351,45 @@ didReceiveResponse:(NSURLResponse *)response // We need to create a new instance for progressive decoding to avoid conflicts for (idcoder in [SDWebImageCodersManager sharedManager].coders) { if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] && - [((id)coder) canIncrementallyDecodeFromData:imageData]) { - self.progressiveCoder = [[[coder class] alloc] init]; + [((id)coder) canIncrementalDecodeFromData:imageData]) { + self.progressiveCoder = [[[coder class] alloc] initIncremental]; break; } } } + [self.progressiveCoder updateIncrementalData:imageData finished:finished]; // progressive decode the image in coder queue dispatch_async(self.coderQueue, ^{ - UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished]; + // check whether we should use `SDAnimatedImage` + UIImage *image; + if ([self.context valueForKey:SDWebImageContextAnimatedImageClass]) { + Class animatedImageClass = [self.context valueForKey:SDWebImageContextAnimatedImageClass]; + if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)] && [animatedImageClass instancesRespondToSelector:@selector(initWithAnimatedCoder:scale:)] && [self.progressiveCoder conformsToProtocol:@protocol(SDWebImageAnimatedCoder)]) { + CGFloat scale = SDImageScaleForKey(self.cacheKey); + image = [[animatedImageClass alloc] initWithAnimatedCoder:(id)self.progressiveCoder scale:scale]; + } + } + if (!image) { + BOOL decodeFirstFrame = self.options & SDWebImageDownloaderDecodeFirstFrameOnly; + image = [self.progressiveCoder incrementalDecodedImageWithOptions:@{SDWebImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame)}]; + image = [self scaledImageForKey:self.cacheKey image:image]; + } if (image) { - NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; - image = [self scaledImageForKey:key image:image]; - if (self.shouldDecompressImages) { + BOOL shouldDecode = self.shouldDecompressImages; + 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) { image = [SDWebImageCoderHelper decodedImageWithImage:image]; } + if (!finished) { + image.sd_isIncremental = YES; + } // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding. @@ -431,14 +455,13 @@ didReceiveResponse:(NSURLResponse *)response // decode the image in coder queue dispatch_async(self.coderQueue, ^{ BOOL decodeFirstFrame = self.options & SDWebImageDownloaderDecodeFirstFrameOnly; - NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; 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); + CGFloat scale = SDImageScaleForKey(self.cacheKey); image = [[animatedImageClass alloc] initWithData:imageData scale:scale]; if (self.options & SDWebImageDownloaderPreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) { [((id)image) preloadAllFrames]; @@ -448,10 +471,10 @@ didReceiveResponse:(NSURLResponse *)response } if (!image) { image = [[SDWebImageCodersManager sharedManager] decodedImageWithData:imageData options:@{SDWebImageCoderDecodeFirstFrameOnly : @(decodeFirstFrame)}]; - image = [self scaledImageForKey:key image:image]; + image = [self scaledImageForKey:self.cacheKey image:image]; } - BOOL shouldDecode = YES; + BOOL shouldDecode = self.shouldDecompressImages; if ([image conformsToProtocol:@protocol(SDAnimatedImage)]) { // `SDAnimatedImage` do not decode shouldDecode = NO; @@ -459,14 +482,13 @@ didReceiveResponse:(NSURLResponse *)response // animated image do not decode shouldDecode = NO; } + if (shouldDecode) { - if (self.shouldDecompressImages) { - BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages; - if (shouldScaleDown) { - image = [SDWebImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0]; - } else { - image = [SDWebImageCoderHelper decodedImageWithImage:image]; - } + BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages; + if (shouldScaleDown) { + image = [SDWebImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0]; + } else { + image = [SDWebImageCoderHelper decodedImageWithImage:image]; } } CGSize imageSize = image.size; @@ -519,6 +541,13 @@ didReceiveResponse:(NSURLResponse *)response } #pragma mark Helper methods +- (NSString *)cacheKey { + if (!_cacheKey) { + _cacheKey = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; + } + return _cacheKey; +} + - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image { return SDScaledImageForKey(key, image); } diff --git a/SDWebImage/SDWebImageGIFCoder.h b/SDWebImage/SDWebImageGIFCoder.h index 84a9cb69..c92cbcea 100644 --- a/SDWebImage/SDWebImageGIFCoder.h +++ b/SDWebImage/SDWebImageGIFCoder.h @@ -15,7 +15,7 @@ @note Use `SDWebImageGIFCoder` for fully animated GIFs. For `UIImageView`, it will produce animated `UIImage`(`NSImage` on macOS) for rendering. For `SDAnimatedImageView`, it will use `SDAnimatedImage` for rendering. @note The recommended approach for animated GIFs is using `SDAnimatedImage` with `SDAnimatedImageView`. It's more performant than `UIImageView` for GIF displaying(especially on memory usage) */ -@interface SDWebImageGIFCoder : NSObject +@interface SDWebImageGIFCoder : NSObject @property (nonatomic, class, readonly, nonnull) SDWebImageGIFCoder *sharedCoder; diff --git a/SDWebImage/SDWebImageGIFCoder.m b/SDWebImage/SDWebImageGIFCoder.m index 17403262..cfe2d207 100644 --- a/SDWebImage/SDWebImageGIFCoder.m +++ b/SDWebImage/SDWebImageGIFCoder.m @@ -26,14 +26,12 @@ @implementation SDWebImageGIFCoder { size_t _width, _height; -#if SD_UIKIT || SD_WATCH - UIImageOrientation _orientation; -#endif CGImageSourceRef _imageSource; NSData *_imageData; NSUInteger _loopCount; NSUInteger _frameCount; NSArray *_frames; + BOOL _finished; } - (void)dealloc @@ -167,6 +165,71 @@ return frameDuration; } +#pragma mark - Progressive Decode + +- (BOOL)canIncrementalDecodeFromData:(NSData *)data { + return ([NSData sd_imageFormatForImageData:data] == SDImageFormatGIF); +} + +- (instancetype)initIncremental { + self = [super init]; + if (self) { + _imageSource = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)@{(__bridge_transfer NSString *)kCGImageSourceShouldCache : @(YES)}); +#if SD_UIKIT + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; +#endif + } + return self; +} + +- (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished { + if (_finished) { + return; + } + _imageData = data; + _finished = finished; + + // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ + // Thanks to the author @Nyx0uf + + // Update the data source, we must pass ALL the data, not just the new bytes + CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished); + + if (_width + _height == 0) { + CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL); + if (properties) { + CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); + if (val) CFNumberGetValue(val, kCFNumberLongType, &_height); + val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); + if (val) CFNumberGetValue(val, kCFNumberLongType, &_width); + CFRelease(properties); + } + } + + // For animated image progressive decoding because the frame count and duration may be changed. + [self scanAndCheckFramesValidWithImageSource:_imageSource]; +} + +- (UIImage *)incrementalDecodedImageWithOptions:(SDWebImageCoderOptions *)options { + UIImage *image; + + if (_width + _height > 0) { + // Create the image + CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL); + + if (partialImageRef) { +#if SD_UIKIT || SD_WATCH + image = [[UIImage alloc] initWithCGImage:partialImageRef]; +#elif SD_MAC + image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize]; +#endif + CGImageRelease(partialImageRef); + } + } + + return image; +} + #pragma mark - Encode - (BOOL)canEncodeToFormat:(SDImageFormat)format { return (format == SDImageFormatGIF); @@ -252,8 +315,7 @@ return self; } -- (BOOL)scanAndCheckFramesValidWithImageSource:(CGImageSourceRef)imageSource -{ +- (BOOL)scanAndCheckFramesValidWithImageSource:(CGImageSourceRef)imageSource { if (!imageSource) { return NO; } @@ -275,26 +337,26 @@ return YES; } -- (NSUInteger)animatedImageLoopCount -{ +- (NSData *)animatedImageData { + return _imageData; +} + +- (NSUInteger)animatedImageLoopCount { return _loopCount; } -- (NSUInteger)animatedImageFrameCount -{ +- (NSUInteger)animatedImageFrameCount { return _frameCount; } -- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index -{ +- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index { if (index >= _frameCount) { return 0; } return _frames[index].duration; } -- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index -{ +- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL); if (!imageRef) { return nil; @@ -309,7 +371,7 @@ #if SD_MAC UIImage *image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize]; #else - UIImage *image = [UIImage imageWithCGImage:newImageRef]; + UIImage *image = [[UIImage alloc] initWithCGImage:newImageRef]; #endif CGImageRelease(newImageRef); return image; diff --git a/SDWebImage/SDWebImageImageIOCoder.m b/SDWebImage/SDWebImageImageIOCoder.m index b6e2ea5b..0dd5a381 100644 --- a/SDWebImage/SDWebImageImageIOCoder.m +++ b/SDWebImage/SDWebImageImageIOCoder.m @@ -19,6 +19,7 @@ #endif CGImageSourceRef _imageSource; NSUInteger _frameCount; + BOOL _finished; } - (void)dealloc { @@ -63,7 +64,7 @@ } } -- (BOOL)canIncrementallyDecodeFromData:(NSData *)data { +- (BOOL)canIncrementalDecodeFromData:(NSData *)data { switch ([NSData sd_imageFormatForImageData:data]) { case SDImageFormatWebP: // Do not support WebP progressive decoding @@ -100,7 +101,7 @@ } #pragma mark - Progressive Decode -- (instancetype)initIncrementally { +- (instancetype)initIncremental { self = [super init]; if (self) { _imageSource = CGImageSourceCreateIncremental(NULL); @@ -111,8 +112,11 @@ return self; } -- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished { - UIImage *image; +- (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished { + if (_finished) { + return; + } + _finished = finished; // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ // Thanks to the author @Nyx0uf @@ -142,6 +146,10 @@ #endif } } +} + +- (UIImage *)incrementalDecodedImageWithOptions:(SDWebImageCoderOptions *)options { + UIImage *image; if (_width + _height > 0) { // Create the image @@ -176,13 +184,6 @@ } } - if (finished) { - if (_imageSource) { - CFRelease(_imageSource); - _imageSource = NULL; - } - } - return image; } diff --git a/SDWebImage/SDWebImageWebPCoder.m b/SDWebImage/SDWebImageWebPCoder.m index 47af8ea2..cf295292 100644 --- a/SDWebImage/SDWebImageWebPCoder.m +++ b/SDWebImage/SDWebImageWebPCoder.m @@ -57,6 +57,7 @@ dispatch_semaphore_signal(self->_lock); CGContextRef _canvas; BOOL _hasAnimation; BOOL _hasAlpha; + BOOL _finished; CGFloat _canvasWidth; CGFloat _canvasHeight; dispatch_semaphore_t _lock; @@ -92,7 +93,7 @@ dispatch_semaphore_signal(self->_lock); return ([NSData sd_imageFormatForImageData:data] == SDImageFormatWebP); } -- (BOOL)canIncrementallyDecodeFromData:(NSData *)data { +- (BOOL)canIncrementalDecodeFromData:(NSData *)data { return ([NSData sd_imageFormatForImageData:data] == SDImageFormatWebP); } @@ -176,7 +177,7 @@ dispatch_semaphore_signal(self->_lock); } #pragma mark - Progressive Decode -- (instancetype)initIncrementally { +- (instancetype)initIncremental { self = [super init]; if (self) { // Progressive images need transparent, so always use premultiplied RGBA @@ -185,16 +186,24 @@ dispatch_semaphore_signal(self->_lock); return self; } -- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished { - if (!_idec) { - return nil; +- (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished { + if (_finished) { + return; } - UIImage *image; - + _imageData = data; + _finished = finished; VP8StatusCode status = WebPIUpdate(_idec, data.bytes, data.length); if (status != VP8_STATUS_OK && status != VP8_STATUS_SUSPENDED) { - return nil; + return; } +} + +- (BOOL)incrementalFinished { + return _finished; +} + +- (UIImage *)incrementalDecodedImageWithOptions:(SDWebImageCoderOptions *)options { + UIImage *image; int width = 0; int height = 0; @@ -250,13 +259,6 @@ dispatch_semaphore_signal(self->_lock); CGContextRelease(canvas); } - if (finished) { - if (_idec) { - WebPIDelete(_idec); - _idec = NULL; - } - } - return image; } @@ -517,8 +519,7 @@ static void FreeImageData(void *info, const void *data, size_t size) { return self; } -- (BOOL)scanAndCheckFramesValidWithDemuxer:(WebPDemuxer *)demuxer -{ +- (BOOL)scanAndCheckFramesValidWithDemuxer:(WebPDemuxer *)demuxer { if (!demuxer) { return NO; } @@ -586,6 +587,10 @@ static void FreeImageData(void *info, const void *data, size_t size) { return YES; } +- (NSData *)animatedImageData { + return _imageData; +} + - (NSUInteger)animatedImageLoopCount { return _loopCount; } diff --git a/SDWebImage/UIImage+WebCache.h b/SDWebImage/UIImage+WebCache.h index efa1c4c6..0c7bf351 100644 --- a/SDWebImage/UIImage+WebCache.h +++ b/SDWebImage/UIImage+WebCache.h @@ -29,4 +29,9 @@ */ @property (nonatomic, assign, readonly) BOOL sd_isAnimated; +/** + Indicating whether the image is during incremental decoding and may not contains full pixels. + */ +@property (nonatomic, assign) BOOL sd_isIncremental; + @end diff --git a/SDWebImage/UIImage+WebCache.m b/SDWebImage/UIImage+WebCache.m index 60d3e2f7..c7a82779 100644 --- a/SDWebImage/UIImage+WebCache.m +++ b/SDWebImage/UIImage+WebCache.m @@ -7,11 +7,11 @@ */ #import "UIImage+WebCache.h" +#import "NSImage+Additions.h" +#import "objc/runtime.h" #if SD_UIKIT -#import "objc/runtime.h" - @implementation UIImage (WebCache) - (NSUInteger)sd_imageLoopCount { @@ -32,6 +32,15 @@ return (self.images != nil); } +- (void)setSd_isIncremental:(BOOL)sd_isIncremental { + objc_setAssociatedObject(self, @selector(sd_isIncremental), @(sd_isIncremental), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (BOOL)sd_isIncremental { + NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isIncremental)); + return value.boolValue; +} + @end #endif @@ -42,39 +51,39 @@ - (NSUInteger)sd_imageLoopCount { NSUInteger imageLoopCount = 0; - for (NSImageRep *rep in self.representations) { - if ([rep isKindOfClass:[NSBitmapImageRep class]]) { - NSBitmapImageRep *bitmapRep = (NSBitmapImageRep *)rep; - imageLoopCount = [[bitmapRep valueForProperty:NSImageLoopCount] unsignedIntegerValue]; - break; - } + NSBitmapImageRep *bitmapImageRep = self.bitmapImageRep; + if (bitmapImageRep) { + imageLoopCount = [[bitmapImageRep valueForProperty:NSImageLoopCount] unsignedIntegerValue]; } return imageLoopCount; } - (void)setSd_imageLoopCount:(NSUInteger)sd_imageLoopCount { - for (NSImageRep *rep in self.representations) { - if ([rep isKindOfClass:[NSBitmapImageRep class]]) { - NSBitmapImageRep *bitmapRep = (NSBitmapImageRep *)rep; - [bitmapRep setProperty:NSImageLoopCount withValue:@(sd_imageLoopCount)]; - break; - } + NSBitmapImageRep *bitmapImageRep = self.bitmapImageRep; + if (bitmapImageRep) { + [bitmapImageRep setProperty:NSImageLoopCount withValue:@(sd_imageLoopCount)]; } } - (BOOL)sd_isAnimated { BOOL isGIF = NO; - for (NSImageRep *rep in self.representations) { - if ([rep isKindOfClass:[NSBitmapImageRep class]]) { - NSBitmapImageRep *bitmapRep = (NSBitmapImageRep *)rep; - NSUInteger frameCount = [[bitmapRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; - isGIF = frameCount > 1 ? YES : NO; - break; - } + NSBitmapImageRep *bitmapImageRep = self.bitmapImageRep; + if (bitmapImageRep) { + NSUInteger frameCount = [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; + isGIF = frameCount > 1 ? YES : NO; } return isGIF; } +- (void)setSd_isIncremental:(BOOL)sd_isIncremental { + objc_setAssociatedObject(self, @selector(sd_isIncremental), @(sd_isIncremental), OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + +- (BOOL)sd_isIncremental { + NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isIncremental)); + return value.boolValue; +} + @end #endif