Use the better way to detect lazy/non-lazy CGImage. Only do force decoding for lazy image

This effect the thumbnail decoding (which produce non-lazy CGImage, but accidentally been force decoded)
This commit is contained in:
DreamPiggy 2024-08-02 17:25:02 +08:00
parent 600e1b68af
commit ecedea2e06
4 changed files with 63 additions and 10 deletions

View File

@ -107,6 +107,12 @@ typedef struct SDImagePixelFormat {
*/ */
+ (BOOL)CGImageContainsAlpha:(_Nonnull CGImageRef)cgImage; + (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. 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. 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.

View File

@ -381,6 +381,45 @@ static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to over
return hasAlpha; 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
// <CGImage 0x10740ffe0> (IP) -> YES
// <CGImage 0x10740ffe0> (DP) -> NO
NSArray<NSString *> *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 { + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp]; 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) // Check policy (automatic)
CGImageRef cgImage = image.CGImage; CGImageRef cgImage = image.CGImage;
if (cgImage) { if (cgImage) {
CFStringRef uttype = CGImageGetUTType(cgImage); // Check if it's lazy CGImage wrapper or not
if (uttype) { BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:cgImage];
// Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier` if (isLazy) {
// Lazy CGImage should trigger force decode before rendering
return YES; return YES;
} else { } 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]; BOOL isSupported = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage];
return !isSupported; return !isSupported;
} }

View File

@ -479,7 +479,6 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
if (!imageRef) { if (!imageRef) {
return nil; return nil;
} }
BOOL isDecoded = NO;
// Thumbnail image post-process // Thumbnail image post-process
if (!createFullImage) { if (!createFullImage) {
if (preserveAspectRatio) { if (preserveAspectRatio) {
@ -491,19 +490,19 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
if (scaledImageRef) { if (scaledImageRef) {
CGImageRelease(imageRef); CGImageRelease(imageRef);
imageRef = scaledImageRef; imageRef = scaledImageRef;
isDecoded = YES;
} }
} }
} }
// Check whether output CGImage is decoded // Check whether output CGImage is decoded
BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:imageRef];
if (!lazyDecode) { if (!lazyDecode) {
if (!isDecoded) { if (isLazy) {
// Use CoreGraphics to trigger immediately decode // Use CoreGraphics to trigger immediately decode to drop lazy CGImage
CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef]; CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
if (decodedImageRef) { if (decodedImageRef) {
CGImageRelease(imageRef); CGImageRelease(imageRef);
imageRef = decodedImageRef; imageRef = decodedImageRef;
isDecoded = YES; isLazy = NO;
} }
} }
} else if (animatedImage) { } else if (animatedImage) {
@ -545,7 +544,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation]; UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
#endif #endif
CGImageRelease(imageRef); CGImageRelease(imageRef);
image.sd_isDecoded = isDecoded; image.sd_isDecoded = !isLazy;
return image; return image;
} }

View File

@ -353,6 +353,10 @@
CGSize imageSize = image.size; CGSize imageSize = image.size;
expect(imageSize.width).equal(400); expect(imageSize.width).equal(400);
expect(imageSize.height).equal(263); 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 { - (void)test23ThatThumbnailEncodeCalculation {
@ -360,6 +364,10 @@
NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath]; NSData *testImageData = [NSData dataWithContentsOfFile:testImagePath];
UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:nil]; UIImage *image = [SDImageIOCoder.sharedCoder decodedImageWithData:testImageData options:nil];
expect(image.size).equal(CGSizeMake(5250, 3450)); 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 CGSize thumbnailSize = CGSizeMake(4000, 4000); // 3450 < 4000 < 5250
NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEG options:@{ NSData *encodedData = [SDImageIOCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatJPEG options:@{
SDImageCoderEncodeMaxPixelSize: @(thumbnailSize) SDImageCoderEncodeMaxPixelSize: @(thumbnailSize)