SDImageIOAnimatedCoder.m 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. /*
  2. * This file is part of the SDWebImage package.
  3. * (c) Olivier Poitrey <rs@dailymotion.com>
  4. *
  5. * For the full copyright and license information, please view the LICENSE
  6. * file that was distributed with this source code.
  7. */
  8. #import "SDImageIOAnimatedCoder.h"
  9. #import "NSImage+Compatibility.h"
  10. #import "UIImage+Metadata.h"
  11. #import "NSData+ImageContentType.h"
  12. #import "SDImageCoderHelper.h"
  13. #import "SDAnimatedImageRep.h"
  14. #import "UIImage+ForceDecode.h"
  15. // Specify DPI for vector format in CGImageSource, like PDF
  16. static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizationDPI";
  17. // Specify File Size for lossy format encoding, like JPEG
  18. static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
  19. @interface SDImageIOCoderFrame : NSObject
  20. @property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
  21. @property (nonatomic, assign) NSTimeInterval duration; // Frame duration in seconds
  22. @end
  23. @implementation SDImageIOCoderFrame
  24. @end
  25. @implementation SDImageIOAnimatedCoder {
  26. size_t _width, _height;
  27. CGImageSourceRef _imageSource;
  28. NSData *_imageData;
  29. CGFloat _scale;
  30. NSUInteger _loopCount;
  31. NSUInteger _frameCount;
  32. NSArray<SDImageIOCoderFrame *> *_frames;
  33. BOOL _finished;
  34. BOOL _preserveAspectRatio;
  35. CGSize _thumbnailSize;
  36. }
  37. - (void)dealloc
  38. {
  39. if (_imageSource) {
  40. CFRelease(_imageSource);
  41. _imageSource = NULL;
  42. }
  43. #if SD_UIKIT
  44. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  45. #endif
  46. }
  47. - (void)didReceiveMemoryWarning:(NSNotification *)notification
  48. {
  49. if (_imageSource) {
  50. for (size_t i = 0; i < _frameCount; i++) {
  51. CGImageSourceRemoveCacheAtIndex(_imageSource, i);
  52. }
  53. }
  54. }
  55. #pragma mark - Subclass Override
  56. + (SDImageFormat)imageFormat {
  57. @throw [NSException exceptionWithName:NSInternalInconsistencyException
  58. reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
  59. userInfo:nil];
  60. }
  61. + (NSString *)imageUTType {
  62. @throw [NSException exceptionWithName:NSInternalInconsistencyException
  63. reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
  64. userInfo:nil];
  65. }
  66. + (NSString *)dictionaryProperty {
  67. @throw [NSException exceptionWithName:NSInternalInconsistencyException
  68. reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
  69. userInfo:nil];
  70. }
  71. + (NSString *)unclampedDelayTimeProperty {
  72. @throw [NSException exceptionWithName:NSInternalInconsistencyException
  73. reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
  74. userInfo:nil];
  75. }
  76. + (NSString *)delayTimeProperty {
  77. @throw [NSException exceptionWithName:NSInternalInconsistencyException
  78. reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
  79. userInfo:nil];
  80. }
  81. + (NSString *)loopCountProperty {
  82. @throw [NSException exceptionWithName:NSInternalInconsistencyException
  83. reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
  84. userInfo:nil];
  85. }
  86. + (NSUInteger)defaultLoopCount {
  87. @throw [NSException exceptionWithName:NSInternalInconsistencyException
  88. reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
  89. userInfo:nil];
  90. }
  91. #pragma mark - Utils
  92. + (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source {
  93. NSUInteger loopCount = self.defaultLoopCount;
  94. NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil);
  95. NSDictionary *containerProperties = imageProperties[self.dictionaryProperty];
  96. if (containerProperties) {
  97. NSNumber *containerLoopCount = containerProperties[self.loopCountProperty];
  98. if (containerLoopCount != nil) {
  99. loopCount = containerLoopCount.unsignedIntegerValue;
  100. }
  101. }
  102. return loopCount;
  103. }
  104. + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
  105. NSDictionary *options = @{
  106. (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
  107. (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
  108. };
  109. NSTimeInterval frameDuration = 0.1;
  110. CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
  111. if (!cfFrameProperties) {
  112. return frameDuration;
  113. }
  114. NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
  115. NSDictionary *containerProperties = frameProperties[self.dictionaryProperty];
  116. NSNumber *delayTimeUnclampedProp = containerProperties[self.unclampedDelayTimeProperty];
  117. if (delayTimeUnclampedProp != nil) {
  118. frameDuration = [delayTimeUnclampedProp doubleValue];
  119. } else {
  120. NSNumber *delayTimeProp = containerProperties[self.delayTimeProperty];
  121. if (delayTimeProp != nil) {
  122. frameDuration = [delayTimeProp doubleValue];
  123. }
  124. }
  125. // Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
  126. // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify
  127. // a duration of <= 10 ms. See <rdar://problem/7689300> and <http://webkit.org/b/36082>
  128. // for more information.
  129. if (frameDuration < 0.011) {
  130. frameDuration = 0.1;
  131. }
  132. CFRelease(cfFrameProperties);
  133. return frameDuration;
  134. }
  135. + (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options {
  136. // Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :)
  137. // Parse the image properties
  138. NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
  139. NSUInteger pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] unsignedIntegerValue];
  140. NSUInteger pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] unsignedIntegerValue];
  141. CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
  142. if (!exifOrientation) {
  143. exifOrientation = kCGImagePropertyOrientationUp;
  144. }
  145. CFStringRef uttype = CGImageSourceGetType(source);
  146. // Check vector format
  147. BOOL isVector = NO;
  148. if ([NSData sd_imageFormatFromUTType:uttype] == SDImageFormatPDF) {
  149. isVector = YES;
  150. }
  151. NSMutableDictionary *decodingOptions;
  152. if (options) {
  153. decodingOptions = [NSMutableDictionary dictionaryWithDictionary:options];
  154. } else {
  155. decodingOptions = [NSMutableDictionary dictionary];
  156. }
  157. CGImageRef imageRef;
  158. if (thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height)) {
  159. if (isVector) {
  160. if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
  161. // Provide the default pixel count for vector images, simply just use the screen size
  162. #if SD_WATCH
  163. thumbnailSize = WKInterfaceDevice.currentDevice.screenBounds.size;
  164. #elif SD_UIKIT
  165. thumbnailSize = UIScreen.mainScreen.bounds.size;
  166. #elif SD_MAC
  167. thumbnailSize = NSScreen.mainScreen.frame.size;
  168. #endif
  169. }
  170. CGFloat maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
  171. NSUInteger DPIPerPixel = 2;
  172. NSUInteger rasterizationDPI = maxPixelSize * DPIPerPixel;
  173. decodingOptions[kSDCGImageSourceRasterizationDPI] = @(rasterizationDPI);
  174. }
  175. imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)decodingOptions);
  176. } else {
  177. decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio);
  178. CGFloat maxPixelSize;
  179. if (preserveAspectRatio) {
  180. CGFloat pixelRatio = pixelWidth / pixelHeight;
  181. CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height;
  182. if (pixelRatio > thumbnailRatio) {
  183. maxPixelSize = thumbnailSize.width;
  184. } else {
  185. maxPixelSize = thumbnailSize.height;
  186. }
  187. } else {
  188. maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
  189. }
  190. decodingOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize);
  191. decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageIfAbsent] = @(YES);
  192. imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)decodingOptions);
  193. }
  194. if (!imageRef) {
  195. return nil;
  196. }
  197. if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
  198. if (preserveAspectRatio) {
  199. // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice
  200. exifOrientation = kCGImagePropertyOrientationUp;
  201. } else {
  202. // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size
  203. CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize];
  204. CGImageRelease(imageRef);
  205. imageRef = scaledImageRef;
  206. }
  207. }
  208. #if SD_UIKIT || SD_WATCH
  209. UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
  210. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation];
  211. #else
  212. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
  213. #endif
  214. CGImageRelease(imageRef);
  215. return image;
  216. }
  217. #pragma mark - Decode
  218. - (BOOL)canDecodeFromData:(nullable NSData *)data {
  219. return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat);
  220. }
  221. - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
  222. if (!data) {
  223. return nil;
  224. }
  225. CGFloat scale = 1;
  226. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  227. if (scaleFactor != nil) {
  228. scale = MAX([scaleFactor doubleValue], 1);
  229. }
  230. CGSize thumbnailSize = CGSizeZero;
  231. NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
  232. if (thumbnailSizeValue != nil) {
  233. #if SD_MAC
  234. thumbnailSize = thumbnailSizeValue.sizeValue;
  235. #else
  236. thumbnailSize = thumbnailSizeValue.CGSizeValue;
  237. #endif
  238. }
  239. BOOL preserveAspectRatio = YES;
  240. NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
  241. if (preserveAspectRatioValue != nil) {
  242. preserveAspectRatio = preserveAspectRatioValue.boolValue;
  243. }
  244. #if SD_MAC
  245. // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
  246. // Which decode frames in time and reduce memory usage
  247. if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
  248. SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
  249. NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
  250. imageRep.size = size;
  251. NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
  252. [animatedImage addRepresentation:imageRep];
  253. return animatedImage;
  254. }
  255. #endif
  256. CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
  257. if (!source) {
  258. return nil;
  259. }
  260. size_t count = CGImageSourceGetCount(source);
  261. UIImage *animatedImage;
  262. BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
  263. if (decodeFirstFrame || count <= 1) {
  264. animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
  265. } else {
  266. NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
  267. for (size_t i = 0; i < count; i++) {
  268. UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
  269. if (!image) {
  270. continue;
  271. }
  272. NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
  273. SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
  274. [frames addObject:frame];
  275. }
  276. NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
  277. animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
  278. animatedImage.sd_imageLoopCount = loopCount;
  279. }
  280. animatedImage.sd_imageFormat = self.class.imageFormat;
  281. CFRelease(source);
  282. return animatedImage;
  283. }
  284. #pragma mark - Progressive Decode
  285. - (BOOL)canIncrementalDecodeFromData:(NSData *)data {
  286. return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat);
  287. }
  288. - (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options {
  289. self = [super init];
  290. if (self) {
  291. NSString *imageUTType = self.class.imageUTType;
  292. _imageSource = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : imageUTType});
  293. CGFloat scale = 1;
  294. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  295. if (scaleFactor != nil) {
  296. scale = MAX([scaleFactor doubleValue], 1);
  297. }
  298. _scale = scale;
  299. CGSize thumbnailSize = CGSizeZero;
  300. NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
  301. if (thumbnailSizeValue != nil) {
  302. #if SD_MAC
  303. thumbnailSize = thumbnailSizeValue.sizeValue;
  304. #else
  305. thumbnailSize = thumbnailSizeValue.CGSizeValue;
  306. #endif
  307. }
  308. _thumbnailSize = thumbnailSize;
  309. BOOL preserveAspectRatio = YES;
  310. NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
  311. if (preserveAspectRatioValue != nil) {
  312. preserveAspectRatio = preserveAspectRatioValue.boolValue;
  313. }
  314. _preserveAspectRatio = preserveAspectRatio;
  315. #if SD_UIKIT
  316. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  317. #endif
  318. }
  319. return self;
  320. }
  321. - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
  322. if (_finished) {
  323. return;
  324. }
  325. _imageData = data;
  326. _finished = finished;
  327. // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
  328. // Thanks to the author @Nyx0uf
  329. // Update the data source, we must pass ALL the data, not just the new bytes
  330. CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
  331. if (_width + _height == 0) {
  332. NSDictionary *options = @{
  333. (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
  334. (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
  335. };
  336. CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, (__bridge CFDictionaryRef)options);
  337. if (properties) {
  338. CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
  339. if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
  340. val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
  341. if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
  342. CFRelease(properties);
  343. }
  344. }
  345. // For animated image progressive decoding because the frame count and duration may be changed.
  346. [self scanAndCheckFramesValidWithImageSource:_imageSource];
  347. }
  348. - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
  349. UIImage *image;
  350. if (_width + _height > 0) {
  351. // Create the image
  352. CGFloat scale = _scale;
  353. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  354. if (scaleFactor != nil) {
  355. scale = MAX([scaleFactor doubleValue], 1);
  356. }
  357. image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:nil];
  358. if (image) {
  359. image.sd_imageFormat = self.class.imageFormat;
  360. }
  361. }
  362. return image;
  363. }
  364. #pragma mark - Encode
  365. - (BOOL)canEncodeToFormat:(SDImageFormat)format {
  366. return (format == self.class.imageFormat);
  367. }
  368. - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options {
  369. if (!image) {
  370. return nil;
  371. }
  372. CGImageRef imageRef = image.CGImage;
  373. if (!imageRef) {
  374. // Earily return, supports CGImage only
  375. return nil;
  376. }
  377. if (format != self.class.imageFormat) {
  378. return nil;
  379. }
  380. NSMutableData *imageData = [NSMutableData data];
  381. CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
  382. NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
  383. // Create an image destination. Animated Image does not support EXIF image orientation TODO
  384. // The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
  385. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frames.count ?: 1, NULL);
  386. if (!imageDestination) {
  387. // Handle failure.
  388. return nil;
  389. }
  390. NSMutableDictionary *properties = [NSMutableDictionary dictionary];
  391. // Encoding Options
  392. double compressionQuality = 1;
  393. if (options[SDImageCoderEncodeCompressionQuality]) {
  394. compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
  395. }
  396. properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);
  397. CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor];
  398. if (backgroundColor) {
  399. properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor);
  400. }
  401. CGSize maxPixelSize = CGSizeZero;
  402. NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize];
  403. if (maxPixelSizeValue != nil) {
  404. #if SD_MAC
  405. maxPixelSize = maxPixelSizeValue.sizeValue;
  406. #else
  407. maxPixelSize = maxPixelSizeValue.CGSizeValue;
  408. #endif
  409. }
  410. NSUInteger pixelWidth = CGImageGetWidth(imageRef);
  411. NSUInteger pixelHeight = CGImageGetHeight(imageRef);
  412. CGFloat finalPixelSize = 0;
  413. if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > 0 && pixelHeight > 0) {
  414. CGFloat pixelRatio = pixelWidth / pixelHeight;
  415. CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
  416. if (pixelRatio > maxPixelSizeRatio) {
  417. finalPixelSize = maxPixelSize.width;
  418. } else {
  419. finalPixelSize = maxPixelSize.height;
  420. }
  421. properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
  422. }
  423. NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue];
  424. if (maxFileSize > 0) {
  425. properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize);
  426. // Remove the quality if we have file size limit
  427. properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil;
  428. }
  429. BOOL embedThumbnail = NO;
  430. if (options[SDImageCoderEncodeEmbedThumbnail]) {
  431. embedThumbnail = [options[SDImageCoderEncodeEmbedThumbnail] boolValue];
  432. }
  433. properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
  434. BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
  435. if (encodeFirstFrame || frames.count == 0) {
  436. // for static single images
  437. CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
  438. } else {
  439. // for animated images
  440. NSUInteger loopCount = image.sd_imageLoopCount;
  441. NSDictionary *containerProperties = @{
  442. self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)}
  443. };
  444. // container level properties (applies for `CGImageDestinationSetProperties`, not individual frames)
  445. CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)containerProperties);
  446. for (size_t i = 0; i < frames.count; i++) {
  447. SDImageFrame *frame = frames[i];
  448. NSTimeInterval frameDuration = frame.duration;
  449. CGImageRef frameImageRef = frame.image.CGImage;
  450. properties[self.class.dictionaryProperty] = @{self.class.delayTimeProperty : @(frameDuration)};
  451. CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)properties);
  452. }
  453. }
  454. // Finalize the destination.
  455. if (CGImageDestinationFinalize(imageDestination) == NO) {
  456. // Handle failure.
  457. imageData = nil;
  458. }
  459. CFRelease(imageDestination);
  460. return [imageData copy];
  461. }
  462. #pragma mark - SDAnimatedImageCoder
  463. - (nullable instancetype)initWithAnimatedImageData:(nullable NSData *)data options:(nullable SDImageCoderOptions *)options {
  464. if (!data) {
  465. return nil;
  466. }
  467. self = [super init];
  468. if (self) {
  469. CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
  470. if (!imageSource) {
  471. return nil;
  472. }
  473. BOOL framesValid = [self scanAndCheckFramesValidWithImageSource:imageSource];
  474. if (!framesValid) {
  475. CFRelease(imageSource);
  476. return nil;
  477. }
  478. CGFloat scale = 1;
  479. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  480. if (scaleFactor != nil) {
  481. scale = MAX([scaleFactor doubleValue], 1);
  482. }
  483. _scale = scale;
  484. CGSize thumbnailSize = CGSizeZero;
  485. NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
  486. if (thumbnailSizeValue != nil) {
  487. #if SD_MAC
  488. thumbnailSize = thumbnailSizeValue.sizeValue;
  489. #else
  490. thumbnailSize = thumbnailSizeValue.CGSizeValue;
  491. #endif
  492. }
  493. _thumbnailSize = thumbnailSize;
  494. BOOL preserveAspectRatio = YES;
  495. NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
  496. if (preserveAspectRatioValue != nil) {
  497. preserveAspectRatio = preserveAspectRatioValue.boolValue;
  498. }
  499. _preserveAspectRatio = preserveAspectRatio;
  500. _imageSource = imageSource;
  501. _imageData = data;
  502. #if SD_UIKIT
  503. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  504. #endif
  505. }
  506. return self;
  507. }
  508. - (BOOL)scanAndCheckFramesValidWithImageSource:(CGImageSourceRef)imageSource {
  509. if (!imageSource) {
  510. return NO;
  511. }
  512. NSUInteger frameCount = CGImageSourceGetCount(imageSource);
  513. NSUInteger loopCount = [self.class imageLoopCountWithSource:imageSource];
  514. NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray array];
  515. for (size_t i = 0; i < frameCount; i++) {
  516. SDImageIOCoderFrame *frame = [[SDImageIOCoderFrame alloc] init];
  517. frame.index = i;
  518. frame.duration = [self.class frameDurationAtIndex:i source:imageSource];
  519. [frames addObject:frame];
  520. }
  521. _frameCount = frameCount;
  522. _loopCount = loopCount;
  523. _frames = [frames copy];
  524. return YES;
  525. }
  526. - (NSData *)animatedImageData {
  527. return _imageData;
  528. }
  529. - (NSUInteger)animatedImageLoopCount {
  530. return _loopCount;
  531. }
  532. - (NSUInteger)animatedImageFrameCount {
  533. return _frameCount;
  534. }
  535. - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
  536. if (index >= _frameCount) {
  537. return 0;
  538. }
  539. return _frames[index].duration;
  540. }
  541. - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
  542. if (index >= _frameCount) {
  543. return nil;
  544. }
  545. // Animated Image should not use the CGContext solution to force decode. Prefers to use Image/IO built in method, which is safer and memory friendly, see https://github.com/SDWebImage/SDWebImage/issues/2961
  546. NSDictionary *options = @{
  547. (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
  548. (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
  549. };
  550. UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:options];
  551. if (!image) {
  552. return nil;
  553. }
  554. image.sd_imageFormat = self.class.imageFormat;
  555. image.sd_isDecoded = YES;;
  556. return image;
  557. }
  558. @end