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
This commit is contained in:
parent
fd1950de05
commit
d5dccaeeef
|
@ -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];
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue