Merge pull request #3634 from dreampiggy/bugfix/workaround_hacky_imageio_png_indexed_color

Introduce the hacky workaround for iOS 17 ImageIO indexed color png decode (Apple's bug)
This commit is contained in:
DreamPiggy 2023-11-15 16:11:20 +08:00 committed by GitHub
commit 213be11357
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 212 additions and 2 deletions

View File

@ -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];

View File

@ -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;