From d5dccaeeef910acfabf75c142b31fc3555a9708d Mon Sep 17 00:00:00 2001 From: DreamPiggy Date: Fri, 10 Nov 2023 18:39:47 +0800 Subject: [PATCH] Introduce the hacky workaround for iOS 17 ImageIO indexed color png code bug This use the runtime detection instead of available check, more stable When we detect the ImageIO is buggy, we will change the correct alpha info with non-premultiplied --- SDWebImage/Core/SDImageIOAnimatedCoder.m | 212 ++++++++++++++++++++++- SDWebImage/Core/UIImage+Transform.m | 2 + 2 files changed, 212 insertions(+), 2 deletions(-) diff --git a/SDWebImage/Core/SDImageIOAnimatedCoder.m b/SDWebImage/Core/SDImageIOAnimatedCoder.m index 9ceef23b..f5d33a04 100644 --- a/SDWebImage/Core/SDImageIOAnimatedCoder.m +++ b/SDWebImage/Core/SDImageIOAnimatedCoder.m @@ -32,7 +32,7 @@ static NSString * kSDCGImageSourceSkipMetadata = @"kCGImageSourceSkipMetadata"; // 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) { +static CGImageRef __nullable SDCGImageCreateMutableCopy(CGImageRef cg_nullable image, CGBitmapInfo bitmapInfo) { if (!image) return nil; size_t width = CGImageGetWidth(image); size_t height = CGImageGetHeight(image); @@ -40,7 +40,6 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable 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); @@ -49,6 +48,207 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) { return newImage; } +static inline CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) { + if (!image) return nil; + return SDCGImageCreateMutableCopy(image, CGImageGetBitmapInfo(image)); +} + +static BOOL SDLoadOnePixelBitmapBuffer(CGImageRef imageRef, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a) { + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); + CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; + CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; + + // Get pixels + CGDataProviderRef provider = CGImageGetDataProvider(imageRef); + if (!provider) { + return NO; + } + CFDataRef data = CGDataProviderCopyData(provider); + if (!data) { + return NO; + } + + CFRange range = CFRangeMake(0, 4); // one pixel + if (CFDataGetLength(data) < range.location + range.length) { + CFRelease(data); + return NO; + } + uint8_t pixel[4] = {0}; + CFDataGetBytes(data, range, pixel); + CFRelease(data); + + BOOL byteOrderNormal = NO; + switch (byteOrderInfo) { + case kCGBitmapByteOrderDefault: { + byteOrderNormal = YES; + } break; + case kCGBitmapByteOrder16Little: + case kCGBitmapByteOrder32Little: { + } break; + case kCGBitmapByteOrder16Big: + case kCGBitmapByteOrder32Big: { + byteOrderNormal = YES; + } break; + default: break; + } + switch (alphaInfo) { + case kCGImageAlphaPremultipliedFirst: + case kCGImageAlphaFirst: { + if (byteOrderNormal) { + // ARGB8888 + *a = pixel[0]; + *r = pixel[1]; + *g = pixel[2]; + *b = pixel[3]; + } else { + // BGRA8888 + *b = pixel[0]; + *g = pixel[1]; + *r = pixel[2]; + *a = pixel[3]; + } + } + break; + case kCGImageAlphaPremultipliedLast: + case kCGImageAlphaLast: { + if (byteOrderNormal) { + // RGBA8888 + *r = pixel[0]; + *g = pixel[1]; + *b = pixel[2]; + *a = pixel[3]; + } else { + // ABGR8888 + *a = pixel[0]; + *b = pixel[1]; + *g = pixel[2]; + *r = pixel[3]; + } + } + break; + case kCGImageAlphaNone: { + if (byteOrderNormal) { + // RGB + *r = pixel[0]; + *g = pixel[1]; + *b = pixel[2]; + } else { + // BGR + *b = pixel[0]; + *g = pixel[1]; + *r = pixel[2]; + } + } + break; + case kCGImageAlphaNoneSkipLast: { + if (byteOrderNormal) { + // RGBX + *r = pixel[0]; + *g = pixel[1]; + *b = pixel[2]; + } else { + // XBGR + *b = pixel[1]; + *g = pixel[2]; + *r = pixel[3]; + } + } + break; + case kCGImageAlphaNoneSkipFirst: { + if (byteOrderNormal) { + // XRGB + *r = pixel[1]; + *g = pixel[2]; + *b = pixel[3]; + } else { + // BGRX + *b = pixel[0]; + *g = pixel[1]; + *r = pixel[2]; + } + } + break; + case kCGImageAlphaOnly: { + // A + *a = pixel[0]; + } + break; + default: + break; + } + + return YES; +} + +static CGImageRef SDImageIOPNGPluginBuggyCreateWorkaround(CGImageRef cgImage) CF_RETURNS_RETAINED { + CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage); + CGImageAlphaInfo alphaInfo = (bitmapInfo & kCGBitmapAlphaInfoMask); + CGImageAlphaInfo newAlphaInfo = alphaInfo; + if (alphaInfo == kCGImageAlphaPremultipliedLast) { + newAlphaInfo = kCGImageAlphaLast; + } else if (alphaInfo == kCGImageAlphaPremultipliedFirst) { + newAlphaInfo = kCGImageAlphaFirst; + } + if (newAlphaInfo != alphaInfo) { + CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; + CGBitmapInfo newBitmapInfo = newAlphaInfo | byteOrderInfo; + if (SD_OPTIONS_CONTAINS(bitmapInfo, kCGBitmapFloatComponents)) { + // Keep float components + newBitmapInfo |= kCGBitmapFloatComponents; + } + // Create new CGImage with corrected alpha info... + CGImageRef newCGImage = SDCGImageCreateMutableCopy(cgImage, newBitmapInfo); + return newCGImage; + } else { + CGImageRetain(cgImage); + return cgImage; + } +} + +static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) { + // See: #3605 FB13322459 + // ImageIO on iOS 17 (17.0~17.2), there is one serious problem on ImageIO PNG plugin. The decode result for indexed color PNG use the wrong CGImageAlphaInfo + // The returned CGImageAlphaInfo is alpha last, but the actual bitmap data is premultiplied alpha first, which cause many runtime render bug. + // So, we do a hack workaround: + // 1. Decode a indexed color PNG in runtime + // 2. If the bitmap is premultiplied alpha, then assume it's buggy + // 3. If buggy, then all premultiplied `CGImageAlphaInfo` will assume to be non-premultiplied + // :) + + if (@available(iOS 17, tvOS 17, macOS 14, watchOS 11, *)) { + // Continue + } else { + return NO; + } + static BOOL isBuggy = NO; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSString *base64String = @"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUyMjKlMgnVAAAAAXRSTlMyiDGJ5gAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="; + NSData *onePixelIndexedPNGData = [[NSData alloc] initWithBase64EncodedString:base64String options:NSDataBase64DecodingIgnoreUnknownCharacters]; + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)onePixelIndexedPNGData, nil); + NSCParameterAssert(source); + CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil); + NSCParameterAssert(cgImage); + uint8_t r, g, b, a = 0; + BOOL success = SDLoadOnePixelBitmapBuffer(cgImage, &r, &g, &b, &a); + if (!success) { + isBuggy = NO; // Impossible... + } else { + if (r == 50 && g == 50 && b == 50 && a == 50) { + // Correct value + isBuggy = NO; + } else { +#if DEBUG + NSLog(@"Detected the current OS's ImageIO PNG Decoder is buggy on indexed color PNG. Perform workaround solution..."); + isBuggy = YES; +#endif + } + } + }); + + return isBuggy; +} + @interface SDImageIOCoderFrame : NSObject @property (nonatomic, assign) NSUInteger index; // Frame index (zero based) @@ -323,6 +523,14 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) { #endif } } + // :) + CFStringRef uttype = CGImageSourceGetType(source); + SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype]; + if (imageFormat == SDImageFormatPNG && SDImageIOPNGPluginBuggyNeedWorkaround()) { + CGImageRef newImageRef = SDImageIOPNGPluginBuggyCreateWorkaround(imageRef); + CGImageRelease(imageRef); + imageRef = newImageRef; + } #if SD_UIKIT || SD_WATCH UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation]; diff --git a/SDWebImage/Core/UIImage+Transform.m b/SDWebImage/Core/UIImage+Transform.m index 43122df1..3ce0c97d 100644 --- a/SDWebImage/Core/UIImage+Transform.m +++ b/SDWebImage/Core/UIImage+Transform.m @@ -68,8 +68,10 @@ static inline UIColor * SDGetColorFromGrayscale(Pixel_88 pixel, CGBitmapInfo bit case kCGBitmapByteOrderDefault: { byteOrderNormal = YES; } break; + case kCGBitmapByteOrder16Little: case kCGBitmapByteOrder32Little: { } break; + case kCGBitmapByteOrder16Big: case kCGBitmapByteOrder32Big: { byteOrderNormal = YES; } break;