SDWebImageImageIOCoder.m 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  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 "SDWebImageImageIOCoder.h"
  9. #import "SDWebImageCoderHelper.h"
  10. #import "NSImage+WebCache.h"
  11. #import <ImageIO/ImageIO.h>
  12. #import "NSData+ImageContentType.h"
  13. #import "UIImage+MultiFormat.h"
  14. #if SD_UIKIT || SD_WATCH
  15. static const size_t kBytesPerPixel = 4;
  16. static const size_t kBitsPerComponent = 8;
  17. /*
  18. * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set
  19. * Suggested value for iPad1 and iPhone 3GS: 60.
  20. * Suggested value for iPad2 and iPhone 4: 120.
  21. * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
  22. */
  23. static const CGFloat kDestImageSizeMB = 60.0f;
  24. /*
  25. * Defines the maximum size in MB of a tile used to decode image when the flag `SDWebImageScaleDownLargeImages` is set
  26. * Suggested value for iPad1 and iPhone 3GS: 20.
  27. * Suggested value for iPad2 and iPhone 4: 40.
  28. * Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
  29. */
  30. static const CGFloat kSourceImageTileSizeMB = 20.0f;
  31. static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
  32. static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
  33. static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
  34. static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
  35. static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet.
  36. #endif
  37. @implementation SDWebImageImageIOCoder {
  38. size_t _width, _height;
  39. #if SD_UIKIT || SD_WATCH
  40. UIImageOrientation _orientation;
  41. #endif
  42. CGImageSourceRef _imageSource;
  43. }
  44. - (void)dealloc {
  45. if (_imageSource) {
  46. CFRelease(_imageSource);
  47. _imageSource = NULL;
  48. }
  49. }
  50. + (instancetype)sharedCoder {
  51. static SDWebImageImageIOCoder *coder;
  52. static dispatch_once_t onceToken;
  53. dispatch_once(&onceToken, ^{
  54. coder = [[SDWebImageImageIOCoder alloc] init];
  55. });
  56. return coder;
  57. }
  58. #pragma mark - Decode
  59. - (BOOL)canDecodeFromData:(nullable NSData *)data {
  60. switch ([NSData sd_imageFormatForImageData:data]) {
  61. case SDImageFormatWebP:
  62. // Do not support WebP decoding
  63. return NO;
  64. case SDImageFormatHEIC:
  65. // Check HEIC decoding compatibility
  66. return [[self class] canDecodeFromHEICFormat];
  67. case SDImageFormatHEIF:
  68. // Check HEIF decoding compatibility
  69. return [[self class] canDecodeFromHEIFFormat];
  70. default:
  71. return YES;
  72. }
  73. }
  74. - (BOOL)canIncrementallyDecodeFromData:(NSData *)data {
  75. switch ([NSData sd_imageFormatForImageData:data]) {
  76. case SDImageFormatWebP:
  77. // Do not support WebP progressive decoding
  78. return NO;
  79. case SDImageFormatHEIC:
  80. // Check HEIC decoding compatibility
  81. return [[self class] canDecodeFromHEICFormat];
  82. case SDImageFormatHEIF:
  83. // Check HEIF decoding compatibility
  84. return [[self class] canDecodeFromHEIFFormat];
  85. default:
  86. return YES;
  87. }
  88. }
  89. - (UIImage *)decodedImageWithData:(NSData *)data {
  90. if (!data) {
  91. return nil;
  92. }
  93. UIImage *image = [[UIImage alloc] initWithData:data];
  94. image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
  95. return image;
  96. }
  97. - (UIImage *)incrementallyDecodedImageWithData:(NSData *)data finished:(BOOL)finished {
  98. if (!_imageSource) {
  99. _imageSource = CGImageSourceCreateIncremental(NULL);
  100. }
  101. UIImage *image;
  102. // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
  103. // Thanks to the author @Nyx0uf
  104. // Update the data source, we must pass ALL the data, not just the new bytes
  105. CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
  106. if (_width + _height == 0) {
  107. CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
  108. if (properties) {
  109. NSInteger orientationValue = 1;
  110. CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
  111. if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
  112. val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
  113. if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
  114. val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
  115. if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
  116. CFRelease(properties);
  117. // When we draw to Core Graphics, we lose orientation information,
  118. // which means the image below born of initWithCGIImage will be
  119. // oriented incorrectly sometimes. (Unlike the image born of initWithData
  120. // in didCompleteWithError.) So save it here and pass it on later.
  121. #if SD_UIKIT || SD_WATCH
  122. _orientation = [SDWebImageCoderHelper imageOrientationFromEXIFOrientation:orientationValue];
  123. #endif
  124. }
  125. }
  126. if (_width + _height > 0) {
  127. // Create the image
  128. CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(_imageSource, 0, NULL);
  129. if (partialImageRef) {
  130. #if SD_UIKIT || SD_WATCH
  131. image = [[UIImage alloc] initWithCGImage:partialImageRef scale:1 orientation:_orientation];
  132. #elif SD_MAC
  133. image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
  134. #endif
  135. CGImageRelease(partialImageRef);
  136. image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
  137. }
  138. }
  139. if (finished) {
  140. if (_imageSource) {
  141. CFRelease(_imageSource);
  142. _imageSource = NULL;
  143. }
  144. }
  145. return image;
  146. }
  147. - (UIImage *)decompressedImageWithImage:(UIImage *)image
  148. data:(NSData *__autoreleasing _Nullable *)data
  149. options:(nullable NSDictionary<NSString*, NSObject*>*)optionsDict {
  150. #if SD_MAC
  151. return image;
  152. #endif
  153. #if SD_UIKIT || SD_WATCH
  154. BOOL shouldScaleDown = NO;
  155. if (optionsDict != nil) {
  156. NSNumber *scaleDownLargeImagesOption = nil;
  157. if ([optionsDict[SDWebImageCoderScaleDownLargeImagesKey] isKindOfClass:[NSNumber class]]) {
  158. scaleDownLargeImagesOption = (NSNumber *)optionsDict[SDWebImageCoderScaleDownLargeImagesKey];
  159. }
  160. if (scaleDownLargeImagesOption != nil) {
  161. shouldScaleDown = [scaleDownLargeImagesOption boolValue];
  162. }
  163. }
  164. if (!shouldScaleDown) {
  165. return [self sd_decompressedImageWithImage:image];
  166. } else {
  167. UIImage *scaledDownImage = [self sd_decompressedAndScaledDownImageWithImage:image];
  168. if (scaledDownImage && !CGSizeEqualToSize(scaledDownImage.size, image.size)) {
  169. // if the image is scaled down, need to modify the data pointer as well
  170. SDImageFormat format = [NSData sd_imageFormatForImageData:*data];
  171. NSData *imageData = [self encodedDataWithImage:scaledDownImage format:format];
  172. if (imageData) {
  173. *data = imageData;
  174. }
  175. }
  176. return scaledDownImage;
  177. }
  178. #endif
  179. }
  180. #if SD_UIKIT || SD_WATCH
  181. - (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
  182. if (![[self class] shouldDecodeImage:image]) {
  183. return image;
  184. }
  185. // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
  186. // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
  187. @autoreleasepool{
  188. CGImageRef imageRef = image.CGImage;
  189. // device color space
  190. CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
  191. BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
  192. // iOS display alpha info (BRGA8888/BGRX8888)
  193. CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
  194. bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
  195. size_t width = CGImageGetWidth(imageRef);
  196. size_t height = CGImageGetHeight(imageRef);
  197. // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
  198. // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
  199. // to create bitmap graphics contexts without alpha info.
  200. CGContextRef context = CGBitmapContextCreate(NULL,
  201. width,
  202. height,
  203. kBitsPerComponent,
  204. 0,
  205. colorspaceRef,
  206. bitmapInfo);
  207. if (context == NULL) {
  208. return image;
  209. }
  210. // Draw the image into the context and retrieve the new bitmap image without alpha
  211. CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
  212. CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
  213. UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
  214. CGContextRelease(context);
  215. CGImageRelease(imageRefWithoutAlpha);
  216. return imageWithoutAlpha;
  217. }
  218. }
  219. - (nullable UIImage *)sd_decompressedAndScaledDownImageWithImage:(nullable UIImage *)image {
  220. if (![[self class] shouldDecodeImage:image]) {
  221. return image;
  222. }
  223. if (![[self class] shouldScaleDownImage:image]) {
  224. return [self sd_decompressedImageWithImage:image];
  225. }
  226. CGContextRef destContext;
  227. // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
  228. // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
  229. @autoreleasepool {
  230. CGImageRef sourceImageRef = image.CGImage;
  231. CGSize sourceResolution = CGSizeZero;
  232. sourceResolution.width = CGImageGetWidth(sourceImageRef);
  233. sourceResolution.height = CGImageGetHeight(sourceImageRef);
  234. CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
  235. // Determine the scale ratio to apply to the input image
  236. // that results in an output image of the defined size.
  237. // see kDestImageSizeMB, and how it relates to destTotalPixels.
  238. CGFloat imageScale = sqrt(kDestTotalPixels / sourceTotalPixels);
  239. CGSize destResolution = CGSizeZero;
  240. destResolution.width = (int)(sourceResolution.width * imageScale);
  241. destResolution.height = (int)(sourceResolution.height * imageScale);
  242. // device color space
  243. CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
  244. BOOL hasAlpha = SDCGImageRefContainsAlpha(sourceImageRef);
  245. // iOS display alpha info (BGRA8888/BGRX8888)
  246. CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
  247. bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
  248. // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
  249. // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
  250. // to create bitmap graphics contexts without alpha info.
  251. destContext = CGBitmapContextCreate(NULL,
  252. destResolution.width,
  253. destResolution.height,
  254. kBitsPerComponent,
  255. 0,
  256. colorspaceRef,
  257. bitmapInfo);
  258. if (destContext == NULL) {
  259. return image;
  260. }
  261. CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
  262. // Now define the size of the rectangle to be used for the
  263. // incremental blits from the input image to the output image.
  264. // we use a source tile width equal to the width of the source
  265. // image due to the way that iOS retrieves image data from disk.
  266. // iOS must decode an image from disk in full width 'bands', even
  267. // if current graphics context is clipped to a subrect within that
  268. // band. Therefore we fully utilize all of the pixel data that results
  269. // from a decoding opertion by achnoring our tile size to the full
  270. // width of the input image.
  271. CGRect sourceTile = CGRectZero;
  272. sourceTile.size.width = sourceResolution.width;
  273. // The source tile height is dynamic. Since we specified the size
  274. // of the source tile in MB, see how many rows of pixels high it
  275. // can be given the input image width.
  276. sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
  277. sourceTile.origin.x = 0.0f;
  278. // The output tile is the same proportions as the input tile, but
  279. // scaled to image scale.
  280. CGRect destTile;
  281. destTile.size.width = destResolution.width;
  282. destTile.size.height = sourceTile.size.height * imageScale;
  283. destTile.origin.x = 0.0f;
  284. // The source seem overlap is proportionate to the destination seem overlap.
  285. // this is the amount of pixels to overlap each tile as we assemble the ouput image.
  286. float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
  287. CGImageRef sourceTileImageRef;
  288. // calculate the number of read/write operations required to assemble the
  289. // output image.
  290. int iterations = (int)( sourceResolution.height / sourceTile.size.height );
  291. // If tile height doesn't divide the image height evenly, add another iteration
  292. // to account for the remaining pixels.
  293. int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
  294. if(remainder) {
  295. iterations++;
  296. }
  297. // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
  298. float sourceTileHeightMinusOverlap = sourceTile.size.height;
  299. sourceTile.size.height += sourceSeemOverlap;
  300. destTile.size.height += kDestSeemOverlap;
  301. for( int y = 0; y < iterations; ++y ) {
  302. @autoreleasepool {
  303. sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
  304. destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
  305. sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
  306. if( y == iterations - 1 && remainder ) {
  307. float dify = destTile.size.height;
  308. destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
  309. dify -= destTile.size.height;
  310. destTile.origin.y += dify;
  311. }
  312. CGContextDrawImage( destContext, destTile, sourceTileImageRef );
  313. CGImageRelease( sourceTileImageRef );
  314. }
  315. }
  316. CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
  317. CGContextRelease(destContext);
  318. if (destImageRef == NULL) {
  319. return image;
  320. }
  321. UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
  322. CGImageRelease(destImageRef);
  323. if (destImage == nil) {
  324. return image;
  325. }
  326. return destImage;
  327. }
  328. }
  329. #endif
  330. #pragma mark - Encode
  331. - (BOOL)canEncodeToFormat:(SDImageFormat)format {
  332. switch (format) {
  333. case SDImageFormatWebP:
  334. // Do not support WebP encoding
  335. return NO;
  336. case SDImageFormatHEIC:
  337. // Check HEIC encoding compatibility
  338. return [[self class] canEncodeToHEICFormat];
  339. case SDImageFormatHEIF:
  340. // Check HEIF encoding compatibility
  341. return [[self class] canEncodeToHEIFFormat];
  342. default:
  343. return YES;
  344. }
  345. }
  346. - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format {
  347. if (!image) {
  348. return nil;
  349. }
  350. if (format == SDImageFormatUndefined) {
  351. BOOL hasAlpha = SDCGImageRefContainsAlpha(image.CGImage);
  352. if (hasAlpha) {
  353. format = SDImageFormatPNG;
  354. } else {
  355. format = SDImageFormatJPEG;
  356. }
  357. }
  358. NSMutableData *imageData = [NSMutableData data];
  359. CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:format];
  360. // Create an image destination.
  361. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
  362. if (!imageDestination) {
  363. // Handle failure.
  364. return nil;
  365. }
  366. NSMutableDictionary *properties = [NSMutableDictionary dictionary];
  367. #if SD_UIKIT || SD_WATCH
  368. NSInteger exifOrientation = [SDWebImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
  369. [properties setValue:@(exifOrientation) forKey:(__bridge NSString *)kCGImagePropertyOrientation];
  370. #endif
  371. // Add your image to the destination.
  372. CGImageDestinationAddImage(imageDestination, image.CGImage, (__bridge CFDictionaryRef)properties);
  373. // Finalize the destination.
  374. if (CGImageDestinationFinalize(imageDestination) == NO) {
  375. // Handle failure.
  376. imageData = nil;
  377. }
  378. CFRelease(imageDestination);
  379. return [imageData copy];
  380. }
  381. #pragma mark - Helper
  382. + (BOOL)shouldDecodeImage:(nullable UIImage *)image {
  383. // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
  384. if (image == nil) {
  385. return NO;
  386. }
  387. // do not decode animated images
  388. if (image.images != nil) {
  389. return NO;
  390. }
  391. return YES;
  392. }
  393. + (BOOL)canDecodeFromHEICFormat {
  394. static BOOL canDecode = NO;
  395. static dispatch_once_t onceToken;
  396. dispatch_once(&onceToken, ^{
  397. CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIC];
  398. NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageSourceCopyTypeIdentifiers();
  399. if ([imageUTTypes containsObject:(__bridge NSString *)(imageUTType)]) {
  400. canDecode = YES;
  401. }
  402. });
  403. return canDecode;
  404. }
  405. + (BOOL)canDecodeFromHEIFFormat {
  406. static BOOL canDecode = NO;
  407. static dispatch_once_t onceToken;
  408. dispatch_once(&onceToken, ^{
  409. CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIF];
  410. NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageSourceCopyTypeIdentifiers();
  411. if ([imageUTTypes containsObject:(__bridge NSString *)(imageUTType)]) {
  412. canDecode = YES;
  413. }
  414. });
  415. return canDecode;
  416. }
  417. + (BOOL)canEncodeToHEICFormat {
  418. static BOOL canEncode = NO;
  419. static dispatch_once_t onceToken;
  420. dispatch_once(&onceToken, ^{
  421. NSMutableData *imageData = [NSMutableData data];
  422. CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIC];
  423. // Create an image destination.
  424. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
  425. if (!imageDestination) {
  426. // Can't encode to HEIC
  427. canEncode = NO;
  428. } else {
  429. // Can encode to HEIC
  430. CFRelease(imageDestination);
  431. canEncode = YES;
  432. }
  433. });
  434. return canEncode;
  435. }
  436. + (BOOL)canEncodeToHEIFFormat {
  437. static BOOL canEncode = NO;
  438. static dispatch_once_t onceToken;
  439. dispatch_once(&onceToken, ^{
  440. NSMutableData *imageData = [NSMutableData data];
  441. CFStringRef imageUTType = [NSData sd_UTTypeFromSDImageFormat:SDImageFormatHEIF];
  442. // Create an image destination.
  443. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
  444. if (!imageDestination) {
  445. // Can't encode to HEIF
  446. canEncode = NO;
  447. } else {
  448. // Can encode to HEIF
  449. CFRelease(imageDestination);
  450. canEncode = YES;
  451. }
  452. });
  453. return canEncode;
  454. }
  455. #if SD_UIKIT || SD_WATCH
  456. + (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
  457. BOOL shouldScaleDown = YES;
  458. CGImageRef sourceImageRef = image.CGImage;
  459. CGSize sourceResolution = CGSizeZero;
  460. sourceResolution.width = CGImageGetWidth(sourceImageRef);
  461. sourceResolution.height = CGImageGetHeight(sourceImageRef);
  462. float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
  463. float imageScale = kDestTotalPixels / sourceTotalPixels;
  464. if (imageScale < 1) {
  465. shouldScaleDown = YES;
  466. } else {
  467. shouldScaleDown = NO;
  468. }
  469. return shouldScaleDown;
  470. }
  471. #endif
  472. @end