Ver código fonte

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 ano atrás
pai
commit
92a7ab93e0

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

@@ -22,11 +22,21 @@
     if (!self.imageView.sd_imageIndicator) {
         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
                       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.animationRepeatCount = 0;
 }

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

@@ -59,6 +59,9 @@
         [SDWebImageDownloader sharedDownloader].config.executionOrder = SDWebImageDownloaderLIFOExecutionOrder;
         
         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://assets.sbnation.com/assets/2512203/dogflops.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"?>
-<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>
         <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"/>
     </dependencies>
     <scenes>
@@ -677,35 +677,15 @@
                         <rect key="frame" x="0.0" y="0.0" width="480" height="400"/>
                         <autoresizingMask key="autoresizingMask"/>
                         <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">
-                                <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">
                                     <behavior key="behavior" pushIn="YES" changeContents="YES" lightByContents="YES"/>
                                     <font key="font" metaFont="system"/>
                                 </buttonCell>
+                                <constraints>
+                                    <constraint firstAttribute="height" constant="26" id="WoQ-RY-bSV"/>
+                                </constraints>
                             </button>
                         </subviews>
                         <constraints>
@@ -715,10 +695,6 @@
                     </view>
                     <connections>
                         <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>
                 </viewController>
                 <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 ()
 
-@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;
 
 @end
@@ -23,26 +28,39 @@
 
 - (void)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
-    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;
     
+#pragma mark - Static Image
     // NSImageView + Static Image
     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];
+    // 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
-    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"];
     NSMenuItem *item1 = [menu1 addItemWithTitle:@"Toggle Animation" action:@selector(toggleAnimation:) keyEquivalent:@""];
     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
     self.imageView4.sd_imageTransition = SDWebImageTransition.fadeTransition;
     self.imageView4.imageScaling = NSImageScaleProportionallyUpOrDown;
@@ -53,12 +71,36 @@
     item2.tag = 2;
     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.action = @selector(clearCacheButtonClicked:);
     [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"]];
 }
 
+- (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 {
     NSButton *button = (NSButton *)sender;
     button.state = NSControlStateValueOn;
@@ -69,7 +111,7 @@
 }
 
 - (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) {
         imageView.animates = NO;
     } 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.
 @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 default value is equal to the scale of the main screen.
 @property (nonatomic) CGFloat scale;

+ 3 - 36
SDWebImage/Core/SDGraphicsImageRenderer.m

@@ -8,12 +8,7 @@
 
 #import "SDGraphicsImageRenderer.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
 @synthesize scale = _scale;
@@ -131,21 +126,7 @@
             self.uiformat = uiformat;
         } else {
 #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.opaque = NO;
 #if SD_UIKIT
@@ -172,21 +153,7 @@
             self.uiformat = uiformat;
         } else {
 #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.opaque = NO;
 #if SD_UIKIT

+ 9 - 0
SDWebImage/Core/SDImageCacheDefine.m

@@ -12,6 +12,7 @@
 #import "SDAnimatedImage.h"
 #import "UIImage+Metadata.h"
 #import "SDInternalMacros.h"
+#import "SDDeviceHelper.h"
 
 #import <CoreServices/CoreServices.h>
 
@@ -49,6 +50,12 @@ SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext *
         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
     mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
     mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
@@ -57,6 +64,7 @@ SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext *
     mutableCoderOptions[SDImageCoderDecodeTypeIdentifierHint] = typeIdentifierHint;
     mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint;
     mutableCoderOptions[SDImageCoderDecodeScaleDownLimitBytes] = scaleDownLimitBytesValue;
+    mutableCoderOptions[SDImageCoderDecodeToHDR] = decodeToHDR;
     
     return [mutableCoderOptions copy];
 }
