Forráskód Böngészése

Support decoding ISO HDR (avif/heic/jpeg-xl, etc) (#3778)
* Support HDR

* Support HDR

* When using SDWebImageProgressiveLoad and setting it to SDWebImageContextImageDecodeToHDR, it will be decoded to HDR only when loading is complete.

* When SDImageIOAnimatedCoder turns on incremental, it does not need to be decoded to HDR when it is not completed

* Sample image to remove sensitive information

* 1、use remote resources for HDR testing
2、add simulator log for HDR

* Support HDR encode

* Support HDR encode

* imageRef release error

* HDR Decoded is not required for decoding, otherwise the type will be lost

* HDR encode format

* hdrImageRef not released correctly

* HDR image must be lazy decode

* HDR image must be lazy decode

* JEPG HDR image must be lazy decode, otherwise it will crash

* HDR, encoding properties add kCGImageDestinationEncodeToISOGainmap, compatible with SDR displays while preserving HDR

* 支持 decode to HDR

* HDR encoding is not currently supported

* add UIImage.sd_isHighDynamicRange, use check UIImage is HDR

* refactor: Do not hack on HEICS and distinguish static/aniamted image encoding UTI

* refactor: Move cross-platform screen info into SDDeviceHelper

* change: Do not disable force decode when turn on decodeToHDR

Need actually check the HDR info of CGImage, or better, we can pre-decode to drop lazy HDR image

* change: use UIImage.imageRendererFormat when force decode using graphics renderer

This can inherit the possible info like dynamic range

* fix: When decode HDR image to SDR, need specify the decode request

Tested on macOS 14.5 and iOS 18.0 behavior

* test: Added unit test for HDR decoding

* demo: Update the macOS demo to show the HDR image

* test: workaround the SDR decode on Simulator environment

* change: The `sd_isHighDynamicRange` should check CGImage as fallback as well

* test: temp disable a unused test case

---------

Co-authored-by: DreamPiggy <lizhuoli1126@126.com>

DreamPiggy 1 éve
szülő
commit
92a7ab93e0

+ 13 - 3
Examples/SDWebImage Demo/DetailViewController.m

@@ -22,11 +22,21 @@
     if (!self.imageView.sd_imageIndicator) {
     if (!self.imageView.sd_imageIndicator) {
         self.imageView.sd_imageIndicator = SDWebImageProgressIndicator.defaultIndicator;
         self.imageView.sd_imageIndicator = SDWebImageProgressIndicator.defaultIndicator;
     }
     }
+    BOOL isHDR = [self.imageURL.absoluteString containsString:@"HDR"];
+    if (@available(iOS 17.0, *)) {
+        self.imageView.preferredImageDynamicRange = isHDR ? UIImageDynamicRangeHigh : UIImageDynamicRangeUnspecified;
+    }
+    SDWebImageContext *context = @{
+        SDWebImageContextImageDecodeToHDR: @(isHDR)
+    };
     [self.imageView sd_setImageWithURL:self.imageURL
     [self.imageView sd_setImageWithURL:self.imageURL
                       placeholderImage:nil
                       placeholderImage:nil
-                               options:SDWebImageProgressiveLoad | SDWebImageScaleDownLargeImages
-                               context:@{SDWebImageContextImageForceDecodePolicy: @(SDImageForceDecodePolicyNever)}
-    ];
+                               options:SDWebImageFromLoaderOnly | SDWebImageScaleDownLargeImages
+                               context:context
+                              progress:nil
+                             completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
+        NSLog(@"isHighDynamicRange %@", @(image.sd_isHighDynamicRange));
+    }];
     self.imageView.shouldCustomLoopCount = YES;
     self.imageView.shouldCustomLoopCount = YES;
     self.imageView.animationRepeatCount = 0;
     self.imageView.animationRepeatCount = 0;
 }
 }

+ 3 - 0
Examples/SDWebImage Demo/MasterViewController.m

