diff --git a/SDWebImage/Core/SDImageCoderHelper.h b/SDWebImage/Core/SDImageCoderHelper.h index d0b51152..fe51c4f5 100644 --- a/SDWebImage/Core/SDImageCoderHelper.h +++ b/SDWebImage/Core/SDImageCoderHelper.h @@ -107,6 +107,12 @@ typedef struct SDImagePixelFormat { */ + (BOOL)CGImageContainsAlpha:(_Nonnull CGImageRef)cgImage; +/** + Detect whether the CGImage is lazy and not-yet decoded. (lazy means, only when the caller access the underlying bitmap buffer via provider like `CGDataProviderCopyData` or `CGDataProviderRetainBytePtr`, the decoder will allocate memory, it's a lazy allocation) + The implementation use the Core Graphics internal to check whether the CGImage is `CGImageProvider` based, or `CGDataProvider` based. The `CGDataProvider` based is treated as non-lazy. + */ ++ (BOOL)CGImageIsLazy:(_Nonnull CGImageRef)cgImage; + /** Create a decoded CGImage by the provided CGImage. This follows The Create Rule and you are response to call release after usage. It will detect whether image contains alpha channel, then create a new bitmap context with the same size of image, and draw it. This can ensure that the image do not need extra decoding after been set to the imageView. diff --git a/SDWebImage/Core/SDImageCoderHelper.m b/SDWebImage/Core/SDImageCoderHelper.m index 0808685b..422e0f98 100644 --- a/SDWebImage/Core/SDImageCoderHelper.m +++ b/SDWebImage/Core/SDImageCoderHelper.m @@ -381,6 +381,45 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over return hasAlpha; } ++ (BOOL)CGImageIsLazy:(CGImageRef)cgImage { + if (!cgImage) { + return NO; + } + // CoreGraphics use CGImage's C struct filed (offset 0xd8 on iOS 17.0) + // But since the description of `CGImageRef` always contains the `[DP]` (DataProvider) and `[IP]` (ImageProvider), we can use this as a hint + NSString *description = (__bridge_transfer NSString *)CFCopyDescription(cgImage); + if (description) { + // Solution 1: Parse the description to get provider + // (IP) -> YES + // (DP) -> NO + NSArray *lines = [description componentsSeparatedByString:@"\n"]; + if (lines.count > 0) { + NSString *firstLine = lines[0]; + NSRange startRange = [firstLine rangeOfString:@"("]; + NSRange endRange = [firstLine rangeOfString:@")"]; + if (startRange.location != NSNotFound && endRange.location != NSNotFound) { + NSRange resultRange = NSMakeRange(startRange.location + 1, endRange.location - startRange.location - 1); + NSString *providerString = [firstLine substringWithRange:resultRange]; + if ([providerString isEqualToString:@"IP"]) { + return YES; + } else if ([providerString isEqualToString:@"DP"]) { + return NO; + } else { + // New cases ? fallback + } + } + } + } + // Solution 2: Use UTI metadata + CFStringRef uttype = CGImageGetUTType(cgImage); + if (uttype) { + // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier` metadata for lazy decoded CGImage + return YES; + } else { + return NO; + } +} + + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage { return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp]; } @@ -930,12 +969,13 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over // Check policy (automatic) CGImageRef cgImage = image.CGImage; if (cgImage) { - CFStringRef uttype = CGImageGetUTType(cgImage); - if (uttype) { - // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier` + // Check if it's lazy CGImage wrapper or not + BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:cgImage]; + if (isLazy) { + // Lazy CGImage should trigger force decode before rendering return YES; } else { - // Now, let's check if the CGImage is hardware supported (not byte-aligned will cause extra copy) + // Now, let's check if this non-lazy CGImage is hardware supported (not byte-aligned will cause extra copy) BOOL isSupported = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage]; return !isSupported; } diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index d30da12f..27665b46 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -479,7 +479,6 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) { if (!imageRef) { return nil; } - BOOL isDecoded = NO; // Thumbnail image post-process if (!createFullImage) { if (preserveAspectRatio) { @@ -491,19 +490,19 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) { if (scaledImageRef) { CGImageRelease(imageRef); imageRef = scaledImageRef; - isDecoded = YES; } } } // Check whether output CGImage is decoded + BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:imageRef]; if (!lazyDecode) { - if (!isDecoded) { - // Use CoreGraphics to trigger immediately decode + if (isLazy) { + // Use CoreGraphics to trigger immediately decode to drop lazy CGImage CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef]; if (decodedImageRef) { CGImageRelease(imageRef); imageRef = decodedImageRef; - isDecoded = YES; + isLazy = NO; } } } else if (animatedImage) { @@ -545,7 +544,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) { UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation]; #endif CGImageRelease(imageRef); - image.sd_isDecoded = isDecoded; + image.sd_isDecoded = !isLazy; return image; } diff --git a/Tests/Tests/SDImageCoderTests.m b/Tests/Tests/SDImageCoderTests.m index 5545f8b6..32866943 100644 --- a/Tests/Tests/SDImageCoderTests.m +++ b/Tests/Tests/SDImageCoderTests.m @@ -353,6 +353,10 @@ 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 { @@ -360,6 +364,10 @@ 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)