diff --git a/Dependency/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.h b/Dependency/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.h index 656a58b..678ddcc 100755 --- a/Dependency/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.h +++ b/Dependency/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.h @@ -13,7 +13,7 @@ #if __has_include() #import #else -#import "FLAnimatedImageView.h" +#import "FLAnimatedImage.h" #endif #import "SDWebImageManager.h" diff --git a/Dependency/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.m b/Dependency/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.m index 04a1c85..ed5db34 100755 --- a/Dependency/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.m +++ b/Dependency/SDWebImage/FLAnimatedImage/FLAnimatedImageView+WebCache.m @@ -13,7 +13,6 @@ #import "UIView+WebCacheOperation.h" #import "UIView+WebCache.h" #import "NSData+ImageContentType.h" -#import "FLAnimatedImage.h" #import "UIImageView+WebCache.h" @implementation FLAnimatedImageView (WebCache) @@ -53,17 +52,25 @@ - (void)sd_setImageWithURL:(nullable NSURL *)url options:options operationKey:nil setImageBlock:^(UIImage *image, NSData *imageData) { + // This setImageBlock may not called from main queue SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData]; + FLAnimatedImage *animatedImage; if (imageFormat == SDImageFormatGIF) { - weakSelf.animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData]; - weakSelf.image = nil; - } else { - weakSelf.image = image; - weakSelf.animatedImage = nil; + animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData]; } + dispatch_main_async_safe(^{ + if (animatedImage) { + weakSelf.animatedImage = animatedImage; + weakSelf.image = nil; + } else { + weakSelf.image = image; + weakSelf.animatedImage = nil; + } + }); } progress:progressBlock - completed:completedBlock]; + completed:completedBlock + context:@{SDWebImageInternalSetImageInGlobalQueueKey: @(YES)}]; } @end diff --git a/Dependency/SDWebImage/NSData+ImageContentType.h b/Dependency/SDWebImage/NSData+ImageContentType.h index b23b0bd..0ca226d 100755 --- a/Dependency/SDWebImage/NSData+ImageContentType.h +++ b/Dependency/SDWebImage/NSData+ImageContentType.h @@ -16,7 +16,8 @@ typedef NS_ENUM(NSInteger, SDImageFormat) { SDImageFormatPNG, SDImageFormatGIF, SDImageFormatTIFF, - SDImageFormatWebP + SDImageFormatWebP, + SDImageFormatHEIC }; @interface NSData (ImageContentType) @@ -30,4 +31,12 @@ typedef NS_ENUM(NSInteger, SDImageFormat) { */ + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data; +/** + Convert SDImageFormat to UTType + + @param format Format as SDImageFormat + @return The UTType as CFStringRef + */ ++ (nonnull CFStringRef)sd_UTTypeFromSDImageFormat:(SDImageFormat)format; + @end diff --git a/Dependency/SDWebImage/NSData+ImageContentType.m b/Dependency/SDWebImage/NSData+ImageContentType.m index 7364715..6a59061 100755 --- a/Dependency/SDWebImage/NSData+ImageContentType.m +++ b/Dependency/SDWebImage/NSData+ImageContentType.m @@ -8,7 +8,16 @@ */ #import "NSData+ImageContentType.h" +#if SD_MAC +#import +#else +#import +#endif +// Currently Image/IO does not support WebP +#define kSDUTTypeWebP ((__bridge CFStringRef)@"public.webp") +// AVFileTypeHEIC is defined in AVFoundation via iOS 11, we use this without import AVFoundation +#define kSDUTTypeHEIC ((__bridge CFStringRef)@"public.heic") @implementation NSData (ImageContentType) @@ -17,6 +26,7 @@ + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data { return SDImageFormatUndefined; } + // File signatures table: http://www.garykessler.net/library/file_sigs.html uint8_t c; [data getBytes:&c length:1]; switch (c) { @@ -29,18 +39,60 @@ + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data { case 0x49: case 0x4D: return SDImageFormatTIFF; - case 0x52: - // R as RIFF for WEBP - if (data.length < 12) { - return SDImageFormatUndefined; + case 0x52: { + if (data.length >= 12) { + //RIFF....WEBP + NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; + if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { + return SDImageFormatWebP; + } } - - NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; - if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { - return SDImageFormatWebP; + break; + } + case 0x00: { + if (data.length >= 12) { + //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx + NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding]; + if ([testString isEqualToString:@"ftypheic"] + || [testString isEqualToString:@"ftypheix"] + || [testString isEqualToString:@"ftyphevc"] + || [testString isEqualToString:@"ftyphevx"]) { + return SDImageFormatHEIC; + } } + break; + } } return SDImageFormatUndefined; } ++ (nonnull CFStringRef)sd_UTTypeFromSDImageFormat:(SDImageFormat)format { + CFStringRef UTType; + switch (format) { + case SDImageFormatJPEG: + UTType = kUTTypeJPEG; + break; + case SDImageFormatPNG: + UTType = kUTTypePNG; + break; + case SDImageFormatGIF: + UTType = kUTTypeGIF; + break; + case SDImageFormatTIFF: + UTType = kUTTypeTIFF; + break; + case SDImageFormatWebP: + UTType = kSDUTTypeWebP; + break; + case SDImageFormatHEIC: + UTType = kSDUTTypeHEIC; + break; + default: + // default is kUTTypePNG + UTType = kUTTypePNG; + break; + } + return UTType; +} + @end diff --git a/Dependency/SDWebImage/NSImage+WebCache.m b/Dependency/SDWebImage/NSImage+WebCache.m index 518b498..140ed6c 100755 --- a/Dependency/SDWebImage/NSImage+WebCache.m +++ b/Dependency/SDWebImage/NSImage+WebCache.m @@ -23,7 +23,16 @@ - (CGImageRef)CGImage { } - (BOOL)isGIF { - return NO; + 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; + } + } + return isGIF; } @end diff --git a/Dependency/SDWebImage/SDImageCache.m b/Dependency/SDWebImage/SDImageCache.m index acd3f2b..15d27d1 100755 --- a/Dependency/SDWebImage/SDImageCache.m +++ b/Dependency/SDWebImage/SDImageCache.m @@ -7,12 +7,9 @@ */ #import "SDImageCache.h" -#import "SDWebImageDecoder.h" -#import "UIImage+MultiFormat.h" #import -#import "UIImage+GIF.h" -#import "NSData+ImageContentType.h" #import "NSImage+WebCache.h" +#import "SDWebImageCodersManager.h" // See https://github.com/rs/SDWebImage/pull/1141 for discussion @interface AutoPurgeCache : NSCache @@ -171,10 +168,11 @@ - (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key { } unsigned char r[CC_MD5_DIGEST_LENGTH]; CC_MD5(str, (CC_LONG)strlen(str), r); + NSURL *keyURL = [NSURL URLWithString:key]; + NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension; NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@", r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], - r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]]; - + r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]]; return filename; } @@ -220,9 +218,9 @@ - (void)storeImage:(nullable UIImage *)image @autoreleasepool { NSData *data = imageData; if (!data && image) { - SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data]; - data = [image sd_imageDataAsFormat:imageFormatFromData]; - } + // If we do not have any data to detect image format, use PNG format + data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:SDImageFormatPNG]; + } [self storeImageDataToDisk:data forKey:key]; } @@ -311,14 +309,14 @@ - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key { - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key { NSString *defaultPath = [self defaultCachePathForKey:key]; - NSData *data = [NSData dataWithContentsOfFile:defaultPath]; + NSData *data = [NSData dataWithContentsOfFile:defaultPath options:self.config.diskCacheReadingOptions error:nil]; if (data) { return data; } // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name // checking the key with and without the extension - data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension]; + data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil]; if (data) { return data; } @@ -326,14 +324,14 @@ - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString * NSArray *customPaths = [self.customPaths copy]; for (NSString *path in customPaths) { NSString *filePath = [self cachePathForKey:key inPath:path]; - NSData *imageData = [NSData dataWithContentsOfFile:filePath]; + NSData *imageData = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; if (imageData) { return imageData; } // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name // checking the key with and without the extension - imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension]; + imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension options:self.config.diskCacheReadingOptions error:nil]; if (imageData) { return imageData; } @@ -345,10 +343,10 @@ - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString * - (nullable UIImage *)diskImageForKey:(nullable NSString *)key { NSData *data = [self diskImageDataBySearchingAllPathsForKey:key]; if (data) { - UIImage *image = [UIImage sd_imageWithData:data]; + UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data]; image = [self scaledImageForKey:key image:image]; if (self.config.shouldDecompressImages) { - image = [UIImage decodedImageWithImage:image]; + image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}]; } return image; } else { @@ -372,7 +370,7 @@ - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key don UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { NSData *diskData = nil; - if ([image isGIF]) { + if (image.images) { diskData = [self diskImageDataBySearchingAllPathsForKey:key]; } if (doneBlock) { diff --git a/Dependency/SDWebImage/SDImageCacheConfig.h b/Dependency/SDWebImage/SDImageCacheConfig.h index b1523e2..20f2d10 100755 --- a/Dependency/SDWebImage/SDImageCacheConfig.h +++ b/Dependency/SDWebImage/SDImageCacheConfig.h @@ -18,7 +18,7 @@ @property (assign, nonatomic) BOOL shouldDecompressImages; /** - * disable iCloud backup [defaults to YES] + * disable iCloud backup [defaults to YES] */ @property (assign, nonatomic) BOOL shouldDisableiCloud; @@ -28,7 +28,13 @@ @property (assign, nonatomic) BOOL shouldCacheImagesInMemory; /** - * The maximum length of time to keep an image in the cache, in seconds + * The reading options while reading cache from disk. + * Defaults to 0. You can set this to mapped file to improve performance. + */ +@property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions; + +/** + * The maximum length of time to keep an image in the cache, in seconds. */ @property (assign, nonatomic) NSInteger maxCacheAge; diff --git a/Dependency/SDWebImage/SDImageCacheConfig.m b/Dependency/SDWebImage/SDImageCacheConfig.m index 6b588eb..7a5af6c 100755 --- a/Dependency/SDWebImage/SDImageCacheConfig.m +++ b/Dependency/SDWebImage/SDImageCacheConfig.m @@ -17,6 +17,7 @@ - (instancetype)init { _shouldDecompressImages = YES; _shouldDisableiCloud = YES; _shouldCacheImagesInMemory = YES; + _diskCacheReadingOptions = 0; _maxCacheAge = kDefaultCacheMaxCacheAge; _maxCacheSize = 0; } diff --git a/Dependency/SDWebImage/SDWebImageCoder.h b/Dependency/SDWebImage/SDWebImageCoder.h new file mode 100755 index 0000000..7c0a63f --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageCoder.h @@ -0,0 +1,119 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import +#import "SDWebImageCompat.h" +#import "NSData+ImageContentType.h" + +/** + A Boolean value indicating whether to scale down large images during decompressing. (NSNumber) + */ +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageCoderScaleDownLargeImagesKey; + +/** + Return the shared device-dependent RGB color space created with CGColorSpaceCreateDeviceRGB. + + @return The device-dependent RGB color space + */ +CG_EXTERN CGColorSpaceRef _Nonnull SDCGColorSpaceGetDeviceRGB(void); + +/** + Check whether CGImageRef contains alpha channel. + + @param imageRef The CGImageRef + @return Return YES if CGImageRef contains alpha channel, otherwise return NO + */ +CG_EXTERN BOOL SDCGImageRefContainsAlpha(_Nullable CGImageRef imageRef); + + +/** + This is the image coder protocol to provide custom image decoding/encoding. + These methods are all required to implement. + @note Pay attention that these methods are not called from main queue. + */ +@protocol SDWebImageCoder + +@required +#pragma mark - Decoding +/** + Returns YES if this coder can decode some data. Otherwise, the data should be passed to another coder. + + @param data The image data so we can look at it + @return YES if this coder can decode the data, NO otherwise + */ +- (BOOL)canDecodeFromData:(nullable NSData *)data; + +/** + Decode the image data to image. + + @param data The image data to be decoded + @return The decoded image from data + */ +- (nullable UIImage *)decodedImageWithData:(nullable NSData *)data; + +/** + Decompress the image with original image and image data. + + @param image The original image to be decompressed + @param data The pointer to original image data. The pointer itself is nonnull but image data can be null. This data will set to cache if needed. If you do not need to modify data at the sametime, ignore this param. + @param optionsDict A dictionary containing any decompressing options. Pass {SDWebImageCoderScaleDownLargeImagesKey: @(YES)} to scale down large images + @return The decompressed image + */ +- (nullable UIImage *)decompressedImageWithImage:(nullable UIImage *)image + data:(NSData * _Nullable * _Nonnull)data + options:(nullable NSDictionary*)optionsDict; + +#pragma mark - Encoding + +/** + Returns YES if this coder can encode some image. Otherwise, it should be passed to another coder. + + @param format The image format + @return YES if this coder can encode the image, NO otherwise + */ +- (BOOL)canEncodeToFormat:(SDImageFormat)format; + +/** + Encode the image to image data. + + @param image The image to be encoded + @param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible + @return The encoded image data + */ +- (nullable NSData *)encodedDataWithImage:(nullable UIImage *)image format:(SDImageFormat)format; + +@end + + +/** + This is the image coder protocol to provide custom progressive image decoding. + These methods are all required to implement. + @note Pay attention that these methods are not called from main queue. + */ +@protocol SDWebImageProgressiveCoder + +@required +/** + Returns YES if this coder can incremental decode some data. Otherwise, it should be passed to another coder. + + @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; + +/** + Incremental decode the image data to image. + + @param data The image data has been downloaded so far + @param finished Whether the download has finished + @warning 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 + @return The decoded image from data + */ +- (nullable UIImage *)incrementallyDecodedImageWithData:(nullable NSData *)data finished:(BOOL)finished; + +@end diff --git a/Dependency/SDWebImage/SDWebImageCoder.m b/Dependency/SDWebImage/SDWebImageCoder.m new file mode 100755 index 0000000..9357fe5 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageCoder.m @@ -0,0 +1,31 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "SDWebImageCoder.h" + +NSString * const SDWebImageCoderScaleDownLargeImagesKey = @"scaleDownLargeImages"; + +CGColorSpaceRef SDCGColorSpaceGetDeviceRGB(void) { + static CGColorSpaceRef colorSpace; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + colorSpace = CGColorSpaceCreateDeviceRGB(); + }); + return colorSpace; +} + +BOOL SDCGImageRefContainsAlpha(CGImageRef imageRef) { + if (!imageRef) { + return NO; + } + CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef); + BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone || + alphaInfo == kCGImageAlphaNoneSkipFirst || + alphaInfo == kCGImageAlphaNoneSkipLast); + return hasAlpha; +} diff --git a/Dependency/SDWebImage/SDWebImageCoderHelper.h b/Dependency/SDWebImage/SDWebImageCoderHelper.h new file mode 100755 index 0000000..cdafd88 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageCoderHelper.h @@ -0,0 +1,52 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import +#import "SDWebImageCompat.h" +#import "SDWebImageFrame.h" + +@interface SDWebImageCoderHelper : NSObject + +/** + Return an animated image with frames array. + For UIKit, this will apply the patch and then create animated UIImage. The patch is because that `+[UIImage animatedImageWithImages:duration:]` just use the averate of duration for each image. So it will not work if different frame has different duration. Therefore we repeat the specify frame for specify times to let it work. + For AppKit, NSImage does not support animates other than GIF. This will try to encode the frames to GIF format and then create an animated NSImage for rendering. + + @param frames The frames array. If no frames or frames is empty, return nil + @return A animated image for rendering on UIImageView(UIKit) or NSImageView(AppKit) + */ ++ (UIImage * _Nullable)animatedImageWithFrames:(NSArray * _Nullable)frames; + +/** + Return frames array from an animated image. + For UIKit, this will unapply the patch for the description above and then create frames array. This will also work for normal animated UIImage. + For AppKit, NSImage does not support animates other than GIF. This will try to decode the GIF imageRep and then create frames array. + + @param animatedImage A animated image. If it's not animated, return nil + @return The frames array + */ ++ (NSArray * _Nullable)framesFromAnimatedImage:(UIImage * _Nullable)animatedImage; + +#if SD_UIKIT || SD_WATCH +/** + Convert an EXIF image orientation to an iOS one. + + @param exifOrientation EXIF orientation + @return iOS orientation + */ ++ (UIImageOrientation)imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation; +/** + Convert an iOS orientation to an EXIF image orientation. + + @param imageOrientation iOS orientation + @return EXIF orientation + */ ++ (NSInteger)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation; +#endif + +@end diff --git a/Dependency/SDWebImage/SDWebImageCoderHelper.m b/Dependency/SDWebImage/SDWebImageCoderHelper.m new file mode 100755 index 0000000..b2b651a --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageCoderHelper.m @@ -0,0 +1,255 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "SDWebImageCoderHelper.h" +#import "SDWebImageFrame.h" +#import "UIImage+MultiFormat.h" +#import "NSImage+WebCache.h" +#import + +@implementation SDWebImageCoderHelper + ++ (UIImage *)animatedImageWithFrames:(NSArray *)frames { + NSUInteger frameCount = frames.count; + if (frameCount == 0) { + return nil; + } + + UIImage *animatedImage; + +#if SD_UIKIT || SD_WATCH + NSUInteger durations[frameCount]; + for (size_t i = 0; i < frameCount; i++) { + durations[i] = frames[i].duration * 1000; + } + NSUInteger const gcd = gcdArray(frameCount, durations); + __block NSUInteger totalDuration = 0; + NSMutableArray *animatedImages = [NSMutableArray arrayWithCapacity:frameCount]; + [frames enumerateObjectsUsingBlock:^(SDWebImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) { + UIImage *image = frame.image; + NSUInteger duration = frame.duration * 1000; + totalDuration += duration; + NSUInteger repeatCount; + if (gcd) { + repeatCount = duration / gcd; + } else { + repeatCount = 1; + } + for (size_t i = 0; i < repeatCount; ++i) { + [animatedImages addObject:image]; + } + }]; + + animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f]; + +#else + + NSMutableData *imageData = [NSMutableData data]; + CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatGIF]; + // Create an image destination. GIF does not support EXIF image orientation + CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frameCount, NULL); + if (!imageDestination) { + // Handle failure. + return nil; + } + + for (size_t i = 0; i < frameCount; i++) { + @autoreleasepool { + SDWebImageFrame *frame = frames[i]; + float frameDuration = frame.duration; + CGImageRef frameImageRef = frame.image.CGImage; + NSDictionary *frameProperties = @{(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary : @{(__bridge_transfer NSString *)kCGImagePropertyGIFUnclampedDelayTime : @(frameDuration)}}; + CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties); + } + } + // Finalize the destination. + if (CGImageDestinationFinalize(imageDestination) == NO) { + // Handle failure. + CFRelease(imageDestination); + return nil; + } + CFRelease(imageDestination); + animatedImage = [[NSImage alloc] initWithData:imageData]; +#endif + + return animatedImage; +} + ++ (NSArray *)framesFromAnimatedImage:(UIImage *)animatedImage { + if (!animatedImage) { + return nil; + } + + NSMutableArray *frames = [NSMutableArray array]; + NSUInteger frameCount = 0; + +#if SD_UIKIT || SD_WATCH + NSArray *animatedImages = animatedImage.images; + frameCount = animatedImages.count; + if (frameCount == 0) { + return nil; + } + + NSTimeInterval avgDuration = animatedImage.duration / frameCount; + if (avgDuration == 0) { + avgDuration = 0.1; // if it's a animated image but no duration, set it to default 100ms (this do not have that 10ms limit like GIF or WebP to allow custom coder provide the limit) + } + + __block NSUInteger index = 0; + __block NSUInteger repeatCount = 1; + __block UIImage *previousImage = animatedImages.firstObject; + [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) { + // ignore first + if (idx == 0) { + return; + } + if ([image isEqual:previousImage]) { + repeatCount++; + } else { + SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; + [frames addObject:frame]; + repeatCount = 1; + index++; + } + previousImage = image; + // last one + if (idx == frameCount - 1) { + SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; + [frames addObject:frame]; + } + }]; + +#else + + NSBitmapImageRep *bitmapRep; + for (NSImageRep *imageRep in animatedImage.representations) { + if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { + bitmapRep = (NSBitmapImageRep *)imageRep; + break; + } + } + if (bitmapRep) { + frameCount = [[bitmapRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; + } + + if (frameCount == 0) { + return nil; + } + + for (size_t i = 0; i < frameCount; i++) { + @autoreleasepool { + // NSBitmapImageRep need to manually change frame. "Good taste" API + [bitmapRep setProperty:NSImageCurrentFrame withValue:@(i)]; + float frameDuration = [[bitmapRep valueForProperty:NSImageCurrentFrameDuration] floatValue]; + NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapRep.CGImage size:CGSizeZero]; + SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:frameImage duration:frameDuration]; + [frames addObject:frame]; + } + } +#endif + + return frames; +} + +#if SD_UIKIT || SD_WATCH +// Convert an EXIF image orientation to an iOS one. ++ (UIImageOrientation)imageOrientationFromEXIFOrientation:(NSInteger)exifOrientation { + // CGImagePropertyOrientation is available on iOS 8 above. Currently kept for compatibility + UIImageOrientation imageOrientation = UIImageOrientationUp; + switch (exifOrientation) { + case 1: + imageOrientation = UIImageOrientationUp; + break; + case 3: + imageOrientation = UIImageOrientationDown; + break; + case 8: + imageOrientation = UIImageOrientationLeft; + break; + case 6: + imageOrientation = UIImageOrientationRight; + break; + case 2: + imageOrientation = UIImageOrientationUpMirrored; + break; + case 4: + imageOrientation = UIImageOrientationDownMirrored; + break; + case 5: + imageOrientation = UIImageOrientationLeftMirrored; + break; + case 7: + imageOrientation = UIImageOrientationRightMirrored; + break; + default: + break; + } + return imageOrientation; +} + +// Convert an iOS orientation to an EXIF image orientation. ++ (NSInteger)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation { + // CGImagePropertyOrientation is available on iOS 8 above. Currently kept for compatibility + NSInteger exifOrientation = 1; + switch (imageOrientation) { + case UIImageOrientationUp: + exifOrientation = 1; + break; + case UIImageOrientationDown: + exifOrientation = 3; + break; + case UIImageOrientationLeft: + exifOrientation = 8; + break; + case UIImageOrientationRight: + exifOrientation = 6; + break; + case UIImageOrientationUpMirrored: + exifOrientation = 2; + break; + case UIImageOrientationDownMirrored: + exifOrientation = 4; + break; + case UIImageOrientationLeftMirrored: + exifOrientation = 5; + break; + case UIImageOrientationRightMirrored: + exifOrientation = 7; + break; + default: + break; + } + return exifOrientation; +} +#endif + +#pragma mark - Helper Fuction +#if SD_UIKIT || SD_WATCH +static NSUInteger gcd(NSUInteger a, NSUInteger b) { + NSUInteger c; + while (a != 0) { + c = a; + a = b % a; + b = c; + } + return b; +} + +static NSUInteger gcdArray(size_t const count, NSUInteger const * const values) { + if (count == 0) { + return 0; + } + NSUInteger result = values[0]; + for (size_t i = 1; i < count; ++i) { + result = gcd(values[i], result); + } + return result; +} +#endif + +@end diff --git a/Dependency/SDWebImage/SDWebImageCodersManager.h b/Dependency/SDWebImage/SDWebImageCodersManager.h new file mode 100755 index 0000000..5c3d4b3 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageCodersManager.h @@ -0,0 +1,58 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import +#import "SDWebImageCoder.h" + +/** + Global object holding the array of coders, so that we avoid passing them from object to object. + Uses a priority queue behind scenes, which means the latest added coders have the highest priority. + This is done so when encoding/decoding something, we go through the list and ask each coder if they can handle the current data. + That way, users can add their custom coders while preserving our existing prebuilt ones + + Note: the `coders` getter will return the coders in their reversed order + Example: + - by default we internally set coders = `IOCoder`, `WebPCoder`. (`GIFCoder` is not recommended to add only if you want to get GIF support without `FLAnimatedImage`) + - calling `coders` will return `@[WebPCoder, IOCoder]` + - call `[addCoder:[MyCrazyCoder new]]` + - calling `coders` now returns `@[MyCrazyCoder, WebPCoder, IOCoder]` + + Coders + ------ + A coder must conform to the `SDWebImageCoder` protocol or even to `SDWebImageProgressiveCoder` if it supports progressive decoding + Conformance is important because that way, they will implement `canDecodeFromData` or `canEncodeToFormat` + Those methods are called on each coder in the array (using the priority order) until one of them returns YES. + That means that coder can decode that data / encode to that format + */ +@interface SDWebImageCodersManager : NSObject + +/** + Shared reusable instance + */ ++ (nonnull instancetype)sharedInstance; + +/** + All coders in coders manager. The coders array is a priority queue, which means the later added coder will have the highest priority + */ +@property (nonatomic, strong, readwrite, nullable) NSArray* coders; + +/** + Add a new coder to the end of coders array. Which has the highest priority. + + @param coder coder + */ +- (void)addCoder:(nonnull id)coder; + +/** + Remove a coder in the coders array. + + @param coder coder + */ +- (void)removeCoder:(nonnull id)coder; + +@end diff --git a/Dependency/SDWebImage/SDWebImageCodersManager.m b/Dependency/SDWebImage/SDWebImageCodersManager.m new file mode 100755 index 0000000..23bc4c4 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageCodersManager.m @@ -0,0 +1,137 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "SDWebImageCodersManager.h" +#import "SDWebImageImageIOCoder.h" +#import "SDWebImageGIFCoder.h" +#ifdef SD_WEBP +#import "SDWebImageWebPCoder.h" +#endif + +@interface SDWebImageCodersManager () + +@property (nonatomic, strong, nonnull) NSMutableArray* mutableCoders; +@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t mutableCodersAccessQueue; + +@end + +@implementation SDWebImageCodersManager + ++ (nonnull instancetype)sharedInstance { + static dispatch_once_t once; + static id instance; + dispatch_once(&once, ^{ + instance = [self new]; + }); + return instance; +} + +- (instancetype)init { + if (self = [super init]) { + // initialize with default coders + _mutableCoders = [@[[SDWebImageImageIOCoder sharedCoder]] mutableCopy]; +#ifdef SD_WEBP + [_mutableCoders addObject:[SDWebImageWebPCoder sharedCoder]]; +#endif + _mutableCodersAccessQueue = dispatch_queue_create("com.hackemist.SDWebImageCodersManager", DISPATCH_QUEUE_CONCURRENT); + } + return self; +} + +- (void)dealloc { + SDDispatchQueueRelease(_mutableCodersAccessQueue); +} + +#pragma mark - Coder IO operations + +- (void)addCoder:(nonnull id)coder { + if ([coder conformsToProtocol:@protocol(SDWebImageCoder)]) { + dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{ + [self.mutableCoders addObject:coder]; + }); + } +} + +- (void)removeCoder:(nonnull id)coder { + dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{ + [self.mutableCoders removeObject:coder]; + }); +} + +- (NSArray *)coders { + __block NSArray *sortedCoders = nil; + dispatch_sync(self.mutableCodersAccessQueue, ^{ + sortedCoders = (NSArray *)[[[self.mutableCoders copy] reverseObjectEnumerator] allObjects]; + }); + return sortedCoders; +} + +- (void)setCoders:(NSArray *)coders { + dispatch_barrier_sync(self.mutableCodersAccessQueue, ^{ + self.mutableCoders = [coders mutableCopy]; + }); +} + +#pragma mark - SDWebImageCoder +- (BOOL)canDecodeFromData:(NSData *)data { + for (id coder in self.coders) { + if ([coder canDecodeFromData:data]) { + return YES; + } + } + return NO; +} + +- (BOOL)canEncodeToFormat:(SDImageFormat)format { + for (id coder in self.coders) { + if ([coder canEncodeToFormat:format]) { + return YES; + } + } + return NO; +} + +- (UIImage *)decodedImageWithData:(NSData *)data { + if (!data) { + return nil; + } + for (id coder in self.coders) { + if ([coder canDecodeFromData:data]) { + return [coder decodedImageWithData:data]; + } + } + return nil; +} + +- (UIImage *)decompressedImageWithImage:(UIImage *)image + data:(NSData *__autoreleasing _Nullable *)data + options:(nullable NSDictionary*)optionsDict { + if (!image) { + return nil; + } + for (id coder in self.coders) { + if ([coder canDecodeFromData:*data]) { + return [coder decompressedImageWithImage:image data:data options:optionsDict]; + } + } + return nil; +} + +- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format { + if (!image) { + return nil; + } + for (id coder in self.coders) { + if ([coder canEncodeToFormat:format]) { + return [coder encodedDataWithImage:image format:format]; + } + } + return nil; +} + +@end diff --git a/Dependency/SDWebImage/SDWebImageCompat.h b/Dependency/SDWebImage/SDWebImageCompat.h index 866d92b..e950a6a 100755 --- a/Dependency/SDWebImage/SDWebImageCompat.h +++ b/Dependency/SDWebImage/SDWebImageCompat.h @@ -93,11 +93,11 @@ #define SDDispatchQueueSetterSementics assign #endif -extern UIImage *SDScaledImageForKey(NSString *key, UIImage *image); +FOUNDATION_EXPORT UIImage *SDScaledImageForKey(NSString *key, UIImage *image); -typedef void(^SDWebImageNoParamsBlock)(); +typedef void(^SDWebImageNoParamsBlock)(void); -extern NSString *const SDWebImageErrorDomain; +FOUNDATION_EXPORT NSString *const SDWebImageErrorDomain; #ifndef dispatch_main_async_safe #define dispatch_main_async_safe(block)\ @@ -107,5 +107,3 @@ extern NSString *const SDWebImageErrorDomain; dispatch_async(dispatch_get_main_queue(), block);\ } #endif - -static int64_t kAsyncTestTimeout = 5; diff --git a/Dependency/SDWebImage/SDWebImageCompat.m b/Dependency/SDWebImage/SDWebImageCompat.m index 12e6858..d49334d 100755 --- a/Dependency/SDWebImage/SDWebImageCompat.m +++ b/Dependency/SDWebImage/SDWebImageCompat.m @@ -7,8 +7,7 @@ */ #import "SDWebImageCompat.h" - -#import "objc/runtime.h" +#import "UIImage+MultiFormat.h" #if !__has_feature(objc_arc) #error SDWebImage is ARC only. Either turn on ARC for the project or use -fobjc-arc flag @@ -30,16 +29,9 @@ } UIImage *animatedImage = [UIImage animatedImageWithImages:scaledImages duration:image.duration]; -#ifdef SD_WEBP if (animatedImage) { - SEL sd_webpLoopCount = NSSelectorFromString(@"sd_webpLoopCount"); - NSNumber *value = objc_getAssociatedObject(image, sd_webpLoopCount); - NSInteger loopCount = value.integerValue; - if (loopCount) { - objc_setAssociatedObject(animatedImage, sd_webpLoopCount, @(loopCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } + animatedImage.sd_imageLoopCount = image.sd_imageLoopCount; } -#endif return animatedImage; } else { #if SD_WATCH diff --git a/Dependency/SDWebImage/SDWebImageDecoder.m b/Dependency/SDWebImage/SDWebImageDecoder.m deleted file mode 100755 index cf5e676..0000000 --- a/Dependency/SDWebImage/SDWebImageDecoder.m +++ /dev/null @@ -1,279 +0,0 @@ -/* - * This file is part of the SDWebImage package. - * (c) Olivier Poitrey - * (c) james - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -#import "SDWebImageDecoder.h" - -@implementation UIImage (ForceDecode) - -#if SD_UIKIT || SD_WATCH -static const size_t kBytesPerPixel = 4; -static const size_t kBitsPerComponent = 8; - -+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image { - if (![UIImage shouldDecodeImage:image]) { - return image; - } - - // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. - // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; - @autoreleasepool{ - - CGImageRef imageRef = image.CGImage; - CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef]; - - size_t width = CGImageGetWidth(imageRef); - size_t height = CGImageGetHeight(imageRef); - size_t bytesPerRow = kBytesPerPixel * width; - - // kCGImageAlphaNone is not supported in CGBitmapContextCreate. - // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast - // to create bitmap graphics contexts without alpha info. - CGContextRef context = CGBitmapContextCreate(NULL, - width, - height, - kBitsPerComponent, - bytesPerRow, - colorspaceRef, - kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); - if (context == NULL) { - return image; - } - - // Draw the image into the context and retrieve the new bitmap image without alpha - CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); - CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); - UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha - scale:image.scale - orientation:image.imageOrientation]; - - CGContextRelease(context); - CGImageRelease(imageRefWithoutAlpha); - - return imageWithoutAlpha; - } -} - -/* - * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set - * Suggested value for iPad1 and iPhone 3GS: 60. - * Suggested value for iPad2 and iPhone 4: 120. - * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30. - */ -static const CGFloat kDestImageSizeMB = 60.0f; - -/* - * Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set - * Suggested value for iPad1 and iPhone 3GS: 20. - * Suggested value for iPad2 and iPhone 4: 40. - * Suggested value for iPhone 3G and iPod 2 and earlier devices: 10. - */ -static const CGFloat kSourceImageTileSizeMB = 20.0f; - -static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; -static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; -static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB; -static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB; - -static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet. - -+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image { - if (![UIImage shouldDecodeImage:image]) { - return image; - } - - if (![UIImage shouldScaleDownImage:image]) { - return [UIImage decodedImageWithImage:image]; - } - - CGContextRef destContext; - - // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. - // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; - @autoreleasepool { - CGImageRef sourceImageRef = image.CGImage; - - CGSize sourceResolution = CGSizeZero; - sourceResolution.width = CGImageGetWidth(sourceImageRef); - sourceResolution.height = CGImageGetHeight(sourceImageRef); - float sourceTotalPixels = sourceResolution.width * sourceResolution.height; - // Determine the scale ratio to apply to the input image - // that results in an output image of the defined size. - // see kDestImageSizeMB, and how it relates to destTotalPixels. - float imageScale = kDestTotalPixels / sourceTotalPixels; - CGSize destResolution = CGSizeZero; - destResolution.width = (int)(sourceResolution.width*imageScale); - destResolution.height = (int)(sourceResolution.height*imageScale); - - // current color space - CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef]; - - size_t bytesPerRow = kBytesPerPixel * destResolution.width; - - // Allocate enough pixel data to hold the output image. - void* destBitmapData = malloc( bytesPerRow * destResolution.height ); - if (destBitmapData == NULL) { - return image; - } - - // kCGImageAlphaNone is not supported in CGBitmapContextCreate. - // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast - // to create bitmap graphics contexts without alpha info. - destContext = CGBitmapContextCreate(destBitmapData, - destResolution.width, - destResolution.height, - kBitsPerComponent, - bytesPerRow, - colorspaceRef, - kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); - - if (destContext == NULL) { - free(destBitmapData); - return image; - } - CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh); - - // Now define the size of the rectangle to be used for the - // incremental blits from the input image to the output image. - // we use a source tile width equal to the width of the source - // image due to the way that iOS retrieves image data from disk. - // iOS must decode an image from disk in full width 'bands', even - // if current graphics context is clipped to a subrect within that - // band. Therefore we fully utilize all of the pixel data that results - // from a decoding opertion by achnoring our tile size to the full - // width of the input image. - CGRect sourceTile = CGRectZero; - sourceTile.size.width = sourceResolution.width; - // The source tile height is dynamic. Since we specified the size - // of the source tile in MB, see how many rows of pixels high it - // can be given the input image width. - sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width ); - sourceTile.origin.x = 0.0f; - // The output tile is the same proportions as the input tile, but - // scaled to image scale. - CGRect destTile; - destTile.size.width = destResolution.width; - destTile.size.height = sourceTile.size.height * imageScale; - destTile.origin.x = 0.0f; - // The source seem overlap is proportionate to the destination seem overlap. - // this is the amount of pixels to overlap each tile as we assemble the ouput image. - float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height); - CGImageRef sourceTileImageRef; - // calculate the number of read/write operations required to assemble the - // output image. - int iterations = (int)( sourceResolution.height / sourceTile.size.height ); - // If tile height doesn't divide the image height evenly, add another iteration - // to account for the remaining pixels. - int remainder = (int)sourceResolution.height % (int)sourceTile.size.height; - if(remainder) { - iterations++; - } - // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations. - float sourceTileHeightMinusOverlap = sourceTile.size.height; - sourceTile.size.height += sourceSeemOverlap; - destTile.size.height += kDestSeemOverlap; - for( int y = 0; y < iterations; ++y ) { - @autoreleasepool { - sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap; - destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap); - sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile ); - if( y == iterations - 1 && remainder ) { - float dify = destTile.size.height; - destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale; - dify -= destTile.size.height; - destTile.origin.y += dify; - } - CGContextDrawImage( destContext, destTile, sourceTileImageRef ); - CGImageRelease( sourceTileImageRef ); - } - } - - CGImageRef destImageRef = CGBitmapContextCreateImage(destContext); - CGContextRelease(destContext); - if (destImageRef == NULL) { - return image; - } - UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; - CGImageRelease(destImageRef); - if (destImage == nil) { - return image; - } - return destImage; - } -} - -+ (BOOL)shouldDecodeImage:(nullable UIImage *)image { - // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error - if (image == nil) { - return NO; - } - - // do not decode animated images - if (image.images != nil) { - return NO; - } - - CGImageRef imageRef = image.CGImage; - - CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef); - BOOL anyAlpha = (alpha == kCGImageAlphaFirst || - alpha == kCGImageAlphaLast || - alpha == kCGImageAlphaPremultipliedFirst || - alpha == kCGImageAlphaPremultipliedLast); - // do not decode images with alpha - if (anyAlpha) { - return NO; - } - - return YES; -} - -+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image { - BOOL shouldScaleDown = YES; - - CGImageRef sourceImageRef = image.CGImage; - CGSize sourceResolution = CGSizeZero; - sourceResolution.width = CGImageGetWidth(sourceImageRef); - sourceResolution.height = CGImageGetHeight(sourceImageRef); - float sourceTotalPixels = sourceResolution.width * sourceResolution.height; - float imageScale = kDestTotalPixels / sourceTotalPixels; - if (imageScale < 1) { - shouldScaleDown = YES; - } else { - shouldScaleDown = NO; - } - - return shouldScaleDown; -} - -+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef { - // current - CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef)); - CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef); - - BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown || - imageColorSpaceModel == kCGColorSpaceModelMonochrome || - imageColorSpaceModel == kCGColorSpaceModelCMYK || - imageColorSpaceModel == kCGColorSpaceModelIndexed); - if (unsupportedColorSpace) { - colorspaceRef = CGColorSpaceCreateDeviceRGB(); - CFAutorelease(colorspaceRef); - } - return colorspaceRef; -} -#elif SD_MAC -+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image { - return image; -} - -+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image { - return image; -} -#endif - -@end diff --git a/Dependency/SDWebImage/SDWebImageDownloader.h b/Dependency/SDWebImage/SDWebImageDownloader.h index 7e85bf6..808b0bc 100755 --- a/Dependency/SDWebImage/SDWebImageDownloader.h +++ b/Dependency/SDWebImage/SDWebImageDownloader.h @@ -23,7 +23,6 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { /** * Call completion block with nil image/imageData if the image was read from NSURLCache * (to be combined with `SDWebImageDownloaderUseNSURLCache`). - * I think this option should be renamed to 'SDWebImageDownloaderUsingCachedResponseDontLoad' */ SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, @@ -68,8 +67,8 @@ typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { SDWebImageDownloaderLIFOExecutionOrder }; -extern NSString * _Nonnull const SDWebImageDownloadStartNotification; -extern NSString * _Nonnull const SDWebImageDownloadStopNotification; +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStartNotification; +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStopNotification; typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL); @@ -112,13 +111,11 @@ typedef SDHTTPHeadersDictionary * _Nullable (^SDWebImageDownloaderHeadersFilterB */ @property (readonly, nonatomic) NSUInteger currentDownloadCount; - /** * The timeout value (in seconds) for the download operation. Default: 15.0. */ @property (assign, nonatomic) NSTimeInterval downloadTimeout; - /** * The configuration in use by the internal NSURLSession. * Mutating this object directly has no effect. diff --git a/Dependency/SDWebImage/SDWebImageDownloader.m b/Dependency/SDWebImage/SDWebImageDownloader.m index 29e7d59..24e5e0d 100755 --- a/Dependency/SDWebImage/SDWebImageDownloader.m +++ b/Dependency/SDWebImage/SDWebImageDownloader.m @@ -8,7 +8,6 @@ #import "SDWebImageDownloader.h" #import "SDWebImageDownloaderOperation.h" -#import @implementation SDWebImageDownloadToken @end @@ -166,16 +165,10 @@ - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url } // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise - NSURLRequestCachePolicy cachePolicy = NSURLRequestReloadIgnoringLocalCacheData; - if (options & SDWebImageDownloaderUseNSURLCache) { - if (options & SDWebImageDownloaderIgnoreCachedResponse) { - cachePolicy = NSURLRequestReturnCacheDataDontLoad; - } else { - cachePolicy = NSURLRequestUseProtocolCachePolicy; - } - } - - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval]; + NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; + NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url + cachePolicy:cachePolicy + timeoutInterval:timeoutInterval]; request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies); request.HTTPShouldUsePipelining = YES; @@ -224,7 +217,7 @@ - (void)cancel:(nullable SDWebImageDownloadToken *)token { - (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(nullable NSURL *)url - createCallback:(SDWebImageDownloaderOperation *(^)())createCallback { + createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback { // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data. if (url == nil) { if (completedBlock != nil) { diff --git a/Dependency/SDWebImage/SDWebImageDownloaderOperation.h b/Dependency/SDWebImage/SDWebImageDownloaderOperation.h index b190855..40721b2 100755 --- a/Dependency/SDWebImage/SDWebImageDownloaderOperation.h +++ b/Dependency/SDWebImage/SDWebImageDownloaderOperation.h @@ -10,10 +10,10 @@ #import "SDWebImageDownloader.h" #import "SDWebImageOperation.h" -extern NSString * _Nonnull const SDWebImageDownloadStartNotification; -extern NSString * _Nonnull const SDWebImageDownloadReceiveResponseNotification; -extern NSString * _Nonnull const SDWebImageDownloadStopNotification; -extern NSString * _Nonnull const SDWebImageDownloadFinishNotification; +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStartNotification; +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadReceiveResponseNotification; +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadStopNotification; +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageDownloadFinishNotification; diff --git a/Dependency/SDWebImage/SDWebImageDownloaderOperation.m b/Dependency/SDWebImage/SDWebImageDownloaderOperation.m index 6c7dd55..479c6b0 100755 --- a/Dependency/SDWebImage/SDWebImageDownloaderOperation.m +++ b/Dependency/SDWebImage/SDWebImageDownloaderOperation.m @@ -7,11 +7,9 @@ */ #import "SDWebImageDownloaderOperation.h" -#import "SDWebImageDecoder.h" -#import "UIImage+MultiFormat.h" -#import #import "SDWebImageManager.h" #import "NSImage+WebCache.h" +#import "SDWebImageCodersManager.h" NSString *const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification"; NSString *const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification"; @@ -30,6 +28,7 @@ @interface SDWebImageDownloaderOperation () @property (assign, nonatomic, getter = isExecuting) BOOL executing; @property (assign, nonatomic, getter = isFinished) BOOL finished; @property (strong, nonatomic, nullable) NSMutableData *imageData; +@property (copy, nonatomic, nullable) NSData *cachedData; // This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run // the task associated with this operation @@ -45,14 +44,11 @@ @interface SDWebImageDownloaderOperation () @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; #endif +@property (strong, nonatomic, nullable) id progressiveCoder; + @end -@implementation SDWebImageDownloaderOperation { - size_t width, height; -#if SD_UIKIT || SD_WATCH - UIImageOrientation orientation; -#endif -} +@implementation SDWebImageDownloaderOperation @synthesize executing = _executing; @synthesize finished = _finished; @@ -143,6 +139,14 @@ - (void)start { }]; } #endif + if (self.options & SDWebImageDownloaderIgnoreCachedResponse) { + // Grab the cached data for later check + NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request]; + if (cachedResponse) { + self.cachedData = cachedResponse.data; + } + } + NSURLSession *session = self.unownedSession; if (!self.unownedSession) { NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; @@ -223,11 +227,25 @@ - (void)done { } - (void)reset { + __weak typeof(self) weakSelf = self; dispatch_barrier_async(self.barrierQueue, ^{ - [self.callbackBlocks removeAllObjects]; + [weakSelf.callbackBlocks removeAllObjects]; }); self.dataTask = nil; - self.imageData = nil; + + NSOperationQueue *delegateQueue; + if (self.unownedSession) { + delegateQueue = self.unownedSession.delegateQueue; + } else { + delegateQueue = self.ownedSession.delegateQueue; + } + if (delegateQueue) { + NSAssert(delegateQueue.maxConcurrentOperationCount == 1, @"NSURLSession delegate queue should be a serial queue"); + [delegateQueue addOperationWithBlock:^{ + weakSelf.imageData = nil; + }]; + } + if (self.ownedSession) { [self.ownedSession invalidateAndCancel]; self.ownedSession = nil; @@ -301,82 +319,34 @@ - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)data [self.imageData appendData:data]; if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) { - // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ - // Thanks to the author @Nyx0uf - + // Get the image data + NSData *imageData = [self.imageData copy]; // Get the total bytes downloaded - const NSInteger totalSize = self.imageData.length; - - // Update the data source, we must pass ALL the data, not just the new bytes - CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL); - - if (width + height == 0) { - CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); - if (properties) { - NSInteger orientationValue = -1; - CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); - if (val) CFNumberGetValue(val, kCFNumberLongType, &height); - val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); - if (val) CFNumberGetValue(val, kCFNumberLongType, &width); - val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); - if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); - CFRelease(properties); - - // When we draw to Core Graphics, we lose orientation information, - // which means the image below born of initWithCGIImage will be - // oriented incorrectly sometimes. (Unlike the image born of initWithData - // in didCompleteWithError.) So save it here and pass it on later. -#if SD_UIKIT || SD_WATCH - orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)]; -#endif - } - } - - if (width + height > 0 && totalSize < self.expectedSize) { - // Create the image - CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL); - -#if SD_UIKIT || SD_WATCH - // Workaround for iOS anamorphic image - if (partialImageRef) { - const size_t partialHeight = CGImageGetHeight(partialImageRef); - CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); - CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); - CGColorSpaceRelease(colorSpace); - if (bmContext) { - CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef); - CGImageRelease(partialImageRef); - partialImageRef = CGBitmapContextCreateImage(bmContext); - CGContextRelease(bmContext); - } - else { - CGImageRelease(partialImageRef); - partialImageRef = nil; + const NSInteger totalSize = imageData.length; + // Get the finish status + BOOL finished = (totalSize >= self.expectedSize); + + if (!self.progressiveCoder) { + // We need to create a new instance for progressive decoding to avoid conflicts + for (idcoder in [SDWebImageCodersManager sharedInstance].coders) { + if ([coder conformsToProtocol:@protocol(SDWebImageProgressiveCoder)] && + [((id)coder) canIncrementallyDecodeFromData:imageData]) { + self.progressiveCoder = [[[coder class] alloc] init]; + break; } } -#endif - - if (partialImageRef) { -#if SD_UIKIT || SD_WATCH - UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation]; -#elif SD_MAC - UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize]; -#endif - NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; - UIImage *scaledImage = [self scaledImageForKey:key image:image]; - if (self.shouldDecompressImages) { - image = [UIImage decodedImageWithImage:scaledImage]; - } - else { - image = scaledImage; - } - CGImageRelease(partialImageRef); - - [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO]; + } + + UIImage *image = [self.progressiveCoder incrementallyDecodedImageWithData:imageData finished:finished]; + if (image) { + NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; + image = [self scaledImageForKey:key image:image]; + if (self.shouldDecompressImages) { + image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&data options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}]; } + + [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO]; } - - CFRelease(imageSource); } for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) { @@ -420,32 +390,44 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didComp if ([self callbacksForKey:kCompletedCallbackKey].count > 0) { /** * If you specified to use `NSURLCache`, then the response you get here is what you need. - * if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`, - * the response data will be nil. - * So we don't need to check the cache option here, since the system will obey the cache option */ - if (self.imageData) { - UIImage *image = [UIImage sd_imageWithData:self.imageData]; - NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; - image = [self scaledImageForKey:key image:image]; - - // Do not force decoding animated GIFs - if (!image.images) { - if (self.shouldDecompressImages) { - if (self.options & SDWebImageDownloaderScaleDownLargeImages) { -#if SD_UIKIT || SD_WATCH - image = [UIImage decodedAndScaledDownImageWithImage:image]; - [self.imageData setData:UIImagePNGRepresentation(image)]; + NSData *imageData = [self.imageData copy]; + if (imageData) { + /** if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`, + * then we should check if the cached data is equal to image data + */ + if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) { + // call completion block with nil + [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES]; + } else { + UIImage *image = [[SDWebImageCodersManager sharedInstance] decodedImageWithData:imageData]; + NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL]; + image = [self scaledImageForKey:key image:image]; + + BOOL shouldDecode = YES; + // Do not force decoding animated GIFs and WebPs + if (image.images) { + shouldDecode = NO; + } else { +#ifdef SD_WEBP + SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData]; + if (imageFormat == SDImageFormatWebP) { + shouldDecode = NO; + } #endif - } else { - image = [UIImage decodedImageWithImage:image]; + } + + if (shouldDecode) { + if (self.shouldDecompressImages) { + BOOL shouldScaleDown = self.options & SDWebImageDownloaderScaleDownLargeImages; + image = [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&imageData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(shouldScaleDown)}]; } } - } - if (CGSizeEqualToSize(image.size, CGSizeZero)) { - [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]]; - } else { - [self callCompletionBlocksWithImage:image imageData:self.imageData error:nil finished:YES]; + if (CGSizeEqualToSize(image.size, CGSizeZero)) { + [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}]]; + } else { + [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES]; + } } } else { [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]]; @@ -486,32 +468,6 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didRece } #pragma mark Helper methods - -#if SD_UIKIT || SD_WATCH -+ (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value { - switch (value) { - case 1: - return UIImageOrientationUp; - case 3: - return UIImageOrientationDown; - case 8: - return UIImageOrientationLeft; - case 6: - return UIImageOrientationRight; - case 2: - return UIImageOrientationUpMirrored; - case 4: - return UIImageOrientationDownMirrored; - case 5: - return UIImageOrientationLeftMirrored; - case 7: - return UIImageOrientationRightMirrored; - default: - return UIImageOrientationUp; - } -} -#endif - - (nullable UIImage *)scaledImageForKey:(nullable NSString *)key image:(nullable UIImage *)image { return SDScaledImageForKey(key, image); } diff --git a/Dependency/SDWebImage/SDWebImageFrame.h b/Dependency/SDWebImage/SDWebImageFrame.h new file mode 100755 index 0000000..d8ba181 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageFrame.h @@ -0,0 +1,34 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import +#import "SDWebImageCompat.h" + +@interface SDWebImageFrame : NSObject + +// This class is used for creating animated images via `animatedImageWithFrames` in `SDWebImageCoderHelper`. Attension if you need animated images loop count, use `sd_imageLoopCount` property in `UIImage+MultiFormat` + +/** + The image of current frame. You should not set an animated image. + */ +@property (nonatomic, strong, readonly, nonnull) UIImage *image; +/** + The duration of current frame to be displayed. The number is seconds but not milliseconds. You should not set this to zero. + */ +@property (nonatomic, readonly, assign) NSTimeInterval duration; + +/** + Create a frame instance with specify image and duration + + @param image current frame's image + @param duration current frame's duration + @return frame instance + */ ++ (instancetype _Nonnull)frameWithImage:(UIImage * _Nonnull)image duration:(NSTimeInterval)duration; + +@end diff --git a/Dependency/SDWebImage/SDWebImageFrame.m b/Dependency/SDWebImage/SDWebImageFrame.m new file mode 100755 index 0000000..b0aefe5 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageFrame.m @@ -0,0 +1,28 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "SDWebImageFrame.h" + +@interface SDWebImageFrame () + +@property (nonatomic, strong, readwrite, nonnull) UIImage *image; +@property (nonatomic, readwrite, assign) NSTimeInterval duration; + +@end + +@implementation SDWebImageFrame + ++ (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration { + SDWebImageFrame *frame = [[SDWebImageFrame alloc] init]; + frame.image = image; + frame.duration = duration; + + return frame; +} + +@end diff --git a/Dependency/SDWebImage/SDWebImageGIFCoder.h b/Dependency/SDWebImage/SDWebImageGIFCoder.h new file mode 100755 index 0000000..30521f9 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageGIFCoder.h @@ -0,0 +1,23 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import +#import "SDWebImageCoder.h" + +/** + Built in coder using ImageIO that supports GIF encoding/decoding + @note `SDWebImageIOCoder` supports GIF but only as static (will use the 1st frame). + @note Use `SDWebImageGIFCoder` for fully animated GIFs - less performant than `FLAnimatedImage` + @note If you decide to make all `UIImageView`(including `FLAnimatedImageView`) instance support GIF. You should add this coder to `SDWebImageCodersManager` and make sure that it has a higher priority than `SDWebImageIOCoder` + @note The recommended approach for animated GIFs is using `FLAnimatedImage`. It's more performant than `UIImageView` for GIF displaying + */ +@interface SDWebImageGIFCoder : NSObject + ++ (nonnull instancetype)sharedCoder; + +@end diff --git a/Dependency/SDWebImage/SDWebImageGIFCoder.m b/Dependency/SDWebImage/SDWebImageGIFCoder.m new file mode 100755 index 0000000..005f2b8 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageGIFCoder.m @@ -0,0 +1,183 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "SDWebImageGIFCoder.h" +#import "NSImage+WebCache.h" +#import +#import "NSData+ImageContentType.h" +#import "UIImage+MultiFormat.h" +#import "SDWebImageCoderHelper.h" + +@implementation SDWebImageGIFCoder + ++ (instancetype)sharedCoder { + static SDWebImageGIFCoder *coder; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + coder = [[SDWebImageGIFCoder alloc] init]; + }); + return coder; +} + +#pragma mark - Decode +- (BOOL)canDecodeFromData:(nullable NSData *)data { + return ([NSData sd_imageFormatForImageData:data] == SDImageFormatGIF); +} + +- (UIImage *)decodedImageWithData:(NSData *)data { + if (!data) { + return nil; + } + +#if SD_MAC + return [[UIImage alloc] initWithData:data]; +#else + + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); + if (!source) { + return nil; + } + size_t count = CGImageSourceGetCount(source); + + UIImage *animatedImage; + + if (count <= 1) { + animatedImage = [[UIImage alloc] initWithData:data]; + } else { + NSMutableArray *frames = [NSMutableArray array]; + + for (size_t i = 0; i < count; i++) { + CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL); + if (!imageRef) { + continue; + } + + float duration = [self sd_frameDurationAtIndex:i source:source]; +#if SD_WATCH + CGFloat scale = 1; + scale = [WKInterfaceDevice currentDevice].screenScale; +#elif SD_UIKIT + CGFloat scale = 1; + scale = [UIScreen mainScreen].scale; +#endif + UIImage *image = [UIImage imageWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp]; + CGImageRelease(imageRef); + + SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:image duration:duration]; + [frames addObject:frame]; + } + + NSUInteger loopCount = 0; + NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil); + NSDictionary *gifProperties = [imageProperties valueForKey:(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary]; + if (gifProperties) { + NSNumber *gifLoopCount = [gifProperties valueForKey:(__bridge_transfer NSString *)kCGImagePropertyGIFLoopCount]; + if (gifLoopCount) { + loopCount = gifLoopCount.unsignedIntegerValue; + } + } + + animatedImage = [SDWebImageCoderHelper animatedImageWithFrames:frames]; + animatedImage.sd_imageLoopCount = loopCount; + } + + CFRelease(source); + + return animatedImage; +#endif +} + +- (float)sd_frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source { + float frameDuration = 0.1f; + CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil); + NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties; + NSDictionary *gifProperties = frameProperties[(NSString *)kCGImagePropertyGIFDictionary]; + + NSNumber *delayTimeUnclampedProp = gifProperties[(NSString *)kCGImagePropertyGIFUnclampedDelayTime]; + if (delayTimeUnclampedProp) { + frameDuration = [delayTimeUnclampedProp floatValue]; + } else { + NSNumber *delayTimeProp = gifProperties[(NSString *)kCGImagePropertyGIFDelayTime]; + if (delayTimeProp) { + frameDuration = [delayTimeProp floatValue]; + } + } + + // Many annoying ads specify a 0 duration to make an image flash as quickly as possible. + // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify + // a duration of <= 10 ms. See and + // for more information. + + if (frameDuration < 0.011f) { + frameDuration = 0.100f; + } + + CFRelease(cfFrameProperties); + return frameDuration; +} + +- (UIImage *)decompressedImageWithImage:(UIImage *)image + data:(NSData *__autoreleasing _Nullable *)data + options:(nullable NSDictionary*)optionsDict { + // GIF do not decompress + return image; +} + +#pragma mark - Encode +- (BOOL)canEncodeToFormat:(SDImageFormat)format { + return (format == SDImageFormatGIF); +} + +- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format { + if (!image) { + return nil; + } + + if (format != SDImageFormatGIF) { + return nil; + } + + NSMutableData *imageData = [NSMutableData data]; + CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatGIF]; + NSArray *frames = [SDWebImageCoderHelper framesFromAnimatedImage:image]; + + // Create an image destination. GIF does not support EXIF image orientation + CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frames.count, NULL); + if (!imageDestination) { + // Handle failure. + return nil; + } + if (frames.count == 0) { + // for static single GIF images + CGImageDestinationAddImage(imageDestination, image.CGImage, nil); + } 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); + + for (size_t i = 0; i < frames.count; i++) { + SDWebImageFrame *frame = frames[i]; + float frameDuration = frame.duration; + CGImageRef frameImageRef = frame.image.CGImage; + NSDictionary *frameProperties = @{(__bridge_transfer NSString *)kCGImagePropertyGIFDictionary : @{(__bridge_transfer NSString *)kCGImagePropertyGIFUnclampedDelayTime : @(frameDuration)}}; + CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties); + } + } + // Finalize the destination. + if (CGImageDestinationFinalize(imageDestination) == NO) { + // Handle failure. + imageData = nil; + } + + CFRelease(imageDestination); + + return [imageData copy]; +} + +@end diff --git a/Dependency/SDWebImage/SDWebImageImageIOCoder.h b/Dependency/SDWebImage/SDWebImageImageIOCoder.h new file mode 100755 index 0000000..524d9bb --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageImageIOCoder.h @@ -0,0 +1,27 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import +#import "SDWebImageCoder.h" + +/** + Built in coder that supports PNG, JPEG, TIFF, includes support for progressive decoding. + + GIF + Also supports static GIF (meaning will only handle the 1st frame). + For a full GIF support, we recommend `FLAnimatedImage` or our less performant `SDWebImageGIFCoder` + + HEIC + This coder also supports HEIC format because ImageIO supports it natively. But it depends on the system capabilities, so it won't work on all devices. + Hardware works if: (iOS 11 || macOS 10.13) && (isMac || isIPhoneAndA10FusionChipAbove) && (!Simulator) + */ +@interface SDWebImageImageIOCoder : NSObject + ++ (nonnull instancetype)sharedCoder; + +@end diff --git a/Dependency/SDWebImage/SDWebImageImageIOCoder.m b/Dependency/SDWebImage/SDWebImageImageIOCoder.m new file mode 100755 index 0000000..4e549eb --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageImageIOCoder.m @@ -0,0 +1,557 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "SDWebImageImageIOCoder.h" +#import "SDWebImageCoderHelper.h" +#import "NSImage+WebCache.h" +#import +#import "NSData+ImageContentType.h" + +#if SD_UIKIT || SD_WATCH +static const size_t kBytesPerPixel = 4; +static const size_t kBitsPerComponent = 8; + +/* + * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set + * Suggested value for iPad1 and iPhone 3GS: 60. + * Suggested value for iPad2 and iPhone 4: 120. + * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30. + */ +static const CGFloat kDestImageSizeMB = 60.0f; + +/* + * Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set + * Suggested value for iPad1 and iPhone 3GS: 20. + * Suggested value for iPad2 and iPhone 4: 40. + * Suggested value for iPhone 3G and iPod 2 and earlier devices: 10. + */ +static const CGFloat kSourceImageTileSizeMB = 20.0f; + +static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; +static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; +static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB; +static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB; + +static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet. +#endif + +@implementation SDWebImageImageIOCoder { + size_t _width, _height; +#if SD_UIKIT || SD_WATCH + UIImageOrientation _orientation; +#endif + CGImageSourceRef _imageSource; +} + +- (void)dealloc { + if (_imageSource) { + CFRelease(_imageSource); + _imageSource = NULL; + } +} + ++ (instancetype)sharedCoder { + static SDWebImageImageIOCoder *coder; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + coder = [[SDWebImageImageIOCoder alloc] init]; + }); + return coder; +} + +#pragma mark - Decode +- (BOOL)canDecodeFromData:(nullable NSData *)data { + switch ([NSData sd_imageFormatForImageData:data]) { + case SDImageFormatWebP: + // Do not support WebP decoding + return NO; + default: + return YES; + } +} + +- (BOOL)canIncrementallyDecodeFromData:(NSData *)data { + switch ([NSData sd_imageFormatForImageData:data]) { + case SDImageFormatWebP: + // Do not support WebP progressive decoding + return NO; + default: + return YES; + } +} + +- (UIImage *)decodedImageWithData:(NSData *)data { + if (!data) { + return nil; + } + + UIImage *image = [[UIImage alloc] initWithData:data]; + +#if SD_MAC + return image; +#else + if (!image) { + return nil; + } + + SDImageFormat format = [NSData sd_imageFormatForImageData:data]; + if (format == SDImageFormatGIF) { + // static single GIF need to be created animated for `FLAnimatedImage` logic + // GIF does not support EXIF image orientation + image = [UIImage animatedImageWithImages:@[image] duration:image.duration]; + return image; + } + UIImageOrientation orientation = [[self class] sd_imageOrientationFromImageData:data]; + if (orientation != UIImageOrientationUp) { + image = [UIImage imageWithCGImage:image.CGImage + scale:image.scale + orientation:orientation]; + } + + return image; +#endif +} + +- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished { + if (!_imageSource) { + _imageSource = CGImageSourceCreateIncremental(NULL); + } + UIImage *image; + + // 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) { + NSInteger orientationValue = 1; + CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); + if (val) CFNumberGetValue(val, kCFNumberLongType, &_height); + val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); + if (val) CFNumberGetValue(val, kCFNumberLongType, &_width); + val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); + if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); + CFRelease(properties); + + // When we draw to Core Graphics, we lose orientation information, + // which means the image below born of initWithCGIImage will be + // oriented incorrectly sometimes. (Unlike the image born of initWithData + // in didCompleteWithError.) So save it here and pass it on later. +#if SD_UIKIT || SD_WATCH + _orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue]; +#endif + } + } + + if (_width + _height > 0) { + // Create the image + CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL); + +#if SD_UIKIT || SD_WATCH + // Workaround for iOS anamorphic image + if (partialImageRef) { + const size_t partialHeight = CGImageGetHeight(partialImageRef); + CGColorSpaceRef colorSpace = SDCGColorSpaceGetDeviceRGB(); + CGContextRef bmContext = CGBitmapContextCreate(NULL, _width, _height, 8, _width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst); + if (bmContext) { + CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = _width, .size.height = partialHeight}, partialImageRef); + CGImageRelease(partialImageRef); + partialImageRef = CGBitmapContextCreateImage(bmContext); + CGContextRelease(bmContext); + } + else { + CGImageRelease(partialImageRef); + partialImageRef = nil; + } + } +#endif + + if (partialImageRef) { +#if SD_UIKIT || SD_WATCH + image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:_orientation]; +#elif SD_MAC + image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize]; +#endif + CGImageRelease(partialImageRef); + } + } + + if (finished) { + if (_imageSource) { + CFRelease(_imageSource); + _imageSource = NULL; + } + } + + return image; +} + +- (UIImage *)decompressedImageWithImage:(UIImage *)image + data:(NSData *__autoreleasing _Nullable *)data + options:(nullable NSDictionary*)optionsDict { +#if SD_MAC + return image; +#endif +#if SD_UIKIT || SD_WATCH + BOOL shouldScaleDown = NO; + if (optionsDict != nil) { + NSNumber *scaleDownLargeImagesOption = nil; + if ([optionsDict[SDWebImageCoderScaleDownLargeImagesKey] isKindOfClass:[NSNumber class]]) { + scaleDownLargeImagesOption = (NSNumber *)optionsDict[SDWebImageCoderScaleDownLargeImagesKey]; + } + if (scaleDownLargeImagesOption != nil) { + shouldScaleDown = [scaleDownLargeImagesOption boolValue]; + } + } + if (!shouldScaleDown) { + return [self sd_decompressedImageWithImage:image]; + } else { + UIImage *scaledDownImage = [self sd_decompressedAndScaledDownImageWithImage:image]; + if (scaledDownImage && !CGSizeEqualToSize(scaledDownImage.size, image.size)) { + // if the image is scaled down, need to modify the data pointer as well + SDImageFormat format = [NSData sd_imageFormatForImageData:*data]; + NSData *imageData = [self encodedDataWithImage:scaledDownImage format:format]; + if (imageData) { + *data = imageData; + } + } + return scaledDownImage; + } +#endif +} + +#if SD_UIKIT || SD_WATCH +- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image { + if (![[self class] shouldDecodeImage:image]) { + return image; + } + + // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. + // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; + @autoreleasepool{ + + CGImageRef imageRef = image.CGImage; + CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:imageRef]; + + size_t width = CGImageGetWidth(imageRef); + size_t height = CGImageGetHeight(imageRef); + size_t bytesPerRow = kBytesPerPixel * width; + + // kCGImageAlphaNone is not supported in CGBitmapContextCreate. + // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast + // to create bitmap graphics contexts without alpha info. + CGContextRef context = CGBitmapContextCreate(NULL, + width, + height, + kBitsPerComponent, + bytesPerRow, + colorspaceRef, + kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); + if (context == NULL) { + return image; + } + + // Draw the image into the context and retrieve the new bitmap image without alpha + CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); + CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context); + UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha + scale:image.scale + orientation:image.imageOrientation]; + + CGContextRelease(context); + CGImageRelease(imageRefWithoutAlpha); + + return imageWithoutAlpha; + } +} + +- (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image { + if (![[self class] shouldDecodeImage:image]) { + return image; + } + + if (![[self class] shouldScaleDownImage:image]) { + return [self sd_decompressedImageWithImage:image]; + } + + CGContextRef destContext; + + // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. + // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; + @autoreleasepool { + CGImageRef sourceImageRef = image.CGImage; + + CGSize sourceResolution = CGSizeZero; + sourceResolution.width = CGImageGetWidth(sourceImageRef); + sourceResolution.height = CGImageGetHeight(sourceImageRef); + float sourceTotalPixels = sourceResolution.width * sourceResolution.height; + // Determine the scale ratio to apply to the input image + // that results in an output image of the defined size. + // see kDestImageSizeMB, and how it relates to destTotalPixels. + float imageScale = kDestTotalPixels / sourceTotalPixels; + CGSize destResolution = CGSizeZero; + destResolution.width = (int)(sourceResolution.width*imageScale); + destResolution.height = (int)(sourceResolution.height*imageScale); + + // current color space + CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:sourceImageRef]; + + size_t bytesPerRow = kBytesPerPixel * destResolution.width; + + // kCGImageAlphaNone is not supported in CGBitmapContextCreate. + // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast + // to create bitmap graphics contexts without alpha info. + destContext = CGBitmapContextCreate(NULL, + destResolution.width, + destResolution.height, + kBitsPerComponent, + bytesPerRow, + colorspaceRef, + kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast); + + if (destContext == NULL) { + return image; + } + CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh); + + // Now define the size of the rectangle to be used for the + // incremental blits from the input image to the output image. + // we use a source tile width equal to the width of the source + // image due to the way that iOS retrieves image data from disk. + // iOS must decode an image from disk in full width 'bands', even + // if current graphics context is clipped to a subrect within that + // band. Therefore we fully utilize all of the pixel data that results + // from a decoding opertion by achnoring our tile size to the full + // width of the input image. + CGRect sourceTile = CGRectZero; + sourceTile.size.width = sourceResolution.width; + // The source tile height is dynamic. Since we specified the size + // of the source tile in MB, see how many rows of pixels high it + // can be given the input image width. + sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width ); + sourceTile.origin.x = 0.0f; + // The output tile is the same proportions as the input tile, but + // scaled to image scale. + CGRect destTile; + destTile.size.width = destResolution.width; + destTile.size.height = sourceTile.size.height * imageScale; + destTile.origin.x = 0.0f; + // The source seem overlap is proportionate to the destination seem overlap. + // this is the amount of pixels to overlap each tile as we assemble the ouput image. + float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height); + CGImageRef sourceTileImageRef; + // calculate the number of read/write operations required to assemble the + // output image. + int iterations = (int)( sourceResolution.height / sourceTile.size.height ); + // If tile height doesn't divide the image height evenly, add another iteration + // to account for the remaining pixels. + int remainder = (int)sourceResolution.height % (int)sourceTile.size.height; + if(remainder) { + iterations++; + } + // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations. + float sourceTileHeightMinusOverlap = sourceTile.size.height; + sourceTile.size.height += sourceSeemOverlap; + destTile.size.height += kDestSeemOverlap; + for( int y = 0; y < iterations; ++y ) { + @autoreleasepool { + sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap; + destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap); + sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile ); + if( y == iterations - 1 && remainder ) { + float dify = destTile.size.height; + destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale; + dify -= destTile.size.height; + destTile.origin.y += dify; + } + CGContextDrawImage( destContext, destTile, sourceTileImageRef ); + CGImageRelease( sourceTileImageRef ); + } + } + + CGImageRef destImageRef = CGBitmapContextCreateImage(destContext); + CGContextRelease(destContext); + if (destImageRef == NULL) { + return image; + } + UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; + CGImageRelease(destImageRef); + if (destImage == nil) { + return image; + } + return destImage; + } +} +#endif + +#pragma mark - Encode +- (BOOL)canEncodeToFormat:(SDImageFormat)format { + switch (format) { + case SDImageFormatWebP: + // Do not support WebP encoding + return NO; + case SDImageFormatHEIC: + // Check HEIC encoding compatibility + return [[self class] canEncodeToHEICFormat]; + default: + return YES; + } +} + +- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format { + if (!image) { + return nil; + } + + if (format == SDImageFormatUndefined) { + BOOL hasAlpha = SDCGImageRefContainsAlpha(image.CGImage); + if (hasAlpha) { + format = SDImageFormatPNG; + } else { + format = SDImageFormatJPEG; + } + } + + NSMutableData *imageData = [NSMutableData data]; + CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:format]; + + // Create an image destination. + CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL); + if (!imageDestination) { + // Handle failure. + return nil; + } + + NSMutableDictionary *properties = [NSMutableDictionary dictionary]; +#if SD_UIKIT || SD_WATCH + NSInteger exifOrientation = [SDWebImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation]; + [properties setValue:@(exifOrientation) forKey:(__bridge_transfer NSString *)kCGImagePropertyOrientation]; +#endif + + // Add your image to the destination. + CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties); + + // Finalize the destination. + if (CGImageDestinationFinalize(imageDestination) == NO) { + // Handle failure. + imageData = nil; + } + + CFRelease(imageDestination); + + return [imageData copy]; +} + +#pragma mark - Helper ++ (BOOL)shouldDecodeImage:(nullable UIImage *)image { + // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error + if (image == nil) { + return NO; + } + + // do not decode animated images + if (image.images != nil) { + return NO; + } + + CGImageRef imageRef = image.CGImage; + + BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef); + // do not decode images with alpha + if (hasAlpha) { + return NO; + } + + return YES; +} + ++ (BOOL)canEncodeToHEICFormat { + static BOOL canEncode = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableData *imageData = [NSMutableData data]; + CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIC]; + + // Create an image destination. + CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL); + if (!imageDestination) { + // Can't encode to HEIC + canEncode = NO; + } else { + // Can encode to HEIC + CFRelease(imageDestination); + canEncode = YES; + } + }); + return canEncode; +} + +#if SD_UIKIT || SD_WATCH +#pragma mark EXIF orientation tag converter ++ (UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData { + UIImageOrientation result = UIImageOrientationUp; + CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); + if (imageSource) { + CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); + if (properties) { + CFTypeRef val; + NSInteger exifOrientation; + val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); + if (val) { + CFNumberGetValue(val, kCFNumberNSIntegerType, &exifOrientation); + result = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation]; + } // else - if it's not set it remains at up + CFRelease((CFTypeRef) properties); + } else { + //NSLog(@"NO PROPERTIES, FAIL"); + } + CFRelease(imageSource); + } + return result; +} +#endif + +#if SD_UIKIT || SD_WATCH ++ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image { + BOOL shouldScaleDown = YES; + + CGImageRef sourceImageRef = image.CGImage; + CGSize sourceResolution = CGSizeZero; + sourceResolution.width = CGImageGetWidth(sourceImageRef); + sourceResolution.height = CGImageGetHeight(sourceImageRef); + float sourceTotalPixels = sourceResolution.width * sourceResolution.height; + float imageScale = kDestTotalPixels / sourceTotalPixels; + if (imageScale < 1) { + shouldScaleDown = YES; + } else { + shouldScaleDown = NO; + } + + return shouldScaleDown; +} + ++ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef { + // current + CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef)); + CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef); + + BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown || + imageColorSpaceModel == kCGColorSpaceModelMonochrome || + imageColorSpaceModel == kCGColorSpaceModelCMYK || + imageColorSpaceModel == kCGColorSpaceModelIndexed); + if (unsupportedColorSpace) { + colorspaceRef = SDCGColorSpaceGetDeviceRGB(); + } + return colorspaceRef; +} +#endif + +@end diff --git a/Dependency/SDWebImage/SDWebImageManager.m b/Dependency/SDWebImage/SDWebImageManager.m index a983c86..df9b841 100755 --- a/Dependency/SDWebImage/SDWebImageManager.m +++ b/Dependency/SDWebImage/SDWebImageManager.m @@ -7,8 +7,8 @@ */ #import "SDWebImageManager.h" -#import #import "NSImage+WebCache.h" +#import @interface SDWebImageCombinedOperation : NSObject @@ -228,11 +228,14 @@ - (void)diskImageExistsForURL:(nullable NSURL *)url [self safelyRemoveOperationFromRunning:strongOperation]; } }]; - operation.cancelBlock = ^{ - [self.imageDownloader cancel:subOperationToken]; - __strong __typeof(weakOperation) strongOperation = weakOperation; - [self safelyRemoveOperationFromRunning:strongOperation]; - }; + @synchronized(operation) { + // Need same lock to ensure cancelBlock called because cancel method can be called in different queue + operation.cancelBlock = ^{ + [self.imageDownloader cancel:subOperationToken]; + __strong __typeof(weakOperation) strongOperation = weakOperation; + [self safelyRemoveOperationFromRunning:strongOperation]; + }; + } } else if (cachedImage) { __strong __typeof(weakOperation) strongOperation = weakOperation; [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; @@ -319,18 +322,16 @@ - (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock { } - (void)cancel { - self.cancelled = YES; - if (self.cacheOperation) { - [self.cacheOperation cancel]; - self.cacheOperation = nil; - } - if (self.cancelBlock) { - self.cancelBlock(); - - // TODO: this is a temporary fix to #809. - // Until we can figure the exact cause of the crash, going with the ivar instead of the setter -// self.cancelBlock = nil; - _cancelBlock = nil; + @synchronized(self) { + self.cancelled = YES; + if (self.cacheOperation) { + [self.cacheOperation cancel]; + self.cacheOperation = nil; + } + if (self.cancelBlock) { + self.cancelBlock(); + self.cancelBlock = nil; + } } } diff --git a/Dependency/SDWebImage/SDWebImageWebPCoder.h b/Dependency/SDWebImage/SDWebImageWebPCoder.h new file mode 100755 index 0000000..634b575 --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageWebPCoder.h @@ -0,0 +1,23 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#ifdef SD_WEBP + +#import +#import "SDWebImageCoder.h" + +/** + Built in coder that supports WebP and animated WebP + */ +@interface SDWebImageWebPCoder : NSObject + ++ (nonnull instancetype)sharedCoder; + +@end + +#endif diff --git a/Dependency/SDWebImage/SDWebImageWebPCoder.m b/Dependency/SDWebImage/SDWebImageWebPCoder.m new file mode 100755 index 0000000..7dbe1af --- /dev/null +++ b/Dependency/SDWebImage/SDWebImageWebPCoder.m @@ -0,0 +1,421 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#ifdef SD_WEBP + +#import "SDWebImageWebPCoder.h" +#import "SDWebImageCoderHelper.h" +#import "NSImage+WebCache.h" +#import "UIImage+MultiFormat.h" +#if __has_include() && __has_include() && __has_include() && __has_include() +#import +#import +#import +#import +#else +#import "webp/decode.h" +#import "webp/encode.h" +#import "webp/demux.h" +#import "webp/mux.h" +#endif + +@implementation SDWebImageWebPCoder { + WebPIDecoder *_idec; +} + +- (void)dealloc { + if (_idec) { + WebPIDelete(_idec); + _idec = NULL; + } +} + ++ (instancetype)sharedCoder { + static SDWebImageWebPCoder *coder; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + coder = [[SDWebImageWebPCoder alloc] init]; + }); + return coder; +} + +#pragma mark - Decode +- (BOOL)canDecodeFromData:(nullable NSData *)data { + return ([NSData sd_imageFormatForImageData:data] == SDImageFormatWebP); +} + +- (BOOL)canIncrementallyDecodeFromData:(NSData *)data { + return ([NSData sd_imageFormatForImageData:data] == SDImageFormatWebP); +} + +- (UIImage *)decodedImageWithData:(NSData *)data { + if (!data) { + return nil; + } + + WebPData webpData; + WebPDataInit(&webpData); + webpData.bytes = data.bytes; + webpData.size = data.length; + WebPDemuxer *demuxer = WebPDemux(&webpData); + if (!demuxer) { + return nil; + } + + uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS); + int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT); + int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); + int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); + CGBitmapInfo bitmapInfo; + if (!(flags & ALPHA_FLAG)) { + bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast; + } else { + bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast; + } + CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo); + if (!canvas) { + WebPDemuxDelete(demuxer); + return nil; + } + + if (!(flags & ANIMATION_FLAG)) { + // for static single webp image + UIImage *staticImage = [self sd_rawWebpImageWithData:webpData]; + if (staticImage) { + // draw on CGBitmapContext can reduce memory usage + CGImageRef imageRef = staticImage.CGImage; + size_t width = CGImageGetWidth(imageRef); + size_t height = CGImageGetHeight(imageRef); + CGContextDrawImage(canvas, CGRectMake(0, 0, width, height), imageRef); + CGImageRef newImageRef = CGBitmapContextCreateImage(canvas); +#if SD_UIKIT || SD_WATCH + staticImage = [[UIImage alloc] initWithCGImage:newImageRef]; +#else + staticImage = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize]; +#endif + CGImageRelease(newImageRef); + } + WebPDemuxDelete(demuxer); + CGContextRelease(canvas); + return staticImage; + } + + // for animated webp image + WebPIterator iter; + if (!WebPDemuxGetFrame(demuxer, 1, &iter)) { + WebPDemuxReleaseIterator(&iter); + WebPDemuxDelete(demuxer); + CGContextRelease(canvas); + return nil; + } + + NSMutableArray *frames = [NSMutableArray array]; + + do { + @autoreleasepool { + UIImage *image = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter]; + if (!image) { + continue; + } + + int duration = iter.duration; + if (duration <= 10) { + // WebP standard says 0 duration is used for canvas updating but not showing image, but actually Chrome and other implementations set it to 100ms if duration is lower or equal than 10ms + // Some animated WebP images also created without duration, we should keep compatibility + duration = 100; + } + SDWebImageFrame *frame = [SDWebImageFrame frameWithImage:image duration:duration / 1000.f]; + [frames addObject:frame]; + } + + } while (WebPDemuxNextFrame(&iter)); + + WebPDemuxReleaseIterator(&iter); + WebPDemuxDelete(demuxer); + CGContextRelease(canvas); + + UIImage *animatedImage = [SDWebImageCoderHelper animatedImageWithFrames:frames]; + animatedImage.sd_imageLoopCount = loopCount; + + return animatedImage; +} + +- (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished { + if (!_idec) { + // Progressive images need transparent, so always use premultiplied RGBA + _idec = WebPINewRGB(MODE_rgbA, NULL, 0, 0); + if (!_idec) { + return nil; + } + } + + UIImage *image; + + VP8StatusCode status = WebPIUpdate(_idec, data.bytes, data.length); + if (status != VP8_STATUS_OK && status != VP8_STATUS_SUSPENDED) { + return nil; + } + + int width = 0; + int height = 0; + int last_y = 0; + int stride = 0; + uint8_t *rgba = WebPIDecGetRGB(_idec, &last_y, &width, &height, &stride); + if (width + height > 0 && height >= last_y) { + // Construct a UIImage from the decoded RGBA value array + CGDataProviderRef provider = + CGDataProviderCreateWithData(NULL, rgba, 0, NULL); + CGColorSpaceRef colorSpaceRef = SDCGColorSpaceGetDeviceRGB(); + + CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast; + size_t components = 4; + CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; + // Why to use last_y for image height is because of libwebp's bug (https://bugs.chromium.org/p/webp/issues/detail?id=362) + // It will not keep memory barrier safe on x86 architechure (macOS & iPhone simulator) but on ARM architecture (iPhone & iPad & tv & watch) it works great + // If different threads use WebPIDecGetRGB to grab rgba bitmap, it will contain the previous decoded bitmap data + // So this will cause our drawed image looks strange(above is the current part but below is the previous part) + // We only grab the last_y height and draw the last_y heigh instead of total height image + // Besides fix, this can enhance performance since we do not need to create extra bitmap + CGImageRef imageRef = CGImageCreate(width, last_y, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); + + CGDataProviderRelease(provider); + + if (!imageRef) { + return nil; + } + + CGContextRef canvas = CGBitmapContextCreate(NULL, width, height, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo); + if (!canvas) { + CGImageRelease(imageRef); + return nil; + } + + // Only draw the last_y image height, keep remains transparent, in Core Graphics coordinate system + CGContextDrawImage(canvas, CGRectMake(0, height - last_y, width, last_y), imageRef); + CGImageRef newImageRef = CGBitmapContextCreateImage(canvas); + CGImageRelease(imageRef); + if (!newImageRef) { + CGContextRelease(canvas); + return nil; + } + +#if SD_UIKIT || SD_WATCH + image = [[UIImage alloc] initWithCGImage:newImageRef]; +#else + image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize]; +#endif + CGImageRelease(newImageRef); + CGContextRelease(canvas); + } + + if (finished) { + if (_idec) { + WebPIDelete(_idec); + _idec = NULL; + } + } + + return image; +} + +- (UIImage *)decompressedImageWithImage:(UIImage *)image + data:(NSData *__autoreleasing _Nullable *)data + options:(nullable NSDictionary*)optionsDict { + // WebP do not decompress + return image; +} + +- (nullable UIImage *)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter { + UIImage *image = [self sd_rawWebpImageWithData:iter.fragment]; + if (!image) { + return nil; + } + + size_t canvasWidth = CGBitmapContextGetWidth(canvas); + size_t canvasHeight = CGBitmapContextGetHeight(canvas); + CGSize size = CGSizeMake(canvasWidth, canvasHeight); + CGFloat tmpX = iter.x_offset; + CGFloat tmpY = size.height - iter.height - iter.y_offset; + CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height); + BOOL shouldBlend = iter.blend_method == WEBP_MUX_BLEND; + + // If not blend, cover the target image rect. (firstly clear then draw) + if (!shouldBlend) { + CGContextClearRect(canvas, imageRect); + } + CGContextDrawImage(canvas, imageRect, image.CGImage); + CGImageRef newImageRef = CGBitmapContextCreateImage(canvas); + +#if SD_UIKIT || SD_WATCH + image = [UIImage imageWithCGImage:newImageRef]; +#elif SD_MAC + image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize]; +#endif + + CGImageRelease(newImageRef); + + if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { + CGContextClearRect(canvas, imageRect); + } + + return image; +} + +- (nullable UIImage *)sd_rawWebpImageWithData:(WebPData)webpData { + WebPDecoderConfig config; + if (!WebPInitDecoderConfig(&config)) { + return nil; + } + + if (WebPGetFeatures(webpData.bytes, webpData.size, &config.input) != VP8_STATUS_OK) { + return nil; + } + + config.output.colorspace = config.input.has_alpha ? MODE_rgbA : MODE_RGB; + config.options.use_threads = 1; + + // Decode the WebP image data into a RGBA value array + if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) { + return nil; + } + + int width = config.input.width; + int height = config.input.height; + if (config.options.use_scaling) { + width = config.options.scaled_width; + height = config.options.scaled_height; + } + + // Construct a UIImage from the decoded RGBA value array + CGDataProviderRef provider = + CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData); + CGColorSpaceRef colorSpaceRef = SDCGColorSpaceGetDeviceRGB(); + CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast; + size_t components = config.input.has_alpha ? 4 : 3; + CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; + CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); + + CGDataProviderRelease(provider); + +#if SD_UIKIT || SD_WATCH + UIImage *image = [[UIImage alloc] initWithCGImage:imageRef]; +#else + UIImage *image = [[UIImage alloc] initWithCGImage:imageRef size:NSZeroSize]; +#endif + CGImageRelease(imageRef); + + return image; +} + +#pragma mark - Encode +- (BOOL)canEncodeToFormat:(SDImageFormat)format { + return (format == SDImageFormatWebP); +} + +- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format { + if (!image) { + return nil; + } + + NSData *data; + + NSArray *frames = [SDWebImageCoderHelper framesFromAnimatedImage:image]; + if (frames.count == 0) { + // for static single webp image + data = [self sd_encodedWebpDataWithImage:image]; + } else { + // for animated webp image + WebPMux *mux = WebPMuxNew(); + if (!mux) { + return nil; + } + for (size_t i = 0; i < frames.count; i++) { + SDWebImageFrame *currentFrame = frames[i]; + NSData *webpData = [self sd_encodedWebpDataWithImage:currentFrame.image]; + int duration = currentFrame.duration * 1000; + WebPMuxFrameInfo frame = { .bitstream.bytes = webpData.bytes, + .bitstream.size = webpData.length, + .duration = duration, + .id = WEBP_CHUNK_ANMF, + .dispose_method = WEBP_MUX_DISPOSE_BACKGROUND, // each frame will clear canvas + .blend_method = WEBP_MUX_NO_BLEND + }; + if (WebPMuxPushFrame(mux, &frame, 0) != WEBP_MUX_OK) { + WebPMuxDelete(mux); + return nil; + } + } + + int loopCount = (int)image.sd_imageLoopCount; + WebPMuxAnimParams params = { .bgcolor = 0, + .loop_count = loopCount + }; + if (WebPMuxSetAnimationParams(mux, ¶ms) != WEBP_MUX_OK) { + WebPMuxDelete(mux); + return nil; + } + + WebPData outputData; + WebPMuxError error = WebPMuxAssemble(mux, &outputData); + WebPMuxDelete(mux); + if (error != WEBP_MUX_OK) { + return nil; + } + data = [NSData dataWithBytes:outputData.bytes length:outputData.size]; + WebPDataClear(&outputData); + } + + return data; +} + +- (nullable NSData *)sd_encodedWebpDataWithImage:(nullable UIImage *)image { + if (!image) { + return nil; + } + + NSData *webpData; + CGImageRef imageRef = image.CGImage; + + size_t width = CGImageGetWidth(imageRef); + size_t height = CGImageGetHeight(imageRef); + if (width == 0 || width > WEBP_MAX_DIMENSION) { + return nil; + } + if (height == 0 || height > WEBP_MAX_DIMENSION) { + return nil; + } + + size_t bytesPerRow = CGImageGetBytesPerRow(imageRef); + CGDataProviderRef dataProvider = CGImageGetDataProvider(imageRef); + CFDataRef dataRef = CGDataProviderCopyData(dataProvider); + 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); + CFRelease(dataRef); + rgba = NULL; + + if (size) { + // success + webpData = [NSData dataWithBytes:data length:size]; + } + if (data) { + WebPFree(data); + } + + return webpData; +} + +static void FreeImageData(void *info, const void *data, size_t size) { + free((void *)data); +} + +@end + +#endif diff --git a/Dependency/SDWebImage/UIButton+WebCache.h b/Dependency/SDWebImage/UIButton+WebCache.h index f47ab8c..b6c4a2a 100755 --- a/Dependency/SDWebImage/UIButton+WebCache.h +++ b/Dependency/SDWebImage/UIButton+WebCache.h @@ -17,13 +17,13 @@ */ @interface UIButton (WebCache) +#pragma mark - Image + /** * Get the current image URL. */ - (nullable NSURL *)sd_currentImageURL; -#pragma mark - Image - /** * Get the image URL for a control state. * @@ -130,6 +130,18 @@ #pragma mark - Background image +/** + * Get the current background image URL. + */ +- (nullable NSURL *)sd_currentBackgroundImageURL; + +/** + * Get the background image URL for a control state. + * + * @param state Which state you want to know the URL for. The values are described in UIControlState. + */ +- (nullable NSURL *)sd_backgroundImageURLForState:(UIControlState)state; + /** * Set the backgroundImageView `image` with an `url`. * diff --git a/Dependency/SDWebImage/UIButton+WebCache.m b/Dependency/SDWebImage/UIButton+WebCache.m index 63f75e4..7c861e8 100755 --- a/Dependency/SDWebImage/UIButton+WebCache.m +++ b/Dependency/SDWebImage/UIButton+WebCache.m @@ -16,26 +16,34 @@ static char imageURLStorageKey; -typedef NSMutableDictionary SDStateImageURLDictionary; +typedef NSMutableDictionary SDStateImageURLDictionary; + +static inline NSString * imageURLKeyForState(UIControlState state) { + return [NSString stringWithFormat:@"image_%lu", (unsigned long)state]; +} + +static inline NSString * backgroundImageURLKeyForState(UIControlState state) { + return [NSString stringWithFormat:@"backgroundImage_%lu", (unsigned long)state]; +} @implementation UIButton (WebCache) +#pragma mark - Image + - (nullable NSURL *)sd_currentImageURL { - NSURL *url = self.imageURLStorage[@(self.state)]; + NSURL *url = self.imageURLStorage[imageURLKeyForState(self.state)]; if (!url) { - url = self.imageURLStorage[@(UIControlStateNormal)]; + url = self.imageURLStorage[imageURLKeyForState(UIControlStateNormal)]; } return url; } - (nullable NSURL *)sd_imageURLForState:(UIControlState)state { - return self.imageURLStorage[@(state)]; + return self.imageURLStorage[imageURLKeyForState(state)]; } -#pragma mark - Image - - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state { [self sd_setImageWithURL:url forState:state placeholderImage:nil options:0 completed:nil]; } @@ -62,12 +70,11 @@ - (void)sd_setImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { if (!url) { - [self.imageURLStorage removeObjectForKey:@(state)]; - return; + [self.imageURLStorage removeObjectForKey:imageURLKeyForState(state)]; + } else { + self.imageURLStorage[imageURLKeyForState(state)] = url; } - self.imageURLStorage[@(state)] = url; - __weak typeof(self)weakSelf = self; [self sd_internalSetImageWithURL:url placeholderImage:placeholder @@ -82,6 +89,20 @@ - (void)sd_setImageWithURL:(nullable NSURL *)url #pragma mark - Background image +- (nullable NSURL *)sd_currentBackgroundImageURL { + NSURL *url = self.imageURLStorage[backgroundImageURLKeyForState(self.state)]; + + if (!url) { + url = self.imageURLStorage[backgroundImageURLKeyForState(UIControlStateNormal)]; + } + + return url; +} + +- (nullable NSURL *)sd_backgroundImageURLForState:(UIControlState)state { + return self.imageURLStorage[backgroundImageURLKeyForState(state)]; +} + - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:nil options:0 completed:nil]; } @@ -108,12 +129,11 @@ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { if (!url) { - [self.imageURLStorage removeObjectForKey:@(state)]; - return; + [self.imageURLStorage removeObjectForKey:backgroundImageURLKeyForState(state)]; + } else { + self.imageURLStorage[backgroundImageURLKeyForState(state)] = url; } - self.imageURLStorage[@(state)] = url; - __weak typeof(self)weakSelf = self; [self sd_internalSetImageWithURL:url placeholderImage:placeholder diff --git a/Dependency/SDWebImage/SDWebImageDecoder.h b/Dependency/SDWebImage/UIImage+ForceDecode.h similarity index 85% rename from Dependency/SDWebImage/SDWebImageDecoder.h rename to Dependency/SDWebImage/UIImage+ForceDecode.h index 8f8df86..708c37b 100755 --- a/Dependency/SDWebImage/SDWebImageDecoder.h +++ b/Dependency/SDWebImage/UIImage+ForceDecode.h @@ -1,13 +1,11 @@ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey - * (c) james * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -#import #import "SDWebImageCompat.h" @interface UIImage (ForceDecode) diff --git a/Dependency/SDWebImage/UIImage+ForceDecode.m b/Dependency/SDWebImage/UIImage+ForceDecode.m new file mode 100755 index 0000000..ee55aee --- /dev/null +++ b/Dependency/SDWebImage/UIImage+ForceDecode.m @@ -0,0 +1,30 @@ +/* + * This file is part of the SDWebImage package. + * (c) Olivier Poitrey + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +#import "UIImage+ForceDecode.h" +#import "SDWebImageCodersManager.h" + +@implementation UIImage (ForceDecode) + ++ (UIImage *)decodedImageWithImage:(UIImage *)image { + if (!image) { + return nil; + } + NSData *tempData; + return [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&tempData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(NO)}]; +} + ++ (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image { + if (!image) { + return nil; + } + NSData *tempData; + return [[SDWebImageCodersManager sharedInstance] decompressedImageWithImage:image data:&tempData options:@{SDWebImageCoderScaleDownLargeImagesKey: @(YES)}]; +} + +@end diff --git a/Dependency/SDWebImage/UIImage+GIF.h b/Dependency/SDWebImage/UIImage+GIF.h index 479d572..a3a6646 100755 --- a/Dependency/SDWebImage/UIImage+GIF.h +++ b/Dependency/SDWebImage/UIImage+GIF.h @@ -12,12 +12,13 @@ @interface UIImage (GIF) /** - * Compatibility method - creates an animated UIImage from an NSData, it will only contain the 1st frame image + * 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. */ + (UIImage *)sd_animatedGIFWithData:(NSData *)data; /** - * Checks if an UIImage instance is a GIF. Will use the `images` array + * Checks if an UIImage instance is a GIF. Will use the `images` array. */ - (BOOL)isGIF; diff --git a/Dependency/SDWebImage/UIImage+GIF.m b/Dependency/SDWebImage/UIImage+GIF.m index 6bee9a8..6fbca7a 100755 --- a/Dependency/SDWebImage/UIImage+GIF.m +++ b/Dependency/SDWebImage/UIImage+GIF.m @@ -8,8 +8,7 @@ */ #import "UIImage+GIF.h" -#import -#import "objc/runtime.h" +#import "SDWebImageGIFCoder.h" #import "NSImage+WebCache.h" @implementation UIImage (GIF) @@ -18,41 +17,7 @@ + (UIImage *)sd_animatedGIFWithData:(NSData *)data { if (!data) { return nil; } - -#if SD_MAC - return [[UIImage alloc] initWithData:data]; -#endif - - CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); - - size_t count = CGImageSourceGetCount(source); - - UIImage *staticImage; - - if (count <= 1) { - staticImage = [[UIImage alloc] initWithData:data]; - } else { - // we will only retrieve the 1st frame. the full GIF support is available via the FLAnimatedImageView category. - // this here is only code to allow drawing animated images as static ones -#if SD_WATCH - CGFloat scale = 1; - scale = [WKInterfaceDevice currentDevice].screenScale; -#elif SD_UIKIT - CGFloat scale = 1; - scale = [UIScreen mainScreen].scale; -#endif - - CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL); -#if SD_UIKIT || SD_WATCH - UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp]; - staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f]; -#endif - CGImageRelease(CGImage); - } - - CFRelease(source); - - return staticImage; + return [[SDWebImageGIFCoder sharedCoder] decodedImageWithData:data]; } - (BOOL)isGIF { diff --git a/Dependency/SDWebImage/UIImage+MultiFormat.h b/Dependency/SDWebImage/UIImage+MultiFormat.h index bec411e..c0792d1 100755 --- a/Dependency/SDWebImage/UIImage+MultiFormat.h +++ b/Dependency/SDWebImage/UIImage+MultiFormat.h @@ -11,6 +11,18 @@ @interface UIImage (MultiFormat) +/** + * UIKit: + * For static image format, this value is always 0. + * For animated image format, 0 means infinite looping. + * Note that because of the limitations of categories this property can get out of sync if you create another instance with CGImage or other methods. + * AppKit: + * NSImage currently only support animated via GIF imageRep unlike UIImage. + * The getter of this property will get the loop count from GIF imageRep + * The setter of this property will set the loop count from GIF imageRep + */ +@property (nonatomic, assign) NSUInteger sd_imageLoopCount; + + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data; - (nullable NSData *)sd_imageData; - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat; diff --git a/Dependency/SDWebImage/UIImage+MultiFormat.m b/Dependency/SDWebImage/UIImage+MultiFormat.m index 378f389..664e096 100755 --- a/Dependency/SDWebImage/UIImage+MultiFormat.m +++ b/Dependency/SDWebImage/UIImage+MultiFormat.m @@ -7,115 +7,56 @@ */ #import "UIImage+MultiFormat.h" -#import "UIImage+GIF.h" -#import "NSData+ImageContentType.h" -#import -#ifdef SD_WEBP -#import "UIImage+WebP.h" -#endif +#import "objc/runtime.h" +#import "SDWebImageCodersManager.h" @implementation UIImage (MultiFormat) -+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data { - if (!data) { - return nil; - } - - UIImage *image; - SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data]; - if (imageFormat == SDImageFormatGIF) { - image = [UIImage sd_animatedGIFWithData:data]; - } -#ifdef SD_WEBP - else if (imageFormat == SDImageFormatWebP) - { - image = [UIImage sd_imageWithWebPData:data]; - } -#endif - else { - image = [[UIImage alloc] initWithData:data]; -#if SD_UIKIT || SD_WATCH - UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data]; - if (orientation != UIImageOrientationUp) { - image = [UIImage imageWithCGImage:image.CGImage - scale:image.scale - orientation:orientation]; +#if SD_MAC +- (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; } -#endif } - - - return image; + return imageLoopCount; } -#if SD_UIKIT || SD_WATCH -+(UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData { - UIImageOrientation result = UIImageOrientationUp; - CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); - if (imageSource) { - CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); - if (properties) { - CFTypeRef val; - int exifOrientation; - val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); - if (val) { - CFNumberGetValue(val, kCFNumberIntType, &exifOrientation); - result = [self sd_exifOrientationToiOSOrientation:exifOrientation]; - } // else - if it's not set it remains at up - CFRelease((CFTypeRef) properties); - } else { - //NSLog(@"NO PROPERTIES, FAIL"); +- (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; } - CFRelease(imageSource); } - return result; } -#pragma mark EXIF orientation tag converter -// Convert an EXIF image orientation to an iOS one. -// reference see here: http://sylvana.net/jpegcrop/exif_orientation.html -+ (UIImageOrientation) sd_exifOrientationToiOSOrientation:(int)exifOrientation { - UIImageOrientation orientation = UIImageOrientationUp; - switch (exifOrientation) { - case 1: - orientation = UIImageOrientationUp; - break; - - case 3: - orientation = UIImageOrientationDown; - break; - - case 8: - orientation = UIImageOrientationLeft; - break; - - case 6: - orientation = UIImageOrientationRight; - break; - - case 2: - orientation = UIImageOrientationUpMirrored; - break; - - case 4: - orientation = UIImageOrientationDownMirrored; - break; - - case 5: - orientation = UIImageOrientationLeftMirrored; - break; +#else - case 7: - orientation = UIImageOrientationRightMirrored; - break; - default: - break; +- (NSUInteger)sd_imageLoopCount { + NSUInteger imageLoopCount = 0; + NSNumber *value = objc_getAssociatedObject(self, @selector(sd_imageLoopCount)); + if ([value isKindOfClass:[NSNumber class]]) { + imageLoopCount = value.unsignedIntegerValue; } - return orientation; + return imageLoopCount; +} + +- (void)setSd_imageLoopCount:(NSUInteger)sd_imageLoopCount { + NSNumber *value = @(sd_imageLoopCount); + objc_setAssociatedObject(self, @selector(sd_imageLoopCount), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } #endif ++ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data { + return [[SDWebImageCodersManager sharedInstance] decodedImageWithData:data]; +} + - (nullable NSData *)sd_imageData { return [self sd_imageDataAsFormat:SDImageFormatUndefined]; } @@ -123,36 +64,7 @@ - (nullable NSData *)sd_imageData { - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat { NSData *imageData = nil; if (self) { -#if SD_UIKIT || SD_WATCH - int alphaInfo = CGImageGetAlphaInfo(self.CGImage); - BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone || - alphaInfo == kCGImageAlphaNoneSkipFirst || - alphaInfo == kCGImageAlphaNoneSkipLast); - - BOOL usePNG = hasAlpha; - - // the imageFormat param has priority here. But if the format is undefined, we relly on the alpha channel - if (imageFormat != SDImageFormatUndefined) { - usePNG = (imageFormat == SDImageFormatPNG); - } - - if (usePNG) { - imageData = UIImagePNGRepresentation(self); - } else { - imageData = UIImageJPEGRepresentation(self, (CGFloat)1.0); - } -#else - NSBitmapImageFileType imageFileType = NSJPEGFileType; - if (imageFormat == SDImageFormatGIF) { - imageFileType = NSGIFFileType; - } else if (imageFormat == SDImageFormatPNG) { - imageFileType = NSPNGFileType; - } - - imageData = [NSBitmapImageRep representationOfImageRepsInArray:self.representations - usingType:imageFileType - properties:@{}]; -#endif + imageData = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:self format:imageFormat]; } return imageData; } diff --git a/Dependency/SDWebImage/UIImage+WebP.h b/Dependency/SDWebImage/UIImage+WebP.h index 6d6ba89..139eebd 100755 --- a/Dependency/SDWebImage/UIImage+WebP.h +++ b/Dependency/SDWebImage/UIImage+WebP.h @@ -19,8 +19,9 @@ * Note that because of the limitations of categories this property can get out of sync * if you create another instance with CGImage or other methods. * @return WebP image loop count + * @deprecated use `sd_imageLoopCount` instead. */ -- (NSInteger)sd_webpLoopCount; +- (NSInteger)sd_webpLoopCount __deprecated_msg("Method deprecated. Use `sd_imageLoopCount` in `UIImage+MultiFormat.h`"); + (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data; diff --git a/Dependency/SDWebImage/UIImage+WebP.m b/Dependency/SDWebImage/UIImage+WebP.m index 7d7d19a..0c4a9c5 100755 --- a/Dependency/SDWebImage/UIImage+WebP.m +++ b/Dependency/SDWebImage/UIImage+WebP.m @@ -9,290 +9,20 @@ #ifdef SD_WEBP #import "UIImage+WebP.h" -#import "webp/decode.h" -#import "webp/mux_types.h" -#import "webp/demux.h" -#import "NSImage+WebCache.h" - -#import "objc/runtime.h" - -// Callback for CGDataProviderRelease -static void FreeImageData(void *info, const void *data, size_t size) { - free((void *)data); -} +#import "SDWebImageWebPCoder.h" +#import "UIImage+MultiFormat.h" @implementation UIImage (WebP) -- (NSInteger)sd_webpLoopCount -{ - NSNumber *value = objc_getAssociatedObject(self, @selector(sd_webpLoopCount)); - return value.integerValue; +- (NSInteger)sd_webpLoopCount { + return self.sd_imageLoopCount; } + (nullable UIImage *)sd_imageWithWebPData:(nullable NSData *)data { if (!data) { return nil; } - - WebPData webpData; - WebPDataInit(&webpData); - webpData.bytes = data.bytes; - webpData.size = data.length; - WebPDemuxer *demuxer = WebPDemux(&webpData); - if (!demuxer) { - return nil; - } - - uint32_t flags = WebPDemuxGetI(demuxer, WEBP_FF_FORMAT_FLAGS); - if (!(flags & ANIMATION_FLAG)) { - // for static single webp image - UIImage *staticImage = [self sd_rawWebpImageWithData:webpData]; - WebPDemuxDelete(demuxer); - return staticImage; - } - - WebPIterator iter; - if (!WebPDemuxGetFrame(demuxer, 1, &iter)) { - WebPDemuxReleaseIterator(&iter); - WebPDemuxDelete(demuxer); - return nil; - } - -#if SD_UIKIT || SD_WATCH - int loopCount = WebPDemuxGetI(demuxer, WEBP_FF_LOOP_COUNT); -#endif - int frameCount = WebPDemuxGetI(demuxer, WEBP_FF_FRAME_COUNT); - int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH); - int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT); - CGBitmapInfo bitmapInfo; - if (!(flags & ALPHA_FLAG)) { - bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast; - } else { - bitmapInfo = kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast; - } - CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, SDCGColorSpaceGetDeviceRGB(), bitmapInfo); - if (!canvas) { - WebPDemuxReleaseIterator(&iter); - WebPDemuxDelete(demuxer); - return nil; - } - - NSMutableArray *images = [NSMutableArray array]; - NSTimeInterval totalDuration = 0; - int durations[frameCount]; - - do { - UIImage *image; - if (iter.blend_method == WEBP_MUX_BLEND) { - image = [self sd_blendWebpImageWithCanvas:canvas iterator:iter]; - } else { - image = [self sd_nonblendWebpImageWithCanvas:canvas iterator:iter]; - } - - if (!image) { - continue; - } - - [images addObject:image]; - -#if SD_MAC - break; -#endif - - int duration = iter.duration; - if (duration <= 10) { - // WebP standard says 0 duration is used for canvas updating but not showing image, but actually Chrome and other implementations set it to 100ms if duration is lower or equal than 10ms - // Some animated WebP images also created without duration, we should keep compatibility - duration = 100; - } - totalDuration += duration; - size_t count = images.count; - durations[count - 1] = duration; - - } while (WebPDemuxNextFrame(&iter)); - - WebPDemuxReleaseIterator(&iter); - WebPDemuxDelete(demuxer); - CGContextRelease(canvas); - - UIImage *finalImage = nil; -#if SD_UIKIT || SD_WATCH - NSArray *animatedImages = [self sd_animatedImagesWithImages:images durations:durations totalDuration:totalDuration]; - finalImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.0]; - if (finalImage) { - objc_setAssociatedObject(finalImage, @selector(sd_webpLoopCount), @(loopCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC); - } -#elif SD_MAC - finalImage = images.firstObject; -#endif - return finalImage; -} - - -+ (nullable UIImage *)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter { - UIImage *image = [self sd_rawWebpImageWithData:iter.fragment]; - if (!image) { - return nil; - } - - size_t canvasWidth = CGBitmapContextGetWidth(canvas); - size_t canvasHeight = CGBitmapContextGetHeight(canvas); - CGSize size = CGSizeMake(canvasWidth, canvasHeight); - CGFloat tmpX = iter.x_offset; - CGFloat tmpY = size.height - iter.height - iter.y_offset; - CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height); - - CGContextDrawImage(canvas, imageRect, image.CGImage); - CGImageRef newImageRef = CGBitmapContextCreateImage(canvas); - -#if SD_UIKIT || SD_WATCH - image = [UIImage imageWithCGImage:newImageRef]; -#elif SD_MAC - image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize]; -#endif - - CGImageRelease(newImageRef); - - if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { - CGContextClearRect(canvas, imageRect); - } - - return image; -} - -+ (nullable UIImage *)sd_nonblendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter { - UIImage *image = [self sd_rawWebpImageWithData:iter.fragment]; - if (!image) { - return nil; - } - - size_t canvasWidth = CGBitmapContextGetWidth(canvas); - size_t canvasHeight = CGBitmapContextGetHeight(canvas); - CGSize size = CGSizeMake(canvasWidth, canvasHeight); - CGFloat tmpX = iter.x_offset; - CGFloat tmpY = size.height - iter.height - iter.y_offset; - CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height); - - CGContextClearRect(canvas, imageRect); - CGContextDrawImage(canvas, imageRect, image.CGImage); - CGImageRef newImageRef = CGBitmapContextCreateImage(canvas); - -#if SD_UIKIT || SD_WATCH - image = [UIImage imageWithCGImage:newImageRef]; -#elif SD_MAC - image = [[UIImage alloc] initWithCGImage:newImageRef size:NSZeroSize]; -#endif - - CGImageRelease(newImageRef); - - if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) { - CGContextClearRect(canvas, imageRect); - } - - return image; -} - -+ (nullable UIImage *)sd_rawWebpImageWithData:(WebPData)webpData { - WebPDecoderConfig config; - if (!WebPInitDecoderConfig(&config)) { - return nil; - } - - if (WebPGetFeatures(webpData.bytes, webpData.size, &config.input) != VP8_STATUS_OK) { - return nil; - } - - config.output.colorspace = config.input.has_alpha ? MODE_rgbA : MODE_RGB; - config.options.use_threads = 1; - - // Decode the WebP image data into a RGBA value array - if (WebPDecode(webpData.bytes, webpData.size, &config) != VP8_STATUS_OK) { - return nil; - } - - int width = config.input.width; - int height = config.input.height; - if (config.options.use_scaling) { - width = config.options.scaled_width; - height = config.options.scaled_height; - } - - // Construct a UIImage from the decoded RGBA value array - CGDataProviderRef provider = - CGDataProviderCreateWithData(NULL, config.output.u.RGBA.rgba, config.output.u.RGBA.size, FreeImageData); - CGColorSpaceRef colorSpaceRef = SDCGColorSpaceGetDeviceRGB(); - CGBitmapInfo bitmapInfo = config.input.has_alpha ? kCGBitmapByteOrder32Big | kCGImageAlphaPremultipliedLast : kCGBitmapByteOrder32Big | kCGImageAlphaNoneSkipLast; - size_t components = config.input.has_alpha ? 4 : 3; - CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault; - CGImageRef imageRef = CGImageCreate(width, height, 8, components * 8, components * width, colorSpaceRef, bitmapInfo, provider, NULL, NO, renderingIntent); - - CGDataProviderRelease(provider); - -#if SD_UIKIT || SD_WATCH - UIImage *image = [[UIImage alloc] initWithCGImage:imageRef]; -#else - UIImage *image = [[UIImage alloc] initWithCGImage:imageRef size:NSZeroSize]; -#endif - CGImageRelease(imageRef); - - return image; -} - -+ (NSArray *)sd_animatedImagesWithImages:(NSArray *)images durations:(int const * const)durations totalDuration:(NSTimeInterval)totalDuration -{ - // [UIImage animatedImageWithImages:duration:] only use the average duration for per frame - // divide the total duration to implement per frame duration for animated WebP - NSUInteger count = images.count; - if (!count) { - return nil; - } - if (count == 1) { - return images; - } - - int const gcd = gcdArray(count, durations); - NSMutableArray *animatedImages = [NSMutableArray arrayWithCapacity:count]; - [images enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) { - int duration = durations[idx]; - int repeatCount; - if (gcd) { - repeatCount = duration / gcd; - } else { - repeatCount = 1; - } - for (int i = 0; i < repeatCount; ++i) { - [animatedImages addObject:image]; - } - }]; - - return animatedImages; -} - -static CGColorSpaceRef SDCGColorSpaceGetDeviceRGB() { - static CGColorSpaceRef space; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - space = CGColorSpaceCreateDeviceRGB(); - }); - return space; -} - -static int gcdArray(size_t const count, int const * const values) { - int result = values[0]; - for (size_t i = 1; i < count; ++i) { - result = gcd(values[i], result); - } - return result; -} - -static int gcd(int a,int b) { - int c; - while (a != 0) { - c = a; - a = b % a; - b = c; - } - return b; + return [[SDWebImageWebPCoder sharedCoder] decodedImageWithData:data]; } @end diff --git a/Dependency/SDWebImage/UIView+WebCache.h b/Dependency/SDWebImage/UIView+WebCache.h index 4c57519..0067837 100755 --- a/Dependency/SDWebImage/UIView+WebCache.h +++ b/Dependency/SDWebImage/UIView+WebCache.h @@ -12,6 +12,8 @@ #import "SDWebImageManager.h" +FOUNDATION_EXPORT NSString * _Nonnull const SDWebImageInternalSetImageInGlobalQueueKey; + typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable imageData); @interface UIView (WebCache) @@ -50,6 +52,34 @@ typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable ima progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; +/** + * Set the imageView `image` with an `url` and optionally a placeholder image. + * + * The download is asynchronous and cached. + * + * @param url The url for the image. + * @param placeholder The image to be set initially, until the image request finishes. + * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. + * @param operationKey A string to be used as the operation key. If nil, will use the class name + * @param setImageBlock Block used for custom set image code + * @param progressBlock A block called while image is downloading + * @note the progress block is executed on a background queue + * @param completedBlock A block called when operation has been completed. This block has no return value + * and takes the requested UIImage as first parameter. In case of error the image parameter + * is nil and the second parameter may contain an NSError. The third parameter is a Boolean + * indicating if the image was retrieved from the local cache or from the network. + * The fourth parameter is the original image url. + * @param context A context with extra information to perform specify changes or processes. + */ +- (void)sd_internalSetImageWithURL:(nullable NSURL *)url + placeholderImage:(nullable UIImage *)placeholder + options:(SDWebImageOptions)options + operationKey:(nullable NSString *)operationKey + setImageBlock:(nullable SDSetImageBlock)setImageBlock + progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock + completed:(nullable SDExternalCompletionBlock)completedBlock + context:(nullable NSDictionary *)context; + /** * Cancel the current download */ diff --git a/Dependency/SDWebImage/UIView+WebCache.m b/Dependency/SDWebImage/UIView+WebCache.m index 3e2c6da..20b944c 100755 --- a/Dependency/SDWebImage/UIView+WebCache.m +++ b/Dependency/SDWebImage/UIView+WebCache.m @@ -13,6 +13,8 @@ #import "objc/runtime.h" #import "UIView+WebCacheOperation.h" +NSString * const SDWebImageInternalSetImageInGlobalQueueKey = @"setImageInGlobalQueue"; + static char imageURLKey; #if SD_UIKIT @@ -34,6 +36,17 @@ - (void)sd_internalSetImageWithURL:(nullable NSURL *)url setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { + return [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey setImageBlock:setImageBlock progress:progressBlock completed:completedBlock context:nil]; +} + +- (void)sd_internalSetImageWithURL:(nullable NSURL *)url + placeholderImage:(nullable UIImage *)placeholder + options:(SDWebImageOptions)options + operationKey:(nullable NSString *)operationKey + setImageBlock:(nullable SDSetImageBlock)setImageBlock + progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock + completed:(nullable SDExternalCompletionBlock)completedBlock + context:(nullable NSDictionary *)context { NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]); [self sd_cancelImageLoadOperationWithKey:validOperationKey]; objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); @@ -54,28 +67,48 @@ - (void)sd_internalSetImageWithURL:(nullable NSURL *)url id operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { __strong __typeof (wself) sself = wself; [sself sd_removeActivityIndicator]; - if (!sself) { - return; - } - dispatch_main_async_safe(^{ - if (!sself) { - return; - } - if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { - completedBlock(image, error, cacheType, url); - return; - } else if (image) { - [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock]; + if (!sself) { return; } + BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); + BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) || + (!image && !(options & SDWebImageDelayPlaceholder))); + SDWebImageNoParamsBlock callCompletedBlockClojure = ^{ + if (!sself) { return; } + if (!shouldNotSetImage) { [sself sd_setNeedsLayout]; - } else { - if ((options & SDWebImageDelayPlaceholder)) { - [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock]; - [sself sd_setNeedsLayout]; - } } - if (completedBlock && finished) { + if (completedBlock && shouldCallCompletedBlock) { completedBlock(image, error, cacheType, url); } + }; + + // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set + // OR + // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set + if (shouldNotSetImage) { + dispatch_main_async_safe(callCompletedBlockClojure); + return; + } + + UIImage *targetImage = nil; + NSData *targetData = nil; + if (image) { + // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set + targetImage = image; + targetData = data; + } else if (options & SDWebImageDelayPlaceholder) { + // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set + targetImage = placeholder; + targetData = nil; + } + BOOL shouldUseGlobalQueue = NO; + if (context && [context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey]) { + shouldUseGlobalQueue = [[context valueForKey:SDWebImageInternalSetImageInGlobalQueueKey] boolValue]; + } + dispatch_queue_t targetQueue = shouldUseGlobalQueue ? dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0) : dispatch_get_main_queue(); + + dispatch_async(targetQueue, ^{ + [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock]; + dispatch_main_async_safe(callCompletedBlockClojure); }); }]; [self sd_setImageLoadOperation:operation forKey:validOperationKey];