@@ -59,6 +59,9 @@
         [SDWebImageDownloader sharedDownloader].config.executionOrder = SDWebImageDownloaderLIFOExecutionOrder;
         [SDWebImageDownloader sharedDownloader].config.executionOrder = SDWebImageDownloaderLIFOExecutionOrder;
         
         
         self.objects = [NSMutableArray arrayWithObjects:
         self.objects = [NSMutableArray arrayWithObjects:
+                    @"https://raw.githubusercontent.com/CloudlessMoon/SuperResources/master/Images/HEIC/TestHDR1.heic",
+                    @"https://raw.githubusercontent.com/CloudlessMoon/SuperResources/master/Images/JPG/TestHDR1.JPG",
+                    @"https://raw.githubusercontent.com/CloudlessMoon/SuperResources/master/Images/JPG/TestHDR2.JPG",
                     @"http://www.httpwatch.com/httpgallery/authentication/authenticatedimage/default.aspx?0.35786508303135633",     // requires HTTP auth, used to demo the NTLM auth
                     @"http://www.httpwatch.com/httpgallery/authentication/authenticatedimage/default.aspx?0.35786508303135633",     // requires HTTP auth, used to demo the NTLM auth
                     @"http://assets.sbnation.com/assets/2512203/dogflops.gif",
                     @"http://assets.sbnation.com/assets/2512203/dogflops.gif",
                     @"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif",
                     @"https://raw.githubusercontent.com/liyong03/YLGIFImage/master/YLGIFImageDemo/YLGIFImageDemo/joy.gif",

+ 6 - 30
Examples/SDWebImage OSX Demo/Base.lproj/Main.storyboard

@@ -1,8 +1,8 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
+<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
     <dependencies>
     <dependencies>
         <deployment identifier="macosx"/>
         <deployment identifier="macosx"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="21507"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
     </dependencies>
     <scenes>
     <scenes>
@@ -677,35 +677,15 @@
                         <rect key="frame" x="0.0" y="0.0" width="480" height="400"/>
                         <rect key="frame" x="0.0" y="0.0" width="480" height="400"/>
                         <autoresizingMask key="autoresizingMask"/>
                         <autoresizingMask key="autoresizingMask"/>
                         <subviews>
                         <subviews>
-                            <imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nbD-Cx-g7b">
-                                <rect key="frame" x="20" y="252" width="204" height="128"/>
-                                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
-                                <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="vAn-1d-apO"/>
-                            </imageView>
-                            <imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kv0-67-hkh">
-                                <rect key="frame" x="256" y="252" width="204" height="128"/>
-                                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
-                                <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="f0P-c9-GMe"/>
-                            </imageView>
-                            <imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JIp-Or-vBM" customClass="SDAnimatedImageView">
-                                <rect key="frame" x="20" y="116" width="204" height="128"/>
-                                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
-                                <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="NJq-m3-LlB"/>
-                            </imageView>
-                            <imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="khI-tY-l0M" customClass="SDAnimatedImageView">
-                                <rect key="frame" x="256" y="116" width="204" height="128"/>
-                                <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
-                                <imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="WbV-Do-9qy"/>
-                            </imageView>
                             <button translatesAutoresizingMaskIntoConstraints="NO" id="NqE-Zi-qhY">
                             <button translatesAutoresizingMaskIntoConstraints="NO" id="NqE-Zi-qhY">
-                                <rect key="frame" x="212" y="17" width="56" height="31"/>
-                                <constraints>
-                                    <constraint firstAttribute="height" constant="26" id="WoQ-RY-bSV"/>
-                                </constraints>
+                                <rect key="frame" x="211" y="16" width="58" height="33"/>
                                 <buttonCell key="cell" type="bevel" title="Clear" bezelStyle="regularSquare" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="OYN-fG-Plb">
                                 <buttonCell key="cell" type="bevel" title="Clear" bezelStyle="regularSquare" alignment="center" borderStyle="border" imageScaling="proportionallyUpOrDown" inset="2" id="OYN-fG-Plb">
                                     <behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
                                     <behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
                                     <font key="font" metaFont="system"/>
                                     <font key="font" metaFont="system"/>
                                 </buttonCell>
                                 </buttonCell>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="26" id="WoQ-RY-bSV"/>
+                                </constraints>
                             </button>
                             </button>
                         </subviews>
                         </subviews>
                         <constraints>
                         <constraints>
@@ -715,10 +695,6 @@
                     </view>
                     </view>
                     <connections>
                     <connections>
                         <outlet property="clearCacheButton" destination="NqE-Zi-qhY" id="eoz-cU-wWs"/>
                         <outlet property="clearCacheButton" destination="NqE-Zi-qhY" id="eoz-cU-wWs"/>
-                        <outlet property="imageView1" destination="nbD-Cx-g7b" id="t2R-8w-ybH"/>
-                        <outlet property="imageView2" destination="kv0-67-hkh" id="i4k-5c-bno"/>
-                        <outlet property="imageView3" destination="JIp-Or-vBM" id="Qcf-og-59T"/>
-                        <outlet property="imageView4" destination="khI-tY-l0M" id="STy-c1-ihV"/>
                     </connections>
                     </connections>
                 </viewController>
                 </viewController>
                 <customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
                 <customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>

+ 55 - 13
Examples/SDWebImage OSX Demo/ViewController.m

@@ -11,10 +11,15 @@
 
 
 @interface ViewController ()
 @interface ViewController ()
 
 
-@property (weak) IBOutlet NSImageView *imageView1;
-@property (weak) IBOutlet NSImageView *imageView2;
-@property (weak) IBOutlet SDAnimatedImageView *imageView3;
-@property (weak) IBOutlet SDAnimatedImageView *imageView4;
+@property (strong) NSImageView *imageView1;
+@property (strong) SDAnimatedImageView *imageView2;
+
+@property (strong) NSImageView *imageView3;
+@property (strong) SDAnimatedImageView *imageView4;
+
+@property (strong) NSImageView *imageView5;
+@property (strong) NSImageView *imageView6;
+
 @property (weak) IBOutlet NSButton *clearCacheButton;
 @property (weak) IBOutlet NSButton *clearCacheButton;
 
 
 @end
 @end
@@ -23,26 +28,39 @@
 
 
 - (void)viewDidLoad {
 - (void)viewDidLoad {
     [super viewDidLoad];
     [super viewDidLoad];
+    self.imageView1 = [NSImageView new];
+    self.imageView2 = [SDAnimatedImageView new];
+    self.imageView3 = [NSImageView new];
+    self.imageView4 = [SDAnimatedImageView new];
+    self.imageView5 = [NSImageView new];
+    self.imageView6 = [NSImageView new];
+    
+    [self.view addSubview:self.imageView1];
+    [self.view addSubview:self.imageView2];
+    [self.view addSubview:self.imageView3];
+    [self.view addSubview:self.imageView4];
+    [self.view addSubview:self.imageView5];
+    [self.view addSubview:self.imageView6];
     
     
     // For animated GIF rendering, set `animates` to YES or will only show the first frame
     // For animated GIF rendering, set `animates` to YES or will only show the first frame
-    self.imageView2.animates = YES; // `SDAnimatedImageRep` can be used for built-in `NSImageView` to support better GIF & APNG rendering as well. No need `SDAnimatedImageView`
+    self.imageView3.animates = YES; // `SDAnimatedImageRep` can be used for built-in `NSImageView` to support better GIF & APNG rendering as well. No need `SDAnimatedImageView`
     self.imageView4.animates = YES;
     self.imageView4.animates = YES;
     
     
+#pragma mark - Static Image
     // NSImageView + Static Image
     // NSImageView + Static Image
     self.imageView1.sd_imageIndicator = SDWebImageProgressIndicator.defaultIndicator;
     self.imageView1.sd_imageIndicator = SDWebImageProgressIndicator.defaultIndicator;
     [self.imageView1 sd_setImageWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_2.jpg"] placeholderImage:nil options:SDWebImageProgressiveLoad];
     [self.imageView1 sd_setImageWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/recurser/exif-orientation-examples/master/Landscape_2.jpg"] placeholderImage:nil options:SDWebImageProgressiveLoad];
+    // SDAnimatedImageView + Static Image
+    [self.imageView2 sd_setImageWithURL:[NSURL URLWithString:@"https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png"]];
     
     
+#pragma mark - Animated Image
     // NSImageView + Animated Image
     // NSImageView + Animated Image
-    self.imageView2.sd_imageIndicator = SDWebImageActivityIndicator.largeIndicator;
-    [self.imageView2 sd_setImageWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/onevcat/APNGKit/2.2.0/Tests/APNGKitTests/Resources/General/APNG-cube.apng"]];
+    self.imageView3.sd_imageIndicator = SDWebImageActivityIndicator.largeIndicator;
+    [self.imageView3 sd_setImageWithURL:[NSURL URLWithString:@"https://raw.githubusercontent.com/onevcat/APNGKit/2.2.0/Tests/APNGKitTests/Resources/General/APNG-cube.apng"]];
     NSMenu *menu1 = [[NSMenu alloc] initWithTitle:@"Toggle Animation"];
     NSMenu *menu1 = [[NSMenu alloc] initWithTitle:@"Toggle Animation"];
     NSMenuItem *item1 = [menu1 addItemWithTitle:@"Toggle Animation" action:@selector(toggleAnimation:) keyEquivalent:@""];
     NSMenuItem *item1 = [menu1 addItemWithTitle:@"Toggle Animation" action:@selector(toggleAnimation:) keyEquivalent:@""];
     item1.tag = 1;
     item1.tag = 1;
-    self.imageView2.menu = menu1;
-    
-    // SDAnimatedImageView + Static Image
-    [self.imageView3 sd_setImageWithURL:[NSURL URLWithString:@"https://nr-platform.s3.amazonaws.com/uploads/platform/published_extension/branding_icon/275/AmazonS3.png"]];
-    
+    self.imageView3.menu = menu1;
     // SDAnimatedImageView + Animated Image
     // SDAnimatedImageView + Animated Image
     self.imageView4.sd_imageTransition = SDWebImageTransition.fadeTransition;
     self.imageView4.sd_imageTransition = SDWebImageTransition.fadeTransition;
     self.imageView4.imageScaling = NSImageScaleProportionallyUpOrDown;
     self.imageView4.imageScaling = NSImageScaleProportionallyUpOrDown;
@@ -53,12 +71,36 @@
     item2.tag = 2;
     item2.tag = 2;
     self.imageView4.menu = menu2;
     self.imageView4.menu = menu2;
     
     
+#pragma mark - HDR Image
+    // HDR Image
+    if (@available(macOS 14.0, *)) {
+        self.imageView5.preferredImageDynamicRange = NSImageDynamicRangeHigh;
+        self.imageView6.preferredImageDynamicRange = NSImageDynamicRangeHigh;
+    }
+    [self.imageView5 sd_setImageWithURL:[NSURL URLWithString:@"https://lightroom.adobe.com/v2c/spaces/113ab046f0d04b40aa7f8e10285961a7/assets/cd191116be514e1288ca6ea372303139/revisions/2749aff3294e404c9ffce3518e467d4a/renditions/99673919d096b42650b448f6516089cc.avif"] placeholderImage:nil options:0 context:@{SDWebImageContextImageDecodeToHDR : @(YES)}];
+    // SDR Image
+    [self.imageView6 sd_setImageWithURL:[NSURL URLWithString:@"https://lightroom.adobe.com/v2c/spaces/113ab046f0d04b40aa7f8e10285961a7/assets/cd191116be514e1288ca6ea372303139/revisions/2749aff3294e404c9ffce3518e467d4a/renditions/99673919d096b42650b448f6516089cc"] placeholderImage:nil options:0 context:@{SDWebImageContextImageDecodeToHDR : @(NO)}];
+    
     self.clearCacheButton.target = self;
     self.clearCacheButton.target = self;
     self.clearCacheButton.action = @selector(clearCacheButtonClicked:);
     self.clearCacheButton.action = @selector(clearCacheButtonClicked:);
     [self.clearCacheButton sd_setImageWithURL:[NSURL URLWithString:@"https://png.icons8.com/color/100/000000/delete-sign.png"]];
     [self.clearCacheButton sd_setImageWithURL:[NSURL URLWithString:@"https://png.icons8.com/color/100/000000/delete-sign.png"]];
     [self.clearCacheButton sd_setAlternateImageWithURL:[NSURL URLWithString:@"https://png.icons8.com/color/100/000000/checkmark.png"]];
     [self.clearCacheButton sd_setAlternateImageWithURL:[NSURL URLWithString:@"https://png.icons8.com/color/100/000000/checkmark.png"]];
 }
 }
 
 
+- (void)viewDidLayout {
+    [super viewDidLayout];
+    CGFloat space = 20;
+    CGFloat imageWidth = (self.view.frame.size.width - space * 4) / 3;
+    CGFloat imageHeight = (self.view.frame.size.height - space * 3) / 2;
+    
+    self.imageView1.frame = CGRectMake(space * 1 + imageWidth * 0, space, imageWidth, imageHeight);
+    self.imageView2.frame = CGRectMake(self.imageView1.frame.origin.x, self.imageView1.frame.origin.y + imageHeight + space, imageWidth, imageHeight);
+    self.imageView3.frame = CGRectMake(space * 2 + imageWidth * 1, space, imageWidth, imageHeight);
+    self.imageView4.frame = CGRectMake(self.imageView3.frame.origin.x, self.imageView3.frame.origin.y + imageHeight + space, imageWidth, imageHeight);
+    self.imageView5.frame = CGRectMake(space * 3 + imageWidth * 2, space, imageWidth, imageHeight);
+    self.imageView6.frame = CGRectMake(self.imageView5.frame.origin.x, self.imageView5.frame.origin.y + imageHeight + space, imageWidth, imageHeight);
+}
+
 - (void)clearCacheButtonClicked:(NSResponder *)sender {
 - (void)clearCacheButtonClicked:(NSResponder *)sender {
     NSButton *button = (NSButton *)sender;
     NSButton *button = (NSButton *)sender;
     button.state = NSControlStateValueOn;
     button.state = NSControlStateValueOn;
@@ -69,7 +111,7 @@
 }
 }
 
 
 - (void)toggleAnimation:(NSMenuItem *)sender {
 - (void)toggleAnimation:(NSMenuItem *)sender {
-    NSImageView *imageView = sender.tag == 1 ? self.imageView2 : self.imageView4;
+    NSImageView *imageView = sender.tag == 1 ? self.imageView3 : self.imageView4;
     if (imageView.animates) {
     if (imageView.animates) {
         imageView.animates = NO;
         imageView.animates = NO;
     } else {
     } else {

+ 5 - 0
SDWebImage/Core/SDGraphicsImageRenderer.h

@@ -34,6 +34,11 @@ typedef NS_ENUM(NSInteger, SDGraphicsImageRendererFormatRange) {
 /// A set of drawing attributes that represent the configuration of an image renderer context.
 /// A set of drawing attributes that represent the configuration of an image renderer context.
 @interface SDGraphicsImageRendererFormat : NSObject
 @interface SDGraphicsImageRendererFormat : NSObject
 
 
+#if SD_UIKIT
+/// The underlying uiformat for UIKit. This usage of this API should be careful, which may cause out of sync.
+@property (nonatomic, strong, nonnull) UIGraphicsImageRendererFormat *uiformat API_AVAILABLE(ios(10.0), tvos(10.0));
+#endif
+
 /// The display scale of the image renderer context.
 /// The display scale of the image renderer context.
 /// The default value is equal to the scale of the main screen.
 /// The default value is equal to the scale of the main screen.
 @property (nonatomic) CGFloat scale;
 @property (nonatomic) CGFloat scale;

+ 3 - 36
SDWebImage/Core/SDGraphicsImageRenderer.m

@@ -8,12 +8,7 @@
 
 
 #import "SDGraphicsImageRenderer.h"
 #import "SDGraphicsImageRenderer.h"
 #import "SDImageGraphics.h"
 #import "SDImageGraphics.h"
-
-@interface SDGraphicsImageRendererFormat ()
-#if SD_UIKIT
-@property (nonatomic, strong) UIGraphicsImageRendererFormat *uiformat API_AVAILABLE(ios(10.0), tvos(10.0));
-#endif
-@end
+#import "SDDeviceHelper.h"
 
 
 @implementation SDGraphicsImageRendererFormat
 @implementation SDGraphicsImageRendererFormat
 @synthesize scale = _scale;
 @synthesize scale = _scale;
@@ -131,21 +126,7 @@
             self.uiformat = uiformat;
             self.uiformat = uiformat;
         } else {
         } else {
 #endif
 #endif
-#if SD_VISION
-            CGFloat screenScale = UITraitCollection.currentTraitCollection.displayScale;
-#elif SD_WATCH
-            CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale;
-#elif SD_UIKIT
-            CGFloat screenScale = [UIScreen mainScreen].scale;
-#elif SD_MAC
-            NSScreen *mainScreen = nil;
-            if (@available(macOS 10.12, *)) {
-                mainScreen = [NSScreen mainScreen];
-            } else {
-                mainScreen = [NSScreen screens].firstObject;
-            }
-            CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
-#endif
+            CGFloat screenScale = SDDeviceHelper.screenScale;
             self.scale = screenScale;
             self.scale = screenScale;
             self.opaque = NO;
             self.opaque = NO;
 #if SD_UIKIT
 #if SD_UIKIT
@@ -172,21 +153,7 @@
             self.uiformat = uiformat;
             self.uiformat = uiformat;
         } else {
         } else {
 #endif
 #endif
-#if SD_VISION
-            CGFloat screenScale = UITraitCollection.currentTraitCollection.displayScale;
-#elif SD_WATCH
-            CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale;
-#elif SD_UIKIT
-            CGFloat screenScale = [UIScreen mainScreen].scale;
-#elif SD_MAC
-            NSScreen *mainScreen = nil;
-            if (@available(macOS 10.12, *)) {
-                mainScreen = [NSScreen mainScreen];
-            } else {
-                mainScreen = [NSScreen screens].firstObject;
-            }
-            CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
-#endif
+            CGFloat screenScale = SDDeviceHelper.screenScale;
             self.scale = screenScale;
             self.scale = screenScale;
             self.opaque = NO;
             self.opaque = NO;
 #if SD_UIKIT
 #if SD_UIKIT

+ 9 - 0
SDWebImage/Core/SDImageCacheDefine.m

@@ -12,6 +12,7 @@
 #import "SDAnimatedImage.h"
 #import "SDAnimatedImage.h"
 #import "UIImage+Metadata.h"
 #import "UIImage+Metadata.h"
 #import "SDInternalMacros.h"
 #import "SDInternalMacros.h"
+#import "SDDeviceHelper.h"
 
 
 #import <CoreServices/CoreServices.h>
 #import <CoreServices/CoreServices.h>
 
 
@@ -49,6 +50,12 @@ SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext *
         mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:6];
         mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:6];
     }
     }
     
     
+    // Some options need preserve the custom decode options
+    NSNumber *decodeToHDR = context[SDWebImageContextImageDecodeToHDR];
+    if (decodeToHDR == nil) {
+        decodeToHDR = mutableCoderOptions[SDImageCoderDecodeToHDR];
+    }
+    
     // Override individual options
     // Override individual options
     mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
     mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
     mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
     mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
@@ -57,6 +64,7 @@ SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext *
     mutableCoderOptions[SDImageCoderDecodeTypeIdentifierHint] = typeIdentifierHint;
     mutableCoderOptions[SDImageCoderDecodeTypeIdentifierHint] = typeIdentifierHint;
     mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint;
     mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint;
     mutableCoderOptions[SDImageCoderDecodeScaleDownLimitBytes] = scaleDownLimitBytesValue;
     mutableCoderOptions[SDImageCoderDecodeScaleDownLimitBytes] = scaleDownLimitBytesValue;
+    mutableCoderOptions[SDImageCoderDecodeToHDR] = decodeToHDR;
     
     
     return [mutableCoderOptions copy];
     return [mutableCoderOptions copy];
 }
 }
@@ -72,6 +80,7 @@ void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableCont
     mutableContext[SDWebImageContextImagePreserveAspectRatio] = decodeOptions[SDImageCoderDecodePreserveAspectRatio];
     mutableContext[SDWebImageContextImagePreserveAspectRatio] = decodeOptions[SDImageCoderDecodePreserveAspectRatio];
     mutableContext[SDWebImageContextImageThumbnailPixelSize] = decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
     mutableContext[SDWebImageContextImageThumbnailPixelSize] = decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
     mutableContext[SDWebImageContextImageScaleDownLimitBytes] = decodeOptions[SDImageCoderDecodeScaleDownLimitBytes];
     mutableContext[SDWebImageContextImageScaleDownLimitBytes] = decodeOptions[SDImageCoderDecodeScaleDownLimitBytes];
+    mutableContext[SDWebImageContextImageDecodeToHDR] = decodeOptions[SDImageCoderDecodeToHDR];
     
     
     NSString *typeIdentifierHint = decodeOptions[SDImageCoderDecodeTypeIdentifierHint];
     NSString *typeIdentifierHint = decodeOptions[SDImageCoderDecodeTypeIdentifierHint];
     if (!typeIdentifierHint) {
     if (!typeIdentifierHint) {

+ 8 - 0
SDWebImage/Core/SDImageCoder.h

@@ -88,6 +88,14 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDec
  */
  */
 FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleDownLimitBytes;
 FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleDownLimitBytes;
 
 
+/**
+ A Boolean value to provide converting to HDR during decoding.
+ @note Supported by iOS 17 and above when using ImageIO coder (third-party coder can support lower firmware)
+ Defaults to NO, decoder will automatically convert SDR.
+ @note works for `SDImageCoder`
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeToHDR;
+
 // These options are for image encoding
 // These options are for image encoding
 /**
 /**
  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.
  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.

+ 1 - 0
SDWebImage/Core/SDImageCoder.m

@@ -16,6 +16,7 @@ SDImageCoderOption const SDImageCoderDecodeFileExtensionHint = @"decodeFileExten
 SDImageCoderOption const SDImageCoderDecodeTypeIdentifierHint = @"decodeTypeIdentifierHint";
 SDImageCoderOption const SDImageCoderDecodeTypeIdentifierHint = @"decodeTypeIdentifierHint";
 SDImageCoderOption const SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding";
 SDImageCoderOption const SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding";
 SDImageCoderOption const SDImageCoderDecodeScaleDownLimitBytes = @"decodeScaleDownLimitBytes";
 SDImageCoderOption const SDImageCoderDecodeScaleDownLimitBytes = @"decodeScaleDownLimitBytes";
+SDImageCoderOption const SDImageCoderDecodeToHDR = @"decodeToHDR";
 
 
 SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
 SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
 SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";
 SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";

+ 6 - 0
SDWebImage/Core/SDImageCoderHelper.h

@@ -113,6 +113,12 @@ typedef struct SDImagePixelFormat {
  */
  */
 + (BOOL)CGImageIsLazy:(_Nonnull CGImageRef)cgImage;
 + (BOOL)CGImageIsLazy:(_Nonnull CGImageRef)cgImage;
 
 
+/**
+ Check if the CGImage is using HDR color space.
+ @note This use the same implementation like Apple, to checkl if color space uses transfer functions defined in ITU Rec.2100
+ */
++ (BOOL)CGImageIsHDR:(_Nonnull CGImageRef)cgImage;
+
 /**
 /**
  Create a decoded CGImage by the provided CGImage. This follows The Create Rule and you are response to call release after usage.
  Create a decoded CGImage by the provided CGImage. This follows The Create Rule and you are response to call release after usage.
  It will detect whether image contains alpha channel, then create a new bitmap context with the same size of image, and draw it. This can ensure that the image do not need extra decoding after been set to the imageView.
  It will detect whether image contains alpha channel, then create a new bitmap context with the same size of image, and draw it. This can ensure that the image do not need extra decoding after been set to the imageView.

+ 30 - 3
SDWebImage/Core/SDImageCoderHelper.m

@@ -18,6 +18,7 @@
 #import "SDGraphicsImageRenderer.h"
 #import "SDGraphicsImageRenderer.h"
 #import "SDInternalMacros.h"
 #import "SDInternalMacros.h"
 #import "SDDeviceHelper.h"
 #import "SDDeviceHelper.h"
+#import "SDImageIOAnimatedCoderInternal.h"
 #import <Accelerate/Accelerate.h>
 #import <Accelerate/Accelerate.h>
 
 
 #define kCGColorSpaceDeviceRGB CFSTR("kCGColorSpaceDeviceRGB")
 #define kCGColorSpaceDeviceRGB CFSTR("kCGColorSpaceDeviceRGB")
@@ -420,6 +421,20 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     }
     }
 }
 }
 
 
++ (BOOL)CGImageIsHDR:(_Nonnull CGImageRef)cgImage {
+    if (!cgImage) {
+        return NO;
+    }
+    if (@available(macOS 11.0, iOS 14, tvOS 14, watchOS 7.0, *)) {
+        CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage);
+        if (colorSpace) {
+            // Actually `CGColorSpaceIsHDR` use the same impl, but deprecated
+            return CGColorSpaceUsesITUR_2100TF(colorSpace);
+        }
+    }
+    return NO;
+}
+
 + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
 + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
     return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
     return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
 }
 }
@@ -680,12 +695,20 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
 #endif
 #endif
         CGImageRelease(decodedImageRef);
         CGImageRelease(decodedImageRef);
     } else {
     } else {
-        BOOL hasAlpha = [self CGImageContainsAlpha:imageRef];
         // Prefer to use new Image Renderer to re-draw image, instead of low-level CGBitmapContext and CGContextDrawImage
         // Prefer to use new Image Renderer to re-draw image, instead of low-level CGBitmapContext and CGContextDrawImage
         // This can keep both OS compatible and don't fight with Apple's performance optimization
         // This can keep both OS compatible and don't fight with Apple's performance optimization
         SDGraphicsImageRendererFormat *format = SDGraphicsImageRendererFormat.preferredFormat;
         SDGraphicsImageRendererFormat *format = SDGraphicsImageRendererFormat.preferredFormat;
-        format.opaque = !hasAlpha;
-        format.scale = image.scale;
+        // To support most OS compatible like Dynamic Range, prefer the image level format
+#if SD_UIKIT
+        if (@available(iOS 10.0, tvOS 10.0, *)) {
+            format.uiformat = image.imageRendererFormat;
+        } else {
+#endif
+            format.opaque = ![self CGImageContainsAlpha:imageRef];;
+            format.scale = image.scale;
+#if SD_UIKIT
+        }
+#endif
         CGSize imageSize = image.size;
         CGSize imageSize = image.size;
         SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
         SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
         decodedImage = [renderer imageWithActions:^(CGContextRef  _Nonnull context) {
         decodedImage = [renderer imageWithActions:^(CGContextRef  _Nonnull context) {
@@ -962,6 +985,10 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     if (image.sd_isVector) {
     if (image.sd_isVector) {
         return NO;
         return NO;
     }
     }
+    // FIXME: currently our force decode solution is buggy on HDR CGImage
+    if (image.sd_isHighDynamicRange) {
+        return NO;
+    }
     // Check policy (always)
     // Check policy (always)
     if (policy == SDImageForceDecodePolicyAlways) {
     if (policy == SDImageForceDecodePolicyAlways) {
         return YES;
         return YES;

+ 6 - 2
SDWebImage/Core/SDImageHEICCoder.m

@@ -77,8 +77,12 @@ static NSString * kSDCGImagePropertyHEICSUnclampedDelayTime = @"UnclampedDelayTi
 + (NSString *)imageUTType {
 + (NSString *)imageUTType {
     // See: https://nokiatech.github.io/heif/technical.html
     // See: https://nokiatech.github.io/heif/technical.html
     // Actually HEIC has another concept called `non-timed Image Sequence`, which can be encoded using `public.heic`
     // Actually HEIC has another concept called `non-timed Image Sequence`, which can be encoded using `public.heic`
-    // But current SDWebImage does not has this design, I don't know whether there are use case for this
-    // So we just replace and always use `timed Image Sequence`, means, animated image for encoding
+    return (__bridge NSString *)kSDUTTypeHEIC;
+}
+
++ (NSString *)animatedImageUTType {
+    // See: https://nokiatech.github.io/heif/technical.html
+    // We use `timed Image Sequence`, means, `public.heics` for animated image encoding
     return (__bridge NSString *)kSDUTTypeHEICS;
     return (__bridge NSString *)kSDUTTypeHEICS;
 }
 }
 
 

+ 7 - 0
SDWebImage/Core/SDImageIOAnimatedCoder.h

@@ -28,6 +28,13 @@
  @note Subclass override.
  @note Subclass override.
  */
  */
 @property (class, readonly, nonnull) NSString *imageUTType;
 @property (class, readonly, nonnull) NSString *imageUTType;
+/**
+ Some image codec use different UTI Type between animated image and static image.
+ For this case, override this method and return the UTI for animated image encoding.
+ @note Defaults to use the value of `imageUTType`, so it's @optional actually.
+ @note Subclass override.
+ */
+@property (class, readonly, nonnull) NSString *animatedImageUTType;
 /**
 /**
  The image container property key used in Image/IO API. Such as `kCGImagePropertyGIFDictionary`.
  The image container property key used in Image/IO API. Such as `kCGImagePropertyGIFDictionary`.
  @note Subclass override.
  @note Subclass override.

+ 40 - 10
SDWebImage/Core/SDImageIOAnimatedCoder.m

@@ -7,6 +7,7 @@
 */
 */
 
 
 #import "SDImageIOAnimatedCoder.h"
 #import "SDImageIOAnimatedCoder.h"
+#import "SDImageIOAnimatedCoderInternal.h"
 #import "NSImage+Compatibility.h"
 #import "NSImage+Compatibility.h"
 #import "UIImage+Metadata.h"
 #import "UIImage+Metadata.h"
 #import "NSData+ImageContentType.h"
 #import "NSData+ImageContentType.h"
@@ -278,6 +279,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
     CGSize _thumbnailSize;
     CGSize _thumbnailSize;
     NSUInteger _limitBytes;
     NSUInteger _limitBytes;
     BOOL _lazyDecode;
     BOOL _lazyDecode;
+    BOOL _decodeToHDR;
 }
 }
 
 
 - (void)dealloc
 - (void)dealloc
@@ -314,6 +316,10 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
                                  userInfo:nil];
                                  userInfo:nil];
 }
 }
 
 
