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+
|
// 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
|
// 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;
|
if (!image) return nil;
|
||||||
size_t width = CGImageGetWidth(image);
|
size_t width = CGImageGetWidth(image);
|
||||||
size_t height = CGImageGetHeight(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 bitsPerPixel = CGImageGetBitsPerPixel(image);
|
||||||
size_t bytesPerRow = CGImageGetBytesPerRow(image);
|
size_t bytesPerRow = CGImageGetBytesPerRow(image);
|
||||||
CGColorSpaceRef space = CGImageGetColorSpace(image);
|
CGColorSpaceRef space = CGImageGetColorSpace(image);
|
||||||
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
|
|
||||||
CGDataProviderRef provider = CGImageGetDataProvider(image);
|
CGDataProviderRef provider = CGImageGetDataProvider(image);
|
||||||
const CGFloat *decode = CGImageGetDecode(image);
|
const CGFloat *decode = CGImageGetDecode(image);
|
||||||
bool shouldInterpolate = CGImageGetShouldInterpolate(image);
|
bool shouldInterpolate = CGImageGetShouldInterpolate(image);
|
||||||
|
@ -49,6 +48,207 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
||||||
return newImage;
|
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
|
@interface SDImageIOCoderFrame : NSObject
|
||||||
|
|
||||||
@property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
|
@property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
|
||||||
|
@ -323,6 +523,14 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
|
||||||
#endif
|
#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
|
#if SD_UIKIT || SD_WATCH
|
||||||
UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
|
UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
|
||||||
|
|
|
@ -68,8 +68,10 @@ static inline UIColor * SDGetColorFromGrayscale(Pixel_88 pixel, CGBitmapInfo bit
|
||||||
case kCGBitmapByteOrderDefault: {
|
case kCGBitmapByteOrderDefault: {
|
||||||
byteOrderNormal = YES;
|
byteOrderNormal = YES;
|
||||||
} break;
|
} break;
|
||||||
|
case kCGBitmapByteOrder16Little:
|
||||||
case kCGBitmapByteOrder32Little: {
|
case kCGBitmapByteOrder32Little: {
|
||||||
} break;
|
} break;
|
||||||
|
case kCGBitmapByteOrder16Big:
|
||||||
case kCGBitmapByteOrder32Big: {
|
case kCGBitmapByteOrder32Big: {
|
||||||
byteOrderNormal = YES;
|
byteOrderNormal = YES;
|
||||||
} break;
|
} break;
|
||||||
|
|
Loading…
Reference in New Issue