SDImageIOCoder.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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 "SDImageIOCoder.h"
  9. #import "SDImageCoderHelper.h"
  10. #import "NSImage+Compatibility.h"
  11. #import "UIImage+Metadata.h"
  12. #import "SDImageGraphics.h"
  13. #import "SDImageIOAnimatedCoderInternal.h"
  14. #import <ImageIO/ImageIO.h>
  15. #import <CoreServices/CoreServices.h>
  16. // Specify File Size for lossy format encoding, like JPEG
  17. static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
  18. @implementation SDImageIOCoder {
  19. size_t _width, _height;
  20. CGImagePropertyOrientation _orientation;
  21. CGImageSourceRef _imageSource;
  22. CGFloat _scale;
  23. BOOL _finished;
  24. BOOL _preserveAspectRatio;
  25. CGSize _thumbnailSize;
  26. BOOL _lazyDecode;
  27. }
  28. - (void)dealloc {
  29. if (_imageSource) {
  30. CFRelease(_imageSource);
  31. _imageSource = NULL;
  32. }
  33. #if SD_UIKIT
  34. [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  35. #endif
  36. }
  37. - (void)didReceiveMemoryWarning:(NSNotification *)notification
  38. {
  39. if (_imageSource) {
  40. CGImageSourceRemoveCacheAtIndex(_imageSource, 0);
  41. }
  42. }
  43. + (instancetype)sharedCoder {
  44. static SDImageIOCoder *coder;
  45. static dispatch_once_t onceToken;
  46. dispatch_once(&onceToken, ^{
  47. coder = [[SDImageIOCoder alloc] init];
  48. });
  49. return coder;
  50. }
  51. #pragma mark - Bitmap PDF representation
  52. + (UIImage *)createBitmapPDFWithData:(nonnull NSData *)data pageNumber:(NSUInteger)pageNumber targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio {
  53. NSParameterAssert(data);
  54. UIImage *image;
  55. CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
  56. if (!provider) {
  57. return nil;
  58. }
  59. CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider);
  60. CGDataProviderRelease(provider);
  61. if (!document) {
  62. return nil;
  63. }
  64. // `CGPDFDocumentGetPage` page number is 1-indexed.
  65. CGPDFPageRef page = CGPDFDocumentGetPage(document, pageNumber + 1);
  66. if (!page) {
  67. CGPDFDocumentRelease(document);
  68. return nil;
  69. }
  70. CGPDFBox box = kCGPDFMediaBox;
  71. CGRect rect = CGPDFPageGetBoxRect(page, box);
  72. CGRect targetRect = rect;
  73. if (!CGSizeEqualToSize(targetSize, CGSizeZero)) {
  74. targetRect = CGRectMake(0, 0, targetSize.width, targetSize.height);
  75. }
  76. CGFloat xRatio = targetRect.size.width / rect.size.width;
  77. CGFloat yRatio = targetRect.size.height / rect.size.height;
  78. CGFloat xScale = preserveAspectRatio ? MIN(xRatio, yRatio) : xRatio;
  79. CGFloat yScale = preserveAspectRatio ? MIN(xRatio, yRatio) : yRatio;
  80. // `CGPDFPageGetDrawingTransform` will only scale down, but not scale up, so we need calculate the actual scale again
  81. CGRect drawRect = CGRectMake( 0, 0, targetRect.size.width / xScale, targetRect.size.height / yScale);
  82. CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScale, yScale);
  83. CGAffineTransform transform = CGPDFPageGetDrawingTransform(page, box, drawRect, 0, preserveAspectRatio);
  84. SDGraphicsBeginImageContextWithOptions(targetRect.size, NO, 0);
  85. CGContextRef context = SDGraphicsGetCurrentContext();
  86. #if SD_UIKIT || SD_WATCH
  87. // Core Graphics coordinate system use the bottom-left, UIKit use the flipped one
  88. CGContextTranslateCTM(context, 0, targetRect.size.height);
  89. CGContextScaleCTM(context, 1, -1);
  90. #endif
  91. CGContextConcatCTM(context, scaleTransform);
  92. CGContextConcatCTM(context, transform);
  93. CGContextDrawPDFPage(context, page);
  94. image = SDGraphicsGetImageFromCurrentImageContext();
  95. SDGraphicsEndImageContext();
  96. CGPDFDocumentRelease(document);
  97. return image;
  98. }
  99. #pragma mark - Decode
  100. - (BOOL)canDecodeFromData:(nullable NSData *)data {
  101. return YES;
  102. }
  103. - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
  104. if (!data) {
  105. return nil;
  106. }
  107. CGFloat scale = 1;
  108. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  109. if (scaleFactor != nil) {
  110. scale = MAX([scaleFactor doubleValue], 1) ;
  111. }
  112. CGSize thumbnailSize = CGSizeZero;
  113. NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
  114. if (thumbnailSizeValue != nil) {
  115. #if SD_MAC
  116. thumbnailSize = thumbnailSizeValue.sizeValue;
  117. #else
  118. thumbnailSize = thumbnailSizeValue.CGSizeValue;
  119. #endif
  120. }
  121. BOOL preserveAspectRatio = YES;
  122. NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
  123. if (preserveAspectRatioValue != nil) {
  124. preserveAspectRatio = preserveAspectRatioValue.boolValue;
  125. }
  126. // Check vector format
  127. if ([NSData sd_imageFormatForImageData:data] == SDImageFormatPDF) {
  128. // History before iOS 16, ImageIO can decode PDF with rasterization size, but can't ever :(
  129. // So, use CoreGraphics to decode PDF (copy code from SDWebImagePDFCoder, may do refactor in the future)
  130. UIImage *image;
  131. NSUInteger pageNumber = 0; // Still use first page, may added options is user want
  132. #if SD_MAC
  133. // If don't use thumbnail, prefers the built-in generation of vector image
  134. // macOS's `NSImage` supports PDF built-in rendering
  135. if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
  136. NSPDFImageRep *imageRep = [[NSPDFImageRep alloc] initWithData:data];
  137. if (imageRep) {
  138. imageRep.currentPage = pageNumber;
  139. image = [[NSImage alloc] initWithSize:imageRep.size];
  140. [image addRepresentation:imageRep];
  141. image.sd_imageFormat = SDImageFormatPDF;
  142. return image;
  143. }
  144. }
  145. #endif
  146. image = [self.class createBitmapPDFWithData:data pageNumber:pageNumber targetSize:thumbnailSize preserveAspectRatio:preserveAspectRatio];
  147. image.sd_imageFormat = SDImageFormatPDF;
  148. return image;
  149. }
  150. BOOL lazyDecode = YES; // Defaults YES for static image coder
  151. NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
  152. if (lazyDecodeValue != nil) {
  153. lazyDecode = lazyDecodeValue.boolValue;
  154. }
  155. NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint];
  156. if (!typeIdentifierHint) {
  157. // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension
  158. NSString *fileExtensionHint = options[SDImageCoderDecodeFileExtensionHint];
  159. if (fileExtensionHint) {
  160. typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage);
  161. // Ignore dynamic UTI
  162. if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) {
  163. typeIdentifierHint = nil;
  164. }
  165. }
  166. } else if ([typeIdentifierHint isEqual:NSNull.null]) {
  167. // Hack if user don't want to imply file extension
  168. typeIdentifierHint = nil;
  169. }
  170. NSDictionary *creatingOptions = nil;
  171. if (typeIdentifierHint) {
  172. creatingOptions = @{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : typeIdentifierHint};
  173. }
  174. CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)creatingOptions);
  175. if (!source) {
  176. // Try again without UTType hint, the call site from user may provide the wrong UTType
  177. source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
  178. }
  179. if (!source) {
  180. return nil;
  181. }
  182. CFStringRef uttype = CGImageSourceGetType(source);
  183. SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype];
  184. UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
  185. CFRelease(source);
  186. image.sd_imageFormat = imageFormat;
  187. return image;
  188. }
  189. #pragma mark - Progressive Decode
  190. - (BOOL)canIncrementalDecodeFromData:(NSData *)data {
  191. return [self canDecodeFromData:data];
  192. }
  193. - (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options {
  194. self = [super init];
  195. if (self) {
  196. _imageSource = CGImageSourceCreateIncremental(NULL);
  197. CGFloat scale = 1;
  198. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  199. if (scaleFactor != nil) {
  200. scale = MAX([scaleFactor doubleValue], 1);
  201. }
  202. _scale = scale;
  203. CGSize thumbnailSize = CGSizeZero;
  204. NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
  205. if (thumbnailSizeValue != nil) {
  206. #if SD_MAC
  207. thumbnailSize = thumbnailSizeValue.sizeValue;
  208. #else
  209. thumbnailSize = thumbnailSizeValue.CGSizeValue;
  210. #endif
  211. }
  212. _thumbnailSize = thumbnailSize;
  213. BOOL preserveAspectRatio = YES;
  214. NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
  215. if (preserveAspectRatioValue != nil) {
  216. preserveAspectRatio = preserveAspectRatioValue.boolValue;
  217. }
  218. _preserveAspectRatio = preserveAspectRatio;
  219. BOOL lazyDecode = YES; // Defaults YES for static image coder
  220. NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
  221. if (lazyDecodeValue != nil) {
  222. lazyDecode = lazyDecodeValue.boolValue;
  223. }
  224. _lazyDecode = lazyDecode;
  225. #if SD_UIKIT
  226. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
  227. #endif
  228. }
  229. return self;
  230. }
  231. - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
  232. if (_finished) {
  233. return;
  234. }
  235. _finished = finished;
  236. // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
  237. // Thanks to the author @Nyx0uf
  238. // Update the data source, we must pass ALL the data, not just the new bytes
  239. CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
  240. if (_width + _height == 0) {
  241. CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
  242. if (properties) {
  243. NSInteger orientationValue = 1;
  244. CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
  245. if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
  246. val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
  247. if (val) CFNumberGetValue(val, kCFNumberLongType, &_width);
  248. val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
  249. if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
  250. CFRelease(properties);
  251. // When we draw to Core Graphics, we lose orientation information,
  252. // which means the image below born of initWithCGIImage will be
  253. // oriented incorrectly sometimes. (Unlike the image born of initWithData
  254. // in didCompleteWithError.) So save it here and pass it on later.
  255. _orientation = (CGImagePropertyOrientation)orientationValue;
  256. }
  257. }
  258. }
  259. - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
  260. UIImage *image;
  261. if (_width + _height > 0) {
  262. // Create the image
  263. CGFloat scale = _scale;
  264. NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
  265. if (scaleFactor != nil) {
  266. scale = MAX([scaleFactor doubleValue], 1);
  267. }
  268. image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
  269. if (image) {
  270. CFStringRef uttype = CGImageSourceGetType(_imageSource);
  271. image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype];
  272. }
  273. }
  274. return image;
  275. }
  276. #pragma mark - Encode
  277. - (BOOL)canEncodeToFormat:(SDImageFormat)format {
  278. return YES;
  279. }
  280. - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options {
  281. if (!image) {
  282. return nil;
  283. }
  284. CGImageRef imageRef = image.CGImage;
  285. if (!imageRef) {
  286. // Earily return, supports CGImage only
  287. return nil;
  288. }
  289. if (format == SDImageFormatUndefined) {
  290. BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:imageRef];
  291. if (hasAlpha) {
  292. format = SDImageFormatPNG;
  293. } else {
  294. format = SDImageFormatJPEG;
  295. }
  296. }
  297. NSMutableData *imageData = [NSMutableData data];
  298. CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
  299. // Create an image destination.
  300. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL);
  301. if (!imageDestination) {
  302. // Handle failure.
  303. return nil;
  304. }
  305. NSMutableDictionary *properties = [NSMutableDictionary dictionary];
  306. #if SD_UIKIT || SD_WATCH
  307. CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
  308. #else
  309. CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
  310. #endif
  311. properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation);
  312. // Encoding Options
  313. double compressionQuality = 1;
  314. if (options[SDImageCoderEncodeCompressionQuality]) {
  315. compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue];
  316. }
  317. properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality);
  318. CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor];
  319. if (backgroundColor) {
  320. properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor);
  321. }
  322. CGSize maxPixelSize = CGSizeZero;
  323. NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize];
  324. if (maxPixelSizeValue != nil) {
  325. #if SD_MAC
  326. maxPixelSize = maxPixelSizeValue.sizeValue;
  327. #else
  328. maxPixelSize = maxPixelSizeValue.CGSizeValue;
  329. #endif
  330. }
  331. CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef);
  332. CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef);
  333. CGFloat finalPixelSize = 0;
  334. BOOL encodeFullImage = maxPixelSize.width == 0 || maxPixelSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= maxPixelSize.width && pixelHeight <= maxPixelSize.height);
  335. if (!encodeFullImage) {
  336. // Thumbnail Encoding
  337. CGFloat pixelRatio = pixelWidth / pixelHeight;
  338. CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
  339. if (pixelRatio > maxPixelSizeRatio) {
  340. finalPixelSize = MAX(maxPixelSize.width, maxPixelSize.width / pixelRatio);
  341. } else {
  342. finalPixelSize = MAX(maxPixelSize.height, maxPixelSize.height * pixelRatio);
  343. }
  344. properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
  345. }
  346. NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue];
  347. if (maxFileSize > 0) {
  348. properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize);
  349. // Remove the quality if we have file size limit
  350. properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil;
  351. }
  352. BOOL embedThumbnail = NO;
  353. if (options[SDImageCoderEncodeEmbedThumbnail]) {
  354. embedThumbnail = [options[SDImageCoderEncodeEmbedThumbnail] boolValue];
  355. }
  356. properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
  357. // Add your image to the destination.
  358. CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
  359. // Finalize the destination.
  360. if (CGImageDestinationFinalize(imageDestination) == NO) {
  361. // Handle failure.
  362. imageData = nil;
  363. }
  364. CFRelease(imageDestination);
  365. return [imageData copy];
  366. }
  367. @end