Skip to content

Commit 0fe2bb7

Browse files
authored
Merge pull request SDWebImage#3799 from dreampiggy/feature/hdr_encoding
Supports HDR encoding on Apple ImageIO coder
2 parents e618417 + a4dba8f commit 0fe2bb7

File tree

8 files changed

+167
-7
lines changed

8 files changed

+167
-7
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ You can use those directly, or create similar components of your own, by using t
122122
- watchOS 2.0 or later
123123
- macOS 10.11 or later (10.15 for Catalyst)
124124
- visionOS 1.0 or later
125-
- Xcode 14.0 or later (visionOS requires Xcode 15.0)
125+
- Xcode 15.0 or later
126126

127127
#### Backwards compatibility
128128

SDWebImage/Core/SDImageCoder.h

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ typedef NSString * SDImageCoderOption NS_STRING_ENUM;
1515
typedef NSDictionary<SDImageCoderOption, id> SDImageCoderOptions;
1616
typedef NSMutableDictionary<SDImageCoderOption, id> SDImageCoderMutableOptions;
1717

18-
#pragma mark - Coder Options
18+
#pragma mark - Image Decoding Options
1919
// These options are for image decoding
2020
/**
2121
A Boolean value indicating whether to decode the first frame only for animated image during decoding. (NSNumber). If not provide, decode animated image if need.
@@ -89,14 +89,25 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDec
8989
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleDownLimitBytes;
9090

9191
/**
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)
92+
A Boolean (`SDImageHDRType.rawValue`) value (stored inside NSNumber) to provide converting to HDR during decoding. Currently if number is 0 (`SDImageHDRTypeSDR`), use SDR, else use HDR. But we may extend this option to represent `SDImageHDRType` all cases in the future (means, think this options as uint number, but not actual boolean)
9393
@note Supported by iOS 17 and above when using ImageIO coder (third-party coder can support lower firmware)
9494
Defaults to @(NO), decoder will automatically convert SDR.
9595
@note works for `SDImageCoder`
9696
*/
9797
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeToHDR;
9898