++ (NSString *)animatedImageUTType {
+    return [self imageUTType];
+}
+
 + (NSString *)dictionaryProperty {
 + (NSString *)dictionaryProperty {
     @throw [NSException exceptionWithName:NSInternalInconsistencyException
     @throw [NSException exceptionWithName:NSInternalInconsistencyException
                                    reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
                                    reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)]
@@ -421,7 +427,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
     return frameDuration;
     return frameDuration;
 }
 }
 
 
-+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage {
++ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage decodeToHDR:(BOOL)decodeToHDR {
     // `animatedImage` means called from `SDAnimatedImageProvider.animatedImageFrameAtIndex`
     // `animatedImage` means called from `SDAnimatedImageProvider.animatedImageFrameAtIndex`
     NSDictionary *options;
     NSDictionary *options;
     if (animatedImage) {
     if (animatedImage) {
@@ -453,6 +459,14 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
     } else {
     } else {
         decodingOptions = [NSMutableDictionary dictionary];
         decodingOptions = [NSMutableDictionary dictionary];
     }
     }
+    if (@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)) {
+        if (decodeToHDR) {
+            decodingOptions[(__bridge NSString *)kCGImageSourceDecodeRequest] = (__bridge NSString *)kCGImageSourceDecodeToHDR;
+        } else {
+            decodingOptions[(__bridge NSString *)kCGImageSourceDecodeRequest] = (__bridge NSString *)kCGImageSourceDecodeToSDR;
+        }
+    }
+  
     CGImageRef imageRef;
     CGImageRef imageRef;
     BOOL createFullImage = thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height);
     BOOL createFullImage = thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height);
     if (createFullImage) {
     if (createFullImage) {
@@ -478,6 +492,8 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
     if (!imageRef) {
     if (!imageRef) {
         return nil;
         return nil;
     }
     }
+    BOOL isHDRImage = [SDImageCoderHelper CGImageIsHDR:imageRef];
+    
     // Thumbnail image post-process
     // Thumbnail image post-process
     if (!createFullImage) {
     if (!createFullImage) {
         if (preserveAspectRatio) {
         if (preserveAspectRatio) {
@@ -492,9 +508,10 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
             }
             }
         }
         }
     }
     }
