Ver Fonte

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)
DreamPiggy há 1 ano atrás
pai
commit
ecedea2e06

+ 6 - 0
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.

+ 44 - 4
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
+        // <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 {
     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;
             }

+ 5 - 6
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;
 }

+ 8 - 0
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)