99-
// These options are for image encoding
99+
#pragma mark - Image Encoding Options
100+
/**
101+
A NSUInteger (`SDImageHDRType.rawValue`) value (stored inside NSNumber) to provide converting to HDR during encoding. Read the below carefully to choose the value.
102+
@note 0(`SDImageHDRTypeSDR`) means SDR; 1(`SDImageHDRTypeISOHDR`) means ISO HDR (at least using 10 bits per components or above, supported by AVIF/HEIF/JPEG-XL); 2(`SDImageHDRTypeISOGainMap`) means ISO Gain Map HDR (may use 8 bits per components, supported by AVIF/HEIF/JPEG-XL, as well as traditional JPEG)
103+
@note Gain Map like a mask image with metadata, which contains the depth/bright information for pixels (1/4 resolution), which used to convert between HDR and SDR.
104+
@note If you use CIImage as HDR pipeline, you can export as CGImage for encoding. (But it's also recommanded to use CIImage's `JPEGRepresentationOfImage` or `HEIFRepresentationOfImage`)
105+
@note Supported by iOS 18 and above when using ImageIO coder (third-party coder can support lower firmware)
106+
Defaults to @(0), encoder will automatically convert SDR.
107+
@note works for `SDImageCoder`
108+
*/
109+
FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeToHDR;
110+
100111
/**
101112
A Boolean value indicating whether to encode the first frame only for animated image during encoding. (NSNumber). If not provide, encode animated image if need.
102113
@note works for `SDImageCoder`.

SDWebImage/Core/SDImageCoder.m

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
SDImageCoderOption const SDImageCoderDecodeScaleDownLimitBytes = @"decodeScaleDownLimitBytes";
1919
SDImageCoderOption const SDImageCoderDecodeToHDR = @"decodeToHDR";
2020

21+
SDImageCoderOption const SDImageCoderEncodeToHDR = @"encodeToHDR";
2122
SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
2223
SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";
2324
SDImageCoderOption const SDImageCoderEncodeBackgroundColor = @"encodeBackgroundColor";

SDWebImage/Core/SDImageCoderHelper.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ typedef NS_ENUM(NSUInteger, SDImageForceDecodePolicy) {
3333
SDImageForceDecodePolicyAlways
3434
};
3535

36+
/// These enum is used to represent the High Dynamic Range type during image encoding/decoding.
37+
/// There are alao other HDR type in history before ISO Standard (ISO 21496-1), including Google and Apple's old OSs captured photos, but which is non-standard and we do not support.
38+
typedef NS_ENUM(NSUInteger, SDImageHDRType) {
39+
/// SDR, mostly only 8 bits color per components, RGBA8
40+
SDImageHDRTypeSDR = 0,
41+
/// ISO HDR (supported by modern format only, like HEIF/AVIF/JPEG-XL)
42+
SDImageHDRTypeISOHDR = 1,
43+
/// ISO Gain Map based HDR (supported by nearly all format, including tranditional JPEG, which stored the gain map into XMP)
44+
SDImageHDRTypeISOGainMap = 2,
45+
};
46+
3647
/// Byte alignment the bytes size with alignment
3748
/// - Parameters:
3849
/// - size: The bytes size

SDWebImage/Core/SDImageIOAnimatedCoder.m

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@
2828

2929
// Specify File Size for lossy format encoding, like JPEG
3030
static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
31+
// Support Xcode 15 SDK, use raw value instead of symbol
32+
static NSString * kSDCGImageDestinationEncodeRequest = @"kCGImageDestinationEncodeRequest";
33+
static NSString * kSDCGImageDestinationEncodeToSDR = @"kCGImageDestinationEncodeToSDR";
34+
static NSString * kSDCGImageDestinationEncodeToISOHDR = @"kCGImageDestinationEncodeToISOHDR";
35+
static NSString * kSDCGImageDestinationEncodeToISOGainmap = @"kCGImageDestinationEncodeToISOGainmap";
36+
3137

3238
// This strip the un-wanted CGImageProperty, like the internal CGImageSourceRef in iOS 15+
3339
// However, CGImageCreateCopy still keep those CGImageProperty, not suit for our use case
@@ -282,6 +288,18 @@ @implementation SDImageIOAnimatedCoder {
282288
BOOL _decodeToHDR;
283289
}
284290

291+
#if SD_IMAGEIO_HDR_ENCODING
292+
+ (void)initialize {
293+
if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) {
294+
// Use SDK instead of raw value
295+
kSDCGImageDestinationEncodeRequest = (__bridge NSString *)kCGImageDestinationEncodeRequest;
296+
kSDCGImageDestinationEncodeToSDR = (__bridge NSString *)kCGImageDestinationEncodeToSDR;
297+
kSDCGImageDestinationEncodeToISOHDR = (__bridge NSString *)kCGImageDestinationEncodeToISOHDR;
298+
kSDCGImageDestinationEncodeToISOGainmap = (__bridge NSString *)kCGImageDestinationEncodeToISOGainmap;
299+
}
300+
}
301+
#endif
302+
285303
- (void)dealloc
286304
{
287305
if (_imageSource) {
@@ -895,6 +913,21 @@ - (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(N
895913
maxPixelSize = maxPixelSizeValue.CGSizeValue;
896914
#endif
897915
}
916+
// HDR Encoding
917+
NSUInteger encodeToHDR = 0;
918+
if (options[SDImageCoderEncodeToHDR]) {
919+
encodeToHDR = [options[SDImageCoderEncodeToHDR] unsignedIntegerValue];
920+
}
921+
if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) {
922+
if (encodeToHDR == SDImageHDRTypeISOHDR) {
923+
properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToISOHDR;
924+
} else if (encodeToHDR == SDImageHDRTypeISOGainMap) {
925+
properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToISOGainmap;
926+
} else {
927+
properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToSDR;
928+
}
929+
}
930+
898931
CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef);
899932
CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef);
900933
CGFloat finalPixelSize = 0;

SDWebImage/Core/SDImageIOCoder.m

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818

1919
// Specify File Size for lossy format encoding, like JPEG
2020
static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
21+
// Support Xcode 15 SDK, use raw value instead of symbol
22+
static NSString * kSDCGImageDestinationEncodeRequest = @"kCGImageDestinationEncodeRequest";
23+
static NSString * kSDCGImageDestinationEncodeToSDR = @"kCGImageDestinationEncodeToSDR";
24+
static NSString * kSDCGImageDestinationEncodeToISOHDR = @"kCGImageDestinationEncodeToISOHDR";
25+
static NSString * kSDCGImageDestinationEncodeToISOGainmap = @"kCGImageDestinationEncodeToISOGainmap";
26+
2127

2228
@implementation SDImageIOCoder {
2329
size_t _width, _height;
@@ -31,6 +37,18 @@ @implementation SDImageIOCoder {
3137
BOOL _decodeToHDR;
3238
}
3339

40+
#if SD_IMAGEIO_HDR_ENCODING
41+
+ (void)initialize {
42+
if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) {
43+
// Use SDK instead of raw value
44+
kSDCGImageDestinationEncodeRequest = (__bridge NSString *)kCGImageDestinationEncodeRequest;
45+
kSDCGImageDestinationEncodeToSDR = (__bridge NSString *)kCGImageDestinationEncodeToSDR;
46+
kSDCGImageDestinationEncodeToISOHDR = (__bridge NSString *)kCGImageDestinationEncodeToISOHDR;
47+
kSDCGImageDestinationEncodeToISOGainmap = (__bridge NSString *)kCGImageDestinationEncodeToISOGainmap;
48+
}
49+
}
50+
#endif
51+
3452
- (void)dealloc {
3553
if (_imageSource) {
3654
CFRelease(_imageSource);
@@ -381,6 +399,21 @@ - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format o
381399
maxPixelSize = maxPixelSizeValue.CGSizeValue;
382400
#endif
383401
}
402+
// HDR Encoding
403+
NSUInteger encodeToHDR = 0;
404+
if (options[SDImageCoderEncodeToHDR]) {
405+
encodeToHDR = [options[SDImageCoderEncodeToHDR] unsignedIntegerValue];
406+
}
407+
if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) {
408+
if (encodeToHDR == SDImageHDRTypeISOHDR) {
409+
properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToISOHDR;
410+
} else if (encodeToHDR == SDImageHDRTypeISOGainMap) {
411+
properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToISOGainmap;
412+
} else {
413+
properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToSDR;
414+
}
415+
}
416+
384417
CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef);
385418
CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef);
386419
CGFloat finalPixelSize = 0;

SDWebImage/Private/SDImageIOAnimatedCoderInternal.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
#import <ImageIO/ImageIO.h>
1111
#import "SDImageIOAnimatedCoder.h"
1212

13+
// Xcode 16 SDK contains HDR encoding API, but we still support Xcode 15
14+
#define SD_IMAGEIO_HDR_ENCODING (__IPHONE_OS_VERSION_MAX_ALLOWED >= 180000)
15+
1316
// AVFileTypeHEIC/AVFileTypeHEIF is defined in AVFoundation via iOS 11, we use this without import AVFoundation
1417
#define kSDUTTypeHEIC ((__bridge CFStringRef)@"public.heic")
1518
#define kSDUTTypeHEIF ((__bridge CFStringRef)@"public.heif")

Tests/Tests/SDImageCoderTests.m

Lines changed: 71 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -666,9 +666,6 @@ - (void)test32ThatISOHDRDecodeWorks {
666666
expect(SDRBPC).beLessThanOrEqualTo(8);
667667
expect([SDRImage sd_colorAtPoint:CGPointMake(1, 1)]).notTo.beNil();
668668
#endif
669-
670-
// FIXME: Encoding need iOS 18+/macOS 15+
671-
// And need test both GainMap HDR or ISO HDR, TODO
672669
}
673670
}
674671
#endif
@@ -706,6 +703,64 @@ - (void)test33ThatGainMapHDRDecodeWorks {
706703
#endif
707704
}
708705

706+
- (void)test34ThatHDREncodeWorks {
707+
// FIXME: Encoding need iOS 18+/macOS 15+, No simulator
708+
// GitHub Action virtualization framework contains issue for Gain Map HDR convert:
709+
if (SDTestCase.isCI) {
710+
return;
711+
}
712+
// Actually we test 4 cases, because decoded CGImage can contains gain map or not
713+
// heic -> heic / heic -> jpeg / jpeg(gain map) -> heic / jpeg(gain map) -> jpeg
714+
if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) {
715+
NSArray *decodeFormats = @[@"heic", @"jpeg"];
716+
for (NSString *decodeFormat in decodeFormats) {
717+
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestHDR" withExtension:decodeFormat];
718+
NSData *data = [NSData dataWithContentsOfURL:url];
719+
// Decoding
720+
UIImage *HDRImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeToHDR : @(YES)}];
721+
float headroom = CGImageGetContentHeadroom(HDRImage.CGImage);
722+
expect(headroom).beGreaterThan(1);
723+
#if !TARGET_OS_SIMULATOR
724+
NSArray *encodeFormats = @[@"heic", @"jpeg"];
725+
for (NSString *encodeFormat in encodeFormats) {
726+
NSLog(@"Testing HDR encodde from original : %@ to %@", decodeFormat, encodeFormat);
727+
// HEIC with ISO Gain Map
728+
SDImageFormat format = SDImageFormatHEIC;
729+
if ([encodeFormat isEqualToString:@"jpeg"]) {
730+
// JPEG with XMP Gain Map
731+
format = SDImageFormatJPEG;
732+
}
733+
NSData *SDRData = [SDImageIOCoder.sharedCoder encodedDataWithImage:HDRImage format:format options:@{SDImageCoderEncodeToHDR : @(SDImageHDRTypeSDR)}];
734+
NSData *HDRData = [SDImageIOCoder.sharedCoder encodedDataWithImage:HDRImage format:format options:@{SDImageCoderEncodeToHDR : @(SDImageHDRTypeISOHDR)}];
735+
NSData *HDRGainMapData = [SDImageIOCoder.sharedCoder encodedDataWithImage:HDRImage format:format options:@{SDImageCoderEncodeToHDR : @(SDImageHDRTypeISOGainMap)}];
736+
expect(SDRData).notTo.beNil();
737+
expect(HDRData).notTo.beNil();
738+
expect(HDRGainMapData).notTo.beNil();
739+
// JPEG has no built-in support Gain Map, so it stored in XMP and be larger
740+
if ([encodeFormat isEqualToString:@"jpeg"]) {
741+
expect(HDRGainMapData.length).beGreaterThan(HDRData.length);
742+
}
743+
744+
// Check gain map information
745+
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)HDRGainMapData, NULL);
746+
NSDictionary *gainMap = [self gainMapFromImageSource:source];
747+
expect(gainMap.count).beGreaterThan(0);
748+
// At least gain map contains `kCGImageAuxiliaryDataInfoMetadata`
749+
CGImageMetadataRef meta = (__bridge CGImageMetadataRef)(gainMap[(__bridge NSString *)kCGImageAuxiliaryDataInfoMetadata]);
750+
expect(meta).notTo.beNil();
751+
752+
// A check for redecoded CGImage
753+
CGImageRef redecodeSDRImage = CGImageSourceCreateImageAtIndex(source, 0, nil);
754+
expect(redecodeSDRImage).notTo.beNil();
755+
headroom = CGImageGetContentHeadroom(redecodeSDRImage);
756+
expect(headroom).equal(1);
757+
CFRelease(source);
758+
}
759+
#endif
760+
}
761+
}
762+
}
763+
709764
#pragma mark - Utils
710765

711766
- (void)verifyCoder:(id<SDImageCoder>)coder
@@ -835,6 +890,19 @@ - (NSArray *)thumbnailImagesFromImageSource:(CGImageSourceRef)source API_AVAILAB
835890
return thumbnailImages;
836891
}
837892

893+
- (NSDictionary *)gainMapFromImageSource:(CGImageSourceRef)source {
894+
if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) {
895+
CFDictionaryRef ISOGainMap = CGImageSourceCopyAuxiliaryDataInfoAtIndex(source, 0, kCGImageAuxiliaryDataTypeISOGainMap);
896+
CFDictionaryRef HDRGainMap = CGImageSourceCopyAuxiliaryDataInfoAtIndex(source, 0, kCGImageAuxiliaryDataTypeHDRGainMap);
897+
NSDictionary *result = ISOGainMap ? (__bridge_transfer NSDictionary *)ISOGainMap : (__bridge_transfer NSDictionary *)HDRGainMap;
898+
if (HDRGainMap) CFRelease(HDRGainMap);
899+
if (ISOGainMap) CFRelease(ISOGainMap);
900+
return result;
901+
} else {
902+
return nil;
903+
}
904+
}
905+
838906
#pragma mark - Utils
839907
- (CGRect)boxRectFromPDFData:(nonnull NSData *)data {
840908
CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);

0 commit comments

Comments
 (0)