+    
     // Check whether output CGImage is decoded
     // Check whether output CGImage is decoded
     BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:imageRef];
     BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:imageRef];
-    if (!lazyDecode) {
+    if (!lazyDecode && !isHDRImage) {
         if (isLazy) {
         if (isLazy) {
             // Use CoreGraphics to trigger immediately decode to drop lazy CGImage
             // Use CoreGraphics to trigger immediately decode to drop lazy CGImage
             CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
             CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
@@ -504,7 +521,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
                 isLazy = NO;
                 isLazy = NO;
             }
             }
         }
         }
-    } else if (animatedImage) {
+    } else if (animatedImage && !isHDRImage) {
         // 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
         // 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, *)) {
         if (@available(iOS 15, tvOS 15, *)) {
             // User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef
             // User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef
@@ -591,6 +608,8 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
         limitBytes = limitBytesValue.unsignedIntegerValue;
         limitBytes = limitBytesValue.unsignedIntegerValue;
     }
     }
     
     
+    BOOL decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue];
+    
 #if SD_MAC
 #if SD_MAC
     // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
     // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG)
     // Which decode frames in time and reduce memory usage
     // Which decode frames in time and reduce memory usage
@@ -655,12 +674,12 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
     
     
     BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
     BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
     if (decodeFirstFrame || frameCount <= 1) {
     if (decodeFirstFrame || frameCount <= 1) {
-        animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
+        animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO decodeToHDR:decodeToHDR];
     } else {
     } else {
         NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:frameCount];
         NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:frameCount];
         
         
         for (size_t i = 0; i < frameCount; i++) {
         for (size_t i = 0; i < frameCount; i++) {
-            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
+            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO decodeToHDR:decodeToHDR];
             if (!image) {
             if (!image) {
                 continue;
                 continue;
             }
             }
@@ -728,6 +747,9 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
             lazyDecode = lazyDecodeValue.boolValue;
             lazyDecode = lazyDecodeValue.boolValue;
         }
         }
         _lazyDecode = lazyDecode;
         _lazyDecode = lazyDecode;
