SDImageCoderTests.m 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  1. /*
  2. * This file is part of the SDWebImage package.
  3. * (c) Olivier Poitrey <rs@dailymotion.com>
  4. * (c) Matt Galloway
  5. *
  6. * For the full copyright and license information, please view the LICENSE
  7. * file that was distributed with this source code.
  8. */
  9. #import "SDTestCase.h"
  10. #import "UIColor+SDHexString.h"
  11. @interface SDWebImageDecoderTests : SDTestCase
  12. @end
  13. @implementation SDWebImageDecoderTests
  14. - (void)test01ThatDecodedImageWithNilImageReturnsNil {
  15. expect([UIImage sd_decodedImageWithImage:nil]).to.beNil();
  16. expect([UIImage sd_decodedAndScaledDownImageWithImage:nil]).to.beNil();
  17. }
  18. - (void)test02ThatDecodedImageWithImageWorksWithARegularJPGImage {
  19. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
  20. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  21. UIImage *decodedImage = [UIImage sd_decodedImageWithImage:image];
  22. expect(decodedImage).toNot.beNil();
  23. expect(decodedImage).toNot.equal(image);
  24. expect(decodedImage.size.width).to.equal(image.size.width);
  25. expect(decodedImage.size.height).to.equal(image.size.height);
  26. }
  27. - (void)test03ThatDecodedImageWithImageDoesNotDecodeAnimatedImages {
  28. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"gif"];
  29. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  30. #if SD_MAC
  31. UIImage *animatedImage = image;
  32. #else
  33. UIImage *animatedImage = [UIImage animatedImageWithImages:@[image] duration:0];
  34. #endif
  35. UIImage *decodedImage = [UIImage sd_decodedImageWithImage:animatedImage];
  36. expect(decodedImage).toNot.beNil();
  37. expect(decodedImage).to.equal(animatedImage);
  38. }
  39. - (void)test04ThatDecodedImageWithImageWorksWithAlphaImages {
  40. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"png"];
  41. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  42. UIImage *decodedImage = [UIImage sd_decodedImageWithImage:image];
  43. expect(decodedImage).toNot.beNil();
  44. expect(decodedImage).toNot.equal(image);
  45. }
  46. - (void)test05ThatDecodedImageWithImageWorksEvenWithMonochromeImage {
  47. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"MonochromeTestImage" ofType:@"jpg"];
  48. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  49. UIImage *decodedImage = [UIImage sd_decodedImageWithImage:image];
  50. expect(decodedImage).toNot.beNil();
  51. expect(decodedImage).toNot.equal(image);
  52. expect(decodedImage.size.width).to.equal(image.size.width);
  53. expect(decodedImage.size.height).to.equal(image.size.height);
  54. }
  55. - (void)test06ThatDecodeAndScaleDownImageWorks {
  56. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
  57. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  58. UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image limitBytes:(60 * 1024 * 1024)];
  59. expect(decodedImage).toNot.beNil();
  60. expect(decodedImage).toNot.equal(image);
  61. expect(decodedImage.size.width).toNot.equal(image.size.width);
  62. expect(decodedImage.size.height).toNot.equal(image.size.height);
  63. expect(decodedImage.size.width * decodedImage.size.height).to.beLessThanOrEqualTo(60 * 1024 * 1024 / 4); // how many pixels in 60 megs
  64. }
  65. - (void)test07ThatDecodeAndScaleDownImageDoesNotScaleSmallerImage {
  66. // check when user use the larget bytes than image pixels byets, we do not scale up the image (defaults 60MB means 3965x3965 pixels)
  67. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
  68. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  69. UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image];
  70. expect(decodedImage).toNot.beNil();
  71. expect(decodedImage).toNot.equal(image);
  72. expect(decodedImage.size.width).to.equal(image.size.width);
  73. expect(decodedImage.size.height).to.equal(image.size.height);
  74. }
  75. - (void)test07ThatDecodeAndScaleDownImageScaleSmallerBytes {
  76. // Check when user provide too small bytes, we scale it down to 1x1, but not return the force decoded original size image
  77. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
  78. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  79. UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image limitBytes:1];
  80. expect(decodedImage).toNot.beNil();
  81. expect(decodedImage).toNot.equal(image);
  82. expect(decodedImage.size.width).to.equal(1);
  83. expect(decodedImage.size.height).to.equal(1);
  84. }
  85. -(void)test07ThatDecodeAndScaleDownAlwaysCompleteRendering {
  86. // Check that when the height of the image used is not evenly divisible by the height of the tile, the output image can also be rendered completely.
  87. // Check that when the height of the image used will led to loss of precision. the output image can also be rendered completely,
  88. UIColor *imageColor = UIColor.blackColor;
  89. CGSize imageSize = CGSizeMake(1029, 1029);
  90. CGRect imageRect = CGRectMake(0, 0, imageSize.width, imageSize.height);
  91. SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init];
  92. format.scale = 1;
  93. SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
  94. UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
  95. CGContextSetFillColorWithColor(context, [imageColor CGColor]);
  96. CGContextFillRect(context, imageRect);
  97. }];
  98. UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image limitBytes:1 * 1024 * 1024];
  99. UIColor *testColor1 = [decodedImage sd_colorAtPoint:CGPointMake(0, decodedImage.size.height - 1)];
  100. UIColor *testColor2 = [decodedImage sd_colorAtPoint:CGPointMake(0, decodedImage.size.height - 9)];
  101. expect(testColor1.sd_hexString).equal(imageColor.sd_hexString);
  102. expect(testColor2.sd_hexString).equal(imageColor.sd_hexString);
  103. }
  104. - (void)test08ThatEncodeAlphaImageToJPGWithBackgroundColor {
  105. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"png"];
  106. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  107. UIColor *backgroundColor = [UIColor blackColor];
  108. NSData *encodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeBackgroundColor : backgroundColor}];
  109. expect(encodedData).notTo.beNil();
  110. UIImage *decodedImage = [SDImageCodersManager.sharedManager decodedImageWithData:encodedData options:nil];
  111. expect(decodedImage).notTo.beNil();
  112. expect(decodedImage.size.width).to.equal(image.size.width);
  113. expect(decodedImage.size.height).to.equal(image.size.height);
  114. // Check background color, should not be white but the black color
  115. UIColor *testColor = [decodedImage sd_colorAtPoint:CGPointMake(1, 1)];
  116. expect(testColor.sd_hexString).equal(backgroundColor.sd_hexString);
  117. }
  118. - (void)test09ThatJPGImageEncodeWithMaxFileSize {
  119. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
  120. UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
  121. // This large JPEG encoding size between (770KB ~ 2.23MB)
  122. NSUInteger limitFileSize = 1 * 1024 * 1024; // 1MB
  123. // 100 quality (biggest)
  124. NSData *maxEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:nil];
  125. expect(maxEncodedData).notTo.beNil();
  126. expect(maxEncodedData.length).beGreaterThan(limitFileSize);
  127. // 0 quality (smallest)
  128. NSData *minEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeCompressionQuality : @(0.01)}]; // Seems 0 has some bugs in old macOS
  129. expect(minEncodedData).notTo.beNil();
  130. expect(minEncodedData.length).beLessThan(limitFileSize);
  131. NSData *limitEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeMaxFileSize : @(limitFileSize)}];
  132. expect(limitEncodedData).notTo.beNil();
  133. // So, if we limit the file size, the output data should in (770KB ~ 2.23MB)
  134. expect(limitEncodedData.length).beLessThan(maxEncodedData.length);
  135. expect(limitEncodedData.length).beGreaterThan(minEncodedData.length);
  136. }
  137. - (void)test10ThatAnimatedImageCacheImmediatelyWorks {
  138. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"png"];
  139. NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
  140. // Check that animated image rendering should not use lazy decoding (performance related)
  141. CFAbsoluteTime begin = CFAbsoluteTimeGetCurrent();
  142. SDImageAPNGCoder *coder = [[SDImageAPNGCoder alloc] initWithAnimatedImageData:testImageData options:@{SDImageCoderDecodeFirstFrameOnly : @(NO)}];
  143. UIImage *imageWithoutLazyDecoding = [coder animatedImageFrameAtIndex:0];
  144. CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
  145. CFAbsoluteTime duration = end - begin;
  146. expect(imageWithoutLazyDecoding.sd_isDecoded).beTruthy();
  147. // Check that static image rendering should use lazy decoding
  148. CFAbsoluteTime begin2 = CFAbsoluteTimeGetCurrent();
  149. SDImageAPNGCoder *coder2 = SDImageAPNGCoder.sharedCoder;
  150. UIImage *imageWithLazyDecoding = [coder2 decodedImageWithData:testImageData options:@{SDImageCoderDecodeFirstFrameOnly : @(YES)}];
  151. CFAbsoluteTime end2 = CFAbsoluteTimeGetCurrent();
  152. CFAbsoluteTime duration2 = end2 - begin2;
  153. expect(imageWithLazyDecoding.sd_isDecoded).beFalsy();
  154. // lazy decoding need less time (10x)
  155. expect(duration2 * 10.0).beLessThan(duration);
  156. }
  157. - (void)test11ThatAPNGPCoderWorks {
  158. NSURL *APNGURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"apng"];
  159. [self verifyCoder:[SDImageAPNGCoder sharedCoder]
  160. withLocalImageURL:APNGURL
  161. supportsEncoding:YES
  162. isAnimatedImage:YES];
  163. }
  164. - (void)test12ThatGIFCoderWorks {
  165. NSURL *gifURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"gif"];
  166. [self verifyCoder:[SDImageGIFCoder sharedCoder]
  167. withLocalImageURL:gifURL
  168. supportsEncoding:YES
  169. isAnimatedImage:YES];
  170. }
  171. - (void)test12ThatGIFWithoutLoopCountPlayOnce {
  172. // When GIF metadata does not contains any loop count information (`kCGImagePropertyGIFLoopCount`'s value nil)
  173. // The standard says it should just play once. See: http://www6.uniovi.es/gifanim/gifabout.htm
  174. // This behavior is different from other modern animated image format like APNG/WebP. Which will play infinitely
  175. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestLoopCount" ofType:@"gif"];
  176. NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
  177. UIImage *image = [SDImageGIFCoder.sharedCoder decodedImageWithData:testImageData options:nil];
  178. expect(image.sd_imageLoopCount).equal(1);
  179. }
  180. - (void)test13ThatHEICWorks {
  181. if (@available(iOS 11, tvOS 11, macOS 10.13, *)) {
  182. NSURL *heicURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"heic"];
  183. #if SD_MAC
  184. BOOL supportsEncoding = !SDTestCase.isCI; // GitHub Action Mac env currently does not support HEIC encoding
  185. #else
  186. BOOL supportsEncoding = YES; // GitHub Action Mac env with simulator, supported from 20240707.1
  187. #endif
  188. [self verifyCoder:[SDImageIOCoder sharedCoder]
  189. withLocalImageURL:heicURL
  190. supportsEncoding:supportsEncoding
  191. isAnimatedImage:NO];
  192. }
  193. }
  194. - (void)test14ThatHEIFWorks {
  195. if (@available(iOS 11, tvOS 11, macOS 10.13, *)) {
  196. NSURL *heifURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"heif"];
  197. BOOL supportsEncoding = NO; // public.heif UTI alwsays return false, use public.heic
  198. [self verifyCoder:[SDImageIOCoder sharedCoder]
  199. withLocalImageURL:heifURL
  200. supportsEncoding:supportsEncoding
  201. isAnimatedImage:NO];
  202. }
  203. }
  204. - (void)test15ThatCodersManagerWorks {
  205. SDImageCodersManager *manager = [[SDImageCodersManager alloc] init];
  206. manager.coders = @[SDImageIOCoder.sharedCoder];
  207. expect([manager canDecodeFromData:nil]).beTruthy(); // Image/IO will return YES for future format
  208. expect([manager decodedImageWithData:nil options:nil]).beNil();
  209. expect([manager canEncodeToFormat:SDImageFormatUndefined]).beTruthy(); // Image/IO will return YES for future format
  210. expect([manager encodedDataWithImage:nil format:SDImageFormatUndefined options:nil]).beNil();
  211. }
  212. - (void)test16ThatHEICAnimatedWorks {
  213. if (@available(iOS 13, tvOS 13, macOS 10.15, *)) {
  214. NSURL *heicURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"heics"];
  215. BOOL supportsEncoding = !SDTestCase.isCI; // GitHub Action Mac env currently does not support HEICS animated encoding (but HEIC supported, I don't know why)
  216. // See: #3227
  217. BOOL isAnimatedImage = YES;
  218. [self verifyCoder:[SDImageHEICCoder sharedCoder]
  219. withLocalImageURL:heicURL
  220. supportsEncoding:supportsEncoding
  221. encodingFormat:SDImageFormatHEIC
  222. isAnimatedImage:isAnimatedImage
  223. isVectorImage:NO];
  224. }
  225. }
  226. - (void)test17ThatPDFWorks {
  227. NSURL *pdfURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"pdf"];
  228. [self verifyCoder:[SDImageIOCoder sharedCoder]
  229. withLocalImageURL:pdfURL
  230. supportsEncoding:NO
  231. encodingFormat:SDImageFormatUndefined
  232. isAnimatedImage:NO
  233. isVectorImage:YES];
  234. }
  235. #if !SD_TV
  236. - (void)test18ThatStaticWebPWorks {
  237. if (@available(iOS 14, tvOS 14, macOS 11, *)) {
  238. NSURL *staticWebPURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageStatic" withExtension:@"webp"];
  239. [self verifyCoder:[SDImageAWebPCoder sharedCoder]
  240. withLocalImageURL:staticWebPURL
  241. supportsEncoding:NO // Currently (iOS 14.0) seems no encoding support
  242. encodingFormat:SDImageFormatWebP
  243. isAnimatedImage:NO
  244. isVectorImage:NO];
  245. }
  246. }
  247. #endif
  248. #if !SD_TV
  249. - (void)test19ThatAnimatedWebPWorks {
  250. if (@available(iOS 14, tvOS 14, macOS 11, *)) {
  251. NSURL *staticWebPURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"webp"];
  252. [self verifyCoder:[SDImageAWebPCoder sharedCoder]
  253. withLocalImageURL:staticWebPURL
  254. supportsEncoding:NO // Currently (iOS 14.0) seems no encoding support
  255. encodingFormat:SDImageFormatWebP
  256. isAnimatedImage:YES
  257. isVectorImage:NO];
  258. }
  259. }
  260. #endif
  261. - (void)test20ThatImageIOAnimatedCoderAbstractClass {
  262. SDImageIOAnimatedCoder *coder = [[SDImageIOAnimatedCoder alloc] init];
  263. @try {
  264. [coder canEncodeToFormat:SDImageFormatPNG];
  265. XCTFail("Should throw exception");
  266. } @catch (NSException *exception) {
  267. expect(exception);
  268. }
  269. }
  270. - (void)test21ThatEmbedThumbnailHEICWorks {
  271. #if SD_MAC
  272. BOOL supportsEncoding = !SDTestCase.isCI; // GitHub Action Mac env currently does not support HEIC encoding
  273. #else
  274. BOOL supportsEncoding = YES; // GitHub Action Mac env with simulator, supported from 20240707.1
  275. #endif
  276. if (!supportsEncoding) {
  277. return;
  278. }
  279. if (@available(iOS 11, tvOS 11, macOS 10.13, *)) {
  280. // The input HEIC does not contains any embed thumbnail
  281. NSURL *heicURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"heic"];
  282. CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)heicURL, nil);
  283. expect(source).notTo.beNil();
  284. NSArray *thumbnailImages = [self thumbnailImagesFromImageSource:source];
  285. expect(thumbnailImages.count).equal(0);
  286. CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, nil);
  287. #if SD_UIKIT
  288. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:1 orientation: UIImageOrientationUp];
  289. #else
  290. UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:1 orientation:kCGImagePropertyOrientationUp];
  291. #endif
  292. CGImageRelease(imageRef);
  293. // Encode with embed thumbnail
  294. NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatHEIC options:@{SDImageCoderEncodeEmbedThumbnail : @(YES)}];
  295. // The new HEIC contains one embed thumbnail
  296. CGImageSourceRef source2 = CGImageSourceCreateWithData((__bridge CFDataRef)encodedData, nil);
  297. expect(source2).notTo.beNil();
  298. NSArray *thumbnailImages2 = [self thumbnailImagesFromImageSource:source2];
  299. expect(thumbnailImages2.count).equal(1);
  300. // Currently ImageIO has no control to custom embed thumbnail pixel size, just check the behavior :)
  301. NSDictionary *thumbnailImageInfo = thumbnailImages2.firstObject;
  302. NSUInteger thumbnailWidth = [thumbnailImageInfo[(__bridge NSString *)kCGImagePropertyWidth] unsignedIntegerValue];
  303. NSUInteger thumbnailHeight = [thumbnailImageInfo[(__bridge NSString *)kCGImagePropertyHeight] unsignedIntegerValue];
  304. expect(thumbnailWidth).equal(320);
  305. expect(thumbnailHeight).equal(212);
  306. }
  307. }
  308. - (void)test22ThatThumbnailDecodeCalculation {
  309. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
  310. NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
  311. CGSize thumbnailSize = CGSizeMake(400, 300);
  312. UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:@{
  313. SDImageCoderDecodePreserveAspectRatio: @(YES),
  314. SDImageCoderDecodeThumbnailPixelSize: @(thumbnailSize)}];
  315. CGSize imageSize = image.size;
  316. expect(imageSize.width).equal(400);
  317. expect(imageSize.height).equal(263);
  318. // `CGImageSourceCreateThumbnailAtIndex` should always produce non-lazy CGImage
  319. CGImageRef cgImage = image.CGImage;
  320. expect([SDImageCoderHelper CGImageIsLazy:cgImage]).beFalsy();
  321. expect(image.sd_isDecoded).beTruthy();
  322. }
  323. - (void)test23ThatThumbnailEncodeCalculation {
  324. NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
  325. NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
  326. UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:nil];
  327. expect(image.size).equal(CGSizeMake(5250, 3450));
  328. // `CGImageSourceCreateImageAtIndex` should always produce lazy CGImage
  329. CGImageRef cgImage = image.CGImage;
  330. expect([SDImageCoderHelper CGImageIsLazy:cgImage]).beTruthy();
  331. expect(image.sd_isDecoded).beFalsy();
  332. CGSize thumbnailSize = CGSizeMake(4000, 4000); // 3450 < 4000 < 5250
  333. NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEG options:@{
  334. SDImageCoderEncodeMaxPixelSize: @(thumbnailSize)
  335. }];
  336. UIImage *encodedImage = [UIImage sd_imageWithData:encodedData];
  337. expect(encodedImage.size).equal(CGSizeMake(4000, 2629));
  338. }
  339. - (void)test24ThatScaleSizeCalculation {
  340. // preserveAspectRatio true
  341. CGSize size1 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(150, 150) preserveAspectRatio:YES shouldScaleUp:NO];
  342. expect(size1).equal(CGSizeMake(75, 150));
  343. CGSize size2 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(150, 150) preserveAspectRatio:YES shouldScaleUp:YES];
  344. expect(size2).equal(CGSizeMake(75, 150));
  345. CGSize size3 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(300, 300) preserveAspectRatio:YES shouldScaleUp:NO];
  346. expect(size3).equal(CGSizeMake(100, 200));
  347. CGSize size4 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(300, 300) preserveAspectRatio:YES shouldScaleUp:YES];
  348. expect(size4).equal(CGSizeMake(150, 300));
  349. // preserveAspectRatio false
  350. CGSize size5 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(150, 150) preserveAspectRatio:NO shouldScaleUp:NO];
  351. expect(size5).equal(CGSizeMake(100, 150));
  352. CGSize size6 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(150, 150) preserveAspectRatio:NO shouldScaleUp:YES];
  353. expect(size6).equal(CGSizeMake(150, 150));
  354. // 0 value
  355. CGSize size7 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(0, 0) scaleSize:CGSizeMake(999, 999) preserveAspectRatio:NO shouldScaleUp:NO];
  356. expect(size7).equal(CGSizeMake(0, 0));
  357. CGSize size8 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(999, 999) scaleSize:CGSizeMake(0, 0) preserveAspectRatio:NO shouldScaleUp:NO];
  358. expect(size8).equal(CGSizeMake(999, 999));
  359. }
  360. - (void)test25ThatBMPWorks {
  361. NSURL *bmpURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"bmp"];
  362. [self verifyCoder:[SDImageIOCoder sharedCoder]
  363. withLocalImageURL:bmpURL
  364. supportsEncoding:YES
  365. encodingFormat:SDImageFormatBMP
  366. isAnimatedImage:NO
  367. isVectorImage:NO];
  368. }
  369. - (void)test26ThatRawImageTypeHintWorks {
  370. NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"nef"];
  371. NSData *data = [NSData dataWithContentsOfURL:url];
  372. // 1. Test without hint will use TIFF's IFD#0, which size should always be 160x120, see: http://lclevy.free.fr/nef/
  373. UIImage *image1 = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
  374. expect(image1.size).equal(CGSizeMake(160, 120));
  375. expect(image1.sd_imageFormat).equal(SDImageFormatTIFF);
  376. #if SD_MAC || SD_IOS
  377. // 2. Test with NEF file extension should be NEF
  378. UIImage *image2 = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeFileExtensionHint : @"nef"}];
  379. expect(image2.size).equal(CGSizeMake(3008, 2000));
  380. expect(image2.sd_imageFormat).equal(SDImageFormatRAW);
  381. // 3. Test with UTType hint should be NEF
  382. UIImage *image3 = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeTypeIdentifierHint : @"com.nikon.raw-image"}];
  383. expect(image3.size).equal(CGSizeMake(3008, 2000));
  384. expect(image3.sd_imageFormat).equal(SDImageFormatRAW);
  385. #endif
  386. }
  387. - (void)test27ThatEncodeWithFramesWorks {
  388. // Mock
  389. NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
  390. NSUInteger frameCount = 5;
  391. for (size_t i = 0; i < frameCount; i++) {
  392. CGSize size = CGSizeMake(100, 100);
  393. SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size];
  394. UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
  395. CGContextSetRGBFillColor(context, 1.0 / i, 0.0, 0.0, 1.0);
  396. CGContextSetRGBStrokeColor(context, 1.0 / i, 0.0, 0.0, 1.0);
  397. CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
  398. }];
  399. SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0.1];
  400. [frames addObject:frame];
  401. }
  402. // Test old API
  403. UIImage *animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
  404. NSData *data = [SDImageGIFCoder.sharedCoder encodedDataWithImage:animatedImage format:SDImageFormatGIF options:nil];
  405. expect(data).notTo.beNil();
  406. #if SD_MAC
  407. // Test implementation use SDAnimatedImageRep
  408. SDAnimatedImageRep *rep = (SDAnimatedImageRep *)animatedImage.representations.firstObject;
  409. expect([rep isKindOfClass:SDAnimatedImageRep.class]);
  410. expect(rep.animatedImageData).equal(data);
  411. expect(rep.animatedImageFormat).equal(SDImageFormatGIF);
  412. #endif
  413. // Test new API
  414. NSData *data2 = [SDImageGIFCoder.sharedCoder encodedDataWithFrames:frames loopCount:0 format:SDImageFormatGIF options:nil];
  415. expect(data2).notTo.beNil();
  416. }
  417. - (void)test28ThatNotTriggerCACopyImage {
  418. // 10 * 8 pixels, RGBA8888
  419. size_t width = 10;
  420. size_t height = 8;
  421. size_t bitsPerComponent = 8;
  422. size_t components = 4;
  423. size_t bitsPerPixel = bitsPerComponent * components;
  424. size_t bytesPerRow = SDByteAlign(bitsPerPixel / 8 * width, [SDImageCoderHelper preferredPixelFormat:YES].alignment);
  425. size_t size = bytesPerRow * height;
  426. uint8_t bitmap[size];
  427. for (size_t i = 0; i < size; i++) {
  428. bitmap[i] = 255;
  429. }
  430. CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB];
  431. CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:YES].bitmapInfo;
  432. CFDataRef data = CFDataCreate(NULL, bitmap, size);
  433. CGDataProviderRef provider = CGDataProviderCreateWithCFData(data);
  434. CFRelease(data);
  435. BOOL shouldInterpolate = YES;
  436. CGColorRenderingIntent intent = kCGRenderingIntentDefault;
  437. CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent);
  438. CGDataProviderRelease(provider);
  439. XCTAssert(cgImage);
  440. BOOL result = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage];
  441. // Since it's 32 bytes aligned, return true
  442. XCTAssertTrue(result);
  443. // Let's force-decode to check again
  444. #if SD_MAC
  445. UIImage *image = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:kCGImagePropertyOrientationUp];
  446. #else
  447. UIImage *image = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:UIImageOrientationUp];
  448. #endif
  449. CGImageRelease(cgImage);
  450. UIImage *newImage = [SDImageCoderHelper decodedImageWithImage:image policy:SDImageForceDecodePolicyAutomatic];
  451. // Check policy works, since it's supported by CA hardware, which return the input image object, using pointer compare
  452. XCTAssertTrue(image == newImage);
  453. BOOL newResult = [SDImageCoderHelper CGImageIsHardwareSupported:newImage.CGImage];
  454. XCTAssertTrue(newResult);
  455. }
  456. - (void)test28ThatDoTriggerCACopyImage {
  457. // 10 * 8 pixels, RGBA8888
  458. size_t width = 10;
  459. size_t height = 8;
  460. size_t bitsPerComponent = 8;
  461. size_t components = 4;
  462. size_t bitsPerPixel = bitsPerComponent * components;
  463. size_t bytesPerRow = bitsPerPixel / 8 * width;
  464. size_t size = bytesPerRow * height;
  465. uint8_t bitmap[size];
  466. for (size_t i = 0; i < size; i++) {
  467. bitmap[i] = 255;
  468. }
  469. CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB];
  470. CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:YES].bitmapInfo;
  471. CFDataRef data = CFDataCreate(NULL, bitmap, size);
  472. CGDataProviderRef provider = CGDataProviderCreateWithCFData(data);
  473. CFRelease(data);
  474. BOOL shouldInterpolate = YES;
  475. CGColorRenderingIntent intent = kCGRenderingIntentDefault;
  476. CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent);
  477. CGDataProviderRelease(provider);
  478. XCTAssert(cgImage);
  479. BOOL result = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage];
  480. // Since it's not 32 bytes aligned, return false
  481. XCTAssertFalse(result);
  482. // Let's force-decode to check again
  483. #if SD_MAC
  484. UIImage *image = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:kCGImagePropertyOrientationUp];
  485. #else
  486. UIImage *image = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:UIImageOrientationUp];
  487. #endif
  488. CGImageRelease(cgImage);
  489. UIImage *newImage = [SDImageCoderHelper decodedImageWithImage:image policy:SDImageForceDecodePolicyAutomatic];
  490. // Check policy works, since it's not supported by CA hardware, which return the different image object
  491. XCTAssertFalse(image == newImage);
  492. BOOL newResult = [SDImageCoderHelper CGImageIsHardwareSupported:newImage.CGImage];
  493. XCTAssertTrue(newResult);
  494. }
  495. - (void)test29ThatJFIFDecodeOrientationShouldNotApplyTwice {
  496. // I don't think this is SDWebImage's issue, it's Apple's ImgeIO Bug, but user complain about this: #3594
  497. // In W3C standard, JFIF should always be orientation up, and should not contains EXIF orientation
  498. // But some bad image editing tool will generate this kind of image :(
  499. NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestJFIF" withExtension:@"jpg"];
  500. NSData *data = [NSData dataWithContentsOfURL:url];
  501. UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
  502. expect(image.sd_imageFormat).equal(SDImageFormatJPEG);
  503. #if SD_UIKIT
  504. UIImageOrientation orientation = image.imageOrientation;
  505. expect(orientation).equal(UIImageOrientationDown);
  506. #endif
  507. UIImage *systemImage = [[UIImage alloc] initWithData:data];
  508. #if SD_UIKIT
  509. orientation = systemImage.imageOrientation;
  510. if (@available(iOS 18.0, tvOS 18.0, watchOS 11.0, *)) {
  511. // Apple fix/hack this kind of JFIF on iOS 18
  512. expect(orientation).equal(UIImageOrientationUp);
  513. } else {
  514. expect(orientation).equal(UIImageOrientationDown);
  515. }
  516. #endif
  517. // Check bitmap color equal, between our usage of ImageIO decoder and Apple system API behavior
  518. // So, this means, if Apple has bugs, we have bugs too, it's not our fault :)
  519. UIColor *testColor1 = [image sd_colorAtPoint:CGPointMake(1, 1)];
  520. UIColor *testColor2 = [systemImage sd_colorAtPoint:CGPointMake(1, 1)];
  521. CGFloat r1, g1, b1, a1;
  522. CGFloat r2, g2, b2, a2;
  523. [testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
  524. [testColor2 getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
  525. expect(r1).beCloseToWithin(r2, 0.01);
  526. expect(g1).beCloseToWithin(g2, 0.01);
  527. expect(b1).beCloseToWithin(b2, 0.01);
  528. expect(a1).beCloseToWithin(a2, 0.01);
  529. // Manual test again for Apple's API
  530. CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
  531. NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, nil);
  532. NSUInteger exifOrientation = [properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
  533. CFRelease(source);
  534. expect(exifOrientation).equal(kCGImagePropertyOrientationDown);
  535. }
  536. - (void)test30ThatImageIOPNGPluginBuggyWorkaround {
  537. // See: #3634
  538. NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"IndexedPNG" withExtension:@"png"];
  539. NSData *data = [NSData dataWithContentsOfURL:url];
  540. UIImage *decodedImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
  541. UIColor *testColor1 = [decodedImage sd_colorAtPoint:CGPointMake(100, 1)];
  542. CGFloat r1, g1, b1, a1;
  543. [testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
  544. expect(r1).beCloseToWithin(0.60, 0.01);
  545. expect(g1).beCloseToWithin(0.91, 0.01);
  546. expect(b1).beCloseToWithin(0.91, 0.01);
  547. expect(a1).beCloseToWithin(0.20, 0.01);
  548. // RGBA 16 bits PNG should not workaround
  549. url = [[NSBundle bundleForClass:[self class]] URLForResource:@"RGBA16PNG" withExtension:@"png"];
  550. data = [NSData dataWithContentsOfURL:url];
  551. decodedImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
  552. testColor1 = [decodedImage sd_colorAtPoint:CGPointMake(100, 1)];
  553. [testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
  554. expect(r1).beCloseToWithin(0.60, 0.01);
  555. expect(g1).beCloseToWithin(0.60, 0.01);
  556. expect(b1).beCloseToWithin(0.33, 0.01);
  557. expect(a1).beCloseToWithin(0.33, 0.01);
  558. }
  559. - (void)test31ThatSVGShouldUseNativeImageClass {
  560. NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"svg"];
  561. NSData *data = [NSData dataWithContentsOfURL:url];
  562. SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:data];
  563. expect(animatedImage).beNil();
  564. UIImage *image = [UIImage sd_imageWithData:data];
  565. Class SVGCoderClass = NSClassFromString(@"SDImageSVGCoder");
  566. if (SVGCoderClass && [SVGCoderClass sharedCoder]) {
  567. expect(image).notTo.beNil();
  568. // Vector version
  569. expect(image.sd_isVector).beTruthy();
  570. } else {
  571. // Platform does not support SVG
  572. expect(image).beNil();
  573. }
  574. }
  575. - (void)test32ThatHDRDecodeWorks {
  576. // Only test for iOS 17+/macOS 14+/visionOS 1+, or ImageIO decoder does not support HDR
  577. #if SD_MAC || SD_IOS || SD_VISION
  578. if (@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)) {
  579. NSArray *formats = @[@"heic", @"avif", @"jxl"];
  580. for (NSString *format in formats) {
  581. NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestHDR" withExtension:format];
  582. NSData *data = [NSData dataWithContentsOfURL:url];
  583. // Decoding
  584. UIImage *HDRImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeToHDR : @(YES)}];
  585. UIImage *SDRImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeToHDR : @(NO)}];
  586. expect(HDRImage).notTo.beNil();
  587. expect(SDRImage).notTo.beNil();
  588. expect([SDImageCoderHelper CGImageIsHDR:HDRImage.CGImage]).beTruthy();
  589. expect(HDRImage.sd_isHighDynamicRange).beTruthy();
  590. // FIXME: on Simulator, the SDR decode options will not take effect, so SDR is the same as HDR
  591. #if !TARGET_OS_SIMULATOR
  592. expect([SDImageCoderHelper CGImageIsHDR:SDRImage.CGImage]).beFalsy();
  593. expect(SDRImage.sd_isHighDynamicRange).beFalsy();
  594. #endif
  595. // FIXME: Encoding need iOS 18+/macOS 15+
  596. // And need test both GainMap HDR or ISO HDR, TODO
  597. }
  598. }
  599. #endif
  600. }
  601. #pragma mark - Utils
  602. - (void)verifyCoder:(id<SDImageCoder>)coder
  603. withLocalImageURL:(NSURL *)imageUrl
  604. supportsEncoding:(BOOL)supportsEncoding
  605. isAnimatedImage:(BOOL)isAnimated {
  606. [self verifyCoder:coder withLocalImageURL:imageUrl supportsEncoding:supportsEncoding encodingFormat:SDImageFormatUndefined isAnimatedImage:isAnimated isVectorImage:NO];
  607. }
  608. - (void)verifyCoder:(id<SDImageCoder>)coder
  609. withLocalImageURL:(NSURL *)imageUrl
  610. supportsEncoding:(BOOL)supportsEncoding
  611. encodingFormat:(SDImageFormat)encodingFormat
  612. isAnimatedImage:(BOOL)isAnimated
  613. isVectorImage:(BOOL)isVector {
  614. NSData *inputImageData = [NSData dataWithContentsOfURL:imageUrl];
  615. expect(inputImageData).toNot.beNil();
  616. SDImageFormat inputImageFormat = [NSData sd_imageFormatForImageData:inputImageData];
  617. expect(inputImageFormat).toNot.equal(SDImageFormatUndefined);
  618. // 1 - check if we can decode - should be true
  619. expect([coder canDecodeFromData:inputImageData]).to.beTruthy();
  620. // 2 - decode from NSData to UIImage and check it
  621. UIImage *inputImage = [coder decodedImageWithData:inputImageData options:nil];
  622. expect(inputImage).toNot.beNil();
  623. if (isAnimated) {
  624. // 2a - check images count > 0 (only for animated images)
  625. expect(inputImage.sd_isAnimated).to.beTruthy();
  626. // 2b - check image size and scale for each frameImage (only for animated images)
  627. #if SD_UIKIT
  628. CGSize imageSize = inputImage.size;
  629. CGFloat imageScale = inputImage.scale;
  630. [inputImage.images enumerateObjectsUsingBlock:^(UIImage * frameImage, NSUInteger idx, BOOL * stop) {
  631. expect(imageSize).to.equal(frameImage.size);
  632. expect(imageScale).to.equal(frameImage.scale);
  633. }];
  634. #endif
  635. }
  636. // 3 - check thumbnail decoding
  637. CGFloat pixelWidth = inputImage.size.width;
  638. CGFloat pixelHeight = inputImage.size.height;
  639. expect(pixelWidth).beGreaterThan(0);
  640. expect(pixelHeight).beGreaterThan(0);
  641. // check vector format should use 72 DPI
  642. if (isVector) {
  643. CGRect boxRect = [self boxRectFromPDFData:inputImageData];
  644. expect(boxRect.size.width).beGreaterThan(0);
  645. expect(boxRect.size.height).beGreaterThan(0);
  646. // Since 72 DPI is 1:1 from inch size to pixel size
  647. expect(boxRect.size.width).equal(pixelWidth);
  648. expect(boxRect.size.height).equal(pixelHeight);
  649. }
  650. // check thumbnail with scratch
  651. CGFloat thumbnailWidth = 50;
  652. CGFloat thumbnailHeight = 50;
  653. UIImage *thumbImage = [coder decodedImageWithData:inputImageData options:@{
  654. SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)),
  655. SDImageCoderDecodePreserveAspectRatio : @(NO)
  656. }];
  657. expect(thumbImage).toNot.beNil();
  658. expect(thumbImage.size).equal(CGSizeMake(thumbnailWidth, thumbnailHeight));
  659. // check thumbnail with aspect ratio limit
  660. thumbImage = [coder decodedImageWithData:inputImageData options:@{
  661. SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)),
  662. SDImageCoderDecodePreserveAspectRatio : @(YES)
  663. }];
  664. expect(thumbImage).toNot.beNil();
  665. CGFloat ratio = pixelWidth / pixelHeight;
  666. CGFloat thumbnailRatio = thumbnailWidth / thumbnailHeight;
  667. CGSize thumbnailPixelSize;
  668. if (ratio > thumbnailRatio) {
  669. thumbnailPixelSize = CGSizeMake(thumbnailWidth, round(thumbnailWidth / ratio));
  670. } else {
  671. thumbnailPixelSize = CGSizeMake(round(thumbnailHeight * ratio), thumbnailHeight);
  672. }
  673. // Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1
  674. expect(ABS(thumbImage.size.width - thumbnailPixelSize.width)).beLessThanOrEqualTo(1);
  675. expect(ABS(thumbImage.size.height - thumbnailPixelSize.height)).beLessThanOrEqualTo(1);
  676. if (supportsEncoding) {
  677. // 4 - check if we can encode to the original format
  678. if (encodingFormat == SDImageFormatUndefined) {
  679. encodingFormat = inputImageFormat;
  680. }
  681. expect([coder canEncodeToFormat:encodingFormat]).to.beTruthy();
  682. // 5 - encode from UIImage to NSData using the inputImageFormat and check it
  683. NSData *outputImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:nil];
  684. expect(outputImageData).toNot.beNil();
  685. UIImage *outputImage = [coder decodedImageWithData:outputImageData options:nil];
  686. expect(outputImage.size).to.equal(inputImage.size);
  687. expect(outputImage.scale).to.equal(inputImage.scale);
  688. expect(outputImage.sd_imageLoopCount).to.equal(inputImage.sd_imageLoopCount);
  689. // check max pixel size encoding with scratch
  690. CGFloat maxWidth = 50;
  691. CGFloat maxHeight = 50;
  692. CGFloat maxRatio = maxWidth / maxHeight;
  693. CGSize maxPixelSize;
  694. if (ratio > maxRatio) {
  695. maxPixelSize = CGSizeMake(maxWidth, round(maxWidth / ratio));
  696. } else {
  697. maxPixelSize = CGSizeMake(round(maxHeight * ratio), maxHeight);
  698. }
  699. NSData *outputMaxImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:@{SDImageCoderEncodeMaxPixelSize : @(CGSizeMake(maxWidth, maxHeight))}];
  700. UIImage *outputMaxImage = [coder decodedImageWithData:outputMaxImageData options:nil];
  701. // Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1
  702. expect(ABS(outputMaxImage.size.width - maxPixelSize.width)).beLessThanOrEqualTo(1);
  703. expect(ABS(outputMaxImage.size.height - maxPixelSize.height)).beLessThanOrEqualTo(1);
  704. expect(outputMaxImage.sd_imageLoopCount).to.equal(inputImage.sd_imageLoopCount);
  705. }
  706. }
  707. - (NSArray *)thumbnailImagesFromImageSource:(CGImageSourceRef)source API_AVAILABLE(ios(11.0), tvos(11.0), macos(10.13)) {
  708. NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil);
  709. NSDictionary *fileProperties = properties[(__bridge NSString *)kCGImagePropertyFileContentsDictionary];
  710. NSArray *imagesProperties = fileProperties[(__bridge NSString *)kCGImagePropertyImages];
  711. NSDictionary *imageProperties = imagesProperties.firstObject;
  712. NSArray *thumbnailImages = imageProperties[(__bridge NSString *)kCGImagePropertyThumbnailImages];
  713. return thumbnailImages;
  714. }
  715. #pragma mark - Utils
  716. - (CGRect)boxRectFromPDFData:(nonnull NSData *)data {
  717. CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
  718. if (!provider) {
  719. return CGRectZero;
  720. }
  721. CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider);
  722. CGDataProviderRelease(provider);
  723. if (!document) {
  724. return CGRectZero;
  725. }
  726. // `CGPDFDocumentGetPage` page number is 1-indexed.
  727. CGPDFPageRef page = CGPDFDocumentGetPage(document, 1);
  728. if (!page) {
  729. CGPDFDocumentRelease(document);
  730. return CGRectZero;
  731. }
  732. CGRect boxRect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
  733. CGPDFDocumentRelease(document);
  734. return boxRect;
  735. }
  736. @end