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:
commit
213be11357
|
@ -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