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;