+
+        _decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue];
+        
         SD_LOCK_INIT(_lock);
         SD_LOCK_INIT(_lock);
 #if SD_UIKIT
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
@@ -788,7 +810,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
         if (scaleFactor != nil) {
         if (scaleFactor != nil) {
             scale = MAX([scaleFactor doubleValue], 1);
             scale = MAX([scaleFactor doubleValue], 1);
         }
         }
-        image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
+        image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO decodeToHDR:_finished ? _decodeToHDR : NO];
         if (image) {
         if (image) {
             image.sd_imageFormat = self.class.imageFormat;
             image.sd_imageFormat = self.class.imageFormat;
         }
         }
@@ -828,9 +850,15 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
         // Earily return, supports CGImage only
         // Earily return, supports CGImage only
         return nil;
         return nil;
     }
     }
+    BOOL onlyEncodeOnce = [options[SDImageCoderEncodeFirstFrameOnly] boolValue] || frames.count <= 1;
     
     
     NSMutableData *imageData = [NSMutableData data];
     NSMutableData *imageData = [NSMutableData data];
-    NSString *imageUTType = self.class.imageUTType;
+    NSString *imageUTType;
+    if (onlyEncodeOnce) {
+        imageUTType = self.class.imageUTType;
+    } else {
+        imageUTType = self.class.animatedImageUTType;
+    }
     
     
     // Create an image destination. Animated Image does not support EXIF image orientation TODO
     // Create an image destination. Animated Image does not support EXIF image orientation TODO
     // The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
     // The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead.
@@ -894,8 +922,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
     }
     }
     properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
     properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
     
     
-    BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
-    if (encodeFirstFrame || frames.count <= 1) {
+    if (onlyEncodeOnce) {
         // for static single images
         // for static single images
         CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
         CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
     } else {
     } else {
@@ -993,6 +1020,9 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
             lazyDecode = lazyDecodeValue.boolValue;
             lazyDecode = lazyDecodeValue.boolValue;
         }
         }
         _lazyDecode = lazyDecode;
         _lazyDecode = lazyDecode;
+
+        _decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue];
+        
         _imageSource = imageSource;
         _imageSource = imageSource;
         _imageData = data;
         _imageData = data;
 #if SD_UIKIT
 #if SD_UIKIT
@@ -1083,7 +1113,7 @@ static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) {
 }
 }
 
 
 - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
 - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
-    UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:YES];
+    UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:YES decodeToHDR:!_incremental || _finished ? _decodeToHDR : NO];
     if (!image) {
     if (!image) {
         return nil;
         return nil;
     }
     }

+ 9 - 4
SDWebImage/Core/SDImageIOCoder.m

@@ -28,6 +28,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     BOOL _preserveAspectRatio;
     BOOL _preserveAspectRatio;
     CGSize _thumbnailSize;
     CGSize _thumbnailSize;
     BOOL _lazyDecode;
     BOOL _lazyDecode;
+    BOOL _decodeToHDR;
 }
 }
 
 
 - (void)dealloc {
 - (void)dealloc {
@@ -179,6 +180,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         lazyDecode = lazyDecodeValue.boolValue;
         lazyDecode = lazyDecodeValue.boolValue;
     }
     }
     
     
+    BOOL decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue];
+    
     NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint];
     NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint];
     if (!typeIdentifierHint) {
     if (!typeIdentifierHint) {
         // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension
         // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension
@@ -211,7 +214,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     CFStringRef uttype = CGImageSourceGetType(source);
     CFStringRef uttype = CGImageSourceGetType(source);
     SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype];
     SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype];
     
     
