Merge pull request #3461 from dreampiggy/bugfix/yuv420_jpeg_hevc

Fix the iOS 15+ force-decode hack break Apple's HEIF and JPEG YUV420 optimization
This commit is contained in:
DreamPiggy 2022-12-27 19:57:48 +08:00 committed by GitHub
commit 8be9f2c3f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 45 additions and 59 deletions

View File

@ -183,12 +183,8 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
}
+ (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
NSDictionary *options = @{
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
(__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
};
NSTimeInterval frameDuration = 0.1;
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, NULL);
if (!cfFrameProperties) {
return frameDuration;
}
@ -218,10 +214,24 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
return frameDuration;
}
+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode options:(NSDictionary *)options {
// Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :)
+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage {
// `animatedImage` means called from `SDAnimatedImageProvider.animatedImageFrameAtIndex`
NSDictionary *options;
if (animatedImage) {
if (!lazyDecode) {
options = @{
// image decoding and caching should happen at image creation time.
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
};
} else {
options = @{
// image decoding will happen at rendering time
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(NO),
};
}
}
// Parse the image properties
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL);
CGFloat pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
CGFloat pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
@ -287,7 +297,7 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
isDecoded = YES;
}
}
} else {
} else if (animatedImage) {
// iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273
if (@available(iOS 15, tvOS 15, *)) {
// User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef
@ -297,21 +307,19 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
CGImageRelease(imageRef);
imageRef = newImageRef;
}
}
}
#if SD_CHECK_CGIMAGE_RETAIN_SOURCE
if (@available(iOS 15, tvOS 15, *)) {
// Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+)
// If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SDCGImageGetImageSource = dlsym(RTLD_DEFAULT, "CGImageGetImageSource");
});
if (SDCGImageGetImageSource) {
NSCAssert(!SDCGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock");
// Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+)
// If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SDCGImageGetImageSource = dlsym(RTLD_DEFAULT, "CGImageGetImageSource");
});
if (SDCGImageGetImageSource) {
NSCAssert(!SDCGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock");
}
#endif
}
}
#endif
#if SD_UIKIT || SD_WATCH
UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation];
@ -412,12 +420,12 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
if (decodeFirstFrame || count <= 1) {
animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:nil];
animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
} else {
NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:count];
for (size_t i = 0; i < count; i++) {
UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:nil];
UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
if (!image) {
continue;
}
@ -473,7 +481,7 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
preserveAspectRatio = preserveAspectRatioValue.boolValue;
}
_preserveAspectRatio = preserveAspectRatio;
BOOL lazyDecode = YES; // Defaults YES for static image coder
BOOL lazyDecode = NO; // Defaults NO for animated image coder
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
if (lazyDecodeValue != nil) {
lazyDecode = lazyDecodeValue.boolValue;
@ -502,11 +510,7 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
if (_width + _height == 0) {
NSDictionary *options = @{
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
(__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
};
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, (__bridge CFDictionaryRef)options);
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
if (properties) {
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
@ -533,7 +537,7 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
if (scaleFactor != nil) {
scale = MAX([scaleFactor doubleValue], 1);
}
image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode options:nil];
image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
if (image) {
image.sd_imageFormat = self.class.imageFormat;
}
@ -695,6 +699,12 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
preserveAspectRatio = preserveAspectRatioValue.boolValue;
}
_preserveAspectRatio = preserveAspectRatio;
BOOL lazyDecode = NO; // Defaults NO for animated image coder
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
if (lazyDecodeValue != nil) {
lazyDecode = lazyDecodeValue.boolValue;
}
_lazyDecode = lazyDecode;
_imageSource = imageSource;
_imageData = data;
#if SD_UIKIT
@ -785,24 +795,7 @@ static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
}
- (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
NSDictionary *options;
BOOL lazyDecode = NO; // Defaults NO for animated image coder
NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
if (lazyDecodeValue != nil) {
lazyDecode = lazyDecodeValue.boolValue;
}
if (!lazyDecode) {
options = @{
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(NO),
(__bridge NSString *)kCGImageSourceShouldCache : @(NO)
};
} else {
options = @{
(__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
(__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
};
}
UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:lazyDecode options:options];
UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:YES];
if (!image) {
return nil;
}

View File

@ -211,7 +211,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
CFStringRef uttype = CGImageSourceGetType(source);
SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype];
UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode options:nil];
UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
CFRelease(source);
image.sd_imageFormat = imageFormat;
@ -306,7 +306,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
if (scaleFactor != nil) {
scale = MAX([scaleFactor doubleValue], 1);
}
image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode options:nil];
image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
if (image) {
CFStringRef uttype = CGImageSourceGetType(_imageSource);
image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype];

View File

@ -32,7 +32,7 @@
+ (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source;
+ (NSUInteger)imageLoopCountWithSource:(nonnull CGImageSourceRef)source;
+ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode options:(nullable NSDictionary *)options;
+ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage;
+ (BOOL)canEncodeToFormat:(SDImageFormat)format;
+ (BOOL)canDecodeFromFormat:(SDImageFormat)format;

View File

@ -363,14 +363,7 @@
SDImageCoderEncodeMaxPixelSize: @(thumbnailSize)
}];
UIImage *encodedImage = [UIImage sd_imageWithData:encodedData];
// Encode keep aspect ratio, but will use scale down instead of scale up if we strip the image-io related info (to fix some Apple's bug)
// See more in `SDCGImageCreateCopy`
expect(image.sd_isDecoded).beFalsy();
if (@available(iOS 15, tvOS 15, *)) {
expect(encodedImage.size).equal(CGSizeMake(4000, 2628));
} else {
expect(encodedImage.size).equal(CGSizeMake(4000, 2629));
}
expect(encodedImage.size).equal(CGSizeMake(4000, 2629));
}
- (void)test24ThatScaleSizeCalculation {