@@ -72,6 +80,7 @@ void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableCont
     mutableContext[SDWebImageContextImagePreserveAspectRatio] = decodeOptions[SDImageCoderDecodePreserveAspectRatio];
     mutableContext[SDWebImageContextImageThumbnailPixelSize] = decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
     mutableContext[SDWebImageContextImageScaleDownLimitBytes] = decodeOptions[SDImageCoderDecodeScaleDownLimitBytes];
+    mutableContext[SDWebImageContextImageDecodeToHDR] = decodeOptions[SDImageCoderDecodeToHDR];
     
     NSString *typeIdentifierHint = decodeOptions[SDImageCoderDecodeTypeIdentifierHint];
     if (!typeIdentifierHint) {

+ 8 - 0
SDWebImage/Core/SDImageCoder.h

@@ -88,6 +88,14 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDec
  */
 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
 /**
  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 SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding";
 SDImageCoderOption const SDImageCoderDecodeScaleDownLimitBytes = @"decodeScaleDownLimitBytes";
+SDImageCoderOption const SDImageCoderDecodeToHDR = @"decodeToHDR";
 
 SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
 SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";

+ 6 - 0
SDWebImage/Core/SDImageCoderHelper.h

@@ -113,6 +113,12 @@ typedef struct SDImagePixelFormat {
  */
 + (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.
  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 "SDInternalMacros.h"
 #import "SDDeviceHelper.h"
+#import "SDImageIOAnimatedCoderInternal.h"
 #import <Accelerate/Accelerate.h>
 
 #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 {
     return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
 }
@@ -680,12 +695,20 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
 #endif
         CGImageRelease(decodedImageRef);
     } else {
-        BOOL hasAlpha = [self CGImageContainsAlpha:imageRef];
         // 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
         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;
         SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
         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) {
         return NO;
     }
