diff --git a/SDWebImage/Core/SDImageCache.h b/SDWebImage/Core/SDImageCache.h index 96b54e72..9a75580c 100644 --- a/SDWebImage/Core/SDImageCache.h +++ b/SDWebImage/Core/SDImageCache.h @@ -55,6 +55,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { SDImageCacheMatchAnimatedImageClass = 1 << 7, }; +/** + * A token associated with each cache query. Can be used to cancel a cache query + */ @interface SDImageCacheToken : NSObject /** diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index a7be63e4..28e12401 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -10,6 +10,15 @@ #import "SDWebImageCompat.h" #import "SDImageFrame.h" +typedef NS_ENUM(NSUInteger, SDImageCoderDecodeSolution) { + /// automatically choose the solution based on image format, hardware, OS version. This keep balance for compatibility and performance. Default after SDWebImage 5.13.0 + SDImageCoderDecodeSolutionAutomatic, + /// always use CoreGraphics to draw on bitmap context and trigger decode. Best compatibility. Default before SDWebImage 5.13.0 + SDImageCoderDecodeSolutionCoreGraphics, + /// available on iOS/tvOS 15+, use UIKit's new CGImageDecompressor/CMPhoto to decode. Best performance. If failed, will fallback to CoreGraphics as well + SDImageCoderDecodeSolutionUIKit +}; + /** Provide some common helper methods for building the image decoder/encoder. */ @@ -111,6 +120,12 @@ */ + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes; +/** + Control the default force decode solution. Available solutions in `SDImageCoderDecodeSolution`. + @note Defaults to `SDImageCoderDecodeSolutionAutomatic`, which prefers to use UIKit for JPEG/HEIF, and fallback on CoreGraphics. If you want control on your hand, set the other solution. + */ +@property (class, readwrite) SDImageCoderDecodeSolution defaultDecodeSolution; + /** Control the default limit bytes to scale down largest images. This value must be larger than 4 Bytes (at least 1x1 pixel). Defaults to 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 763a9a49..da30b65a 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -16,12 +16,64 @@ #import "UIImage+Metadata.h" #import "SDInternalMacros.h" #import "SDGraphicsImageRenderer.h" +#import "SDInternalMacros.h" #import static inline size_t SDByteAlign(size_t size, size_t alignment) { return ((size + (alignment - 1)) / alignment) * alignment; } +#if SD_UIKIT +static inline UIImage *SDImageDecodeUIKit(UIImage *image) { + // See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay + // Need CGImage-based + if (@available(iOS 15, tvOS 15, *)) { + UIImage *decodedImage = [image imageByPreparingForDisplay]; + if (decodedImage) { + SDImageCopyAssociatedObject(image, decodedImage); + decodedImage.sd_isDecoded = YES; + return decodedImage; + } + } + return nil; +} + +static inline UIImage *SDImageDecodeAndScaleDownUIKit(UIImage *image, CGSize destResolution) { + // See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize + // Need CGImage-based + if (@available(iOS 15, tvOS 15, *)) { + // Calculate thumbnail point size + CGFloat scale = image.scale ?: 1; + CGSize thumbnailSize = CGSizeMake(destResolution.width / scale, destResolution.height / scale); + UIImage *decodedImage = [image imageByPreparingThumbnailOfSize:thumbnailSize]; + if (decodedImage) { + SDImageCopyAssociatedObject(image, decodedImage); + decodedImage.sd_isDecoded = YES; + return decodedImage; + } + } + return nil; +} + +static inline BOOL SDImageSupportsHardwareHEVCDecoder(void) { + static dispatch_once_t onceToken; + static BOOL supportsHardware = NO; + dispatch_once(&onceToken, ^{ + SEL DeviceInfoSelector = SD_SEL_SPI(deviceInfoForKey:); + NSString *HEVCDecoder8bitSupported = @"N8lZxRgC7lfdRS3dRLn+Ag"; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + if ([UIDevice.currentDevice respondsToSelector:DeviceInfoSelector] && [UIDevice.currentDevice performSelector:DeviceInfoSelector withObject:HEVCDecoder8bitSupported]) { + supportsHardware = YES; + } +#pragma clang diagnostic pop + }); + return supportsHardware; +} +#endif + +static SDImageCoderDecodeSolution kDefaultDecodeSolution = SDImageCoderDecodeSolutionAutomatic; + static const size_t kBytesPerPixel = 4; static const size_t kBitsPerComponent = 8; @@ -367,16 +419,23 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return image; } + UIImage *decodedImage; #if SD_UIKIT - // See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay - // Need CGImage-based - if (@available(iOS 15, tvOS 15, *)) { - UIImage *decodedImage = [image imageByPreparingForDisplay]; - if (decodedImage) { - SDImageCopyAssociatedObject(image, decodedImage); - decodedImage.sd_isDecoded = YES; - return decodedImage; + SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution; + if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) { + // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :( + SDImageFormat format = image.sd_imageFormat; + if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) { + decodedImage = SDImageDecodeUIKit(image); + } else if (format == SDImageFormatJPEG) { + decodedImage = SDImageDecodeUIKit(image); } + } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) { + // Arbitrarily call CMPhoto + decodedImage = SDImageDecodeUIKit(image); + } + if (decodedImage) { + return decodedImage; } #endif @@ -392,7 +451,7 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over format.scale = image.scale; CGSize imageSize = image.size; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format]; - UIImage *decodedImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) { + decodedImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) { [image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)]; }]; SDImageCopyAssociatedObject(image, decodedImage); @@ -432,24 +491,26 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale)); destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale)); + UIImage *decodedImage; #if SD_UIKIT - // See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize - // Need CGImage-based - if (@available(iOS 15, tvOS 15, *)) { - // Calculate thumbnail point size - CGFloat scale = image.scale ?: 1; - CGSize thumbnailSize = CGSizeMake(destResolution.width / scale, destResolution.height / scale); - UIImage *decodedImage = [image imageByPreparingThumbnailOfSize:thumbnailSize]; - if (decodedImage) { - SDImageCopyAssociatedObject(image, decodedImage); - decodedImage.sd_isDecoded = YES; - return decodedImage; + SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution; + if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) { + // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :( + SDImageFormat format = image.sd_imageFormat; + if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) { + decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); + } else if (format == SDImageFormatJPEG) { + decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); } + } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) { + // Arbitrarily call CMPhoto + decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); + } + if (decodedImage) { + return decodedImage; } #endif - CGContextRef destContext = NULL; - // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; @autoreleasepool { @@ -469,13 +530,13 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over // RGB888 bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; } - destContext = CGBitmapContextCreate(NULL, - destResolution.width, - destResolution.height, - kBitsPerComponent, - 0, - colorspaceRef, - bitmapInfo); + CGContextRef destContext = CGBitmapContextCreate(NULL, + destResolution.width, + destResolution.height, + kBitsPerComponent, + 0, + colorspaceRef, + bitmapInfo); if (destContext == NULL) { return image; @@ -543,20 +604,25 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return image; } #if SD_MAC - UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp]; + decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp]; #else - UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; + decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; #endif CGImageRelease(destImageRef); - if (destImage == nil) { - return image; - } - SDImageCopyAssociatedObject(image, destImage); - destImage.sd_isDecoded = YES; - return destImage; + SDImageCopyAssociatedObject(image, decodedImage); + decodedImage.sd_isDecoded = YES; + return decodedImage; } } ++ (SDImageCoderDecodeSolution)defaultDecodeSolution { + return kDefaultDecodeSolution; +} + ++ (void)setDefaultDecodeSolution:(SDImageCoderDecodeSolution)defaultDecodeSolution { + kDefaultDecodeSolution = defaultDecodeSolution; +} + + (NSUInteger)defaultScaleDownLimitBytes { return kDestImageLimitBytes; } diff --git a/SDWebImage/Core/UIImage+ForceDecode.m b/SDWebImage/Core/UIImage+ForceDecode.m index 1b20bbd7..9fc72588 100644 --- a/SDWebImage/Core/UIImage+ForceDecode.m +++ b/SDWebImage/Core/UIImage+ForceDecode.m @@ -9,12 +9,30 @@ #import "UIImage+ForceDecode.h" #import "SDImageCoderHelper.h" #import "objc/runtime.h" +#import "NSImage+Compatibility.h" @implementation UIImage (ForceDecode) - (BOOL)sd_isDecoded { NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isDecoded)); - return value.boolValue; + if (value != nil) { + return value.boolValue; + } else { + // Assume only CGImage based can use lazy decoding + CGImageRef cgImage = self.CGImage; + if (cgImage) { + CFStringRef uttype = CGImageGetUTType(self.CGImage); + if (uttype) { + // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier` + return NO; + } else { + // Thumbnail or CGBitmapContext drawn image + return YES; + } + } + } + // Assume others as non-decoded + return NO; } - (void)setSd_isDecoded:(BOOL)sd_isDecoded { diff --git a/SDWebImage/Core/UIImage+Metadata.m b/SDWebImage/Core/UIImage+Metadata.m index 5a8002c4..a526f9b9 100644 --- a/SDWebImage/Core/UIImage+Metadata.m +++ b/SDWebImage/Core/UIImage+Metadata.m @@ -166,10 +166,8 @@ return imageFormat; } // Check CGImage's UTType, may return nil for non-Image/IO based image - if (@available(iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0, *)) { - CFStringRef uttype = CGImageGetUTType(self.CGImage); - imageFormat = [NSData sd_imageFormatFromUTType:uttype]; - } + CFStringRef uttype = CGImageGetUTType(self.CGImage); + imageFormat = [NSData sd_imageFormatFromUTType:uttype]; return imageFormat; }