| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818 |
- /*
- * 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"
- #import "UIImage+ForceDecode.h"
- #import "SDInternalMacros.h"
- #import <ImageIO/ImageIO.h>
- #import <CoreServices/CoreServices.h>
- #if SD_CHECK_CGIMAGE_RETAIN_SOURCE
- #import <dlfcn.h>
- // SPI to check thread safe during Example and Test
- static CGImageSourceRef (*SDCGImageGetImageSource)(CGImageRef);
- #endif
- // Specify File Size for lossy format encoding, like JPEG
- static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
- // This strip the un-wanted CGImageProperty, like the internal CGImageSourceRef in iOS 15+
- // However, CGImageCreateCopy still keep those CGImageProperty, not suit for our use case
- static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
- if (!image) return nil;
- size_t width = CGImageGetWidth(image);
- size_t height = CGImageGetHeight(image);
- size_t bitsPerComponent = CGImageGetBitsPerComponent(image);
- size_t bitsPerPixel = CGImageGetBitsPerPixel(image);
- size_t bytesPerRow = CGImageGetBytesPerRow(image);
- CGColorSpaceRef space = CGImageGetColorSpace(image);
- CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
- CGDataProviderRef provider = CGImageGetDataProvider(image);
- const CGFloat *decode = CGImageGetDecode(image);
- bool shouldInterpolate = CGImageGetShouldInterpolate(image);
- CGColorRenderingIntent intent = CGImageGetRenderingIntent(image);
- CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, provider, decode, shouldInterpolate, intent);
- return newImage;
- }
- @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;
- BOOL _incremental;
- SD_LOCK_DECLARE(_lock); // Lock only apply for incremental animation decoding
- NSData *_imageData;
- CGFloat _scale;
- NSUInteger _loopCount;
- NSUInteger _frameCount;
- NSArray<SDImageIOCoderFrame *> *_frames;
- BOOL _finished;
- BOOL _preserveAspectRatio;
- CGSize _thumbnailSize;
- BOOL _lazyDecode;
- }
- - (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:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
- userInfo:nil];
- }
- + (NSString *)imageUTType {
- @throw [NSException exceptionWithName:NSInternalInconsistencyException
- reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
- userInfo:nil];
- }
- + (NSString *)dictionaryProperty {
- @throw [NSException exceptionWithName:NSInternalInconsistencyException
- reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
- userInfo:nil];
- }
- + (NSString *)unclampedDelayTimeProperty {
- @throw [NSException exceptionWithName:NSInternalInconsistencyException
- reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
- userInfo:nil];
- }
- + (NSString *)delayTimeProperty {
- @throw [NSException exceptionWithName:NSInternalInconsistencyException
- reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
- userInfo:nil];
- }
- + (NSString *)loopCountProperty {
- @throw [NSException exceptionWithName:NSInternalInconsistencyException
- reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
- userInfo:nil];
- }
- + (NSUInteger)defaultLoopCount {
- @throw [NSException exceptionWithName:NSInternalInconsistencyException
- reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
- userInfo:nil];
- }
- #pragma mark - Utils
- + (BOOL)canDecodeFromFormat:(SDImageFormat)format {
- static dispatch_once_t onceToken;
- static NSSet *imageUTTypeSet;
- dispatch_once(&onceToken, ^{
- NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageSourceCopyTypeIdentifiers();
- imageUTTypeSet = [NSSet setWithArray:imageUTTypes];
- });
- CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
- if ([imageUTTypeSet containsObject:(__bridge NSString *)(imageUTType)]) {
- // Can decode from target format
- return YES;
- }
- return NO;
- }
- + (BOOL)canEncodeToFormat:(SDImageFormat)format {
- static dispatch_once_t onceToken;
- static NSSet *imageUTTypeSet;
- dispatch_once(&onceToken, ^{
- NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageDestinationCopyTypeIdentifiers();
- imageUTTypeSet = [NSSet setWithArray:imageUTTypes];
- });
- CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
- if ([imageUTTypeSet containsObject:(__bridge NSString *)(imageUTType)]) {
- // Can encode to target format
- return YES;
- }
- return NO;
- }
- + (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source {
- NSUInteger loopCount = self.defaultLoopCount;
- NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, NULL);
- 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, NULL);
- 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;
- }
- + (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage {
- // `animatedImage` means called from `SDAnimatedImageProvider.animatedImageFrameAtIndex`
- NSDictionary *options;
- if (animatedImage) {
- if (!lazyDecode) {
- options = @{
- // image decoding and caching should happen at image creation time.
- (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
- };
- } else {
- options = @{
- // image decoding will happen at rendering time
- (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(NO),
- };
- }
- }
- // Parse the image properties
- NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL);
- CGFloat pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
- CGFloat pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
- CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
- if (!exifOrientation) {
- exifOrientation = kCGImagePropertyOrientationUp;
- }
- NSMutableDictionary *decodingOptions;
- if (options) {
- decodingOptions = [NSMutableDictionary dictionaryWithDictionary:options];
- } else {
- decodingOptions = [NSMutableDictionary dictionary];
- }
- CGImageRef imageRef;
- BOOL createFullImage = thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height);
- if (createFullImage) {
- imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
- } else {
- decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio);
- CGFloat maxPixelSize;
- if (preserveAspectRatio) {
- CGFloat pixelRatio = pixelWidth / pixelHeight;
- CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height;
- if (pixelRatio > thumbnailRatio) {
- maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.width / pixelRatio);
- } else {
- maxPixelSize = MAX(thumbnailSize.height, thumbnailSize.height * pixelRatio);
- }
- } else {
- maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
- }
- decodingOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize);
- decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageAlways] = @(YES);
- imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
- }
- if (!imageRef) {
- return nil;
- }
- BOOL isDecoded = NO;
- // Thumbnail image post-process
- if (!createFullImage) {
- if (preserveAspectRatio) {
- // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice
- exifOrientation = kCGImagePropertyOrientationUp;
- } else {
- // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size
- CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize];
- if (scaledImageRef) {
- CGImageRelease(imageRef);
- imageRef = scaledImageRef;
- isDecoded = YES;
- }
- }
- }
- // Check whether output CGImage is decoded
- if (!lazyDecode) {
- if (!isDecoded) {
- // Use CoreGraphics to trigger immediately decode
- CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
- if (decodedImageRef) {
- CGImageRelease(imageRef);
- imageRef = decodedImageRef;
- isDecoded = YES;
- }
- }
- } else if (animatedImage) {
- // iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273
- if (@available(iOS 15, tvOS 15, *)) {
- // User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef
- // CGImageRef newImageRef = CGImageCreateCopy(imageRef); // This one does not strip the CGImageProperty
- CGImageRef newImageRef = SDCGImageCreateCopy(imageRef);
- if (newImageRef) {
- CGImageRelease(imageRef);
- imageRef = newImageRef;
- }
- #if SD_CHECK_CGIMAGE_RETAIN_SOURCE
- // Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+)
- // If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- SDCGImageGetImageSource = dlsym(RTLD_DEFAULT, "CGImageGetImageSource");
- });
- if (SDCGImageGetImageSource) {
- NSCAssert(!SDCGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock");
- }
- #endif
- }
- }
-
- #if SD_UIKIT || SD_WATCH
- UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
- UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation];
- #else
- UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
- #endif
- CGImageRelease(imageRef);
- image.sd_isDecoded = isDecoded;
-
- return image;
- }
- #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);
- }
-
- CGSize thumbnailSize = CGSizeZero;
- NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
- if (thumbnailSizeValue != nil) {
- #if SD_MAC
- thumbnailSize = thumbnailSizeValue.sizeValue;
- #else
- thumbnailSize = thumbnailSizeValue.CGSizeValue;
- #endif
- }
-
- BOOL preserveAspectRatio = YES;
- NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
- if (preserveAspectRatioValue != nil) {
- preserveAspectRatio = preserveAspectRatioValue.boolValue;
- }
-
- BOOL lazyDecode = YES; // Defaults YES for static image coder
- NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
- if (lazyDecodeValue != nil) {
- lazyDecode = lazyDecodeValue.boolValue;
- }
-
- #if SD_MAC
- // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
- // Which decode frames in time and reduce memory usage
- if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
- SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
- if (imageRep) {
- NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
- imageRep.size = size;
- NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
- [animatedImage addRepresentation:imageRep];
- animatedImage.sd_imageFormat = self.class.imageFormat;
- return animatedImage;
- }
- }
- #endif
-
- NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint];
- if (!typeIdentifierHint) {
- // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension
- NSString *fileExtensionHint = options[SDImageCoderDecodeFileExtensionHint];
- if (fileExtensionHint) {
- typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage);
- // Ignore dynamic UTI
- if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) {
- typeIdentifierHint = nil;
- }
- }
- } else if ([typeIdentifierHint isEqual:NSNull.null]) {
- // Hack if user don't want to imply file extension
- typeIdentifierHint = nil;
- }
-
- NSDictionary *creatingOptions = nil;
- if (typeIdentifierHint) {
- creatingOptions = @{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : typeIdentifierHint};
- }
- CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)creatingOptions);
- if (!source) {
- // Try again without UTType hint, the call site from user may provide the wrong UTType
- source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
- }
- if (!source) {
- return nil;
- }
-
- size_t count = CGImageSourceGetCount(source);
- UIImage *animatedImage;
-
- BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
- if (decodeFirstFrame || count <= 1) {
- animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
- } else {
- NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:count];
-
- for (size_t i = 0; i < count; i++) {
- UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
- if (!image) {
- continue;
- }
-
- NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
-
- 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;
- }
- #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});
- _incremental = YES;
- CGFloat scale = 1;
- NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
- if (scaleFactor != nil) {
- scale = MAX([scaleFactor doubleValue], 1);
- }
- _scale = scale;
- CGSize thumbnailSize = CGSizeZero;
- NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
- if (thumbnailSizeValue != nil) {
- #if SD_MAC
- thumbnailSize = thumbnailSizeValue.sizeValue;
- #else
- thumbnailSize = thumbnailSizeValue.CGSizeValue;
- #endif
- }
- _thumbnailSize = thumbnailSize;
- BOOL preserveAspectRatio = YES;
- NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
- if (preserveAspectRatioValue != nil) {
- preserveAspectRatio = preserveAspectRatioValue.boolValue;
- }
- _preserveAspectRatio = preserveAspectRatio;
- BOOL lazyDecode = NO; // Defaults NO for animated image coder
- NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
- if (lazyDecodeValue != nil) {
- lazyDecode = lazyDecodeValue.boolValue;
- }
- _lazyDecode = lazyDecode;
- SD_LOCK_INIT(_lock);
- #if SD_UIKIT
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
- #endif
- }
- return self;
- }
- - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
- NSCParameterAssert(_incremental);
- 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);
- }
- }
-
- SD_LOCK(_lock);
- // For animated image progressive decoding because the frame count and duration may be changed.
- [self scanAndCheckFramesValidWithImageSource:_imageSource];
- SD_UNLOCK(_lock);
- }
- - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
- NSCParameterAssert(_incremental);
- UIImage *image;
-
- if (_width + _height > 0) {
- // Create the image
- CGFloat scale = _scale;
- NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
- if (scaleFactor != nil) {
- scale = MAX([scaleFactor doubleValue], 1);
- }
- image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
- if (image) {
- 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;
- }
-
- NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
- if (!frames || frames.count == 0) {
- SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0];
- frames = @[frame];
- }
- return [self encodedDataWithFrames:frames loopCount:image.sd_imageLoopCount format:format options:options];
- }
- - (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
- UIImage *image = frames.firstObject.image; // Primary image
- if (!image) {
- return nil;
- }
- CGImageRef imageRef = image.CGImage;
- if (!imageRef) {
- // Earily return, supports CGImage only
- return nil;
- }
-
- NSMutableData *imageData = [NSMutableData data];
- CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
-
- // 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, imageUTType, frames.count ?: 1, NULL);
- if (!imageDestination) {
- // Handle failure.
- return nil;
- }
- NSMutableDictionary *properties = [NSMutableDictionary dictionary];
- #if SD_UIKIT || SD_WATCH
- CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
- #else
- CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
- #endif
- properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation);
- // Encoding Options
- double compressionQuality = 1;
- if (options[SDImageCoderEncodeCompressionQuality]) {
- compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
- }
- properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);
- CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor];
- if (backgroundColor) {
- properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor);
- }
- CGSize maxPixelSize = CGSizeZero;
- NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize];
- if (maxPixelSizeValue != nil) {
- #if SD_MAC
- maxPixelSize = maxPixelSizeValue.sizeValue;
- #else
- maxPixelSize = maxPixelSizeValue.CGSizeValue;
- #endif
- }
- CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef);
- CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef);
- CGFloat finalPixelSize = 0;
- BOOL encodeFullImage = maxPixelSize.width == 0 || maxPixelSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= maxPixelSize.width && pixelHeight <= maxPixelSize.height);
- if (!encodeFullImage) {
- // Thumbnail Encoding
- CGFloat pixelRatio = pixelWidth / pixelHeight;
- CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
- if (pixelRatio > maxPixelSizeRatio) {
- finalPixelSize = MAX(maxPixelSize.width, maxPixelSize.width / pixelRatio);
- } else {
- finalPixelSize = MAX(maxPixelSize.height, maxPixelSize.height * pixelRatio);
- }
- properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
- }
- NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue];
- if (maxFileSize > 0) {
- properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize);
- // Remove the quality if we have file size limit
- properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil;
- }
- BOOL embedThumbnail = NO;
- if (options[SDImageCoderEncodeEmbedThumbnail]) {
- embedThumbnail = [options[SDImageCoderEncodeEmbedThumbnail] boolValue];
- }
- properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
-
- BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
- if (encodeFirstFrame || frames.count <= 1) {
- // for static single images
- CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
- } else {
- // for animated images
- NSDictionary *containerProperties = @{
- self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)}
- };
- // container level properties (applies for `CGImageDestinationSetProperties`, not individual frames)
- CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)containerProperties);
-
- for (size_t i = 0; i < frames.count; i++) {
- SDImageFrame *frame = frames[i];
- NSTimeInterval frameDuration = frame.duration;
- CGImageRef frameImageRef = frame.image.CGImage;
- properties[self.class.dictionaryProperty] = @{self.class.delayTimeProperty : @(frameDuration)};
- CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)properties);
- }
- }
- // 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;
- CGSize thumbnailSize = CGSizeZero;
- NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
- if (thumbnailSizeValue != nil) {
- #if SD_MAC
- thumbnailSize = thumbnailSizeValue.sizeValue;
- #else
- thumbnailSize = thumbnailSizeValue.CGSizeValue;
- #endif
- }
- _thumbnailSize = thumbnailSize;
- BOOL preserveAspectRatio = YES;
- NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
- if (preserveAspectRatioValue != nil) {
- preserveAspectRatio = preserveAspectRatioValue.boolValue;
- }
- _preserveAspectRatio = preserveAspectRatio;
- BOOL lazyDecode = NO; // Defaults NO for animated image coder
- NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
- if (lazyDecodeValue != nil) {
- lazyDecode = lazyDecodeValue.boolValue;
- }
- _lazyDecode = lazyDecode;
- _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];
- _loopCount = loopCount;
-
- NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray arrayWithCapacity:frameCount];
- 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];
- }
- if (frames.count != frameCount) {
- // frames not match, do not override current value
- return NO;
- }
-
- _frameCount = frameCount;
- _frames = [frames copy];
-
- return YES;
- }
- - (NSData *)animatedImageData {
- return _imageData;
- }
- - (NSUInteger)animatedImageLoopCount {
- return _loopCount;
- }
- - (NSUInteger)animatedImageFrameCount {
- return _frameCount;
- }
- - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
- NSTimeInterval duration;
- // Incremental Animation decoding may update frames when new bytes available
- // Which should use lock to ensure frame count and frames match, ensure atomic logic
- if (_incremental) {
- SD_LOCK(_lock);
- if (index >= _frames.count) {
- SD_UNLOCK(_lock);
- return 0;
- }
- duration = _frames[index].duration;
- SD_UNLOCK(_lock);
- } else {
- if (index >= _frames.count) {
- return 0;
- }
- duration = _frames[index].duration;
- }
- return duration;
- }
- - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
- UIImage *image;
- // Incremental Animation decoding may update frames when new bytes available
- // Which should use lock to ensure frame count and frames match, ensure atomic logic
- if (_incremental) {
- SD_LOCK(_lock);
- if (index >= _frames.count) {
- SD_UNLOCK(_lock);
- return nil;
- }
- image = [self safeAnimatedImageFrameAtIndex:index];
- SD_UNLOCK(_lock);
- } else {
- if (index >= _frames.count) {
- return nil;
- }
- image = [self safeAnimatedImageFrameAtIndex:index];
- }
- return image;
- }
- - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
- UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:YES];
- if (!image) {
- return nil;
- }
- image.sd_imageFormat = self.class.imageFormat;
- return image;
- }
- @end
|