+    // FIXME: currently our force decode solution is buggy on HDR CGImage
+    if (image.sd_isHighDynamicRange) {
+        return NO;
+    }
     // Check policy (always)
     if (policy == SDImageForceDecodePolicyAlways) {
         return YES;

+ 6 - 2
SDWebImage/Core/SDImageHEICCoder.m

@@ -77,8 +77,12 @@ static NSString * kSDCGImagePropertyHEICSUnclampedDelayTime = @"UnclampedDelayTi
 + (NSString *)imageUTType {
     // 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`
-    // 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;
 }
 

+ 7 - 0
SDWebImage/Core/SDImageIOAnimatedCoder.h

@@ -28,6 +28,13 @@
  @note Subclass override.
  */
 @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`.
  @note Subclass override.

+ 40 - 10
SDWebImage/Core/SDImageIOAnimatedCoder.m

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

+ 9 - 4
SDWebImage/Core/SDImageIOCoder.m

@@ -28,6 +28,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     BOOL _preserveAspectRatio;
     CGSize _thumbnailSize;
     BOOL _lazyDecode;
+    BOOL _decodeToHDR;
 }
 
 - (void)dealloc {
@@ -179,6 +180,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         lazyDecode = lazyDecodeValue.boolValue;
     }
     
+    BOOL decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue];
+    
     NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint];
     if (!typeIdentifierHint) {
         // 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);
     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);
     
     image.sd_imageFormat = imageFormat;
@@ -256,6 +259,9 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             lazyDecode = lazyDecodeValue.boolValue;
         }
         _lazyDecode = lazyDecode;
+        
+        _decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue];
+        
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
 #endif
@@ -306,7 +312,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         if (scaleFactor != nil) {
             scale = MAX([scaleFactor doubleValue], 1);
         }
-        image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
+        image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO decodeToHDR:_finished ? _decodeToHDR : NO];
         if (image) {
             CFStringRef uttype = CGImageSourceGetType(_imageSource);
             image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype];
@@ -330,7 +336,6 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         // Earily return, supports CGImage only
         return nil;
     }
-    
     if (format == SDImageFormatUndefined) {
         BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:imageRef];
         if (hasAlpha) {
@@ -339,7 +344,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             format = SDImageFormatJPEG;
         }
     }
-    
+
     NSMutableData *imageData = [NSMutableData data];
     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;
 
+/**
+ 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
 
 /**

+ 1 - 0
SDWebImage/Core/SDWebImageDefine.m

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

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

@@ -95,4 +95,10 @@
  */
 @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

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

@@ -10,6 +10,7 @@
 #import "NSImage+Compatibility.h"
 #import "SDInternalMacros.h"
 #import "objc/runtime.h"
+#import "SDImageCoderHelper.h"
 
 @implementation UIImage (Metadata)
 
@@ -220,4 +221,16 @@
     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

+ 6 - 0
SDWebImage/Private/SDDeviceHelper.h

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

+ 65 - 0
SDWebImage/Private/SDDeviceHelper.m

@@ -30,4 +30,69 @@
     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

+ 2 - 15
SDWebImage/Private/SDImageAssetManager.m

@@ -8,26 +8,13 @@
 
 #import "SDImageAssetManager.h"
 #import "SDInternalMacros.h"
+#import "SDDeviceHelper.h"
 
 static NSArray *SDBundlePreferredScales(void) {
     static NSArray *scales;
     static dispatch_once_t 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) {
             scales = @[@1,@2,@3];
         } else if (screenScale <= 2) {

+ 1 - 1
SDWebImage/Private/SDImageIOAnimatedCoderInternal.h

@@ -32,7 +32,7 @@
 
 + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source;
 + (NSUInteger)imageLoopCountWithSource:(nonnull CGImageSourceRef)source;
-+ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode 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)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 */; };
 		3254C32020641077008D1022 /* 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 */; };
 		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 */; };
@@ -199,6 +211,9 @@
 		32515F9724AF1919005E8F79 /* TestImageStatic.webp */ = {isa = PBXFileReference; lastKnownFileType = file; path = TestImageStatic.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>"; };
+		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>"; };
 		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>"; };
@@ -351,6 +366,9 @@
 				433BBBBA1D7EFA8B0086B6E9 /* MonochromeTestImage.jpg */,
 				324047432271956F007C53E1 /* TestEXIF.png */,
 				3264CD162AAB1E23001E338B /* TestJFIF.jpg */,
+				3261EC882D66235D00F2702E /* TestHDR.avif */,
+				3261EC892D66235D00F2702E /* TestHDR.heic */,
+				3261EC8A2D66235D00F2702E /* TestHDR.jxl */,
 				321F310D27D0DC490042B274 /* TestImage.bmp */,
 				433BBBB61D7EF8200086B6E9 /* TestImage.gif */,
 				326E69462334C0C200B7252C /* TestLoopCount.gif */,
@@ -616,6 +634,9 @@
 				32464AA12B7B1833006BE70E /* MonochromeTestImage.jpg in Resources */,
 				32464AA42B7B1833006BE70E /* TestImageLarge.png 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 */,
 				32464A982B7B1833006BE70E /* TestLoopCount.gif in Resources */,
 			);
@@ -647,6 +668,9 @@
 				32515F9E24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				3299228E2365DC6C00EAFD97 /* TestImageAnimated.heics 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 */,
 				329922852365DC6C00EAFD97 /* TestEXIF.png in Resources */,
 			);
@@ -678,6 +702,9 @@
 				32515F9D24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				327A418D211D660600495442 /* TestImage.heic 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 */,
 				32B99EA5203B31360017FD66 /* TestImageLarge.jpg in Resources */,
 			);
@@ -709,6 +736,9 @@
 				32515F9C24AF1919005E8F79 /* TestImageAnimated.webp in Resources */,
 				326E69472334C0C300B7252C /* TestLoopCount.gif 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 */,
 				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 {
+    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"];
     SDAnimatedImageView *imageView = [SDAnimatedImageView new];
     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
 
 - (void)verifyCoder:(id<SDImageCoder>)coder