-    UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
+    UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO decodeToHDR:decodeToHDR];
     CFRelease(source);
     CFRelease(source);
     
     
     image.sd_imageFormat = imageFormat;
     image.sd_imageFormat = imageFormat;
@@ -256,6 +259,9 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             lazyDecode = lazyDecodeValue.boolValue;
             lazyDecode = lazyDecodeValue.boolValue;
         }
         }
         _lazyDecode = lazyDecode;
         _lazyDecode = lazyDecode;
+        
+        _decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue];
+        
 #if SD_UIKIT
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
 #endif
 #endif
@@ -306,7 +312,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         if (scaleFactor != nil) {
         if (scaleFactor != nil) {
             scale = MAX([scaleFactor doubleValue], 1);
             scale = MAX([scaleFactor doubleValue], 1);
         }
         }
-        image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
+        image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO decodeToHDR:_finished ? _decodeToHDR : NO];
         if (image) {
         if (image) {
             CFStringRef uttype = CGImageSourceGetType(_imageSource);
             CFStringRef uttype = CGImageSourceGetType(_imageSource);
             image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype];
             image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype];
@@ -330,7 +336,6 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         // Earily return, supports CGImage only
         // Earily return, supports CGImage only
         return nil;
         return nil;
     }
     }
-    
     if (format == SDImageFormatUndefined) {
     if (format == SDImageFormatUndefined) {
         BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:imageRef];
         BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:imageRef];
         if (hasAlpha) {
         if (hasAlpha) {
@@ -339,7 +344,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             format = SDImageFormatJPEG;
             format = SDImageFormatJPEG;
         }
         }
     }
     }
-    
+
     NSMutableData *imageData = [NSMutableData data];
     NSMutableData *imageData = [NSMutableData data];
     CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
     CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
     
     

+ 7 - 0
SDWebImage/Core/SDWebImageDefine.h

@@ -337,6 +337,13 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageT
  */
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleDownLimitBytes;
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleDownLimitBytes;
 
 
+/**
+ A Boolean value to provide converting to HDR during decoding.
+ @note Supported by iOS 17 and above when using ImageIO coder (third-party coder can support lower firmware)
+ Defaults to NO, decoder will automatically convert SDR.
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageDecodeToHDR;
+
 #pragma mark - Cache Context Options
 #pragma mark - Cache Context Options
 
 
 /**
 /**

+ 1 - 0
SDWebImage/Core/SDWebImageDefine.m

@@ -148,6 +148,7 @@ SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imag
 SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize";
 SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize";
 SDWebImageContextOption const SDWebImageContextImageTypeIdentifierHint = @"imageTypeIdentifierHint";
 SDWebImageContextOption const SDWebImageContextImageTypeIdentifierHint = @"imageTypeIdentifierHint";
 SDWebImageContextOption const SDWebImageContextImageScaleDownLimitBytes = @"imageScaleDownLimitBytes";
 SDWebImageContextOption const SDWebImageContextImageScaleDownLimitBytes = @"imageScaleDownLimitBytes";
+SDWebImageContextOption const SDWebImageContextImageDecodeToHDR = @"imageDecodeToHDR";
 SDWebImageContextOption const SDWebImageContextImageEncodeOptions = @"imageEncodeOptions";
 SDWebImageContextOption const SDWebImageContextImageEncodeOptions = @"imageEncodeOptions";
 SDWebImageContextOption const SDWebImageContextQueryCacheType = @"queryCacheType";
 SDWebImageContextOption const SDWebImageContextQueryCacheType = @"queryCacheType";
 SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType";
 SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType";

+ 6 - 0
SDWebImage/Core/UIImage+Metadata.h

@@ -95,4 +95,10 @@
  */
  */
 @property (nonatomic, copy) SDImageCoderOptions *sd_decodeOptions;
 @property (nonatomic, copy) SDImageCoderOptions *sd_decodeOptions;
 
 
+/**
+ A bool value indicating that the image is using HDR
+ @note Only valid for CGImage based, for CIImage based, the returned value is not correct.
+ */
+@property (nonatomic, assign, readonly) BOOL sd_isHighDynamicRange;
+
 @end
 @end

+ 13 - 0
SDWebImage/Core/UIImage+Metadata.m

@@ -10,6 +10,7 @@
 #import "NSImage+Compatibility.h"
 #import "NSImage+Compatibility.h"
 #import "SDInternalMacros.h"
 #import "SDInternalMacros.h"
 #import "objc/runtime.h"
 #import "objc/runtime.h"
+#import "SDImageCoderHelper.h"
 
 
 @implementation UIImage (Metadata)
 @implementation UIImage (Metadata)
 
 
@@ -220,4 +221,16 @@
     return nil;
     return nil;
 }
 }
 
 
+- (BOOL)sd_isHighDynamicRange {
+#if SD_MAC
+    return [SDImageCoderHelper CGImageIsHDR:self.CGImage];
+#else
+    if (@available(iOS 17, tvOS 17, watchOS 10, *)) {
+        return self.isHighDynamicRange;
+    } else {
+        return [SDImageCoderHelper CGImageIsHDR:self.CGImage];
+    }
+#endif
+}
+
 @end
 @end

+ 6 - 0
SDWebImage/Private/SDDeviceHelper.h

@@ -12,7 +12,13 @@
 /// Device information helper methods
 /// Device information helper methods
 @interface SDDeviceHelper : NSObject
 @interface SDDeviceHelper : NSObject
 
 
+#pragma mark - RAM
 + (NSUInteger)totalMemory;
 + (NSUInteger)totalMemory;
 + (NSUInteger)freeMemory;
 + (NSUInteger)freeMemory;
 
 
+#pragma mark - Screen
++ (double)screenScale;
++ (double)screenEDR;
++ (double)screenMaxEDR;
+
 @end
 @end

+ 65 - 0
SDWebImage/Private/SDDeviceHelper.m

@@ -30,4 +30,69 @@
     return vm_stat.free_count * page_size;
     return vm_stat.free_count * page_size;
 }
 }
 
 
++ (double)screenScale {
+#if SD_VISION
+    CGFloat screenScale = UITraitCollection.currentTraitCollection.displayScale;
+#elif SD_WATCH
+    CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale;
+#elif SD_UIKIT
+    CGFloat screenScale = [UIScreen mainScreen].scale;
+#elif SD_MAC
+    NSScreen *mainScreen = nil;
+    if (@available(macOS 10.12, *)) {
+        mainScreen = [NSScreen mainScreen];
+    } else {
+        mainScreen = [NSScreen screens].firstObject;
+    }
+    CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
+#endif
+    return screenScale;
+}
+
++ (double)screenEDR {
+#if SD_VISION
+    // no API to query, but it's HDR ready, from the testing, the value is 200 nits
+    CGFloat EDR = 2.0;
+#elif SD_WATCH
+    // currently no HDR support, fallback to SDR
+    CGFloat EDR = 1.0;
+#elif SD_UIKIT
+    CGFloat EDR = 1.0;
+    if (@available(iOS 16.0, tvOS 16.0, *)) {
+        UIScreen *mainScreen = [UIScreen mainScreen];
+        EDR = mainScreen.currentEDRHeadroom;
+    }
+#elif SD_MAC
+    CGFloat EDR = 1.0;
+    if (@available(macOS 10.15, *)) {
+        NSScreen *mainScreen = [NSScreen mainScreen];
+        EDR = mainScreen.maximumExtendedDynamicRangeColorComponentValue;
+    }
+#endif
+    return EDR;
+}
+
++ (double)screenMaxEDR {
+#if SD_VISION
+    // no API to query, but it's HDR ready, from the testing, the value is 200 nits
+    CGFloat maxEDR = 2.0;
+#elif SD_WATCH
+    // currently no HDR support, fallback to SDR
+    CGFloat maxEDR = 1.0;
+#elif SD_UIKIT
+    CGFloat maxEDR = 1.0;
+    if (@available(iOS 16.0, tvOS 16.0, *)) {
+        UIScreen *mainScreen = [UIScreen mainScreen];
+        maxEDR = mainScreen.potentialEDRHeadroom;
+    }
+#elif SD_MAC
+    CGFloat maxEDR = 1.0;
+    if (@available(macOS 10.15, *)) {
+        NSScreen *mainScreen = [NSScreen mainScreen];
+        maxEDR = mainScreen.maximumPotentialExtendedDynamicRangeColorComponentValue;
+    }
+#endif
+    return maxEDR;
+}
+
 @end
 @end

