SDImageIOAnimatedCoder.m 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  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. + (BOOL)canDecodeFromFormat:(SDImageFormat)format {
  93. static dispatch_once_t onceToken;
  94. static NSSet *imageUTTypeSet;
  95. dispatch_once(&onceToken, ^{
  96. NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageSourceCopyTypeIdentifiers();
  97. imageUTTypeSet = [NSSet setWithArray:imageUTTypes];
  98. });
  99. CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
  100. if ([imageUTTypeSet containsObject:(__bridge NSString *)(imageUTType)]) {
  101. // Can decode from target format
  102. return YES;
  103. }
  104. return NO;
  105. }
  106. + (BOOL)canEncodeToFormat:(SDImageFormat)format {
  107. static dispatch_once_t onceToken;
  108. static NSSet *imageUTTypeSet;
  109. dispatch_once(&onceToken, ^{
  110. NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageDestinationCopyTypeIdentifiers();
  111. imageUTTypeSet = [NSSet setWithArray:imageUTTypes];
  112. });
  113. CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
  114. if ([imageUTTypeSet containsObject:(__bridge NSString *)(imageUTType)]) {
  115. // Can encode to target format
  116. return YES;
  117. }
  118. return NO;
  119. }
  120. + (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source {
  121. NSUInteger loopCount = self.defaultLoopCount;
  122. NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, NULL);
  123. NSDictionary *containerProperties = imageProperties[self.dictionaryProperty];
  124. if (containerProperties) {
  125. NSNumber *containerLoopCount = containerProperties[self.loopCountProperty];
  126. if (containerLoopCount != nil) {
  127. loopCount = containerLoopCount.unsignedIntegerValue;
  128. }
  129. }
  130. return loopCount;
  131. }
  132. + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
  133. NSDictionary *options = @{
  134. (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
  135. (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
  136. };
  137. NSTimeInterval frameDuration = 0.1;
  138. CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
  139. if (!cfFrameProperties) {
  140. return frameDuration;
  141. }
  142. NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties;
  143. NSDictionary *containerProperties = frameProperties[self.dictionaryProperty];
  144. NSNumber *delayTimeUnclampedProp = containerProperties[self.unclampedDelayTimeProperty];
  145. if (delayTimeUnclampedProp != nil) {
  146. frameDuration = [delayTimeUnclampedProp doubleValue];
  147. } else {
  148. NSNumber *delayTimeProp = containerProperties[self.delayTimeProperty];
  149. if (delayTimeProp != nil) {
  150. frameDuration = [delayTimeProp doubleValue];
  151. }
  152. }
  153. // Many annoying ads specify a 0 duration to make an image flash as quickly as possible.
  154. // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify
  155. // a duration of <= 10 ms. See <rdar://problem/7689300> and <http://webkit.org/b/36082>
  156. // for more information.
  157. if (frameDuration < 0.011) {
  158. frameDuration = 0.1;
  159. }
  160. CFRelease(cfFrameProperties);
  161. return frameDuration;
  162. }
  163. + (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options {
  164. // Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :)
  165. // Parse the image properties
  166. NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
  167. NSUInteger pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] unsignedIntegerValue];
  168. NSUInteger pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] unsignedIntegerValue];
  169. CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
  170. if (!exifOrientation) {
  171. exifOrientation = kCGImagePropertyOrientationUp;
  172. }
  173. CFStringRef uttype = CGImageSourceGetType(source);
  174. // Check vector format
  175. BOOL isVector = NO;
  176. if ([NSData sd_imageFormatFromUTType:uttype] == SDImageFormatPDF) {
  177. isVector = YES;
  178. }
  179. NSMutableDictionary *decodingOptions;
  180. if (options) {
  181. decodingOptions = [NSMutableDictionary dictionaryWithDictionary:options];
  182. } else {
  183. decodingOptions = [NSMutableDictionary dictionary];
  184. }
  185. CGImageRef imageRef;
  186. BOOL createFullImage = thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height);
  187. if (createFullImage) {
  188. if (isVector) {
  189. if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
  190. // Provide the default pixel count for vector images, simply just use the screen size
  191. #if SD_WATCH
  192. thumbnailSize = WKInterfaceDevice.currentDevice.screenBounds.size;
  193. #elif SD_UIKIT
  194. thumbnailSize = UIScreen.mainScreen.bounds.size;
  195. #elif SD_MAC
  196. thumbnailSize = NSScreen.mainScreen.frame.size;
  197. #endif
  198. }
  199. CGFloat maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
  200. NSUInteger DPIPerPixel = 2;
  201. NSUInteger rasterizationDPI = maxPixelSize * DPIPerPixel;
  202. decodingOptions[kSDCGImageSourceRasterizationDPI] = @(rasterizationDPI);
  203. }
  204. imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
  205. } else {
  206. decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio);
  207. CGFloat maxPixelSize;
  208. if (preserveAspectRatio) {
  209. CGFloat pixelRatio = pixelWidth / pixelHeight;
  210. CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height;
  211. if (pixelRatio > thumbnailRatio) {
  212. maxPixelSize = thumbnailSize.width;
  213. } else {
  214. maxPixelSize = thumbnailSize.height;
  215. }
  216. } else {
  217. maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
  218. }
  219. decodingOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize);
  220. decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageAlways] = @(YES);
  221. imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
  222. }
  223. if (!imageRef) {
  224. return nil;
  225. }
  226. // Thumbnail image post-process
  227. if (!createFullImage) {
  228. if (preserveAspectRatio) {
  229. // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice
  230. exifOrientation = kCGImagePropertyOrientationUp;
  231. } else {
  232. // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size
  233. CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize];
  234. CGImageRelease(imageRef);
  235. imageRef = scaledImageRef;
  236. }
  237. }
  238. #if SD_UIKIT || SD_WATCH
  239. UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
  240. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation];
  241. #else
  242. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
  243. #endif
  244. CGImageRelease(imageRef);
  245. return image;
  246. }
  247. #pragma mark - Decode
  248. - (BOOL)canDecodeFromData:(nullable NSData *)data {
  249. return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat);
  250. }
  251. - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
  252. if (!data) {
  253. return nil;
  254. }
  255. CGFloat scale = 1;
  256. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  257. if (scaleFactor != nil) {
  258. scale = MAX([scaleFactor doubleValue], 1);
  259. }
  260. CGSize thumbnailSize = CGSizeZero;
  261. NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
  262. if (thumbnailSizeValue != nil) {
  263. #if SD_MAC
  264. thumbnailSize = thumbnailSizeValue.sizeValue;
  265. #else
  266. thumbnailSize = thumbnailSizeValue.CGSizeValue;
  267. #endif
  268. }
  269. BOOL preserveAspectRatio = YES;
  270. NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
  271. if (preserveAspectRatioValue != nil) {
  272. preserveAspectRatio = preserveAspectRatioValue.boolValue;
  273. }
  274. #if SD_MAC
  275. // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
  276. // Which decode frames in time and reduce memory usage
  277. if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
  278. SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
  279. NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
  280. imageRep.size = size;
  281. NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
  282. [animatedImage addRepresentation:imageRep];
  283. return animatedImage;
  284. }
  285. #endif
  286. CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
  287. if (!source) {
  288. return nil;
  289. }
  290. size_t count = CGImageSourceGetCount(source);
  291. UIImage *animatedImage;
  292. BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
  293. if (decodeFirstFrame || count <= 1) {
  294. animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
  295. } else {
  296. NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
  297. for (size_t i = 0; i < count; i++) {
  298. UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
  299. if (!image) {
  300. continue;
  301. }
  302. NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source];
  303. SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration];
  304. [frames addObject:frame];
  305. }
  306. NSUInteger loopCount = [self.class imageLoopCountWithSource:source];
  307. animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
  308. animatedImage.sd_imageLoopCount = loopCount;
  309. }
  310. animatedImage.sd_imageFormat = self.class.imageFormat;
  311. CFRelease(source);
  312. return animatedImage;
  313. }
  314. #pragma mark - Progressive Decode
  315. - (BOOL)canIncrementalDecodeFromData:(NSData *)data {
  316. return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat);
  317. }
  318. - (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options {
  319. self = [super init];
  320. if (self) {
  321. NSString *imageUTType = self.class.imageUTType;
  322. _imageSource = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : imageUTType});
  323. CGFloat scale = 1;
  324. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  325. if (scaleFactor != nil) {
  326. scale = MAX([scaleFactor doubleValue], 1);
  327. }
  328. _scale = scale;
  329. CGSize thumbnailSize = CGSizeZero;
  330. NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
  331. if (thumbnailSizeValue != nil) {
  332. #if SD_MAC
  333. thumbnailSize = thumbnailSizeValue.sizeValue;
  334. #else
  335. thumbnailSize = thumbnailSizeValue.CGSizeValue;
  336. #endif
  337. }
  338. _thumbnailSize = thumbnailSize;
  339. BOOL preserveAspectRatio = YES;
  340. NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
  341. if (preserveAspectRatioValue != nil) {
  342. preserveAspectRatio = preserveAspectRatioValue.boolValue;
  343. }
  344. _preserveAspectRatio = preserveAspectRatio;
  345. #if SD_UIKIT
  346. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  347. #endif
  348. }
  349. return self;
  350. }
  351. - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
  352. if (_finished) {
  353. return;
  354. }
  355. _imageData = data;
  356. _finished = finished;
  357. // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
  358. // Thanks to the author @Nyx0uf
  359. // Update the data source, we must pass ALL the data, not just the new bytes
  360. CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
  361. if (_width + _height == 0) {
  362. NSDictionary *options = @{
  363. (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
  364. (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
  365. };
  366. CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, (__bridge CFDictionaryRef)options);
  367. if (properties) {
  368. CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
  369. if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
  370. val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
  371. if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
  372. CFRelease(properties);
  373. }
  374. }
  375. // For animated image progressive decoding because the frame count and duration may be changed.
  376. [self scanAndCheckFramesValidWithImageSource:_imageSource];
  377. }
  378. - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
  379. UIImage *image;
  380. if (_width + _height > 0) {
  381. // Create the image
  382. CGFloat scale = _scale;
  383. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  384. if (scaleFactor != nil) {
  385. scale = MAX([scaleFactor doubleValue], 1);
  386. }
  387. image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:nil];
  388. if (image) {
  389. image.sd_imageFormat = self.class.imageFormat;
  390. }
  391. }
  392. return image;
  393. }
  394. #pragma mark - Encode
  395. - (BOOL)canEncodeToFormat:(SDImageFormat)format {
  396. return (format == self.class.imageFormat);
  397. }
  398. - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options {
  399. if (!image) {
  400. return nil;
  401. }
  402. CGImageRef imageRef = image.CGImage;
  403. if (!imageRef) {
  404. // Earily return, supports CGImage only
  405. return nil;
  406. }
  407. if (format != self.class.imageFormat) {
  408. return nil;
  409. }
  410. NSMutableData *imageData = [NSMutableData data];
  411. CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
  412. NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
  413. // Create an image destination. Animated Image does not support EXIF image orientation TODO
  414. // The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
  415. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frames.count ?: 1, NULL);
  416. if (!imageDestination) {
  417. // Handle failure.
  418. return nil;
  419. }
  420. NSMutableDictionary *properties = [NSMutableDictionary dictionary];
  421. // Encoding Options
  422. double compressionQuality = 1;
  423. if (options[SDImageCoderEncodeCompressionQuality]) {
  424. compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
  425. }
  426. properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);
  427. CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor];
  428. if (backgroundColor) {
  429. properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor);
  430. }
  431. CGSize maxPixelSize = CGSizeZero;
  432. NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize];
  433. if (maxPixelSizeValue != nil) {
  434. #if SD_MAC
  435. maxPixelSize = maxPixelSizeValue.sizeValue;
  436. #else
  437. maxPixelSize = maxPixelSizeValue.CGSizeValue;
  438. #endif
  439. }
  440. NSUInteger pixelWidth = CGImageGetWidth(imageRef);
  441. NSUInteger pixelHeight = CGImageGetHeight(imageRef);
  442. CGFloat finalPixelSize = 0;
  443. if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > maxPixelSize.width && pixelHeight > maxPixelSize.height) {
  444. CGFloat pixelRatio = pixelWidth / pixelHeight;
  445. CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
  446. if (pixelRatio > maxPixelSizeRatio) {
  447. finalPixelSize = maxPixelSize.width;
  448. } else {
  449. finalPixelSize = maxPixelSize.height;
  450. }
  451. properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
  452. }
  453. NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue];
  454. if (maxFileSize > 0) {
  455. properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize);
  456. // Remove the quality if we have file size limit
  457. properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil;
  458. }
  459. BOOL embedThumbnail = NO;
  460. if (options[SDImageCoderEncodeEmbedThumbnail]) {
  461. embedThumbnail = [options[SDImageCoderEncodeEmbedThumbnail] boolValue];
  462. }
  463. properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
  464. BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
  465. if (encodeFirstFrame || frames.count == 0) {
  466. // for static single images
  467. CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
  468. } else {
  469. // for animated images
  470. NSUInteger loopCount = image.sd_imageLoopCount;
  471. NSDictionary *containerProperties = @{
  472. self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)}
  473. };
  474. // container level properties (applies for `CGImageDestinationSetProperties`, not individual frames)
  475. CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)containerProperties);
  476. for (size_t i = 0; i < frames.count; i++) {
  477. SDImageFrame *frame = frames[i];
  478. NSTimeInterval frameDuration = frame.duration;
  479. CGImageRef frameImageRef = frame.image.CGImage;
  480. properties[self.class.dictionaryProperty] = @{self.class.delayTimeProperty : @(frameDuration)};
  481. CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)properties);
  482. }
  483. }
  484. // Finalize the destination.
  485. if (CGImageDestinationFinalize(imageDestination) == NO) {
  486. // Handle failure.
  487. imageData = nil;
  488. }
  489. CFRelease(imageDestination);
  490. return [imageData copy];
  491. }
  492. #pragma mark - SDAnimatedImageCoder
  493. - (nullable instancetype)initWithAnimatedImageData:(nullable NSData *)data options:(nullable SDImageCoderOptions *)options {
  494. if (!data) {
  495. return nil;
  496. }
  497. self = [super init];
  498. if (self) {
  499. CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
  500. if (!imageSource) {
  501. return nil;
  502. }
  503. BOOL framesValid = [self scanAndCheckFramesValidWithImageSource:imageSource];
  504. if (!framesValid) {
  505. CFRelease(imageSource);
  506. return nil;
  507. }
  508. CGFloat scale = 1;
  509. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  510. if (scaleFactor != nil) {
  511. scale = MAX([scaleFactor doubleValue], 1);
  512. }
  513. _scale = scale;
  514. CGSize thumbnailSize = CGSizeZero;
  515. NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
  516. if (thumbnailSizeValue != nil) {
  517. #if SD_MAC
  518. thumbnailSize = thumbnailSizeValue.sizeValue;
  519. #else
  520. thumbnailSize = thumbnailSizeValue.CGSizeValue;
  521. #endif
  522. }
  523. _thumbnailSize = thumbnailSize;
  524. BOOL preserveAspectRatio = YES;
  525. NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
  526. if (preserveAspectRatioValue != nil) {
  527. preserveAspectRatio = preserveAspectRatioValue.boolValue;
  528. }
  529. _preserveAspectRatio = preserveAspectRatio;
  530. _imageSource = imageSource;
  531. _imageData = data;
  532. #if SD_UIKIT
  533. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  534. #endif
  535. }
  536. return self;
  537. }
  538. - (BOOL)scanAndCheckFramesValidWithImageSource:(CGImageSourceRef)imageSource {
  539. if (!imageSource) {
  540. return NO;
  541. }
  542. NSUInteger frameCount = CGImageSourceGetCount(imageSource);
  543. NSUInteger loopCount = [self.class imageLoopCountWithSource:imageSource];
  544. NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray array];
  545. for (size_t i = 0; i < frameCount; i++) {
  546. SDImageIOCoderFrame *frame = [[SDImageIOCoderFrame alloc] init];
  547. frame.index = i;
  548. frame.duration = [self.class frameDurationAtIndex:i source:imageSource];
  549. [frames addObject:frame];
  550. }
  551. _frameCount = frameCount;
  552. _loopCount = loopCount;
  553. _frames = [frames copy];
  554. return YES;
  555. }
  556. - (NSData *)animatedImageData {
  557. return _imageData;
  558. }
  559. - (NSUInteger)animatedImageLoopCount {
  560. return _loopCount;
  561. }
  562. - (NSUInteger)animatedImageFrameCount {
  563. return _frameCount;
  564. }
  565. - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
  566. if (index >= _frameCount) {
  567. return 0;
  568. }
  569. return _frames[index].duration;
  570. }
  571. - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
  572. if (index >= _frameCount) {
  573. return nil;
  574. }
  575. // 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
  576. NSDictionary *options = @{
  577. (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
  578. (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
  579. };
  580. UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:options];
  581. if (!image) {
  582. return nil;
  583. }
  584. image.sd_imageFormat = self.class.imageFormat;
  585. image.sd_isDecoded = YES;;
  586. return image;
  587. }
  588. @end