Skip to content

Commit e618417

Browse files
authored
Merge pull request SDWebImage#3798 from dreampiggy/bugfix/sd_colorAtPoint_early_return
fix: sd_colorAtPoint should early return when pixel format is not supported
2 parents c184125 + 207a03c commit e618417

File tree

7 files changed

+99
-21
lines changed

7 files changed

+99
-21
lines changed

SDWebImage/Core/SDImageCoder.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,9 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDec
8989
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleDownLimitBytes;
9090

9191
/**
92-
A Boolean value to provide converting to HDR during decoding.
92+
A Boolean value (NSNumber) to provide converting to HDR during decoding. Currently if number is 0, use SDR, else use HDR. But we may extend this option to use `NSUInteger` in the future (means, think this options as int number, but not actual boolean)
9393
@note Supported by iOS 17 and above when using ImageIO coder (third-party coder can support lower firmware)
94-
Defaults to NO, decoder will automatically convert SDR.
94+
Defaults to @(NO), decoder will automatically convert SDR.
9595
@note works for `SDImageCoder`
9696
*/
9797
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeToHDR;

SDWebImage/Core/SDWebImageDefine.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -338,9 +338,9 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageT
338338
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleDownLimitBytes;
339339

340340
/**
341-
A Boolean value to provide converting to HDR during decoding.
341+
A Boolean value (NSNumber) to provide converting to HDR during decoding. Currently if number is 0, use SDR, else use HDR. But we may extend this option to use `NSUInteger` in the future (means, think this options as int number, but not actual boolean)
342342
@note Supported by iOS 17 and above when using ImageIO coder (third-party coder can support lower firmware)
343-
Defaults to NO, decoder will automatically convert SDR.
343+
Defaults to @(NO), decoder will automatically convert SDR.
344344
*/
345345
FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageDecodeToHDR;
346346

