SDWebImage/Tests/Tests/SDImageCoderTests.m

793 lines
39 KiB
Objective-C

/*
* This file is part of the SDWebImage package.
* (c) Olivier Poitrey <rs@dailymotion.com>
* (c) Matt Galloway
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
#import "SDTestCase.h"
#import "UIColor+SDHexString.h"
@interface SDWebImageDecoderTests : SDTestCase
@end
@implementation SDWebImageDecoderTests
- (void)test01ThatDecodedImageWithNilImageReturnsNil {
expect([UIImage sd_decodedImageWithImage:nil]).to.beNil();
expect([UIImage sd_decodedAndScaledDownImageWithImage:nil]).to.beNil();
}
- (void)test02ThatDecodedImageWithImageWorksWithARegularJPGImage {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
UIImage *decodedImage = [UIImage sd_decodedImageWithImage:image];
expect(decodedImage).toNot.beNil();
expect(decodedImage).toNot.equal(image);
expect(decodedImage.size.width).to.equal(image.size.width);
expect(decodedImage.size.height).to.equal(image.size.height);
}
- (void)test03ThatDecodedImageWithImageDoesNotDecodeAnimatedImages {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"gif"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
#if SD_MAC
UIImage *animatedImage = image;
#else
UIImage *animatedImage = [UIImage animatedImageWithImages:@[image] duration:0];
#endif
UIImage *decodedImage = [UIImage sd_decodedImageWithImage:animatedImage];
expect(decodedImage).toNot.beNil();
expect(decodedImage).to.equal(animatedImage);
}
- (void)test04ThatDecodedImageWithImageWorksWithAlphaImages {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"png"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
UIImage *decodedImage = [UIImage sd_decodedImageWithImage:image];
expect(decodedImage).toNot.beNil();
expect(decodedImage).toNot.equal(image);
}
- (void)test05ThatDecodedImageWithImageWorksEvenWithMonochromeImage {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"MonochromeTestImage" ofType:@"jpg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
UIImage *decodedImage = [UIImage sd_decodedImageWithImage:image];
expect(decodedImage).toNot.beNil();
expect(decodedImage).toNot.equal(image);
expect(decodedImage.size.width).to.equal(image.size.width);
expect(decodedImage.size.height).to.equal(image.size.height);
}
- (void)test06ThatDecodeAndScaleDownImageWorks {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image limitBytes:(60 * 1024 * 1024)];
expect(decodedImage).toNot.beNil();
expect(decodedImage).toNot.equal(image);
expect(decodedImage.size.width).toNot.equal(image.size.width);
expect(decodedImage.size.height).toNot.equal(image.size.height);
expect(decodedImage.size.width * decodedImage.size.height).to.beLessThanOrEqualTo(60 * 1024 * 1024 / 4); // how many pixels in 60 megs
}
- (void)test07ThatDecodeAndScaleDownImageDoesNotScaleSmallerImage {
// check when user use the larget bytes than image pixels byets, we do not scale up the image (defaults 60MB means 3965x3965 pixels)
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image];
expect(decodedImage).toNot.beNil();
expect(decodedImage).toNot.equal(image);
expect(decodedImage.size.width).to.equal(image.size.width);
expect(decodedImage.size.height).to.equal(image.size.height);
}
- (void)test07ThatDecodeAndScaleDownImageScaleSmallerBytes {
// Check when user provide too small bytes, we scale it down to 1x1, but not return the force decoded original size image
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"jpg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image limitBytes:1];
expect(decodedImage).toNot.beNil();
expect(decodedImage).toNot.equal(image);
expect(decodedImage.size.width).to.equal(1);
expect(decodedImage.size.height).to.equal(1);
}
-(void)test07ThatDecodeAndScaleDownAlwaysCompleteRendering {
// 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.
// Check that when the height of the image used will led to loss of precision. the output image can also be rendered completely,
UIColor *imageColor = UIColor.blackColor;
CGSize imageSize = CGSizeMake(1029, 1029);
CGRect imageRect = CGRectMake(0, 0, imageSize.width, imageSize.height);
SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init];
format.scale = 1;
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
CGContextSetFillColorWithColor(context, [imageColor CGColor]);
CGContextFillRect(context, imageRect);
}];
UIImage *decodedImage = [UIImage sd_decodedAndScaledDownImageWithImage:image limitBytes:1 * 1024 * 1024];
UIColor *testColor1 = [decodedImage sd_colorAtPoint:CGPointMake(0, decodedImage.size.height - 1)];
UIColor *testColor2 = [decodedImage sd_colorAtPoint:CGPointMake(0, decodedImage.size.height - 9)];
expect(testColor1.sd_hexString).equal(imageColor.sd_hexString);
expect(testColor2.sd_hexString).equal(imageColor.sd_hexString);
}
- (void)test08ThatEncodeAlphaImageToJPGWithBackgroundColor {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImage" ofType:@"png"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
UIColor *backgroundColor = [UIColor blackColor];
NSData *encodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeBackgroundColor : backgroundColor}];
expect(encodedData).notTo.beNil();
UIImage *decodedImage = [SDImageCodersManager.sharedManager decodedImageWithData:encodedData options:nil];
expect(decodedImage).notTo.beNil();
expect(decodedImage.size.width).to.equal(image.size.width);
expect(decodedImage.size.height).to.equal(image.size.height);
// Check background color, should not be white but the black color
UIColor *testColor = [decodedImage sd_colorAtPoint:CGPointMake(1, 1)];
expect(testColor.sd_hexString).equal(backgroundColor.sd_hexString);
}
- (void)test09ThatJPGImageEncodeWithMaxFileSize {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
UIImage *image = [[UIImage alloc] initWithContentsOfFile:testImagePath];
// This large JPEG encoding size between (770KB ~ 2.23MB)
NSUInteger limitFileSize = 1 * 1024 * 1024; // 1MB
// 100 quality (biggest)
NSData *maxEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:nil];
expect(maxEncodedData).notTo.beNil();
expect(maxEncodedData.length).beGreaterThan(limitFileSize);
// 0 quality (smallest)
NSData *minEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeCompressionQuality : @(0.01)}]; // Seems 0 has some bugs in old macOS
expect(minEncodedData).notTo.beNil();
expect(minEncodedData.length).beLessThan(limitFileSize);
NSData *limitEncodedData = [SDImageCodersManager.sharedManager encodedDataWithImage:image format:SDImageFormatJPEG options:@{SDImageCoderEncodeMaxFileSize : @(limitFileSize)}];
expect(limitEncodedData).notTo.beNil();
// So, if we limit the file size, the output data should in (770KB ~ 2.23MB)
expect(limitEncodedData.length).beLessThan(maxEncodedData.length);
expect(limitEncodedData.length).beGreaterThan(minEncodedData.length);
}
- (void)test10ThatAnimatedImageCacheImmediatelyWorks {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"png"];
NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
// Check that animated image rendering should not use lazy decoding (performance related)
CFAbsoluteTime begin = CFAbsoluteTimeGetCurrent();
SDImageAPNGCoder *coder = [[SDImageAPNGCoder alloc] initWithAnimatedImageData:testImageData options:@{SDImageCoderDecodeFirstFrameOnly : @(NO)}];
UIImage *imageWithoutLazyDecoding = [coder animatedImageFrameAtIndex:0];
CFAbsoluteTime end = CFAbsoluteTimeGetCurrent();
CFAbsoluteTime duration = end - begin;
expect(imageWithoutLazyDecoding.sd_isDecoded).beTruthy();
// Check that static image rendering should use lazy decoding
CFAbsoluteTime begin2 = CFAbsoluteTimeGetCurrent();
SDImageAPNGCoder *coder2 = SDImageAPNGCoder.sharedCoder;
UIImage *imageWithLazyDecoding = [coder2 decodedImageWithData:testImageData options:@{SDImageCoderDecodeFirstFrameOnly : @(YES)}];
CFAbsoluteTime end2 = CFAbsoluteTimeGetCurrent();
CFAbsoluteTime duration2 = end2 - begin2;
expect(imageWithLazyDecoding.sd_isDecoded).beFalsy();
// lazy decoding need less time (10x)
expect(duration2 * 10.0).beLessThan(duration);
}
- (void)test11ThatAPNGPCoderWorks {
NSURL *APNGURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"apng"];
[self verifyCoder:[SDImageAPNGCoder sharedCoder]
withLocalImageURL:APNGURL
supportsEncoding:YES
isAnimatedImage:YES];
}
- (void)test12ThatGIFCoderWorks {
NSURL *gifURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"gif"];
[self verifyCoder:[SDImageGIFCoder sharedCoder]
withLocalImageURL:gifURL
supportsEncoding:YES
isAnimatedImage:YES];
}
- (void)test12ThatGIFWithoutLoopCountPlayOnce {
// When GIF metadata does not contains any loop count information (`kCGImagePropertyGIFLoopCount`'s value nil)
// The standard says it should just play once. See: http://www6.uniovi.es/gifanim/gifabout.htm
// This behavior is different from other modern animated image format like APNG/WebP. Which will play infinitely
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestLoopCount" ofType:@"gif"];
NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
UIImage *image = [SDImageGIFCoder.sharedCoder decodedImageWithData:testImageData options:nil];
expect(image.sd_imageLoopCount).equal(1);
}
- (void)test13ThatHEICWorks {
if (@available(iOS 11, tvOS 11, macOS 10.13, *)) {
NSURL *heicURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"heic"];
#if SD_MAC
BOOL supportsEncoding = !SDTestCase.isCI; // GitHub Action Mac env currently does not support HEIC encoding
#else
BOOL supportsEncoding = YES; // GitHub Action Mac env with simulator, supported from 20240707.1
#endif
[self verifyCoder:[SDImageIOCoder sharedCoder]
withLocalImageURL:heicURL
supportsEncoding:supportsEncoding
isAnimatedImage:NO];
}
}
- (void)test14ThatHEIFWorks {
if (@available(iOS 11, tvOS 11, macOS 10.13, *)) {
NSURL *heifURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"heif"];
BOOL supportsEncoding = NO; // public.heif UTI alwsays return false, use public.heic
[self verifyCoder:[SDImageIOCoder sharedCoder]
withLocalImageURL:heifURL
supportsEncoding:supportsEncoding
isAnimatedImage:NO];
}
}
- (void)test15ThatCodersManagerWorks {
SDImageCodersManager *manager = [[SDImageCodersManager alloc] init];
manager.coders = @[SDImageIOCoder.sharedCoder];
expect([manager canDecodeFromData:nil]).beTruthy(); // Image/IO will return YES for future format
expect([manager decodedImageWithData:nil options:nil]).beNil();
expect([manager canEncodeToFormat:SDImageFormatUndefined]).beTruthy(); // Image/IO will return YES for future format
expect([manager encodedDataWithImage:nil format:SDImageFormatUndefined options:nil]).beNil();
}
- (void)test16ThatHEICAnimatedWorks {
if (@available(iOS 13, tvOS 13, macOS 10.15, *)) {
NSURL *heicURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"heics"];
BOOL supportsEncoding = !SDTestCase.isCI; // GitHub Action Mac env currently does not support HEICS animated encoding (but HEIC supported, I don't know why)
// See: #3227
BOOL isAnimatedImage = YES;
[self verifyCoder:[SDImageHEICCoder sharedCoder]
withLocalImageURL:heicURL
supportsEncoding:supportsEncoding
encodingFormat:SDImageFormatHEIC
isAnimatedImage:isAnimatedImage
isVectorImage:NO];
}
}
- (void)test17ThatPDFWorks {
NSURL *pdfURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"pdf"];
[self verifyCoder:[SDImageIOCoder sharedCoder]
withLocalImageURL:pdfURL
supportsEncoding:NO
encodingFormat:SDImageFormatUndefined
isAnimatedImage:NO
isVectorImage:YES];
}
#if !SD_TV
- (void)test18ThatStaticWebPWorks {
if (@available(iOS 14, tvOS 14, macOS 11, *)) {
NSURL *staticWebPURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageStatic" withExtension:@"webp"];
[self verifyCoder:[SDImageAWebPCoder sharedCoder]
withLocalImageURL:staticWebPURL
supportsEncoding:NO // Currently (iOS 14.0) seems no encoding support
encodingFormat:SDImageFormatWebP
isAnimatedImage:NO
isVectorImage:NO];
}
}
#endif
#if !SD_TV
- (void)test19ThatAnimatedWebPWorks {
if (@available(iOS 14, tvOS 14, macOS 11, *)) {
NSURL *staticWebPURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImageAnimated" withExtension:@"webp"];
[self verifyCoder:[SDImageAWebPCoder sharedCoder]
withLocalImageURL:staticWebPURL
supportsEncoding:NO // Currently (iOS 14.0) seems no encoding support
encodingFormat:SDImageFormatWebP
isAnimatedImage:YES
isVectorImage:NO];
}
}
#endif
- (void)test20ThatImageIOAnimatedCoderAbstractClass {
SDImageIOAnimatedCoder *coder = [[SDImageIOAnimatedCoder alloc] init];
@try {
[coder canEncodeToFormat:SDImageFormatPNG];
XCTFail("Should throw exception");
} @catch (NSException *exception) {
expect(exception);
}
}
- (void)test21ThatEmbedThumbnailHEICWorks {
#if SD_MAC
BOOL supportsEncoding = !SDTestCase.isCI; // GitHub Action Mac env currently does not support HEIC encoding
#else
BOOL supportsEncoding = YES; // GitHub Action Mac env with simulator, supported from 20240707.1
#endif
if (!supportsEncoding) {
return;
}
if (@available(iOS 11, tvOS 11, macOS 10.13, *)) {
// The input HEIC does not contains any embed thumbnail
NSURL *heicURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"heic"];
CGImageSourceRef source = CGImageSourceCreateWithURL((__bridge CFURLRef)heicURL, nil);
expect(source).notTo.beNil();
NSArray *thumbnailImages = [self thumbnailImagesFromImageSource:source];
expect(thumbnailImages.count).equal(0);
CGImageRef imageRef = CGImageSourceCreateImageAtIndex(source, 0, nil);
#if SD_UIKIT
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:1 orientation: UIImageOrientationUp];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:1 orientation:kCGImagePropertyOrientationUp];
#endif
CGImageRelease(imageRef);
// Encode with embed thumbnail
NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatHEIC options:@{SDImageCoderEncodeEmbedThumbnail : @(YES)}];
// The new HEIC contains one embed thumbnail
CGImageSourceRef source2 = CGImageSourceCreateWithData((__bridge CFDataRef)encodedData, nil);
expect(source2).notTo.beNil();
NSArray *thumbnailImages2 = [self thumbnailImagesFromImageSource:source2];
expect(thumbnailImages2.count).equal(1);
// Currently ImageIO has no control to custom embed thumbnail pixel size, just check the behavior :)
NSDictionary *thumbnailImageInfo = thumbnailImages2.firstObject;
NSUInteger thumbnailWidth = [thumbnailImageInfo[(__bridge NSString *)kCGImagePropertyWidth] unsignedIntegerValue];
NSUInteger thumbnailHeight = [thumbnailImageInfo[(__bridge NSString *)kCGImagePropertyHeight] unsignedIntegerValue];
expect(thumbnailWidth).equal(320);
expect(thumbnailHeight).equal(212);
}
}
- (void)test22ThatThumbnailDecodeCalculation {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
CGSize thumbnailSize = CGSizeMake(400, 300);
UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:@{
SDImageCoderDecodePreserveAspectRatio: @(YES),
SDImageCoderDecodeThumbnailPixelSize: @(thumbnailSize)}];
CGSize imageSize = image.size;
expect(imageSize.width).equal(400);
expect(imageSize.height).equal(263);
// `CGImageSourceCreateThumbnailAtIndex` should always produce non-lazy CGImage
CGImageRef cgImage = image.CGImage;
expect([SDImageCoderHelper CGImageIsLazy:cgImage]).beFalsy();
expect(image.sd_isDecoded).beTruthy();
}
- (void)test23ThatThumbnailEncodeCalculation {
NSString *testImagePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"TestImageLarge" ofType:@"jpg"];
NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:nil];
expect(image.size).equal(CGSizeMake(5250, 3450));
// `CGImageSourceCreateImageAtIndex` should always produce lazy CGImage
CGImageRef cgImage = image.CGImage;
expect([SDImageCoderHelper CGImageIsLazy:cgImage]).beTruthy();
expect(image.sd_isDecoded).beFalsy();
CGSize thumbnailSize = CGSizeMake(4000, 4000); // 3450 < 4000 < 5250
NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEG options:@{
SDImageCoderEncodeMaxPixelSize: @(thumbnailSize)
}];
UIImage *encodedImage = [UIImage sd_imageWithData:encodedData];
expect(encodedImage.size).equal(CGSizeMake(4000, 2629));
}
- (void)test24ThatScaleSizeCalculation {
// preserveAspectRatio true
CGSize size1 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(150, 150) preserveAspectRatio:YES shouldScaleUp:NO];
expect(size1).equal(CGSizeMake(75, 150));
CGSize size2 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(150, 150) preserveAspectRatio:YES shouldScaleUp:YES];
expect(size2).equal(CGSizeMake(75, 150));
CGSize size3 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(300, 300) preserveAspectRatio:YES shouldScaleUp:NO];
expect(size3).equal(CGSizeMake(100, 200));
CGSize size4 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(300, 300) preserveAspectRatio:YES shouldScaleUp:YES];
expect(size4).equal(CGSizeMake(150, 300));
// preserveAspectRatio false
CGSize size5 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(150, 150) preserveAspectRatio:NO shouldScaleUp:NO];
expect(size5).equal(CGSizeMake(100, 150));
CGSize size6 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(100, 200) scaleSize:CGSizeMake(150, 150) preserveAspectRatio:NO shouldScaleUp:YES];
expect(size6).equal(CGSizeMake(150, 150));
// 0 value
CGSize size7 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(0, 0) scaleSize:CGSizeMake(999, 999) preserveAspectRatio:NO shouldScaleUp:NO];
expect(size7).equal(CGSizeMake(0, 0));
CGSize size8 = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(999, 999) scaleSize:CGSizeMake(0, 0) preserveAspectRatio:NO shouldScaleUp:NO];
expect(size8).equal(CGSizeMake(999, 999));
}
- (void)test25ThatBMPWorks {
NSURL *bmpURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"bmp"];
[self verifyCoder:[SDImageIOCoder sharedCoder]
withLocalImageURL:bmpURL
supportsEncoding:YES
encodingFormat:SDImageFormatBMP
isAnimatedImage:NO
isVectorImage:NO];
}
- (void)test26ThatRawImageTypeHintWorks {
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"nef"];
NSData *data = [NSData dataWithContentsOfURL:url];
// 1. Test without hint will use TIFF's IFD#0, which size should always be 160x120, see: http://lclevy.free.fr/nef/
UIImage *image1 = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
expect(image1.size).equal(CGSizeMake(160, 120));
expect(image1.sd_imageFormat).equal(SDImageFormatTIFF);
#if SD_MAC || SD_IOS
// 2. Test with NEF file extension should be NEF
UIImage *image2 = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeFileExtensionHint : @"nef"}];
expect(image2.size).equal(CGSizeMake(3008, 2000));
expect(image2.sd_imageFormat).equal(SDImageFormatRAW);
// 3. Test with UTType hint should be NEF
UIImage *image3 = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeTypeIdentifierHint : @"com.nikon.raw-image"}];
expect(image3.size).equal(CGSizeMake(3008, 2000));
expect(image3.sd_imageFormat).equal(SDImageFormatRAW);
#endif
}
- (void)test27ThatEncodeWithFramesWorks {
// Mock
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
NSUInteger frameCount = 5;
for (size_t i = 0; i < frameCount; i++) {
CGSize size = CGSizeMake(100, 100);
SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size];
UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) {
CGContextSetRGBFillColor(context, 1.0 / i, 0.0, 0.0, 1.0);
CGContextSetRGBStrokeColor(context, 1.0 / i, 0.0, 0.0, 1.0);
CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
}];
SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0.1];
[frames addObject:frame];
}
// Test old API
UIImage *animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames];
NSData *data = [SDImageGIFCoder.sharedCoder encodedDataWithImage:animatedImage format:SDImageFormatGIF options:nil];
expect(data).notTo.beNil();
#if SD_MAC
// Test implementation use SDAnimatedImageRep
SDAnimatedImageRep *rep = (SDAnimatedImageRep *)animatedImage.representations.firstObject;
expect([rep isKindOfClass:SDAnimatedImageRep.class]);
expect(rep.animatedImageData).equal(data);
expect(rep.animatedImageFormat).equal(SDImageFormatGIF);
#endif
// Test new API
NSData *data2 = [SDImageGIFCoder.sharedCoder encodedDataWithFrames:frames loopCount:0 format:SDImageFormatGIF options:nil];
expect(data2).notTo.beNil();
}
- (void)test28ThatNotTriggerCACopyImage {
// 10 * 8 pixels, RGBA8888
size_t width = 10;
size_t height = 8;
size_t bitsPerComponent = 8;
size_t components = 4;
size_t bitsPerPixel = bitsPerComponent * components;
size_t bytesPerRow = SDByteAlign(bitsPerPixel / 8 * width, [SDImageCoderHelper preferredPixelFormat:YES].alignment);
size_t size = bytesPerRow * height;
uint8_t bitmap[size];
for (size_t i = 0; i < size; i++) {
bitmap[i] = 255;
}
CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB];
CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:YES].bitmapInfo;
CFDataRef data = CFDataCreate(NULL, bitmap, size);
CGDataProviderRef provider = CGDataProviderCreateWithCFData(data);
CFRelease(data);
BOOL shouldInterpolate = YES;
CGColorRenderingIntent intent = kCGRenderingIntentDefault;
CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent);
CGDataProviderRelease(provider);
XCTAssert(cgImage);
BOOL result = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage];
// Since it's 32 bytes aligned, return true
XCTAssertTrue(result);
// Let's force-decode to check again
#if SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:kCGImagePropertyOrientationUp];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:UIImageOrientationUp];
#endif
CGImageRelease(cgImage);
UIImage *newImage = [SDImageCoderHelper decodedImageWithImage:image policy:SDImageForceDecodePolicyAutomatic];
// Check policy works, since it's supported by CA hardware, which return the input image object, using pointer compare
XCTAssertTrue(image == newImage);
BOOL newResult = [SDImageCoderHelper CGImageIsHardwareSupported:newImage.CGImage];
XCTAssertTrue(newResult);
}
- (void)test28ThatDoTriggerCACopyImage {
// 10 * 8 pixels, RGBA8888
size_t width = 10;
size_t height = 8;
size_t bitsPerComponent = 8;
size_t components = 4;
size_t bitsPerPixel = bitsPerComponent * components;
size_t bytesPerRow = bitsPerPixel / 8 * width;
size_t size = bytesPerRow * height;
uint8_t bitmap[size];
for (size_t i = 0; i < size; i++) {
bitmap[i] = 255;
}
CGColorSpaceRef colorspace = [SDImageCoderHelper colorSpaceGetDeviceRGB];
CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:YES].bitmapInfo;
CFDataRef data = CFDataCreate(NULL, bitmap, size);
CGDataProviderRef provider = CGDataProviderCreateWithCFData(data);
CFRelease(data);
BOOL shouldInterpolate = YES;
CGColorRenderingIntent intent = kCGRenderingIntentDefault;
CGImageRef cgImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, colorspace, bitmapInfo, provider, NULL, shouldInterpolate, intent);
CGDataProviderRelease(provider);
XCTAssert(cgImage);
BOOL result = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage];
// Since it's not 32 bytes aligned, return false
XCTAssertFalse(result);
// Let's force-decode to check again
#if SD_MAC
UIImage *image = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:kCGImagePropertyOrientationUp];
#else
UIImage *image = [[UIImage alloc] initWithCGImage:cgImage scale:1 orientation:UIImageOrientationUp];
#endif
CGImageRelease(cgImage);
UIImage *newImage = [SDImageCoderHelper decodedImageWithImage:image policy:SDImageForceDecodePolicyAutomatic];
// Check policy works, since it's not supported by CA hardware, which return the different image object
XCTAssertFalse(image == newImage);
BOOL newResult = [SDImageCoderHelper CGImageIsHardwareSupported:newImage.CGImage];
XCTAssertTrue(newResult);
}
- (void)test29ThatJFIFDecodeOrientationShouldNotApplyTwice {
// I don't think this is SDWebImage's issue, it's Apple's ImgeIO Bug, but user complain about this: #3594
// In W3C standard, JFIF should always be orientation up, and should not contains EXIF orientation
// But some bad image editing tool will generate this kind of image :(
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestJFIF" withExtension:@"jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
expect(image.sd_imageFormat).equal(SDImageFormatJPEG);
#if SD_UIKIT
UIImageOrientation orientation = image.imageOrientation;
expect(orientation).equal(UIImageOrientationDown);
#endif
UIImage *systemImage = [[UIImage alloc] initWithData:data];
#if SD_UIKIT
orientation = systemImage.imageOrientation;
if (@available(iOS 18.0, tvOS 18.0, watchOS 11.0, *)) {
// Apple fix/hack this kind of JFIF on iOS 18
expect(orientation).equal(UIImageOrientationUp);
} else {
expect(orientation).equal(UIImageOrientationDown);
}
#endif
// Check bitmap color equal, between our usage of ImageIO decoder and Apple system API behavior
// So, this means, if Apple has bugs, we have bugs too, it's not our fault :)
UIColor *testColor1 = [image sd_colorAtPoint:CGPointMake(1, 1)];
UIColor *testColor2 = [systemImage sd_colorAtPoint:CGPointMake(1, 1)];
CGFloat r1, g1, b1, a1;
CGFloat r2, g2, b2, a2;
[testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
[testColor2 getRed:&r2 green:&g2 blue:&b2 alpha:&a2];
expect(r1).beCloseToWithin(r2, 0.01);
expect(g1).beCloseToWithin(g2, 0.01);
expect(b1).beCloseToWithin(b2, 0.01);
expect(a1).beCloseToWithin(a2, 0.01);
// Manual test again for Apple's API
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, nil);
NSUInteger exifOrientation = [properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
CFRelease(source);
expect(exifOrientation).equal(kCGImagePropertyOrientationDown);
}
- (void)test30ThatImageIOPNGPluginBuggyWorkaround {
// See: #3634
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"IndexedPNG" withExtension:@"png"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *decodedImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
UIColor *testColor1 = [decodedImage sd_colorAtPoint:CGPointMake(100, 1)];
CGFloat r1, g1, b1, a1;
[testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
expect(r1).beCloseToWithin(0.60, 0.01);
expect(g1).beCloseToWithin(0.91, 0.01);
expect(b1).beCloseToWithin(0.91, 0.01);
expect(a1).beCloseToWithin(0.20, 0.01);
// RGBA 16 bits PNG should not workaround
url = [[NSBundle bundleForClass:[self class]] URLForResource:@"RGBA16PNG" withExtension:@"png"];
data = [NSData dataWithContentsOfURL:url];
decodedImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
testColor1 = [decodedImage sd_colorAtPoint:CGPointMake(100, 1)];
[testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
expect(r1).beCloseToWithin(0.60, 0.01);
expect(g1).beCloseToWithin(0.60, 0.01);
expect(b1).beCloseToWithin(0.33, 0.01);
expect(a1).beCloseToWithin(0.33, 0.01);
}
- (void)test31ThatSVGShouldUseNativeImageClass {
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestImage" withExtension:@"svg"];
NSData *data = [NSData dataWithContentsOfURL:url];
SDAnimatedImage *animatedImage = [SDAnimatedImage imageWithData:data];
expect(animatedImage).beNil();
UIImage *image = [UIImage sd_imageWithData:data];
Class SVGCoderClass = NSClassFromString(@"SDImageSVGCoder");
if (SVGCoderClass && [SVGCoderClass sharedCoder]) {
expect(image).notTo.beNil();
// Vector version
expect(image.sd_isVector).beTruthy();
} else {
// Platform does not support SVG
expect(image).beNil();
}
}
#pragma mark - Utils
- (void)verifyCoder:(id<SDImageCoder>)coder
withLocalImageURL:(NSURL *)imageUrl
supportsEncoding:(BOOL)supportsEncoding
isAnimatedImage:(BOOL)isAnimated {
[self verifyCoder:coder withLocalImageURL:imageUrl supportsEncoding:supportsEncoding encodingFormat:SDImageFormatUndefined isAnimatedImage:isAnimated isVectorImage:NO];
}
- (void)verifyCoder:(id<SDImageCoder>)coder
withLocalImageURL:(NSURL *)imageUrl
supportsEncoding:(BOOL)supportsEncoding
encodingFormat:(SDImageFormat)encodingFormat
isAnimatedImage:(BOOL)isAnimated
isVectorImage:(BOOL)isVector {
NSData *inputImageData = [NSData dataWithContentsOfURL:imageUrl];
expect(inputImageData).toNot.beNil();
SDImageFormat inputImageFormat = [NSData sd_imageFormatForImageData:inputImageData];
expect(inputImageFormat).toNot.equal(SDImageFormatUndefined);
// 1 - check if we can decode - should be true
expect([coder canDecodeFromData:inputImageData]).to.beTruthy();
// 2 - decode from NSData to UIImage and check it
UIImage *inputImage = [coder decodedImageWithData:inputImageData options:nil];
expect(inputImage).toNot.beNil();
if (isAnimated) {
// 2a - check images count > 0 (only for animated images)
expect(inputImage.sd_isAnimated).to.beTruthy();
// 2b - check image size and scale for each frameImage (only for animated images)
#if SD_UIKIT
CGSize imageSize = inputImage.size;
CGFloat imageScale = inputImage.scale;
[inputImage.images enumerateObjectsUsingBlock:^(UIImage * frameImage, NSUInteger idx, BOOL * stop) {
expect(imageSize).to.equal(frameImage.size);
expect(imageScale).to.equal(frameImage.scale);
}];
#endif
}
// 3 - check thumbnail decoding
CGFloat pixelWidth = inputImage.size.width;
CGFloat pixelHeight = inputImage.size.height;
expect(pixelWidth).beGreaterThan(0);
expect(pixelHeight).beGreaterThan(0);
// check vector format should use 72 DPI
if (isVector) {
CGRect boxRect = [self boxRectFromPDFData:inputImageData];
expect(boxRect.size.width).beGreaterThan(0);
expect(boxRect.size.height).beGreaterThan(0);
// Since 72 DPI is 1:1 from inch size to pixel size
expect(boxRect.size.width).equal(pixelWidth);
expect(boxRect.size.height).equal(pixelHeight);
}
// check thumbnail with scratch
CGFloat thumbnailWidth = 50;
CGFloat thumbnailHeight = 50;
UIImage *thumbImage = [coder decodedImageWithData:inputImageData options:@{
SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)),
SDImageCoderDecodePreserveAspectRatio : @(NO)
}];
expect(thumbImage).toNot.beNil();
expect(thumbImage.size).equal(CGSizeMake(thumbnailWidth, thumbnailHeight));
// check thumbnail with aspect ratio limit
thumbImage = [coder decodedImageWithData:inputImageData options:@{
SDImageCoderDecodeThumbnailPixelSize : @(CGSizeMake(thumbnailWidth, thumbnailHeight)),
SDImageCoderDecodePreserveAspectRatio : @(YES)
}];
expect(thumbImage).toNot.beNil();
CGFloat ratio = pixelWidth / pixelHeight;
CGFloat thumbnailRatio = thumbnailWidth / thumbnailHeight;
CGSize thumbnailPixelSize;
if (ratio > thumbnailRatio) {
thumbnailPixelSize = CGSizeMake(thumbnailWidth, round(thumbnailWidth / ratio));
} else {
thumbnailPixelSize = CGSizeMake(round(thumbnailHeight * ratio), thumbnailHeight);
}
// Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1
expect(ABS(thumbImage.size.width - thumbnailPixelSize.width)).beLessThanOrEqualTo(1);
expect(ABS(thumbImage.size.height - thumbnailPixelSize.height)).beLessThanOrEqualTo(1);
if (supportsEncoding) {
// 4 - check if we can encode to the original format
if (encodingFormat == SDImageFormatUndefined) {
encodingFormat = inputImageFormat;
}
expect([coder canEncodeToFormat:encodingFormat]).to.beTruthy();
// 5 - encode from UIImage to NSData using the inputImageFormat and check it
NSData *outputImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:nil];
expect(outputImageData).toNot.beNil();
UIImage *outputImage = [coder decodedImageWithData:outputImageData options:nil];
expect(outputImage.size).to.equal(inputImage.size);
expect(outputImage.scale).to.equal(inputImage.scale);
expect(outputImage.sd_imageLoopCount).to.equal(inputImage.sd_imageLoopCount);
// check max pixel size encoding with scratch
CGFloat maxWidth = 50;
CGFloat maxHeight = 50;
CGFloat maxRatio = maxWidth / maxHeight;
CGSize maxPixelSize;
if (ratio > maxRatio) {
maxPixelSize = CGSizeMake(maxWidth, round(maxWidth / ratio));
} else {
maxPixelSize = CGSizeMake(round(maxHeight * ratio), maxHeight);
}
NSData *outputMaxImageData = [coder encodedDataWithImage:inputImage format:encodingFormat options:@{SDImageCoderEncodeMaxPixelSize : @(CGSizeMake(maxWidth, maxHeight))}];
UIImage *outputMaxImage = [coder decodedImageWithData:outputMaxImageData options:nil];
// Image/IO's thumbnail API does not always use round to preserve precision, we check ABS <= 1
expect(ABS(outputMaxImage.size.width - maxPixelSize.width)).beLessThanOrEqualTo(1);
expect(ABS(outputMaxImage.size.height - maxPixelSize.height)).beLessThanOrEqualTo(1);
expect(outputMaxImage.sd_imageLoopCount).to.equal(inputImage.sd_imageLoopCount);
}
}
- (NSArray *)thumbnailImagesFromImageSource:(CGImageSourceRef)source API_AVAILABLE(ios(11.0), tvos(11.0), macos(10.13)) {
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, nil);
NSDictionary *fileProperties = properties[(__bridge NSString *)kCGImagePropertyFileContentsDictionary];
NSArray *imagesProperties = fileProperties[(__bridge NSString *)kCGImagePropertyImages];
NSDictionary *imageProperties = imagesProperties.firstObject;
NSArray *thumbnailImages = imageProperties[(__bridge NSString *)kCGImagePropertyThumbnailImages];
return thumbnailImages;
}
#pragma mark - Utils
- (CGRect)boxRectFromPDFData:(nonnull NSData *)data {
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
if (!provider) {
return CGRectZero;
}
CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider);
CGDataProviderRelease(provider);
if (!document) {
return CGRectZero;
}
// `CGPDFDocumentGetPage` page number is 1-indexed.
CGPDFPageRef page = CGPDFDocumentGetPage(document, 1);
if (!page) {
CGPDFDocumentRelease(document);
return CGRectZero;
}
CGRect boxRect = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
CGPDFDocumentRelease(document);
return boxRect;
}
@end