+ 2 - 15
SDWebImage/Private/SDImageAssetManager.m

@@ -8,26 +8,13 @@
 
 
 #import "SDImageAssetManager.h"
 #import "SDImageAssetManager.h"
 #import "SDInternalMacros.h"
 #import "SDInternalMacros.h"
+#import "SDDeviceHelper.h"
 
 
 static NSArray *SDBundlePreferredScales(void) {
 static NSArray *SDBundlePreferredScales(void) {
     static NSArray *scales;
     static NSArray *scales;
     static dispatch_once_t onceToken;
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
     dispatch_once(&onceToken, ^{
-#if SD_VISION
-        CGFloat screenScale = UITraitCollection.currentTraitCollection.displayScale;
-#elif SD_WATCH
-        CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale;
-#elif SD_UIKIT
-        CGFloat screenScale = [UIScreen mainScreen].scale;
-#elif SD_MAC
-      NSScreen *mainScreen = nil;
-      if (@available(macOS 10.12, *)) {
-          mainScreen = [NSScreen mainScreen];
-      } else {
-          mainScreen = [NSScreen screens].firstObject;
-      }
-      CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
-#endif
+        CGFloat screenScale = SDDeviceHelper.screenScale;
         if (screenScale <= 1) {
         if (screenScale <= 1) {
             scales = @[@1,@2,@3];
             scales = @[@1,@2,@3];
         } else if (screenScale <= 2) {
         } else if (screenScale <= 2) {

+ 1 - 1
SDWebImage/Private/SDImageIOAnimatedCoderInternal.h

@@ -32,7 +32,7 @@
 
 
 + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source;
 + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source;
 + (NSUInteger)imageLoopCountWithSource:(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 animatedImage:(BOOL)animatedImage;
++ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage decodeToHDR:(BOOL)decodeToHDR;
 + (BOOL)canEncodeToFormat:(SDImageFormat)format;
 + (BOOL)canEncodeToFormat:(SDImageFormat)format;
 + (BOOL)canDecodeFromFormat:(SDImageFormat)format;
 + (BOOL)canDecodeFromFormat:(SDImageFormat)format;
 
 

+ 30 - 0
Tests/SDWebImage Tests.xcodeproj/project.pbxproj

@@ -77,6 +77,18 @@
 		32515F9E24AF1919005E8F79 /* TestImageAnimated.webp in Resources */ = {isa = PBXBuildFile; fileRef = 32515F9824AF1919005E8F79 /* TestImageAnimated.webp */; };
 		32515F9E24AF1919005E8F79 /* TestImageAnimated.webp in Resources */ = {isa = PBXBuildFile; fileRef = 32515F9824AF1919005E8F79 /* TestImageAnimated.webp */; };
 		3254C32020641077008D1022 /* SDImageTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3254C31F20641077008D1022 /* SDImageTransformerTests.m */; };
 		3254C32020641077008D1022 /* SDImageTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3254C31F20641077008D1022 /* SDImageTransformerTests.m */; };
 		3254C32120641077008D1022 /* SDImageTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3254C31F20641077008D1022 /* SDImageTransformerTests.m */; };
 		3254C32120641077008D1022 /* SDImageTransformerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3254C31F20641077008D1022 /* SDImageTransformerTests.m */; };
+		3261EC8B2D66235D00F2702E /* TestHDR.avif in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC882D66235D00F2702E /* TestHDR.avif */; };
+		3261EC8C2D66235D00F2702E /* TestHDR.jxl in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC8A2D66235D00F2702E /* TestHDR.jxl */; };
+		3261EC8D2D66235D00F2702E /* TestHDR.heic in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC892D66235D00F2702E /* TestHDR.heic */; };
+		3261EC8E2D66235D00F2702E /* TestHDR.avif in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC882D66235D00F2702E /* TestHDR.avif */; };
+		3261EC8F2D66235D00F2702E /* TestHDR.jxl in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC8A2D66235D00F2702E /* TestHDR.jxl */; };
+		3261EC902D66235D00F2702E /* TestHDR.heic in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC892D66235D00F2702E /* TestHDR.heic */; };
+		3261EC912D66235D00F2702E /* TestHDR.avif in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC882D66235D00F2702E /* TestHDR.avif */; };
+		3261EC922D66235D00F2702E /* TestHDR.jxl in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC8A2D66235D00F2702E /* TestHDR.jxl */; };
+		3261EC932D66235D00F2702E /* TestHDR.heic in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC892D66235D00F2702E /* TestHDR.heic */; };
+		3261EC942D66235D00F2702E /* TestHDR.avif in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC882D66235D00F2702E /* TestHDR.avif */; };
+		3261EC952D66235D00F2702E /* TestHDR.jxl in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC8A2D66235D00F2702E /* TestHDR.jxl */; };
+		3261EC962D66235D00F2702E /* TestHDR.heic in Resources */ = {isa = PBXBuildFile; fileRef = 3261EC892D66235D00F2702E /* TestHDR.heic */; };
 		32648067250232F7004FA0FC /* 1@2x.gif in Resources */ = {isa = PBXBuildFile; fileRef = 32648066250232F7004FA0FC /* 1@2x.gif */; };
 		32648067250232F7004FA0FC /* 1@2x.gif in Resources */ = {isa = PBXBuildFile; fileRef = 32648066250232F7004FA0FC /* 1@2x.gif */; };
 		32648068250232F7004FA0FC /* 1@2x.gif in Resources */ = {isa = PBXBuildFile; fileRef = 32648066250232F7004FA0FC /* 1@2x.gif */; };
 		32648068250232F7004FA0FC /* 1@2x.gif in Resources */ = {isa = PBXBuildFile; fileRef = 32648066250232F7004FA0FC /* 1@2x.gif */; };
 		32648069250232F7004FA0FC /* 1@2x.gif in Resources */ = {isa = PBXBuildFile; fileRef = 32648066250232F7004FA0FC /* 1@2x.gif */; };
 		32648069250232F7004FA0FC /* 1@2x.gif in Resources */ = {isa = PBXBuildFile; fileRef = 32648066250232F7004FA0FC /* 1@2x.gif */; };
@@ -199,6 +211,9 @@
 		32515F9724AF1919005E8F79 /* TestImageStatic.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageStatic.webp; sourceTree = "<group>"; };
 		32515F9724AF1919005E8F79 /* TestImageStatic.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageStatic.webp; sourceTree = "<group>"; };
 		32515F9824AF1919005E8F79 /* TestImageAnimated.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageAnimated.webp; sourceTree = "<group>"; };
 		32515F9824AF1919005E8F79 /* TestImageAnimated.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageAnimated.webp; sourceTree = "<group>"; };
 		3254C31F20641077008D1022 /* SDImageTransformerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDImageTransformerTests.m; sourceTree = "<group>"; };
 		3254C31F20641077008D1022 /* SDImageTransformerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SDImageTransformerTests.m; sourceTree = "<group>"; };
+		3261EC882D66235D00F2702E /* TestHDR.avif */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestHDR.avif; sourceTree = "<group>"; };
+		3261EC892D66235D00F2702E /* TestHDR.heic */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestHDR.heic; sourceTree = "<group>"; };
+		3261EC8A2D66235D00F2702E /* TestHDR.jxl */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestHDR.jxl; sourceTree = "<group>"; };
 		32648066250232F7004FA0FC /* 1@2x.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "1@2x.gif"; sourceTree = "<group>"; };
 		32648066250232F7004FA0FC /* 1@2x.gif */ = {isa = PBXFileReference; lastKnownFileType = image.gif; path = "1@2x.gif"; sourceTree = "<group>"; };
 		3264CD162AAB1E23001E338B /* TestJFIF.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = TestJFIF.jpg; sourceTree = "<group>"; };
 		3264CD162AAB1E23001E338B /* TestJFIF.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = TestJFIF.jpg; sourceTree = "<group>"; };
 		3264FF2D205D42CB00F6BD48 /* SDWebImageTestTransformer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDWebImageTestTransformer.h; sourceTree = "<group>"; };
 		3264FF2D205D42CB00F6BD48 /* SDWebImageTestTransformer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SDWebImageTestTransformer.h; sourceTree = "<group>"; };
@@ -351,6 +366,9 @@
 				433BBBBA1D7EFA8B0086B6E9 /* MonochromeTestImage.jpg */,
 				433BBBBA1D7EFA8B0086B6E9 /* MonochromeTestImage.jpg */,
 				324047432271956F007C53E1 /* TestEXIF.png */,
 				324047432271956F007C53E1 /* TestEXIF.png */,
 				3264CD162AAB1E23001E338B /* TestJFIF.jpg */,
 				3264CD162AAB1E23001E338B /* TestJFIF.jpg */,
