diff --git a/SDWebImage/Core/SDImageCacheDefine.m b/SDWebImage/Core/SDImageCacheDefine.m index 7278c6d6..db3f43fa 100644 --- a/SDWebImage/Core/SDImageCacheDefine.m +++ b/SDWebImage/Core/SDImageCacheDefine.m @@ -116,7 +116,11 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS } if (image) { BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage); - if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) { + BOOL lazyDecode = [coderOptions[SDImageCoderDecodeUseLazyDecoding] boolValue]; + if (lazyDecode) { + // lazyDecode = NO means we should not forceDecode, highest priority + shouldDecode = NO; + } else if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) { // `SDAnimatedImage` do not decode shouldDecode = NO; } else if (image.sd_isAnimated) { diff --git a/SDWebImage/Core/SDImageCoder.h b/SDWebImage/Core/SDImageCoder.h index eec89d10..eeaeb695 100644 --- a/SDWebImage/Core/SDImageCoder.h +++ b/SDWebImage/Core/SDImageCoder.h @@ -61,6 +61,19 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFileExtens */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeTypeIdentifierHint; +/** + A BOOL value indicating whether to use lazy-decoding. Defaults to NO on animated image coder, but defaults to YES on static image coder. + CGImageRef, this image object typically support lazy-decoding, via the `CGDataProviderCreateDirectAccess` or `CGDataProviderCreateSequential` + Which allows you to provide a lazy-called callback to access bitmap buffer, so that you can achieve lazy-decoding when consumer actually need bitmap buffer + UIKit on iOS use heavy on this and ImageIO codec prefers to lazy-decoding for common Hardware-Accelerate format like JPEG/PNG/HEIC + But however, the consumer may access bitmap buffer when running on main queue, like CoreAnimation layer render image. So this is a trade-off + You can force us to disable the lazy-decoding and always allocate bitmap buffer on RAM, but this may have higher ratio of OOM (out of memory) + @note The default value is NO for animated image coder (means `animatedImageFrameAtIndex:`) + @note The default value is YES for static image coder (means `decodedImageWithData:`) + @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. + */ +FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDecoding; + // These options are for image encoding /** A Boolean value indicating whether to encode the first frame only for animated image during encoding. (NSNumber). If not provide, encode animated image if need. diff --git a/SDWebImage/Core/SDImageCoder.m b/SDWebImage/Core/SDImageCoder.m index 141377d5..4f98f273 100644 --- a/SDWebImage/Core/SDImageCoder.m +++ b/SDWebImage/Core/SDImageCoder.m @@ -14,6 +14,7 @@ SDImageCoderOption const SDImageCoderDecodePreserveAspectRatio = @"decodePreserv SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnailPixelSize"; SDImageCoderOption const SDImageCoderDecodeFileExtensionHint = @"decodeFileExtensionHint"; SDImageCoderOption const SDImageCoderDecodeTypeIdentifierHint = @"decodeTypeIdentifierHint"; +SDImageCoderOption const SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding"; SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index bc456561..56261eb3 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -23,6 +23,30 @@ static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizati // Specify File Size for lossy format encoding, like JPEG static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize"; +// Only assert on Debug mode and Simulator +#define SD_CHECK_CGIMAGE_RETAIN_SOURCE DEBUG && TARGET_OS_SIMULATOR && \ + ((__IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_15_0)) || \ + ((__TV_OS_VERSION_MAX_ALLOWED >= __TVOS_15_0)) + +// This strip the un-wanted CGImageProperty, like the internal CGImageSourceRef in iOS 15+ +// However, CGImageCreateCopy still keep those CGImageProperty, not suit for our use case +static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) { + if (!image) return nil; + size_t width = CGImageGetWidth(image); + size_t height = CGImageGetHeight(image); + size_t bitsPerComponent = CGImageGetBitsPerComponent(image); + size_t bitsPerPixel = CGImageGetBitsPerPixel(image); + size_t bytesPerRow = CGImageGetBytesPerRow(image); + CGColorSpaceRef space = CGImageGetColorSpace(image); + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image); + CGDataProviderRef provider = CGImageGetDataProvider(image); + const CGFloat *decode = CGImageGetDecode(image); + bool shouldInterpolate = CGImageGetShouldInterpolate(image); + CGColorRenderingIntent intent = CGImageGetRenderingIntent(image); + CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, provider, decode, shouldInterpolate, intent); + return newImage; +} + @interface SDImageIOCoderFrame : NSObject @property (nonatomic, assign) NSUInteger index; // Frame index (zero based) @@ -46,6 +70,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination BOOL _finished; BOOL _preserveAspectRatio; CGSize _thumbnailSize; + BOOL _lazyDecode; } - (void)dealloc @@ -193,7 +218,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination return frameDuration; } -+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize forceDecode:(BOOL)forceDecode options:(NSDictionary *)options { ++ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode options:(NSDictionary *)options { // Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :) // Parse the image properties NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options); @@ -250,7 +275,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination } } // Check whether output CGImage is decoded - if (forceDecode) { + if (!lazyDecode) { if (!isDecoded) { // Use CoreGraphics to trigger immediately decode CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef]; @@ -258,13 +283,24 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination imageRef = decodedImageRef; isDecoded = YES; } -#if SD_CHECK_CGIMAGE_RETAIN_SOURCE - // Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+) - // If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check - extern CGImageSourceRef CGImageGetImageSource(CGImageRef); - NSCAssert(!CGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock"); -#endif + } else { + // iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273 + if (@available(iOS 15, tvOS 15, *)) { + // User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef + if (imageRef) { + // CGImageRef newImageRef = CGImageCreateCopy(imageRef); + CGImageRef newImageRef = SDCGImageCreateCopy(imageRef); + CGImageRelease(imageRef); + imageRef = newImageRef; + } + } } +#if SD_CHECK_CGIMAGE_RETAIN_SOURCE + // Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+) + // If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check + extern CGImageSourceRef CGImageGetImageSource(CGImageRef); + NSCAssert(!CGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock"); +#endif #if SD_UIKIT || SD_WATCH UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation]; @@ -273,6 +309,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation]; #endif CGImageRelease(imageRef); + image.sd_isDecoded = isDecoded; + return image; } @@ -307,6 +345,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination preserveAspectRatio = preserveAspectRatioValue.boolValue; } + BOOL lazyDecode = YES; // Defaults YES for static image coder + NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; + if (lazyDecodeValue != nil) { + lazyDecode = lazyDecodeValue.boolValue; + } + #if SD_MAC // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG) // Which decode frames in time and reduce memory usage @@ -353,12 +397,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue]; if (decodeFirstFrame || count <= 1) { - animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize forceDecode:NO options:nil]; + animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:nil]; } else { NSMutableArray *frames = [NSMutableArray arrayWithCapacity:count]; for (size_t i = 0; i < count; i++) { - UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize forceDecode:NO options:nil]; + UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:nil]; if (!image) { continue; } @@ -414,6 +458,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination preserveAspectRatio = preserveAspectRatioValue.boolValue; } _preserveAspectRatio = preserveAspectRatio; + BOOL lazyDecode = YES; // Defaults YES for static image coder + NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; + if (lazyDecodeValue != nil) { + lazyDecode = lazyDecodeValue.boolValue; + } + _lazyDecode = lazyDecode; SD_LOCK_INIT(_lock); #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; @@ -468,7 +518,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } - image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize forceDecode:NO options:nil]; + image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode options:nil]; if (image) { image.sd_imageFormat = self.class.imageFormat; } @@ -715,28 +765,27 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index { NSDictionary *options; - BOOL forceDecode = NO; - if (@available(iOS 15, tvOS 15, *)) { - // iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273 - forceDecode = YES; + BOOL lazyDecode = NO; // Defaults NO for animated image coder + NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; + if (lazyDecodeValue != nil) { + lazyDecode = lazyDecodeValue.boolValue; + } + if (!lazyDecode) { options = @{ (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(NO), (__bridge NSString *)kCGImageSourceShouldCache : @(NO) }; } else { - // Animated Image should not use the CGContext solution to force decode on lower firmware. Prefers to use Image/IO built in method, which is safer and memory friendly, see https://github.com/SDWebImage/SDWebImage/issues/2961 - forceDecode = NO; options = @{ (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES), (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage }; } - UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize forceDecode:forceDecode options:options]; + UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:lazyDecode options:options]; if (!image) { return nil; } image.sd_imageFormat = self.class.imageFormat; - image.sd_isDecoded = YES; return image; } diff --git a/SDWebImage/Core/SDImageIOCoder.m b/SDWebImage/Core/SDImageIOCoder.m index 877a999c..39bff408 100644 --- a/SDWebImage/Core/SDImageIOCoder.m +++ b/SDWebImage/Core/SDImageIOCoder.m @@ -28,6 +28,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination BOOL _finished; BOOL _preserveAspectRatio; CGSize _thumbnailSize; + BOOL _lazyDecode; } - (void)dealloc { @@ -112,6 +113,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination preserveAspectRatio = preserveAspectRatioValue.boolValue; } + BOOL lazyDecode = YES; // Defaults YES for static image coder + NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; + if (lazyDecodeValue != nil) { + lazyDecode = lazyDecodeValue.boolValue; + } + NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint]; if (!typeIdentifierHint) { // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension @@ -163,7 +170,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination thumbnailSize = CGSizeZero; } - UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize forceDecode:NO options:decodingOptions]; + UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:decodingOptions]; CFRelease(source); if (!image) { return nil; @@ -205,6 +212,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination preserveAspectRatio = preserveAspectRatioValue.boolValue; } _preserveAspectRatio = preserveAspectRatio; + BOOL lazyDecode = YES; // Defaults YES for static image coder + NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; + if (lazyDecodeValue != nil) { + lazyDecode = lazyDecodeValue.boolValue; + } + _lazyDecode = lazyDecode; #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif @@ -255,7 +268,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } - image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize forceDecode:NO options:nil]; + image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode options:nil]; if (image) { CFStringRef uttype = CGImageSourceGetType(_imageSource); image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype]; diff --git a/SDWebImage/Core/SDImageLoader.m b/SDWebImage/Core/SDImageLoader.m index 03143b0b..c8148d8b 100644 --- a/SDWebImage/Core/SDImageLoader.m +++ b/SDWebImage/Core/SDImageLoader.m @@ -77,14 +77,17 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS } if (image) { BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage); - if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) { + BOOL lazyDecode = [coderOptions[SDImageCoderDecodeUseLazyDecoding] boolValue]; + if (lazyDecode) { + // lazyDecode = NO means we should not forceDecode, highest priority + shouldDecode = NO; + } else if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) { // `SDAnimatedImage` do not decode shouldDecode = NO; } else if (image.sd_isAnimated) { // animated image do not decode shouldDecode = NO; } - if (shouldDecode) { image = [SDImageCoderHelper decodedImageWithImage:image]; } @@ -157,7 +160,11 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im } if (image) { BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage); - if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) { + BOOL lazyDecode = [coderOptions[SDImageCoderDecodeUseLazyDecoding] boolValue]; + if (lazyDecode) { + // lazyDecode = NO means we should not forceDecode, highest priority + shouldDecode = NO; + } else if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) { // `SDAnimatedImage` do not decode shouldDecode = NO; } else if (image.sd_isAnimated) { @@ -167,10 +174,10 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im if (shouldDecode) { image = [SDImageCoderHelper decodedImageWithImage:image]; } - // mark the image as progressive (completed one are not mark as progressive) - image.sd_isIncremental = !finished; // assign the decode options, to let manager check whether to re-decode if needed image.sd_decodeOptions = coderOptions; + // mark the image as progressive (completed one are not mark as progressive) + image.sd_isIncremental = !finished; } return image; diff --git a/SDWebImage/Core/SDWebImageDefine.h b/SDWebImage/Core/SDWebImageDefine.h index 88f2733e..df7c7fa4 100644 --- a/SDWebImage/Core/SDWebImageDefine.h +++ b/SDWebImage/Core/SDWebImageDefine.h @@ -169,7 +169,8 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { /** * By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation. - * However, this process may increase the memory usage as well. If you are experiencing a issue due to excessive memory consumption, This flag can prevent decode the image. + * However, this process may increase the memory usage as well. If you are experiencing an issue due to excessive memory consumption, This flag can prevent decode the image. + * @note 5.14.0 introduce `SDImageCoderDecodeUseLazyDecoding`, use that for better control from codec, instead of post-processing. Which acts the similar like this option but works for SDAnimatedImage as well (this one does not) */ SDWebImageAvoidDecodeImage = 1 << 18, diff --git a/SDWebImage/Core/SDWebImageDownloaderOperation.m b/SDWebImage/Core/SDWebImageDownloaderOperation.m index 8c8f8955..2729dc50 100644 --- a/SDWebImage/Core/SDWebImageDownloaderOperation.m +++ b/SDWebImage/Core/SDWebImageDownloaderOperation.m @@ -602,7 +602,7 @@ didReceiveResponse:(NSURLResponse *)response } }]; } - if (@available(iOS 13.0, *)) { + if (@available(iOS 13.0, tvOS 13.0, macOS 10.15, watchOS 6.0, *)) { [self.coderQueue addBarrierBlock:^{ @strongify(self); if (!self) { diff --git a/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h b/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h index 4807ab77..6d8892db 100644 --- a/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h +++ b/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h @@ -32,7 +32,7 @@ + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source; + (NSUInteger)imageLoopCountWithSource:(nonnull CGImageSourceRef)source; -+ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize forceDecode:(BOOL)forceDecode options:(nullable NSDictionary *)options; ++ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode options:(nullable NSDictionary *)options; + (BOOL)canEncodeToFormat:(SDImageFormat)format; + (BOOL)canDecodeFromFormat:(SDImageFormat)format; diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 7fed7722..510529bd 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -369,7 +369,14 @@ SDImageCoderEncodeMaxPixelSize: @(thumbnailSize) }]; UIImage *encodedImage = [UIImage sd_imageWithData:encodedData]; - expect(encodedImage.size).equal(CGSizeMake(4000, 2629)); + // Encode keep aspect ratio, but will use scale down instead of scale up if we strip the image-io related info (to fix some Apple's bug) + // See more in `SDCGImageCreateCopy` + expect(image.sd_isDecoded).beFalsy(); + if (@available(iOS 15, tvOS 15, *)) { + expect(encodedImage.size).equal(CGSizeMake(4000, 2628)); + } else { + expect(encodedImage.size).equal(CGSizeMake(4000, 2629)); + } } - (void)test24ThatScaleSizeCalculation { diff --git a/Tests/Tests/SDWebImageDownloaderTests.m b/Tests/Tests/SDWebImageDownloaderTests.m index a95bdc62..7639157b 100644 --- a/Tests/Tests/SDWebImageDownloaderTests.m +++ b/Tests/Tests/SDWebImageDownloaderTests.m @@ -345,19 +345,22 @@ - (void)test17ThatMinimumProgressIntervalWorks { XCTestExpectation *expectation = [self expectationWithDescription:@"Minimum progress interval"]; SDWebImageDownloaderConfig *config = SDWebImageDownloaderConfig.defaultDownloaderConfig; - config.minimumProgressInterval = 0.51; // This will make the progress only callback twice (once is 51%, another is 100%) + config.minimumProgressInterval = 0.51; // This will make the progress only callback at most 4 times (-1, 0%, 51%, 100%) SDWebImageDownloader *downloader = [[SDWebImageDownloader alloc] initWithConfig:config]; NSURL *imageURL = [NSURL URLWithString:@"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_1.jpg"]; __block NSUInteger allProgressCount = 0; // All progress (including operation start / first HTTP response, etc) + __block BOOL completed = NO; [downloader downloadImageWithURL:imageURL options:0 progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { allProgressCount++; } completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished) { + if (completed) { + return; + } if (allProgressCount > 0) { [expectation fulfill]; - allProgressCount = 0; - return; + completed = YES; } else { - XCTFail(@"Progress callback more than once"); + XCTFail(@"Completed callback before progress update"); } }];