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