+				3261EC882D66235D00F2702E /* TestHDR.avif */,
+				3261EC892D66235D00F2702E /* TestHDR.heic */,
+				3261EC8A2D66235D00F2702E /* TestHDR.jxl */,
 				321F310D27D0DC490042B274 /* TestImage.bmp */,
 				321F310D27D0DC490042B274 /* TestImage.bmp */,
 				433BBBB61D7EF8200086B6E9 /* TestImage.gif */,
 				433BBBB61D7EF8200086B6E9 /* TestImage.gif */,
 				326E69462334C0C200B7252C /* TestLoopCount.gif */,
 				326E69462334C0C200B7252C /* TestLoopCount.gif */,
@@ -616,6 +634,9 @@
 				32464AA12B7B1833006BE70E /* MonochromeTestImage.jpg in Resources */,
 				32464AA12B7B1833006BE70E /* MonochromeTestImage.jpg in Resources */,
 				32464AA42B7B1833006BE70E /* TestImageLarge.png in Resources */,
 				32464AA42B7B1833006BE70E /* TestImageLarge.png in Resources */,
 				32464A962B7B1833006BE70E /* TestImage.gif in Resources */,
 				32464A962B7B1833006BE70E /* TestImage.gif in Resources */,
+				3261EC8B2D66235D00F2702E /* TestHDR.avif in Resources */,
+				3261EC8C2D66235D00F2702E /* TestHDR.jxl in Resources */,
+				3261EC8D2D66235D00F2702E /* TestHDR.heic in Resources */,
 				32464A952B7B1833006BE70E /* TestImageStatic.webp in Resources */,
 				32464A952B7B1833006BE70E /* TestImageStatic.webp in Resources */,
 				32464A982B7B1833006BE70E /* TestLoopCount.gif in Resources */,
 				32464A982B7B1833006BE70E /* TestLoopCount.gif in Resources */,
 			);
 			);
@@ -647,6 +668,9 @@
 				32515F9E24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				32515F9E24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				3299228E2365DC6C00EAFD97 /* TestImageAnimated.heics in Resources */,
 				3299228E2365DC6C00EAFD97 /* TestImageAnimated.heics in Resources */,
 				32515F9B24AF1919005E8F79 /* TestImageStatic.webp in Resources */,
 				32515F9B24AF1919005E8F79 /* TestImageStatic.webp in Resources */,
+				3261EC912D66235D00F2702E /* TestHDR.avif in Resources */,
+				3261EC922D66235D00F2702E /* TestHDR.jxl in Resources */,
+				3261EC932D66235D00F2702E /* TestHDR.heic in Resources */,
 				329922862365DC6C00EAFD97 /* TestImage.gif in Resources */,
 				329922862365DC6C00EAFD97 /* TestImage.gif in Resources */,
 				329922852365DC6C00EAFD97 /* TestEXIF.png in Resources */,
 				329922852365DC6C00EAFD97 /* TestEXIF.png in Resources */,
 			);
 			);
@@ -678,6 +702,9 @@
 				32515F9D24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				32515F9D24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				327A418D211D660600495442 /* TestImage.heic in Resources */,
 				327A418D211D660600495442 /* TestImage.heic in Resources */,
 				32515F9A24AF1919005E8F79 /* TestImageStatic.webp in Resources */,
 				32515F9A24AF1919005E8F79 /* TestImageStatic.webp in Resources */,
+				3261EC8E2D66235D00F2702E /* TestHDR.avif in Resources */,
+				3261EC8F2D66235D00F2702E /* TestHDR.jxl in Resources */,
+				3261EC902D66235D00F2702E /* TestHDR.heic in Resources */,
 				326E69482334C0C300B7252C /* TestLoopCount.gif in Resources */,
 				326E69482334C0C300B7252C /* TestLoopCount.gif in Resources */,
 				32B99EA5203B31360017FD66 /* TestImageLarge.jpg in Resources */,
 				32B99EA5203B31360017FD66 /* TestImageLarge.jpg in Resources */,
 			);
 			);
@@ -709,6 +736,9 @@
 				32515F9C24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				32515F9C24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				326E69472334C0C300B7252C /* TestLoopCount.gif in Resources */,
 				326E69472334C0C300B7252C /* TestLoopCount.gif in Resources */,
 				32515F9924AF1919005E8F79 /* TestImageStatic.webp in Resources */,
 				32515F9924AF1919005E8F79 /* TestImageStatic.webp in Resources */,
+				3261EC942D66235D00F2702E /* TestHDR.avif in Resources */,
+				3261EC952D66235D00F2702E /* TestHDR.jxl in Resources */,
+				3261EC962D66235D00F2702E /* TestHDR.heic in Resources */,
 				433BBBBB1D7EFA8B0086B6E9 /* MonochromeTestImage.jpg in Resources */,
 				433BBBBB1D7EFA8B0086B6E9 /* MonochromeTestImage.jpg in Resources */,
 				324047442271956F007C53E1 /* TestEXIF.png in Resources */,
 				324047442271956F007C53E1 /* TestEXIF.png in Resources */,
 			);
 			);

BIN
Tests/Tests/Images/TestHDR.avif


BIN
Tests/Tests/Images/TestHDR.heic


BIN
Tests/Tests/Images/TestHDR.jxl


+ 4 - 0
Tests/Tests/SDAnimatedImageTest.m

@@ -311,6 +311,10 @@ static BOOL _isCalled;
 }
 }
 
 
 - (void)test22AnimatedImageViewCategory {
 - (void)test22AnimatedImageViewCategory {
+    if (SDTestCase.isCI) {
+        // This case cause random failure on GitHub Action only (but not local testing). Disabled for now
+        return;
+    }
     XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category"];
     XCTestExpectation *expectation = [self expectationWithDescription:@"test SDAnimatedImageView view category"];
     SDAnimatedImageView *imageView = [SDAnimatedImageView new];
     SDAnimatedImageView *imageView = [SDAnimatedImageView new];
     NSURL *testURL = [NSURL URLWithString:@"https://media.giphy.com/media/3oeji6siihbdrxxi40/giphy.gif"];
     NSURL *testURL = [NSURL URLWithString:@"https://media.giphy.com/media/3oeji6siihbdrxxi40/giphy.gif"];

+ 29 - 0
Tests/Tests/SDImageCoderTests.m

@@ -635,6 +635,35 @@
     }
     }
 }
 }
 
 
+- (void)test32ThatHDRDecodeWorks {
+    // Only test for iOS 17+/macOS 14+/visionOS 1+, or ImageIO decoder does not support HDR
+#if SD_MAC || SD_IOS || SD_VISION
+    if (@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)) {
+        NSArray *formats = @[@"heic", @"avif", @"jxl"];
+        for (NSString *format in formats) {
+            NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"TestHDR" withExtension:format];
+            NSData *data = [NSData dataWithContentsOfURL:url];
+            // Decoding
+            UIImage *HDRImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeToHDR : @(YES)}];
+            UIImage *SDRImage = [SDImageIOCoder.sharedCoder decodedImageWithData:data options:@{SDImageCoderDecodeToHDR : @(NO)}];
+            
+            expect(HDRImage).notTo.beNil();
+            expect(SDRImage).notTo.beNil();
+            
+            expect([SDImageCoderHelper CGImageIsHDR:HDRImage.CGImage]).beTruthy();
+            expect(HDRImage.sd_isHighDynamicRange).beTruthy();
+            // FIXME: on Simulator, the SDR decode options will not take effect, so SDR is the same as HDR
+#if !TARGET_OS_SIMULATOR
+            expect([SDImageCoderHelper CGImageIsHDR:SDRImage.CGImage]).beFalsy();
+            expect(SDRImage.sd_isHighDynamicRange).beFalsy();
+#endif
+            // FIXME: Encoding need iOS 18+/macOS 15+
+            // And need test both GainMap HDR or ISO HDR, TODO
+        }
+    }
+#endif
+}
+
 #pragma mark - Utils
 #pragma mark - Utils
 
 
 - (void)verifyCoder:(id<SDImageCoder>)coder
 - (void)verifyCoder:(id<SDImageCoder>)coder