|
|
@@ -0,0 +1,445 @@
|
|
|
+/*
|
|
|
+* This file is part of the SDWebImage package.
|
|
|
+* (c) Olivier Poitrey <rs@dailymotion.com>
|
|
|
+*
|
|
|
+* For the full copyright and license information, please view the LICENSE
|
|
|
+* file that was distributed with this source code.
|
|
|
+*/
|
|
|
+
|
|
|
+#import "SDImageIOAnimatedCoder.h"
|
|
|
+#import "NSImage+Compatibility.h"
|
|
|
+#import "UIImage+Metadata.h"
|
|
|
+#import "NSData+ImageContentType.h"
|
|
|
+#import "SDImageCoderHelper.h"
|
|
|
+#import "SDAnimatedImageRep.h"
|
|
|
+
|
|
|
+@interface SDImageIOCoderFrame : NSObject
|
|
|
+
|
|
|
+@property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
|
|
|
+@property (nonatomic, assign) NSTimeInterval duration; // Frame duration in seconds
|
|
|
+
|
|
|
+@end
|
|
|
+
|
|
|
+@implementation SDImageIOCoderFrame
|
|
|
+@end
|
|
|
+
|
|
|
+@implementation SDImageIOAnimatedCoder {
|
|
|
+ size_t _width, _height;
|
|
|
+ CGImageSourceRef _imageSource;
|
|
|
+ NSData *_imageData;
|
|
|
+ CGFloat _scale;
|
|
|
+ NSUInteger _loopCount;
|
|
|
+ NSUInteger _frameCount;
|
|
|
+ NSArray<SDImageIOCoderFrame *> *_frames;
|
|
|
+ BOOL _finished;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)dealloc
|
|
|
+{
|
|
|
+ if (_imageSource) {
|
|
|
+ CFRelease(_imageSource);
|
|
|
+ _imageSource = NULL;
|
|
|
+ }
|
|
|
+#if SD_UIKIT
|
|
|
+ [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
|
|
+#endif
|
|
|
+}
|
|
|
+
|
|
|
+- (void)didReceiveMemoryWarning:(NSNotification *)notification
|
|
|
+{
|
|
|
+ if (_imageSource) {
|
|
|
+ for (size_t i = 0; i < _frameCount; i++) {
|
|
|
+ CGImageSourceRemoveCacheAtIndex(_imageSource, i);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - Subclass Override
|
|
|
+
|
|
|
++ (SDImageFormat)imageFormat {
|
|
|
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
|
|
|
+ reason:[NSString stringWithFormat:@"Must override %@ in subclass", NSStringFromSelector(_cmd)]
|
|
|
+ userInfo:nil];
|
|
|
+}
|
|
|
+
|
|
|
++ (NSString *)imageUTType {
|
|
|
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
|
|
|
+ reason:[NSString stringWithFormat:@"Must override %@ in subclass", NSStringFromSelector(_cmd)]
|
|
|
+ userInfo:nil];
|
|
|
+}
|
|
|
+
|
|
|
++ (NSString *)dictionaryProperty {
|
|
|
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
|
|
|
+ reason:[NSString stringWithFormat:@"Must override %@ in subclass", NSStringFromSelector(_cmd)]
|
|
|
+ userInfo:nil];
|
|
|
+}
|
|
|
+
|
|
|
++ (NSString *)unclampedDelayTimeProperty {
|
|
|
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
|
|
|
+ reason:[NSString stringWithFormat:@"Must override %@ in subclass", NSStringFromSelector(_cmd)]
|
|
|
+ userInfo:nil];
|
|
|
+}
|
|
|
+
|
|
|
++ (NSString *)delayTimeProperty {
|
|
|
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
|
|
|
+ reason:[NSString stringWithFormat:@"Must override %@ in subclass", NSStringFromSelector(_cmd)]
|
|
|
+ userInfo:nil];
|
|
|
+}
|
|
|
+
|
|
|
++ (NSString *)loopCountProperty {
|
|
|
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
|
|
|
+ reason:[NSString stringWithFormat:@"Must override %@ in subclass", NSStringFromSelector(_cmd)]
|
|
|
+ userInfo:nil];
|
|
|
+}
|
|
|
+
|
|
|
++ (NSUInteger)defaultLoopCount {
|
|
|
+ @throw [NSException exceptionWithName:NSInternalInconsistencyException
|
|
|
+ reason:[NSString stringWithFormat:@"Must override %@ in subclass", NSStringFromSelector(_cmd)]
|
|
|
+ userInfo:nil];
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - Utils
|
|
|
+
|
|
|
++ (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source {
|
|
|
+ NSUInteger loopCount = 1;
|
|
|
+ NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil);
|
|
|
+ NSDictionary *containerProperties = imageProperties[self.dictionaryProperty];
|
|
|
+ if (containerProperties) {
|
|
|
+ NSNumber *containerLoopCount = containerProperties[self.loopCountProperty];
|
|
|
+ if (containerLoopCount != nil) {
|
|
|
+ loopCount = containerLoopCount.unsignedIntegerValue;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return loopCount;
|
|
|
+}
|
|
|
+
|
|
|
++ (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
|
|
|
+ NSTimeInterval frameDuration = 0.1;
|
|
|
+ CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, nil);
|
|
|
+ if (!cfFrameProperties) {
|
|
|
+ return frameDuration;
|
|
|
+ }
|
|
|
+ NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
|
|
|
+ NSDictionary *containerProperties = frameProperties[self.dictionaryProperty];
|
|
|
+
|
|
|
+ NSNumber *delayTimeUnclampedProp = containerProperties[self.unclampedDelayTimeProperty];
|
|
|
+ if (delayTimeUnclampedProp != nil) {
|
|
|
+ frameDuration = [delayTimeUnclampedProp doubleValue];
|
|
|
+ } else {
|
|
|
+ NSNumber *delayTimeProp = containerProperties[self.delayTimeProperty];
|
|
|
+ if (delayTimeProp != nil) {
|
|
|
+ frameDuration = [delayTimeProp doubleValue];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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 <rdar://problem/7689300> and <http://webkit.org/b/36082>
|
|
|
+ // for more information.
|
|
|
+
|
|
|
+ if (frameDuration < 0.011) {
|
|
|
+ frameDuration = 0.1;
|
|
|
+ }
|
|
|
+
|
|
|
+ CFRelease(cfFrameProperties);
|
|
|
+ return frameDuration;
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - Decode
|
|
|
+- (BOOL)canDecodeFromData:(nullable NSData *)data {
|
|
|
+ return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat);
|
|
|
+}
|
|
|
+
|
|
|
+- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
|
|
|
+ if (!data) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+ CGFloat scale = 1;
|
|
|
+ NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
|
|
|
+ if (scaleFactor != nil) {
|
|
|
+ scale = MAX([scaleFactor doubleValue], 1);
|
|
|
+ }
|
|
|
+
|
|
|
+#if SD_MAC
|
|
|
+ SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
|
|
|
+ NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
|
|
|
+ imageRep.size = size;
|
|
|
+ NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
|
|
|
+ [animatedImage addRepresentation:imageRep];
|
|
|
+ return animatedImage;
|
|
|
+#else
|
|
|
+
|
|
|
+ CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
|
|
|
+ if (!source) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+ size_t count = CGImageSourceGetCount(source);
|
|
|
+ UIImage *animatedImage;
|
|
|
+
|
|
|
+ BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
|
|
|
+ if (decodeFirstFrame || count <= 1) {
|
|
|
+ animatedImage = [[UIImage alloc] initWithData:data scale:scale];
|
|
|
+ } else {
|
|
|
+ NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
|
|
|
+
|
|
|
+ for (size_t i = 0; i < count; i++) {
|
|
|
+ CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, i, NULL);
|
|
|
+ if (!imageRef) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
|
|
|
+ UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:UIImageOrientationUp];
|
|
|
+ CGImageRelease(imageRef);
|
|
|
+
|
|
|
+ SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
|
|
|
+ [frames addObject:frame];
|
|
|
+ }
|
|
|
+
|
|
|
+ NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
|
|
|
+
|
|
|
+ animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
|
|
|
+ animatedImage.sd_imageLoopCount = loopCount;
|
|
|
+ }
|
|
|
+ animatedImage.sd_imageFormat = self.class.imageFormat;
|
|
|
+ CFRelease(source);
|
|
|
+
|
|
|
+ return animatedImage;
|
|
|
+#endif
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - Progressive Decode
|
|
|
+
|
|
|
+- (BOOL)canIncrementalDecodeFromData:(NSData *)data {
|
|
|
+ return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat);
|
|
|
+}
|
|
|
+
|
|
|
+- (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options {
|
|
|
+ self = [super init];
|
|
|
+ if (self) {
|
|
|
+ NSString *imageUTType = self.class.imageUTType;
|
|
|
+ _imageSource = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : imageUTType});
|
|
|
+ CGFloat scale = 1;
|
|
|
+ NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
|
|
|
+ if (scaleFactor != nil) {
|
|
|
+ scale = MAX([scaleFactor doubleValue], 1);
|
|
|
+ }
|
|
|
+ _scale = scale;
|
|
|
+#if SD_UIKIT
|
|
|
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
|
|
+#endif
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+- (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
|
|
|
+ if (_finished) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ _imageData = data;
|
|
|
+ _finished = finished;
|
|
|
+
|
|
|
+ // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
|
|
|
+ // Thanks to the author @Nyx0uf
|
|
|
+
|
|
|
+ // Update the data source, we must pass ALL the data, not just the new bytes
|
|
|
+ CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
|
|
|
+
|
|
|
+ if (_width + _height == 0) {
|
|
|
+ CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
|
|
|
+ if (properties) {
|
|
|
+ CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
|
|
|
+ if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
|
|
|
+ val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
|
|
|
+ if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
|
|
|
+ CFRelease(properties);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // For animated image progressive decoding because the frame count and duration may be changed.
|
|
|
+ [self scanAndCheckFramesValidWithImageSource:_imageSource];
|
|
|
+}
|
|
|
+
|
|
|
+- (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
|
|
|
+ UIImage *image;
|
|
|
+
|
|
|
+ if (_width + _height > 0) {
|
|
|
+ // Create the image
|
|
|
+ CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
|
|
|
+
|
|
|
+ if (partialImageRef) {
|
|
|
+ CGFloat scale = _scale;
|
|
|
+ NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
|
|
|
+ if (scaleFactor != nil) {
|
|
|
+ scale = MAX([scaleFactor doubleValue], 1);
|
|
|
+ }
|
|
|
+#if SD_UIKIT || SD_WATCH
|
|
|
+ image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:UIImageOrientationUp];
|
|
|
+#else
|
|
|
+ image = [[UIImage alloc] initWithCGImage:partialImageRef scale:scale orientation:kCGImagePropertyOrientationUp];
|
|
|
+#endif
|
|
|
+ CGImageRelease(partialImageRef);
|
|
|
+ image.sd_imageFormat = self.class.imageFormat;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return image;
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - Encode
|
|
|
+- (BOOL)canEncodeToFormat:(SDImageFormat)format {
|
|
|
+ return (format == self.class.imageFormat);
|
|
|
+}
|
|
|
+
|
|
|
+- (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options {
|
|
|
+ if (!image) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (format != self.class.imageFormat) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+
|
|
|
+ NSMutableData *imageData = [NSMutableData data];
|
|
|
+ NSString *imageUTType = self.class.imageUTType;
|
|
|
+ NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
|
|
|
+
|
|
|
+ // Create an image destination. Animated Image does not support EXIF image orientation TODO
|
|
|
+ // The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
|
|
|
+ CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, (__bridge CFStringRef)imageUTType, frames.count ?: 1, NULL);
|
|
|
+ if (!imageDestination) {
|
|
|
+ // Handle failure.
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+ NSMutableDictionary *properties = [NSMutableDictionary dictionary];
|
|
|
+ double compressionQuality = 1;
|
|
|
+ if (options[SDImageCoderEncodeCompressionQuality]) {
|
|
|
+ compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
|
|
|
+ }
|
|
|
+ properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);
|
|
|
+
|
|
|
+ BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
|
|
|
+ if (encodeFirstFrame || frames.count == 0) {
|
|
|
+ // for static single images
|
|
|
+ CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
|
|
|
+ } else {
|
|
|
+ // for animated images
|
|
|
+ NSUInteger loopCount = image.sd_imageLoopCount;
|
|
|
+ NSDictionary *containerProperties = @{self.class.loopCountProperty : @(loopCount)};
|
|
|
+ properties[self.class.dictionaryProperty] = containerProperties;
|
|
|
+ CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)properties);
|
|
|
+
|
|
|
+ for (size_t i = 0; i < frames.count; i++) {
|
|
|
+ SDImageFrame *frame = frames[i];
|
|
|
+ NSTimeInterval frameDuration = frame.duration;
|
|
|
+ CGImageRef frameImageRef = frame.image.CGImage;
|
|
|
+ NSDictionary *frameProperties = @{self.class.dictionaryProperty : @{self.class.delayTimeProperty : @(frameDuration)}};
|
|
|
+ CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // Finalize the destination.
|
|
|
+ if (CGImageDestinationFinalize(imageDestination) == NO) {
|
|
|
+ // Handle failure.
|
|
|
+ imageData = nil;
|
|
|
+ }
|
|
|
+
|
|
|
+ CFRelease(imageDestination);
|
|
|
+
|
|
|
+ return [imageData copy];
|
|
|
+}
|
|
|
+
|
|
|
+#pragma mark - SDAnimatedImageCoder
|
|
|
+- (nullable instancetype)initWithAnimatedImageData:(nullable NSData *)data options:(nullable SDImageCoderOptions *)options {
|
|
|
+ if (!data) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+ self = [super init];
|
|
|
+ if (self) {
|
|
|
+ CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
|
|
|
+ if (!imageSource) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+ BOOL framesValid = [self scanAndCheckFramesValidWithImageSource:imageSource];
|
|
|
+ if (!framesValid) {
|
|
|
+ CFRelease(imageSource);
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+ CGFloat scale = 1;
|
|
|
+ NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
|
|
|
+ if (scaleFactor != nil) {
|
|
|
+ scale = MAX([scaleFactor doubleValue], 1);
|
|
|
+ }
|
|
|
+ _scale = scale;
|
|
|
+ _imageSource = imageSource;
|
|
|
+ _imageData = data;
|
|
|
+#if SD_UIKIT
|
|
|
+ [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
|
|
+#endif
|
|
|
+ }
|
|
|
+ return self;
|
|
|
+}
|
|
|
+
|
|
|
+- (BOOL)scanAndCheckFramesValidWithImageSource:(CGImageSourceRef)imageSource {
|
|
|
+ if (!imageSource) {
|
|
|
+ return NO;
|
|
|
+ }
|
|
|
+ NSUInteger frameCount = CGImageSourceGetCount(imageSource);
|
|
|
+ NSUInteger loopCount = [self.class imageLoopCountWithSource:imageSource];
|
|
|
+ NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray array];
|
|
|
+
|
|
|
+ for (size_t i = 0; i < frameCount; i++) {
|
|
|
+ SDImageIOCoderFrame *frame = [[SDImageIOCoderFrame alloc] init];
|
|
|
+ frame.index = i;
|
|
|
+ frame.duration = [self.class frameDurationAtIndex:i source:imageSource];
|
|
|
+ [frames addObject:frame];
|
|
|
+ }
|
|
|
+
|
|
|
+ _frameCount = frameCount;
|
|
|
+ _loopCount = loopCount;
|
|
|
+ _frames = [frames copy];
|
|
|
+
|
|
|
+ return YES;
|
|
|
+}
|
|
|
+
|
|
|
+- (NSData *)animatedImageData {
|
|
|
+ return _imageData;
|
|
|
+}
|
|
|
+
|
|
|
+- (NSUInteger)animatedImageLoopCount {
|
|
|
+ return _loopCount;
|
|
|
+}
|
|
|
+
|
|
|
+- (NSUInteger)animatedImageFrameCount {
|
|
|
+ return _frameCount;
|
|
|
+}
|
|
|
+
|
|
|
+- (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
|
|
|
+ if (index >= _frameCount) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ return _frames[index].duration;
|
|
|
+}
|
|
|
+
|
|
|
+- (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
|
|
|
+ CGImageRef imageRef = CGImageSourceCreateImageAtIndex(_imageSource, index, NULL);
|
|
|
+ if (!imageRef) {
|
|
|
+ return nil;
|
|
|
+ }
|
|
|
+ // Image/IO create CGImage does not decode, so we do this because this is called background queue, this can avoid main queue block when rendering(especially when one more imageViews use the same image instance)
|
|
|
+ CGImageRef newImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
|
|
|
+ if (!newImageRef) {
|
|
|
+ newImageRef = imageRef;
|
|
|
+ } else {
|
|
|
+ CGImageRelease(imageRef);
|
|
|
+ }
|
|
|
+#if SD_MAC
|
|
|
+ UIImage *image = [[UIImage alloc] initWithCGImage:newImageRef scale:_scale orientation:kCGImagePropertyOrientationUp];
|
|
|
+#else
|
|
|
+ UIImage *image = [[UIImage alloc] initWithCGImage:newImageRef scale:_scale orientation:UIImageOrientationUp];
|
|
|
+#endif
|
|
|
+ CGImageRelease(newImageRef);
|
|
|
+ return image;
|
|
|
+}
|
|
|
+
|
|
|
+@end
|
|
|
+
|