SDWebImage/Core/UIImage+Transform.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ typedef NS_OPTIONS(NSUInteger, SDRectCorner) {
123123
@note The overhead of object creation means this method is best suited for infrequent color sampling. For heavy image processing, grab the raw bitmap data and process yourself.
124124
125125
@param point The position of pixel
126+
@warning This API currently support 8 bits per component only (RGBA8888 etc), not RGBA16U/RGBA10
126127
@return The color for specify pixel, or nil if any error occur
127128
*/
128129
- (nullable UIColor *)sd_colorAtPoint:(CGPoint)point;

SDWebImage/Core/UIImage+Transform.m

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ static inline CGRect SDCGRectFitWithScaleMode(CGRect rect, CGSize size, SDImageS
149149
#endif
150150
}
151151

152-
static inline UIColor * SDGetColorFromRGBA(Pixel_8888 pixel, CGBitmapInfo bitmapInfo, CGColorSpaceRef cgColorSpace) {
152+
static inline UIColor * SDGetColorFromRGBA8(Pixel_8888 pixel, CGBitmapInfo bitmapInfo, CGColorSpaceRef cgColorSpace) {
153153
// Get alpha info, byteOrder info
154154
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
155155
CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
@@ -752,6 +752,19 @@ - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point {
752752
return nil;
753753
}
754754

755+
// Check pixel format
756+
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
757+
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
758+
if (@available(iOS 12.0, tvOS 12.0, macOS 10.14, watchOS 5.0, *)) {
759+
CGImagePixelFormatInfo pixelFormat = (bitmapInfo & kCGImagePixelFormatMask);
760+
if (pixelFormat != kCGImagePixelFormatPacked || bitsPerComponent > 8) {
761+
// like RGBA1010102, need bitwise to extract pixel from single uint32_t, we don't support currently
762+
SD_LOG("Unsupported pixel format: %u, bpc: %zu for CGImage: %@", pixelFormat, bitsPerComponent, imageRef);
763+
CGImageRelease(imageRef);
764+
return nil;
765+
}
766+
}
767+
755768
// Get pixels
756769
CGDataProviderRef provider = CGImageGetDataProvider(imageRef);
757770
if (!provider) {
@@ -766,8 +779,7 @@ - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point {
766779

767780
// Get pixel at point
768781
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
769-
size_t components = CGImageGetBitsPerPixel(imageRef) / CGImageGetBitsPerComponent(imageRef);
770-
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
782+
size_t components = CGImageGetBitsPerPixel(imageRef) / bitsPerComponent;
771783

772784
CFRange range = CFRangeMake(bytesPerRow * y + components * x, components);
773785
if (CFDataGetLength(data) < range.location + range.length) {
@@ -793,9 +805,9 @@ - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point {
793805
CFRelease(data);
794806
CGImageRelease(imageRef);
795807
// Convert to color
796-
return SDGetColorFromRGBA(pixel, bitmapInfo, colorSpace);
808+
return SDGetColorFromRGBA8(pixel, bitmapInfo, colorSpace);
797809
} else {
798-
SD_LOG("Unsupported components: %zu", components);
810+
SD_LOG("Unsupported components: %zu for CGImage: %@", components, imageRef);
799811
CFRelease(data);
800812
CGImageRelease(imageRef);
801813
return nil;
@@ -826,6 +838,19 @@ - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point {
826838
return nil;
827839
}
828840

841+
// Check pixel format
842+
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
843+
size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
844+
if (@available(iOS 12.0, tvOS 12.0, macOS 10.14, watchOS 5.0, *)) {
845+
CGImagePixelFormatInfo pixelFormat = (bitmapInfo & kCGImagePixelFormatMask);
846+
if (pixelFormat != kCGImagePixelFormatPacked || bitsPerComponent > 8) {
847+
// like RGBA1010102, need bitwise to extract pixel from single uint32_t, we don't support currently
848+
SD_LOG("Unsupported pixel format: %u, bpc: %zu for CGImage: %@", pixelFormat, bitsPerComponent, imageRef);
849+
CGImageRelease(imageRef);
850+
return nil;
851+
}
852+
}
853+
829854
// Get pixels
830855
CGDataProviderRef provider = CGImageGetDataProvider(imageRef);
831856
if (!provider) {
@@ -840,7 +865,7 @@ - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point {
840865

841866
// Get pixels with rect
842867
size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
843-
size_t components = CGImageGetBitsPerPixel(imageRef) / CGImageGetBitsPerComponent(imageRef);
868+
size_t components = CGImageGetBitsPerPixel(imageRef) / bitsPerComponent;
844869

845870
size_t start = bytesPerRow * CGRectGetMinY(rect) + components * CGRectGetMinX(rect);
846871
size_t end = bytesPerRow * (CGRectGetMaxY(rect) - 1) + components * CGRectGetMaxX(rect);
@@ -855,7 +880,6 @@ - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point {
855880
size_t col = CGRectGetMaxX(rect);
856881

857882
// Convert to color
858-
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
859883
NSMutableArray<UIColor *> *colors = [NSMutableArray arrayWithCapacity:CGRectGetWidth(rect) * CGRectGetHeight(rect)];
860884
// ColorSpace
861885
CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef);
@@ -874,12 +898,13 @@ - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point {
874898
} else {
875899
if (components == 3) {
876900
Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], 0};
877-
color = SDGetColorFromRGBA(pixel, bitmapInfo, colorSpace);
901+
color = SDGetColorFromRGBA8(pixel, bitmapInfo, colorSpace);
878902
} else if (components == 4) {
879903
Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], pixels[index+3]};
880-
color = SDGetColorFromRGBA(pixel, bitmapInfo, colorSpace);
904+
color = SDGetColorFromRGBA8(pixel, bitmapInfo, colorSpace);
881905
} else {
882-
SD_LOG("Unsupported components: %zu", components);
906+
SD_LOG("Unsupported components: %zu for CGImage: %@", components, imageRef);
907+
break;
883908
}
884909
}
885910
if (color) {

Tests/SDWebImage Tests.xcodeproj/project.pbxproj

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
320224F82440C39B00E5B29D /* TestImageLarge.png in Resources */ = {isa = PBXBuildFile; fileRef = 320224F62440C39B00E5B29D /* TestImageLarge.png */; };
1414
320224F92440C39B00E5B29D /* TestImageLarge.png in Resources */ = {isa = PBXBuildFile; fileRef = 320224F62440C39B00E5B29D /* TestImageLarge.png */; };
1515
320630412085A37C006E0FA4 /* SDAnimatedImageTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 32A571552037DB2D002EDAAE /* SDAnimatedImageTest.m */; };
16+
321DDA342D688A8400F7971A /* TestHDR.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 321DDA332D688A8400F7971A /* TestHDR.jpeg */; };
17+
321DDA352D688A8400F7971A /* TestHDR.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 321DDA332D688A8400F7971A /* TestHDR.jpeg */; };
18+
321DDA362D688A8400F7971A /* TestHDR.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 321DDA332D688A8400F7971A /* TestHDR.jpeg */; };
19+
321DDA372D688A8400F7971A /* TestHDR.jpeg in Resources */ = {isa = PBXBuildFile; fileRef = 321DDA332D688A8400F7971A /* TestHDR.jpeg */; };
1620
321F310E27D0DC490042B274 /* TestImage.bmp in Resources */ = {isa = PBXBuildFile; fileRef = 321F310D27D0DC490042B274 /* TestImage.bmp */; };
1721
321F310F27D0DC490042B274 /* TestImage.bmp in Resources */ = {isa = PBXBuildFile; fileRef = 321F310D27D0DC490042B274 /* TestImage.bmp */; };
1822
321F311027D0DC490042B274 /* TestImage.bmp in Resources */ = {isa = PBXBuildFile; fileRef = 321F310D27D0DC490042B274 /* TestImage.bmp */; };
@@ -198,6 +202,7 @@
198202
2D7AF05E1F329763000083C2 /* SDTestCase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SDTestCase.h; sourceTree = "<group>"; };
199203
2D7AF05F1F329763000083C2 /* SDTestCase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SDTestCase.m; sourceTree = "<group>"; };
200204
320224F62440C39B00E5B29D /* TestImageLarge.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = TestImageLarge.png; sourceTree = "<group>"; };
205+
321DDA332D688A8400F7971A /* TestHDR.jpeg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = TestHDR.jpeg; sourceTree = "<group>"; };
201206
321F310D27D0DC490042B274 /* TestImage.bmp */ = {isa = PBXFileReference; lastKnownFileType = image.bmp; path = TestImage.bmp; sourceTree = "<group>"; };
202207
3222417E2272F808002429DB /* SDUtilsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDUtilsTests.m; sourceTree = "<group>"; };
203208
3226ECB920754F7700FAFACF /* SDWebImageTestDownloadOperation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDWebImageTestDownloadOperation.h; sourceTree = "<group>"; };
@@ -368,6 +373,7 @@
368373
3264CD162AAB1E23001E338B /* TestJFIF.jpg */,
369374
3261EC882D66235D00F2702E /* TestHDR.avif */,
370375
3261EC892D66235D00F2702E /* TestHDR.heic */,
376+
321DDA332D688A8400F7971A /* TestHDR.jpeg */,
371377
3261EC8A2D66235D00F2702E /* TestHDR.jxl */,
372378
321F310D27D0DC490042B274 /* TestImage.bmp */,
373379
433BBBB61D7EF8200086B6E9 /* TestImage.gif */,
@@ -630,6 +636,7 @@
630636
32464A9E2B7B1833006BE70E /* TestImageAnimated.webp in Resources */,
631637
32464A972B7B1833006BE70E /* TestImage.bmp in Resources */,
632638
32464AA32B7B1833006BE70E /* TestImage.heif in Resources */,
639+
321DDA352D688A8400F7971A /* TestHDR.jpeg in Resources */,
633640
32B4A4832C082A160004E42C /* TestImage.svg in Resources */,
634641
32464AA12B7B1833006BE70E /* MonochromeTestImage.jpg in Resources */,
635642
32464AA42B7B1833006BE70E /* TestImageLarge.png in Resources */,
@@ -664,6 +671,7 @@
664671
3278F5E42B04C1AC0004A6EE /* IndexedPNG.png in Resources */,
665672
329922842365DC6C00EAFD97 /* MonochromeTestImage.jpg in Resources */,
666673
329922882365DC6C00EAFD97 /* TestImage.jpg in Resources */,
674+
321DDA342D688A8400F7971A /* TestHDR.jpeg in Resources */,
667675
32B4A4822C082A160004E42C /* TestImage.svg in Resources */,
668676
32515F9E24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
669677
3299228E2365DC6C00EAFD97 /* TestImageAnimated.heics in Resources */,
@@ -698,6 +706,7 @@
698706
3278F5E32B04C1AC0004A6EE /* IndexedPNG.png in Resources */,
699707
32B99EA2203B31360017FD66 /* MonochromeTestImage.jpg in Resources */,
700708
32905E65211D786E00460FCF /* TestImage.heif in Resources */,
709+
321DDA372D688A8400F7971A /* TestHDR.jpeg in Resources */,
701710
32B4A4812C082A160004E42C /* TestImage.svg in Resources */,
702711
32515F9D24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
703712
327A418D211D660600495442 /* TestImage.heic in Resources */,
@@ -732,6 +741,7 @@
732741
3278F5E22B04C1AC0004A6EE /* IndexedPNG.png in Resources */,
733742
3297A09F23374D1700814590 /* TestImageAnimated.heics in Resources */,
734743
327054E2206CEFF3006EA328 /* TestImageAnimated.apng in Resources */,
744+
321DDA362D688A8400F7971A /* TestHDR.jpeg in Resources */,
735745
32B4A4802C082A160004E42C /* TestImage.svg in Resources */,
736746
32515F9C24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
737747
326E69472334C0C300B7252C /* TestLoopCount.gif in Resources */,

Tests/Tests/Images/TestHDR.jpeg

854 KB
Loading

Tests/Tests/SDImageCoderTests.m

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -610,12 +610,13 @@ - (void)test30ThatImageIOPNGPluginBuggyWorkaround {
610610
url = [[NSBundle bundleForClass:[self class]] URLForResource:@"RGBA16PNG" withExtension:@"png"];
611611
data = [NSData dataWithContentsOfURL:url];
612612
decodedImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:nil];
613-
testColor1 = [decodedImage sd_colorAtPoint:CGPointMake(100, 1)];
614-
[testColor1 getRed:&r1 green:&g1 blue:&b1 alpha:&a1];
615-
expect(r1).beCloseToWithin(0.60, 0.01);
616-
expect(g1).beCloseToWithin(0.60, 0.01);
617-
expect(b1).beCloseToWithin(0.33, 0.01);
618-
expect(a1).beCloseToWithin(0.33, 0.01);
613+
CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(decodedImage.CGImage);
614+
size_t bpc = CGImageGetBitsPerComponent(decodedImage.CGImage);
615+
expect(bpc).equal(16);
616+
CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
617+
CGImageByteOrderInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
618+
expect(alphaInfo).equal(kCGImageAlphaLast);
619+
expect(byteOrderInfo).equal(kCGImageByteOrder16Little);
619620
}
620621

621622
- (void)test31ThatSVGShouldUseNativeImageClass {
@@ -635,7 +636,7 @@ - (void)test31ThatSVGShouldUseNativeImageClass {
635636
}
636637
}
637638

638-
- (void)test32ThatHDRDecodeWorks {
639+
- (void)test32ThatISOHDRDecodeWorks {
639640
// Only test for iOS 17+/macOS 14+/visionOS 1+, or ImageIO decoder does not support HDR
640641
#if SD_MAC || SD_IOS || SD_VISION
641642
if (@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)) {
@@ -652,18 +653,59 @@ - (void)test32ThatHDRDecodeWorks {
652653

653654
expect([SDImageCoderHelper CGImageIsHDR:HDRImage.CGImage]).beTruthy();
654655
expect(HDRImage.sd_isHighDynamicRange).beTruthy();
656+
// Current we lack of some HDR RGBA1010102
657+
size_t HDRBPC = CGImageGetBitsPerComponent(HDRImage.CGImage);
658+
expect(HDRBPC).beGreaterThan(8);
659+
expect([HDRImage sd_colorAtPoint:CGPointMake(1, 1)]).beNil();
660+
655661
// FIXME: on Simulator, the SDR decode options will not take effect, so SDR is the same as HDR
656662
#if !TARGET_OS_SIMULATOR
657663
expect([SDImageCoderHelper CGImageIsHDR:SDRImage.CGImage]).beFalsy();
658664
expect(SDRImage.sd_isHighDynamicRange).beFalsy();
665+
size_t SDRBPC = CGImageGetBitsPerComponent(SDRImage.CGImage);
666+
expect(SDRBPC).beLessThanOrEqualTo(8);
667+
expect([SDRImage sd_colorAtPoint:CGPointMake(1, 1)]).notTo.beNil();
659668
#endif
669+
660670
// FIXME: Encoding need iOS 18+/macOS 15+
661671
// And need test both GainMap HDR or ISO HDR, TODO
662672
}
663673
}
664674
#endif
665675
}
666676

677+
- (void)test33ThatGainMapHDRDecodeWorks {
678+
// JPEG Gain Map HDR need iOS 18+
679+
// GitHub Action virtualization framework contains issue for Gain Map HDR convert:
680+
// [xctest] +[HDRImageConverter imageConverterWithOptions:]:40: ☀️ Failed to initialize Metal converter, falling back to SIMD for image conversion (slow)
681+
if (SDTestCase.isCI) {
682+
return;
683+
}
684+
#if SD_MAC || SD_IOS || SD_VISION
685+
if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) {
686+
NSArray *formats = @[@"jpeg"];
687+
for (NSString *format in formats) {
688+
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestHDR" withExtension:format];
689+
NSData *data = [NSData dataWithContentsOfURL:url];
690+
// Decoding
691+
UIImage *HDRImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeToHDR : @(YES)}];
692+
UIImage *SDRImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeToHDR : @(NO)}];
693+
694+
expect(HDRImage).notTo.beNil();
695+
expect(SDRImage).notTo.beNil();
696+
697+
// Gain Map HDR does not use Rec. 2100 color space
698+
size_t HDRBPC = CGImageGetBitsPerComponent(HDRImage.CGImage);
699+
size_t SDRBPC = CGImageGetBitsPerComponent(SDRImage.CGImage);
700+
expect(HDRBPC).beGreaterThan(8);
701+
expect(SDRBPC).beLessThanOrEqualTo(8);
702+
// expect([SDImageCoderHelper CGImageIsHDR:HDRImage.CGImage]).beTruthy();
703+
// expect(HDRImage.sd_isHighDynamicRange).beTruthy();
704+
}
705+
}
706+
#endif
707+
}
708+
667709
#pragma mark - Utils
668710

669711
- (void)verifyCoder:(id<SDImageCoder>)coder

0 commit comments

Comments
 (0)