Selaa lähdekoodia

pod: update SDWebImage

dignifiedquire 2 vuotta sitten
vanhempi
commit
ec0afb5e45
95 muutettua tiedostoa jossa 5178 lisäystä ja 2811 poistoa
  1. 2 2
      Podfile
  2. 13 13
      Podfile.lock
  3. 13 13
      Pods/Manifest.lock
  4. 1406 1404
      Pods/Pods.xcodeproj/project.pbxproj
  5. 20 12
      Pods/SDWebImage/README.md
  6. 1 1
      Pods/SDWebImage/SDWebImage/Core/NSButton+WebCache.m
  7. 3 1
      Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.h
  8. 25 13
      Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.m
  9. 2 2
      Pods/SDWebImage/SDWebImage/Core/NSImage+Compatibility.m
  10. 4 1
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.m
  11. 22 0
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.h
  12. 68 39
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.m
  13. 15 4
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageRep.m
  14. 11 0
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.h
  15. 25 5
      Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.m
  16. 54 0
      Pods/SDWebImage/SDWebImage/Core/SDCallbackQueue.h
  17. 110 0
      Pods/SDWebImage/SDWebImage/Core/SDCallbackQueue.m
  18. 22 14
      Pods/SDWebImage/SDWebImage/Core/SDDiskCache.m
  19. 14 2
      Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.m
  20. 2 9
      Pods/SDWebImage/SDWebImage/Core/SDImageAPNGCoder.m
  21. 61 11
      Pods/SDWebImage/SDWebImage/Core/SDImageCache.h
  22. 299 166
      Pods/SDWebImage/SDWebImage/Core/SDImageCache.m
  23. 17 1
      Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.h
  24. 6 1
      Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.m
  25. 42 6
      Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.h
  26. 67 14
      Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.m
  27. 26 23
      Pods/SDWebImage/SDWebImage/Core/SDImageCachesManager.m
  28. 50 2
      Pods/SDWebImage/SDWebImage/Core/SDImageCoder.h
  29. 3 0
      Pods/SDWebImage/SDWebImage/Core/SDImageCoder.m
  30. 27 1
      Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.h
  31. 282 114
      Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.m
  32. 29 17
      Pods/SDWebImage/SDWebImage/Core/SDImageCodersManager.m
  33. 9 1
      Pods/SDWebImage/SDWebImage/Core/SDImageFrame.h
  34. 10 4
      Pods/SDWebImage/SDWebImage/Core/SDImageFrame.m
  35. 2 1
      Pods/SDWebImage/SDWebImage/Core/SDImageGIFCoder.m
  36. 29 7
      Pods/SDWebImage/SDWebImage/Core/SDImageGraphics.m
  37. 0 3
      Pods/SDWebImage/SDWebImage/Core/SDImageHEICCoder.m
  38. 1 2
      Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.h
  39. 235 80
      Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.m
  40. 142 14
      Pods/SDWebImage/SDWebImage/Core/SDImageIOCoder.m
  41. 47 2
      Pods/SDWebImage/SDWebImage/Core/SDImageLoader.h
  42. 35 65
      Pods/SDWebImage/SDWebImage/Core/SDImageLoader.m
  43. 24 15
      Pods/SDWebImage/SDWebImage/Core/SDImageLoadersManager.m
  44. 14 11
      Pods/SDWebImage/SDWebImage/Core/SDMemoryCache.m
  45. 1 1
      Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheSerializer.h
  46. 53 4
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.h
  47. 16 4
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.m
  48. 1 1
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.h
  49. 97 36
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.m
  50. 15 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.h
  51. 11 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.m
  52. 39 1
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.h
  53. 255 90
      Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.m
  54. 5 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageError.h
  55. 3 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageError.m
  56. 1 10
      Pods/SDWebImage/SDWebImage/Core/SDWebImageIndicator.m
  57. 3 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.h
  58. 278 196
      Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.m
  59. 6 0
      Pods/SDWebImage/SDWebImage/Core/SDWebImageOperation.h
  60. 29 4
      Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.h
  61. 83 47
      Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.m
  62. 19 1
      Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.m
  63. 2 1
      Pods/SDWebImage/SDWebImage/Core/UIImage+MemoryCacheCost.m
  64. 35 3
      Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.h
  65. 75 4
      Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.m
  66. 138 34
      Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.m
  67. 8 7
      Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.h
  68. 42 25
      Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.m
  69. 2 2
      Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheOperation.h
  70. 3 5
      Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheOperation.m
  71. 2 0
      Pods/SDWebImage/SDWebImage/Private/SDAssociatedObject.m
  72. 12 12
      Pods/SDWebImage/SDWebImage/Private/SDAsyncBlockOperation.m
  73. 1 1
      Pods/SDWebImage/SDWebImage/Private/SDDisplayLink.h
  74. 56 22
      Pods/SDWebImage/SDWebImage/Private/SDDisplayLink.m
  75. 10 4
      Pods/SDWebImage/SDWebImage/Private/SDImageAssetManager.m
  76. 3 4
      Pods/SDWebImage/SDWebImage/Private/SDImageCachesManagerOperation.m
  77. 16 5
      Pods/SDWebImage/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h
  78. 47 2
      Pods/SDWebImage/SDWebImage/Private/SDInternalMacros.h
  79. 1 0
      Pods/SDWebImage/WebImage/SDWebImage.h
  80. 12 5
      Pods/SDWebImageSVGKitPlugin/README.md
  81. 4 0
      Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDImageSVGKCoder.h
  82. 4 1
      Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDImageSVGKCoder.m
  83. 8 0
      Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDSVGKImage.h
  84. 4 0
      Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDWebImageSVGKitDefine.h
  85. 4 1
      Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDWebImageSVGKitDefine.m
  86. 4 0
      Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SVGKImageView+WebCache.h
  87. 80 4
      Pods/SDWebImageWebPCoder/README.md
  88. 233 175
      Pods/SDWebImageWebPCoder/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m
  89. 135 0
      Pods/SDWebImageWebPCoder/SDWebImageWebPCoder/Classes/SDWebImageWebPCoderDefine.h
  90. 28 0
      Pods/SDWebImageWebPCoder/SDWebImageWebPCoder/Classes/SDWebImageWebPCoderDefine.m
  91. 1 0
      Pods/SDWebImageWebPCoder/SDWebImageWebPCoder/Module/SDWebImageWebPCoder.h
  92. 1 1
      Pods/Target Support Files/SDWebImage/SDWebImage-Info.plist
  93. 1 0
      Pods/Target Support Files/SDWebImage/SDWebImage-umbrella.h
  94. 1 1
      Pods/Target Support Files/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin-Info.plist
  95. 1 1
      Pods/Target Support Files/SDWebImageWebPCoder/SDWebImageWebPCoder-Info.plist

+ 2 - 2
Podfile

@@ -12,7 +12,7 @@ target 'deltachat-ios' do
   # pod 'openssl-ios-bitcode'
   pod 'ReachabilitySwift'
   pod 'SCSiriWaveformView'
-  pod 'SDWebImage', '~> 5.9.1'
+  pod 'SDWebImage'
   pod 'SDWebImageWebPCoder'
   pod 'SDWebImageSVGKitPlugin'
   pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git', :branch => '3.x'
@@ -30,6 +30,6 @@ target 'DcShare' do
   # ignore all warnings from all dependencies
   inhibit_all_warnings!
 
-  pod 'SDWebImage', '~> 5.9.1'
+  pod 'SDWebImage'
   pod 'SDWebImageWebPCoder'
 end

+ 13 - 13
Podfile.lock

@@ -13,15 +13,15 @@ PODS:
   - libwebp/webp (1.2.4)
   - ReachabilitySwift (5.0.0)
   - SCSiriWaveformView (1.1.2)
-  - SDWebImage (5.9.5):
-    - SDWebImage/Core (= 5.9.5)
-  - SDWebImage/Core (5.9.5)
-  - SDWebImageSVGKitPlugin (1.2.0):
-    - SDWebImage/Core (~> 5.6)
-    - SVGKit (>= 2.1)
-  - SDWebImageWebPCoder (0.6.1):
+  - SDWebImage (5.15.6):
+    - SDWebImage/Core (= 5.15.6)
+  - SDWebImage/Core (5.15.6)
+  - SDWebImageSVGKitPlugin (1.4.0):
+    - SDWebImage/Core (~> 5.10)
+    - SVGKit (~> 3.0)
+  - SDWebImageWebPCoder (0.11.0):
     - libwebp (~> 1.0)
-    - SDWebImage/Core (~> 5.7)
+    - SDWebImage/Core (~> 5.15)
   - SVGKit (3.1.0):
     - CocoaLumberjack (~> 3.0)
   - Swifter (1.5.0)
@@ -31,7 +31,7 @@ PODS:
 DEPENDENCIES:
   - ReachabilitySwift
   - SCSiriWaveformView
-  - SDWebImage (~> 5.9.1)
+  - SDWebImage
   - SDWebImageSVGKitPlugin
   - SDWebImageWebPCoder
   - SVGKit (from `https://github.com/SVGKit/SVGKit.git`, branch `3.x`)
@@ -72,14 +72,14 @@ SPEC CHECKSUMS:
   libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
   ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
   SCSiriWaveformView: 016392911fb442c17d6dbad68e666edb13193c02
-  SDWebImage: 0b2ba0d56479bf6a45ecddbfd5558bea93150d25
-  SDWebImageSVGKitPlugin: 06a811c05b9e839982baeb5251d15fe7a79abb82
-  SDWebImageWebPCoder: d0dac55073088d24b2ac1b191a71a8f8d0adac21
+  SDWebImage: d47d81bea8a77187896b620dc79c3c528e8906b9
+  SDWebImageSVGKitPlugin: 7542dd07c344ec3415ded0461a1161a6f087e0c9
+  SDWebImageWebPCoder: 295a6573c512f54ad2dd58098e64e17dcf008499
   SVGKit: 3c8468aab0026048532a3b27a0c81cdd939f0649
   Swifter: aa3514bbb8df8980c118f7bb1b80f2da24e39c2b
   SwiftFormat: 4334264324e20bad415888316165bdc1fc2860bc
   SwiftLint: 1b7561918a19e23bfed960e40759086e70f4dba5
 
-PODFILE CHECKSUM: 7ff757dacef19f015c1ecf6daabd42e59db7d4b5
+PODFILE CHECKSUM: fb3efc118f488a510c66aae9c8d41f4ee9184af9
 
 COCOAPODS: 1.12.1

+ 13 - 13
Pods/Manifest.lock

@@ -13,15 +13,15 @@ PODS:
   - libwebp/webp (1.2.4)
   - ReachabilitySwift (5.0.0)
   - SCSiriWaveformView (1.1.2)
-  - SDWebImage (5.9.5):
-    - SDWebImage/Core (= 5.9.5)
-  - SDWebImage/Core (5.9.5)
-  - SDWebImageSVGKitPlugin (1.2.0):
-    - SDWebImage/Core (~> 5.6)
-    - SVGKit (>= 2.1)
-  - SDWebImageWebPCoder (0.6.1):
+  - SDWebImage (5.15.6):
+    - SDWebImage/Core (= 5.15.6)
+  - SDWebImage/Core (5.15.6)
+  - SDWebImageSVGKitPlugin (1.4.0):
+    - SDWebImage/Core (~> 5.10)
+    - SVGKit (~> 3.0)
+  - SDWebImageWebPCoder (0.11.0):
     - libwebp (~> 1.0)
-    - SDWebImage/Core (~> 5.7)
+    - SDWebImage/Core (~> 5.15)
   - SVGKit (3.1.0):
     - CocoaLumberjack (~> 3.0)
   - Swifter (1.5.0)
@@ -31,7 +31,7 @@ PODS:
 DEPENDENCIES:
   - ReachabilitySwift
   - SCSiriWaveformView
-  - SDWebImage (~> 5.9.1)
+  - SDWebImage
   - SDWebImageSVGKitPlugin
   - SDWebImageWebPCoder
   - SVGKit (from `https://github.com/SVGKit/SVGKit.git`, branch `3.x`)
@@ -72,14 +72,14 @@ SPEC CHECKSUMS:
   libwebp: f62cb61d0a484ba548448a4bd52aabf150ff6eef
   ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
   SCSiriWaveformView: 016392911fb442c17d6dbad68e666edb13193c02
-  SDWebImage: 0b2ba0d56479bf6a45ecddbfd5558bea93150d25
-  SDWebImageSVGKitPlugin: 06a811c05b9e839982baeb5251d15fe7a79abb82
-  SDWebImageWebPCoder: d0dac55073088d24b2ac1b191a71a8f8d0adac21
+  SDWebImage: d47d81bea8a77187896b620dc79c3c528e8906b9
+  SDWebImageSVGKitPlugin: 7542dd07c344ec3415ded0461a1161a6f087e0c9
+  SDWebImageWebPCoder: 295a6573c512f54ad2dd58098e64e17dcf008499
   SVGKit: 3c8468aab0026048532a3b27a0c81cdd939f0649
   Swifter: aa3514bbb8df8980c118f7bb1b80f2da24e39c2b
   SwiftFormat: 4334264324e20bad415888316165bdc1fc2860bc
   SwiftLint: 1b7561918a19e23bfed960e40759086e70f4dba5
 
-PODFILE CHECKSUM: 7ff757dacef19f015c1ecf6daabd42e59db7d4b5
+PODFILE CHECKSUM: fb3efc118f488a510c66aae9c8d41f4ee9184af9
 
 COCOAPODS: 1.12.1

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1406 - 1404
Pods/Pods.xcodeproj/project.pbxproj


+ 20 - 12
Pods/SDWebImage/README.md

@@ -3,7 +3,7 @@
 </p>
 
 
-[![Build Status](http://img.shields.io/travis/SDWebImage/SDWebImage/master.svg?style=flat)](https://travis-ci.org/SDWebImage/SDWebImage)
+[![Build Status](https://github.com/SDWebImage/SDWebImage/actions/workflows/CI.yml/badge.svg)](https://github.com/SDWebImage/SDWebImage/actions/workflows/CI.yml)
 [![Pod Version](http://img.shields.io/cocoapods/v/SDWebImage.svg?style=flat)](http://cocoadocs.org/docsets/SDWebImage/)
 [![Pod Platform](http://img.shields.io/cocoapods/p/SDWebImage.svg?style=flat)](http://cocoadocs.org/docsets/SDWebImage/)
 [![Pod License](http://img.shields.io/cocoapods/l/SDWebImage.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0.html)
@@ -14,6 +14,8 @@
 
 This library provides an async image downloader with cache support. For convenience, we added categories for UI elements like `UIImageView`, `UIButton`, `MKAnnotationView`.
 
+Note: `SD` is the prefix for **Simple Design** (which is the team name in Daily Motion company from the author Olivier Poitrey)
+
 ## Features
 
 - [x] Categories for `UIImageView`, `UIButton`, `MKAnnotationView` adding web image and cache management
@@ -37,8 +39,9 @@ This library provides an async image downloader with cache support. For convenie
 
 ## Supported Image Formats
 
-- Image formats supported by Apple system (JPEG, PNG, TIFF, HEIC, ...), including GIF/APNG/HEIC animation
-- WebP format, including animated WebP (use the [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder) project). Note iOS 14/macOS 11.0 supports built-in WebP decoding (no encoding).
+- Image formats supported by Apple system (JPEG, PNG, TIFF, BMP, ...), including [GIF](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#gif-coder)/[APNG](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#apng-coder) animated image
+- HEIC format from iOS 11/macOS 10.13, including animated HEIC from iOS 13/macOS 10.15 via [SDWebImageHEICCoder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#heic-coder). For lower firmware, use coder plugin [SDWebImageHEIFCoder](https://github.com/SDWebImage/SDWebImageHEIFCoder)
+- WebP format from iOS 14/macOS 11.0 via [SDWebImageAWebPCoder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#awebp-coder). For lower firmware, use coder plugin [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder)
 - Support extendable coder plugins for new image formats like BPG, AVIF. And vector format like PDF, SVG. See all the list in [Image coder plugin List](https://github.com/SDWebImage/SDWebImage/wiki/Coder-Plugin-List)
 
 ## Additional modules and Ecosystem
@@ -61,6 +64,7 @@ The new framework introduce two View structs `WebImage` and `AnimatedImage` for
 - [SDWebImageAVIFCoder](https://github.com/SDWebImage/SDWebImageAVIFCoder) - coder for AVIF (AV1-based) format. Based on [libavif](https://github.com/AOMediaCodec/libavif)
 - [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format. Using built-in frameworks
 - [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format. Using built-in frameworks
+- [SDWebImageSVGNativeCoder](https://github.com/SDWebImage/SDWebImageSVGNativeCoder) - coder for SVG-Native vector format. Based on [svg-native](https://github.com/adobe/svg-native-viewer)
 - [SDWebImageLottieCoder](https://github.com/SDWebImage/SDWebImageLottieCoder) - coder for Lottie animation format. Based on [rlottie](https://github.com/Samsung/rlottie)
 - and more from community!
 
@@ -74,7 +78,7 @@ The new framework introduce two View structs `WebImage` and `AnimatedImage` for
 
 #### Integration with 3rd party libraries
 - [SDWebImageLottiePlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [Lottie-iOS](https://github.com/airbnb/lottie-ios), vector animation rending with remote JSON files
-- [SDWebImageSVGKitPlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [SVGKit](https://github.com/SVGKit/SVGKit), SVG rendering using Core Animation, iOS 8+/macOS 10.10+ support
+- [SDWebImageSVGKitPlugin](https://github.com/SDWebImage/SDWebImageSVGKitPlugin) - plugin to support [SVGKit](https://github.com/SVGKit/SVGKit), SVG rendering using Core Animation, iOS 8+/macOS 10.10+ support
 - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs
 - [SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) - plugin to integrate [YYImage](https://github.com/ibireme/YYImage) & [YYCache](https://github.com/ibireme/YYCache) for image rendering & caching
 
@@ -93,17 +97,18 @@ You can use those directly, or create similar components of your own, by using t
 
 ## Requirements
 
-- iOS 8.0 or later
+- iOS 9.0 or later
 - tvOS 9.0 or later
 - watchOS 2.0 or later
-- macOS 10.10 or later (10.15 for Catalyst)
-- Xcode 10.0 or later
+- macOS 10.11 or later (10.15 for Catalyst)
+- Xcode 11.0 or later
 
 #### Backwards compatibility
 
+- For iOS 8, macOS 10.10 or Xcode < 11, use [any 5.x version up to 5.9.5](https://github.com/SDWebImage/SDWebImage/releases/tag/5.9.5)
 - For iOS 7, macOS 10.9 or Xcode < 8, use [any 4.x version up to 4.4.6](https://github.com/SDWebImage/SDWebImage/releases/tag/4.4.6)
 - For macOS 10.8, use [any 4.x version up to 4.3.0](https://github.com/SDWebImage/SDWebImage/releases/tag/4.3.0)
-- For iOS 5 and 6, use [any 3.x version up to 3.7.6](https://github.com/SDWebImage/SDWebImage/tag/3.7.6)
+- For iOS 5 and 6, use [any 3.x version up to 3.7.6](https://github.com/SDWebImage/SDWebImage/releases/tag/3.7.6)
 - For iOS < 5.0, please use the last [2.0 version](https://github.com/SDWebImage/SDWebImage/tree/2.0-compat).
 
 ## Getting Started
@@ -164,7 +169,7 @@ This animated image solution is available for `iOS`/`tvOS`/`macOS`. The `SDAnima
 
 The `SDAnimatedImageView` supports the familiar image loading category methods, works like drop-in replacement for `UIImageView/NSImageView`.
 
-Don't have UIView (like WatchKit or CALayer)? you can still use `SDAnimatedPlayer` the player engine for advanced playback and rendering.
+Don't have `UIView` (like `WatchKit` or `CALayer`)? you can still use `SDAnimatedPlayer` the player engine for advanced playback and rendering.
 
 See [Animated Image](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) for more detailed information.
 
@@ -207,9 +212,9 @@ pod 'SDWebImage', '~> 5.0'
 
 ##### Swift and static framework
 
-Swift project previously have to use `use_frameworks!` to make all Pods into dynamic framework to let CocoaPods works.
+Swift project previously had to use `use_frameworks!` to make all Pods into dynamic framework to let CocoaPods work.
 
-However, start with `CocoaPods 1.5.0+` (with `Xcode 9+`), which supports to build both Objective-C && Swift code into static framework. You can use modular headers to use SDWebImage as static framework, without the need of `use_frameworks!`:
+However, starting with `CocoaPods 1.5.0+` (with `Xcode 9+`), which supports to build both Objective-C && Swift code into static framework. You can use modular headers to use SDWebImage as static framework, without the need of `use_frameworks!`:
 
 ```
 platform :ios, '8.0'
@@ -238,7 +243,7 @@ Podfile example:
 pod 'SDWebImage/MapKit'
 ```
 
-### Installation with Carthage (iOS 8+)
+### Installation with Carthage
 
 [Carthage](https://github.com/Carthage/Carthage) is a lightweight dependency manager for Swift and Objective-C. It leverages CocoaTouch modules and is less invasive than CocoaPods.
 
@@ -295,6 +300,9 @@ It's also recommend to use the module import syntax, available for CocoaPods(ena
 At this point your workspace should build without error. If you are having problem, post to the Issue and the
 community can help you solve it.
 
+## Data Collection Practices
+As required by the [App privacy details on the App Store](https://developer.apple.com/app-store/app-privacy-details/), here's SDWebImage's list of [Data Collection Practices](https://sdwebimage.github.io/DataCollection/index.html).
+
 ## Author
 - [Olivier Poitrey](https://github.com/rs)
 

+ 1 - 1
Pods/SDWebImage/SDWebImage/Core/NSButton+WebCache.m

@@ -149,7 +149,7 @@ static NSString * const SDAlternateImageOperationKey = @"NSButtonAlternateImageO
     [self sd_cancelImageLoadOperationWithKey:SDAlternateImageOperationKey];
 }
 
-#pragma mar - Private
+#pragma mark - Private
 
 - (NSURL *)sd_currentImageURL {
     return objc_getAssociatedObject(self, @selector(sd_currentImageURL));

+ 3 - 1
Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.h

@@ -25,6 +25,8 @@ static const SDImageFormat SDImageFormatHEIC      = 5;
 static const SDImageFormat SDImageFormatHEIF      = 6;
 static const SDImageFormat SDImageFormatPDF       = 7;
 static const SDImageFormat SDImageFormatSVG       = 8;
+static const SDImageFormat SDImageFormatBMP       = 9;
+static const SDImageFormat SDImageFormatRAW       = 10;
 
 /**
  NSData category about the image content type and UTI.
@@ -45,7 +47,7 @@ static const SDImageFormat SDImageFormatSVG       = 8;
  *
  *  @param format Format as SDImageFormat
  *  @return The UTType as CFStringRef
- *  @note For unknown format, `kUTTypeImage` abstract type will return
+ *  @note For unknown format, `kSDUTTypeImage` abstract type will return
  */
 + (nonnull CFStringRef)sd_UTTypeFromImageFormat:(SDImageFormat)format CF_RETURNS_NOT_RETAINED NS_SWIFT_NAME(sd_UTType(from:));
 

+ 25 - 13
Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.m

@@ -37,6 +37,8 @@
         case 0x49:
         case 0x4D:
             return SDImageFormatTIFF;
+        case 0x42:
+            return SDImageFormatBMP;
         case 0x52: {
             if (data.length >= 12) {
                 //RIFF....WEBP
@@ -87,16 +89,16 @@
     CFStringRef UTType;
     switch (format) {
         case SDImageFormatJPEG:
-            UTType = kUTTypeJPEG;
+            UTType = kSDUTTypeJPEG;
             break;
         case SDImageFormatPNG:
-            UTType = kUTTypePNG;
+            UTType = kSDUTTypePNG;
             break;
         case SDImageFormatGIF:
-            UTType = kUTTypeGIF;
+            UTType = kSDUTTypeGIF;
             break;
         case SDImageFormatTIFF:
-            UTType = kUTTypeTIFF;
+            UTType = kSDUTTypeTIFF;
             break;
         case SDImageFormatWebP:
             UTType = kSDUTTypeWebP;
@@ -108,14 +110,20 @@
             UTType = kSDUTTypeHEIF;
             break;
         case SDImageFormatPDF:
-            UTType = kUTTypePDF;
+            UTType = kSDUTTypePDF;
             break;
         case SDImageFormatSVG:
-            UTType = kUTTypeScalableVectorGraphics;
+            UTType = kSDUTTypeSVG;
+            break;
+        case SDImageFormatBMP:
+            UTType = kSDUTTypeBMP;
+            break;
+        case SDImageFormatRAW:
+            UTType = kSDUTTypeRAW;
             break;
         default:
             // default is kUTTypeImage abstract type
-            UTType = kUTTypeImage;
+            UTType = kSDUTTypeImage;
             break;
     }
     return UTType;
@@ -126,13 +134,13 @@
         return SDImageFormatUndefined;
     }
     SDImageFormat imageFormat;
-    if (CFStringCompare(uttype, kUTTypeJPEG, 0) == kCFCompareEqualTo) {
+    if (CFStringCompare(uttype, kSDUTTypeJPEG, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatJPEG;
-    } else if (CFStringCompare(uttype, kUTTypePNG, 0) == kCFCompareEqualTo) {
+    } else if (CFStringCompare(uttype, kSDUTTypePNG, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatPNG;
-    } else if (CFStringCompare(uttype, kUTTypeGIF, 0) == kCFCompareEqualTo) {
+    } else if (CFStringCompare(uttype, kSDUTTypeGIF, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatGIF;
-    } else if (CFStringCompare(uttype, kUTTypeTIFF, 0) == kCFCompareEqualTo) {
+    } else if (CFStringCompare(uttype, kSDUTTypeTIFF, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatTIFF;
     } else if (CFStringCompare(uttype, kSDUTTypeWebP, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatWebP;
@@ -140,10 +148,14 @@
         imageFormat = SDImageFormatHEIC;
     } else if (CFStringCompare(uttype, kSDUTTypeHEIF, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatHEIF;
-    } else if (CFStringCompare(uttype, kUTTypePDF, 0) == kCFCompareEqualTo) {
+    } else if (CFStringCompare(uttype, kSDUTTypePDF, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatPDF;
-    } else if (CFStringCompare(uttype, kUTTypeScalableVectorGraphics, 0) == kCFCompareEqualTo) {
+    } else if (CFStringCompare(uttype, kSDUTTypeSVG, 0) == kCFCompareEqualTo) {
         imageFormat = SDImageFormatSVG;
+    } else if (CFStringCompare(uttype, kSDUTTypeBMP, 0) == kCFCompareEqualTo) {
+        imageFormat = SDImageFormatBMP;
+    } else if (UTTypeConformsTo(uttype, kSDUTTypeRAW)) {
+        imageFormat = SDImageFormatRAW;
     } else {
         imageFormat = SDImageFormatUndefined;
     }

+ 2 - 2
Pods/SDWebImage/SDWebImage/Core/NSImage+Compatibility.m

@@ -35,8 +35,8 @@
     NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil];
     CGFloat width = imageRep.size.width;
     CGFloat height = imageRep.size.height;
-    NSUInteger pixelWidth = imageRep.pixelsWide;
-    NSUInteger pixelHeight = imageRep.pixelsHigh;
+    CGFloat pixelWidth = (CGFloat)imageRep.pixelsWide;
+    CGFloat pixelHeight = (CGFloat)imageRep.pixelsHigh;
     if (width > 0 && height > 0) {
         CGFloat widthScale = pixelWidth / width;
         CGFloat heightScale = pixelHeight / height;

+ 4 - 1
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.m

@@ -124,7 +124,6 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
     if (!data || data.length == 0) {
         return nil;
     }
-    data = [data copy]; // avoid mutable data
     id<SDAnimatedImageCoder> animatedCoder = nil;
     for (id<SDImageCoder>coder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) {
         if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) {
@@ -314,6 +313,10 @@ static CGFloat SDImageScaleFromPath(NSString *string) {
     return;
 }
 
+- (NSUInteger)sd_imageFrameCount {
+    return self.animatedImageFrameCount;
+}
+
 - (SDImageFormat)sd_imageFormat {
     return self.animatedImageFormat;
 }

+ 22 - 0
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.h

@@ -10,6 +10,25 @@
 #import "SDWebImageCompat.h"
 #import "SDImageCoder.h"
 
+typedef NS_ENUM(NSUInteger, SDAnimatedImagePlaybackMode) {
+    /**
+     * From first to last frame and stop or next loop.
+     */
+    SDAnimatedImagePlaybackModeNormal = 0,
+    /**
+     * From last frame to first frame and stop or next loop.
+     */
+    SDAnimatedImagePlaybackModeReverse,
+    /**
+     * From first frame to last frame and reverse again, like reciprocating.
+     */
+    SDAnimatedImagePlaybackModeBounce,
+    /**
+     * From last frame to first frame and reverse again, like reversed reciprocating.
+     */
+    SDAnimatedImagePlaybackModeReversedBounce,
+};
+
 /// A player to control the playback of animated image, which can be used to drive Animated ImageView or any rendering usage, like CALayer/WatchKit/SwiftUI rendering.
 @interface SDAnimatedImagePlayer : NSObject
 
@@ -37,6 +56,9 @@
 /// `< 0.0` is not supported currently and stop animation. (may support reverse playback in the future)
 @property (nonatomic, assign) double playbackRate;
 
+/// Asynchronous setup animation playback mode. Default mode is SDAnimatedImagePlaybackModeNormal.
+@property (nonatomic, assign) SDAnimatedImagePlaybackMode playbackMode;
+
 /// Provide a max buffer size by bytes. This is used to adjust frame buffer count and can be useful when the decoding cost is expensive (such as Animated WebP software decoding). Default is 0.
 /// `0` means automatically adjust by calculating current memory usage.
 /// `1` means without any buffer cache, each of frames will be decoded and then be freed after rendering. (Lowest Memory and Highest CPU)

+ 68 - 39
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.m

@@ -13,6 +13,7 @@
 #import "SDInternalMacros.h"
 
 @interface SDAnimatedImagePlayer () {
+    SD_LOCK_DECLARE(_lock);
     NSRunLoopMode _runLoopMode;
 }
 
@@ -24,9 +25,9 @@
 @property (nonatomic, assign) NSTimeInterval currentTime;
 @property (nonatomic, assign) BOOL bufferMiss;
 @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable;
+@property (nonatomic, assign) BOOL shouldReverse;
 @property (nonatomic, assign) NSUInteger maxBufferCount;
 @property (nonatomic, strong) NSOperationQueue *fetchQueue;
-@property (nonatomic, strong) dispatch_semaphore_t lock;
 @property (nonatomic, strong) SDDisplayLink *displayLink;
 
 @end
@@ -46,6 +47,7 @@
         self.totalLoopCount = provider.animatedImageLoopCount;
         self.animatedProvider = provider;
         self.playbackRate = 1.0;
+        SD_LOCK_INIT(_lock);
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
 #endif
@@ -68,9 +70,9 @@
 
 - (void)didReceiveMemoryWarning:(NSNotification *)notification {
     [_fetchQueue cancelAllOperations];
-    [_fetchQueue addOperationWithBlock:^{
+    NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
         NSNumber *currentFrameIndex = @(self.currentFrameIndex);
-        SD_LOCK(self.lock);
+        SD_LOCK(self->_lock);
         NSArray *keys = self.frameBuffer.allKeys;
         // only keep the next frame for later rendering
         for (NSNumber * key in keys) {
@@ -78,8 +80,9 @@
                 [self.frameBuffer removeObjectForKey:key];
             }
         }
-        SD_UNLOCK(self.lock);
+        SD_UNLOCK(self->_lock);
     }];
+    [_fetchQueue addOperation:operation];
 }
 
 #pragma mark - Private
@@ -87,6 +90,7 @@
     if (!_fetchQueue) {
         _fetchQueue = [[NSOperationQueue alloc] init];
         _fetchQueue.maxConcurrentOperationCount = 1;
+        _fetchQueue.name = @"com.hackemist.SDAnimatedImagePlayer.fetchQueue";
     }
     return _fetchQueue;
 }
@@ -98,13 +102,6 @@
     return _frameBuffer;
 }
 
-- (dispatch_semaphore_t)lock {
-    if (!_lock) {
-        _lock = dispatch_semaphore_create(1);
-    }
-    return _lock;
-}
-
 - (SDDisplayLink *)displayLink {
     if (!_displayLink) {
         _displayLink = [SDDisplayLink displayLinkWithTarget:self selector:@selector(displayDidRefresh:)];
@@ -142,22 +139,28 @@
     if (self.currentFrameIndex != 0) {
         return;
     }
-    if ([self.animatedProvider isKindOfClass:[UIImage class]]) {
+    if (self.playbackMode == SDAnimatedImagePlaybackModeReverse ||
+               self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) {
+        self.currentFrameIndex = self.totalFrameCount - 1;
+    }
+    
+    if (!self.currentFrame && [self.animatedProvider isKindOfClass:[UIImage class]]) {
         UIImage *image = (UIImage *)self.animatedProvider;
-        // Use the poster image if available
+        // Cache the poster image if available, but should not callback to avoid caller thread issues
         #if SD_MAC
         UIImage *posterFrame = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
         #else
         UIImage *posterFrame = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
         #endif
         if (posterFrame) {
-            self.currentFrame = posterFrame;
-            SD_LOCK(self.lock);
-            self.frameBuffer[@(self.currentFrameIndex)] = self.currentFrame;
-            SD_UNLOCK(self.lock);
-            [self handleFrameChange];
+            // HACK: The first frame should not check duration and immediately display
+            self.needsDisplayWhenImageBecomesAvailable = YES;
+            SD_LOCK(self->_lock);
+            self.frameBuffer[@(self.currentFrameIndex)] = posterFrame;
+            SD_UNLOCK(self->_lock);
         }
     }
+    
 }
 
 - (void)resetCurrentFrameStatus {
@@ -171,18 +174,16 @@
 }
 
 - (void)clearFrameBuffer {
-    SD_LOCK(self.lock);
+    SD_LOCK(_lock);
     [_frameBuffer removeAllObjects];
-    SD_UNLOCK(self.lock);
+    SD_UNLOCK(_lock);
 }
 
 #pragma mark - Animation Control
 - (void)startPlaying {
     [self.displayLink start];
     // Setup frame
-    if (self.currentFrameIndex == 0 && !self.currentFrame) {
-        [self setupCurrentFrame];
-    }
+    [self setupCurrentFrame];
     // Calculate max buffer size
     [self calculateMaxBufferCount];
 }
@@ -242,17 +243,32 @@
     NSUInteger currentFrameIndex = self.currentFrameIndex;
     NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount;
     
+    if (self.playbackMode == SDAnimatedImagePlaybackModeReverse) {
+        nextFrameIndex = currentFrameIndex == 0 ? (totalFrameCount - 1) : (currentFrameIndex - 1) % totalFrameCount;
+        
+    } else if (self.playbackMode == SDAnimatedImagePlaybackModeBounce ||
+               self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) {
+        if (currentFrameIndex == 0) {
+            self.shouldReverse = NO;
+        } else if (currentFrameIndex == totalFrameCount - 1) {
+            self.shouldReverse = YES;
+        }
+        nextFrameIndex = self.shouldReverse ? (currentFrameIndex - 1) : (currentFrameIndex + 1);
+        nextFrameIndex %= totalFrameCount;
+    }
+    
+    
     // Check if we need to display new frame firstly
     BOOL bufferFull = NO;
     if (self.needsDisplayWhenImageBecomesAvailable) {
         UIImage *currentFrame;
-        SD_LOCK(self.lock);
+        SD_LOCK(_lock);
         currentFrame = self.frameBuffer[@(currentFrameIndex)];
-        SD_UNLOCK(self.lock);
+        SD_UNLOCK(_lock);
         
         // Update the current frame
         if (currentFrame) {
-            SD_LOCK(self.lock);
+            SD_LOCK(_lock);
             // Remove the frame buffer if need
             if (self.frameBuffer.count > self.maxBufferCount) {
                 self.frameBuffer[@(currentFrameIndex)] = nil;
@@ -261,7 +277,7 @@
             if (self.frameBuffer.count == totalFrameCount) {
                 bufferFull = YES;
             }
-            SD_UNLOCK(self.lock);
+            SD_UNLOCK(_lock);
             
             // Update the current frame immediately
             self.currentFrame = currentFrame;
@@ -282,7 +298,10 @@
         NSTimeInterval currentDuration = [self.animatedProvider animatedImageDurationAtIndex:currentFrameIndex];
         currentDuration = currentDuration / playbackRate;
         if (self.currentTime < currentDuration) {
-            // Current frame timestamp not reached, return
+            // Current frame timestamp not reached, prefetch frame in advance.
+            [self prefetchFrameAtIndex:currentFrameIndex
+                             nextIndex:nextFrameIndex
+                            bufferFull:bufferFull];
             return;
         }
         
@@ -317,15 +336,25 @@
         return;
     }
     
-    // Check if we should prefetch next frame or current frame
-    // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
-    // Or, most cases, the decode speed is faster than render speed, we fetch next frame
-    NSUInteger fetchFrameIndex = self.bufferMiss? currentFrameIndex : nextFrameIndex;
-    UIImage *fetchFrame;
-    SD_LOCK(self.lock);
-    fetchFrame = self.bufferMiss? nil : self.frameBuffer[@(nextFrameIndex)];
-    SD_UNLOCK(self.lock);
-    
+    [self prefetchFrameAtIndex:currentFrameIndex
+                     nextIndex:nextFrameIndex
+                    bufferFull:bufferFull];
+}
+
+// Check if we should prefetch next frame or current frame
+// When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame
+// Or, most cases, the decode speed is faster than render speed, we fetch next frame
+- (void)prefetchFrameAtIndex:(NSUInteger)currentIndex
+                   nextIndex:(NSUInteger)nextIndex
+                  bufferFull:(BOOL)bufferFull {
+    NSUInteger fetchFrameIndex = currentIndex;
+    UIImage *fetchFrame = nil;
+    if (!self.bufferMiss) {
+        fetchFrameIndex = nextIndex;
+        SD_LOCK(_lock);
+        fetchFrame = self.frameBuffer[@(nextIndex)];
+        SD_UNLOCK(_lock);
+    }
     if (!fetchFrame && !bufferFull && self.fetchQueue.operationCount == 0) {
         // Prefetch next frame in background queue
         id<SDAnimatedImageProvider> animatedProvider = self.animatedProvider;
@@ -339,9 +368,9 @@
 
             BOOL isAnimating = self.displayLink.isRunning;
             if (isAnimating) {
-                SD_LOCK(self.lock);
+                SD_LOCK(self->_lock);
                 self.frameBuffer[@(fetchFrameIndex)] = frame;
-                SD_UNLOCK(self.lock);
+                SD_UNLOCK(self->_lock);
             }
         }];
         [self.fetchQueue addOperation:operation];

+ 15 - 4
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageRep.m

@@ -16,6 +16,11 @@
 #import "SDImageHEICCoder.h"
 #import "SDImageAWebPCoder.h"
 
+@interface SDAnimatedImageRep ()
+/// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
+@property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
+@end
+
 @implementation SDAnimatedImageRep {
     CGImageSourceRef _imageSource;
 }
@@ -27,6 +32,12 @@
     }
 }
 
+- (instancetype)copyWithZone:(NSZone *)zone {
+  SDAnimatedImageRep *imageRep = [super copyWithZone:zone];
+  CFRetain(imageRep->_imageSource);
+  return imageRep;
+}
+
 // `NSBitmapImageRep`'s `imageRepWithData:` is not designed initializer
 + (instancetype)imageRepWithData:(NSData *)data {
     SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
@@ -52,13 +63,13 @@
         if (!type) {
             return self;
         }
-        if (CFStringCompare(type, kUTTypeGIF, 0) == kCFCompareEqualTo) {
+        if (CFStringCompare(type, kSDUTTypeGIF, 0) == kCFCompareEqualTo) {
             // GIF
             // Fix the `NSBitmapImageRep` GIF loop count calculation issue
             // Which will use 0 when there are no loop count information metadata in GIF data
             NSUInteger loopCount = [SDImageGIFCoder imageLoopCountWithSource:imageSource];
             [self setProperty:NSImageLoopCount withValue:@(loopCount)];
-        } else if (CFStringCompare(type, kUTTypePNG, 0) == kCFCompareEqualTo) {
+        } else if (CFStringCompare(type, kSDUTTypePNG, 0) == kCFCompareEqualTo) {
             // APNG
             // Do initialize about frame count, current frame/duration and loop count
             [self setProperty:NSImageFrameCount withValue:@(frameCount)];
@@ -100,10 +111,10 @@
         }
         NSUInteger index = [value unsignedIntegerValue];
         NSTimeInterval frameDuration = 0;
-        if (CFStringCompare(type, kUTTypeGIF, 0) == kCFCompareEqualTo) {
+        if (CFStringCompare(type, kSDUTTypeGIF, 0) == kCFCompareEqualTo) {
             // GIF
             frameDuration = [SDImageGIFCoder frameDurationAtIndex:index source:imageSource];
-        } else if (CFStringCompare(type, kUTTypePNG, 0) == kCFCompareEqualTo) {
+        } else if (CFStringCompare(type, kSDUTTypePNG, 0) == kCFCompareEqualTo) {
             // APNG
             frameDuration = [SDImageAPNGCoder frameDurationAtIndex:index source:imageSource];
         } else if (CFStringCompare(type, kSDUTTypeHEICS, 0) == kCFCompareEqualTo) {

+ 11 - 0
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.h

@@ -11,6 +11,7 @@
 #if SD_UIKIT || SD_MAC
 
 #import "SDAnimatedImage.h"
+#import "SDAnimatedImagePlayer.h"
 
 /**
  A drop-in replacement for UIImageView/NSImageView, you can use this for animated image rendering.
@@ -19,6 +20,12 @@
  For AppKit: use `-setAnimates:` to control animating, `animates` to check animation state. This view is layer-backed.
  */
 @interface SDAnimatedImageView : UIImageView
+/**
+ The internal animation player.
+ This property is only used for advanced usage, like inspecting/debugging animation status, control progressive loading, complicated animation frame index control, etc.
+ @warning Pay attention if you directly update the player's property like `totalFrameCount`, `totalLoopCount`, the same property on `SDAnimatedImageView` may not get synced.
+ */
+@property (nonatomic, strong, readonly, nullable) SDAnimatedImagePlayer *player;
 
 /**
  Current display frame image. This value is KVO Compliance.
@@ -52,6 +59,10 @@
  `< 0.0` is not supported currently and stop animation. (may support reverse playback in the future)
  */
 @property (nonatomic, assign) double playbackRate;
+
+/// Asynchronous setup animation playback mode. Default mode is SDAnimatedImagePlaybackModeNormal.
+@property (nonatomic, assign) SDAnimatedImagePlaybackMode playbackMode;
+
 /**
  Provide a max buffer size by bytes. This is used to adjust frame buffer count and can be useful when the decoding cost is expensive (such as Animated WebP software decoding). Default is 0.
  `0` means automatically adjust by calculating current memory usage.

+ 25 - 5
Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.m

@@ -10,7 +10,6 @@
 
 #if SD_UIKIT || SD_MAC
 
-#import "SDAnimatedImagePlayer.h"
 #import "UIImage+Metadata.h"
 #import "NSImage+Compatibility.h"
 #import "SDInternalMacros.h"
@@ -24,14 +23,15 @@
     NSRunLoopMode _runLoopMode;
     NSUInteger _maxBufferSize;
     double _playbackRate;
+    SDAnimatedImagePlaybackMode _playbackMode;
 }
 
+@property (nonatomic, strong, readwrite) SDAnimatedImagePlayer *player;
 @property (nonatomic, strong, readwrite) UIImage *currentFrame;
 @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex;
 @property (nonatomic, assign, readwrite) NSUInteger currentLoopCount;
 @property (nonatomic, assign) BOOL shouldAnimate;
 @property (nonatomic, assign) BOOL isProgressive;
-@property (nonatomic,strong) SDAnimatedImagePlayer *player; // The animation player.
 @property (nonatomic) CALayer *imageViewLayer; // The actual rendering layer.
 
 @end
@@ -164,6 +164,9 @@
         // Play Rate
         self.player.playbackRate = self.playbackRate;
         
+        // Play Mode
+        self.player.playbackMode = self.playbackMode;
+
         // Setup handler
         @weakify(self);
         self.player.animationFrameHandler = ^(NSUInteger index, UIImage * frame) {
@@ -189,9 +192,8 @@
         
         [self stopAnimating];
         [self checkPlay];
-
-        [self.imageViewLayer setNeedsDisplay];
     }
+    [self.imageViewLayer setNeedsDisplay];
 }
 
 #pragma mark - Configuration
@@ -239,6 +241,19 @@
     return _playbackRate;
 }
 
+- (void)setPlaybackMode:(SDAnimatedImagePlaybackMode)playbackMode {
+    _playbackMode = playbackMode;
+    self.player.playbackMode = playbackMode;
+}
+
+- (SDAnimatedImagePlaybackMode)playbackMode {
+    if (!_initFinished) {
+        return SDAnimatedImagePlaybackModeNormal; // Default mode is normal
+    }
+    return _playbackMode;
+}
+
+
 - (BOOL)shouldIncrementalLoad
 {
     if (!_initFinished) {
@@ -456,7 +471,7 @@
 {
     if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)] && image.sd_isIncremental && [image respondsToSelector:@selector(animatedCoder)]) {
         id<SDAnimatedImageCoder> animatedCoder = [(id<SDAnimatedImage>)image animatedCoder];
-        if ([animatedCoder conformsToProtocol:@protocol(SDProgressiveImageCoder)]) {
+        if ([animatedCoder respondsToSelector:@selector(initIncrementalWithOptions:)]) {
             return (id<SDAnimatedImageCoder, SDProgressiveImageCoder>)animatedCoder;
         }
     }
@@ -477,6 +492,11 @@
         // If we have no animation frames, call super implementation. iOS 14+ UIImageView use this delegate method for rendering.
         if ([UIImageView instancesRespondToSelector:@selector(displayLayer:)]) {
             [super displayLayer:layer];
+        } else {
+            // Fallback to implements the static image rendering by ourselves (like macOS or before iOS 14)
+            currentFrame = super.image;
+            layer.contentsScale = currentFrame.scale;
+            layer.contents = (__bridge id)currentFrame.CGImage;
         }
     }
 }

+ 54 - 0
Pods/SDWebImage/SDWebImage/Core/SDCallbackQueue.h

@@ -0,0 +1,54 @@
+/*
+ * This file is part of the SDWebImage package.
+ * (c) Olivier Poitrey <rs@dailymotion.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+
+#import "SDWebImageCompat.h"
+
+/// SDCallbackPolicy controls how we execute the block on the queue, like whether to use `dispatch_async/dispatch_sync`, check if current queue match target queue, or just invoke without any context.
+typedef NS_ENUM(NSUInteger, SDCallbackPolicy) {
+    /// When the current queue is equal to callback queue, sync/async will just invoke `block` directly without dispatch. Else it use `dispatch_async`/`dispatch_sync` to dispatch block on queue. This is useful for UIKit rendering to ensure all blocks executed in the same runloop
+    SDCallbackPolicySafeExecute = 0,
+    /// Follow async/sync using the correspond `dispatch_async`/`dispatch_sync` to dispatch block on queue
+    SDCallbackPolicyDispatch = 1,
+    /// Ignore any async/sync and just directly invoke `block` in current queue (without `dispatch_async`/`dispatch_sync`)
+    SDCallbackPolicyInvoke = 2
+};
+
+/// SDCallbackQueue is a wrapper used to control how the completionBlock should perform on queues, used by our `Cache`/`Manager`/`Loader`.
+/// Useful when you call SDWebImage in non-main queue and want to avoid it callback into main queue, which may cause issue.
+@interface SDCallbackQueue : NSObject
+
+/// The shared main queue. This is the default value, has the same effect when passing `nil` to `SDWebImageContextCallbackQueue`
+@property (nonnull, class, readonly) SDCallbackQueue *mainQueue;
+
+/// The caller current queue. Using `dispatch_get_current_queue`. This is not a dynamic value and only keep the first call time queue.
+@property (nonnull, class, readonly) SDCallbackQueue *currentQueue;
+
+/// The global concurrent queue (user-initiated QoS). Using `dispatch_get_global_queue`.
+@property (nonnull, class, readonly) SDCallbackQueue *globalQueue;
+
+/// The current queue's callback policy, defaults to `SDCallbackPolicySafeExecute`, which behaves like the old macro  `dispatch_main_async_safe`
+@property (assign, readwrite) SDCallbackPolicy policy;
+
+- (nonnull instancetype)init NS_UNAVAILABLE;
++ (nonnull instancetype)new  NS_UNAVAILABLE;
+/// Create the callback queue with a GCD queue
+/// - Parameter queue: The GCD queue, should not be NULL
+- (nonnull instancetype)initWithDispatchQueue:(nonnull dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER;
+
+#pragma mark - Execution Entry
+
+/// Submits a block for execution and returns after that block finishes executing.
+/// - Parameter block: The block that contains the work to perform.
+- (void)sync:(nonnull dispatch_block_t)block;
+
+/// Schedules a block asynchronously for execution.
+/// - Parameter block: The block that contains the work to perform.
+- (void)async:(nonnull dispatch_block_t)block;
+
+@end

+ 110 - 0
Pods/SDWebImage/SDWebImage/Core/SDCallbackQueue.m

@@ -0,0 +1,110 @@
+/*
+ * This file is part of the SDWebImage package.
+ * (c) Olivier Poitrey <rs@dailymotion.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+
+#import "SDCallbackQueue.h"
+
+@interface SDCallbackQueue ()
+
+@property (nonatomic, strong, nonnull) dispatch_queue_t queue;
+
+@end
+
+static void * SDCallbackQueueKey = &SDCallbackQueueKey;
+static void SDReleaseBlock(void *context) {
+    CFRelease(context);
+}
+
+static void inline SDSafeExecute(dispatch_queue_t _Nonnull queue, dispatch_block_t _Nonnull block, BOOL async) {
+    // Special handle for main queue label only (custom queue can have the same label)
+    const char *label = dispatch_queue_get_label(queue);
+    if (label && label == dispatch_queue_get_label(dispatch_get_main_queue())) {
+        const char *currentLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
+        if (label == currentLabel) {
+            block();
+            return;
+        }
+    }
+    // Check specific to detect queue equal
+    void *specific = dispatch_queue_get_specific(queue, SDCallbackQueueKey);
+    void *currentSpecific = dispatch_get_specific(SDCallbackQueueKey);
+    if (specific && currentSpecific && CFGetTypeID(specific) == CFUUIDGetTypeID() && CFGetTypeID(currentSpecific) == CFUUIDGetTypeID() && CFEqual(specific, currentSpecific)) {
+        block();
+    } else {
+        if (async) {
+            dispatch_async(queue, block);
+        } else {
+            dispatch_sync(queue, block);
+        }
+    }
+}
+
+@implementation SDCallbackQueue
+
+- (instancetype)initWithDispatchQueue:(dispatch_queue_t)queue {
+    self = [super init];
+    if (self) {
+        NSCParameterAssert(queue);
+        CFUUIDRef UUID = CFUUIDCreate(kCFAllocatorDefault);
+        dispatch_queue_set_specific(queue, SDCallbackQueueKey, (void *)UUID, SDReleaseBlock);
+        _queue = queue;
+    }
+    return self;
+}
+
++ (SDCallbackQueue *)mainQueue {
+    static dispatch_once_t onceToken;
+    static SDCallbackQueue *queue;
+    dispatch_once(&onceToken, ^{
+        queue = [[SDCallbackQueue alloc] initWithDispatchQueue:dispatch_get_main_queue()];
+    });
+    return queue;
+}
+
++ (SDCallbackQueue *)currentQueue {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+    SDCallbackQueue *queue = [[SDCallbackQueue alloc] initWithDispatchQueue:dispatch_get_current_queue()];
+#pragma clang diagnostic pop
+    return queue;
+}
+
++ (SDCallbackQueue *)globalQueue {
+    SDCallbackQueue *queue = [[SDCallbackQueue alloc] initWithDispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
+    return queue;
+}
+
+- (void)sync:(nonnull dispatch_block_t)block {
+    switch (self.policy) {
+        case SDCallbackPolicySafeExecute:
+            SDSafeExecute(self.queue, block, NO);
+            break;
+        case SDCallbackPolicyDispatch:
+            dispatch_sync(self.queue, block);
+            break;
+        case SDCallbackPolicyInvoke:
+            block();
+            break;
+    }
+}
+
+- (void)async:(nonnull dispatch_block_t)block {
+    switch (self.policy) {
+        case SDCallbackPolicySafeExecute:
+            SDSafeExecute(self.queue, block, YES);
+            break;
+        case SDCallbackPolicyDispatch:
+            dispatch_async(self.queue, block);
+            break;
+        case SDCallbackPolicyInvoke:
+            block();
+            break;
+    }
+}
+
+@end

+ 22 - 14
Pods/SDWebImage/SDWebImage/Core/SDDiskCache.m

@@ -43,6 +43,8 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis
     } else {
         self.fileManager = [NSFileManager new];
     }
+  
+    [self createDirectory];
 }
 
 - (BOOL)containsDataForKey:(NSString *)key {
@@ -80,22 +82,13 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis
 - (void)setData:(NSData *)data forKey:(NSString *)key {
     NSParameterAssert(data);
     NSParameterAssert(key);
-    if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
-        [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
-    }
     
     // get cache Path for image key
     NSString *cachePathForKey = [self cachePathForKey:key];
     // transform to NSURL
-    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
+    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey isDirectory:NO];
     
     [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];
-    
-    // disable iCloud backup
-    if (self.config.shouldDisableiCloud) {
-        // ignore iCloud backup resource value error
-        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
-    }
 }
 
 - (NSData *)extendedDataForKey:(NSString *)key {
@@ -131,10 +124,20 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis
 
 - (void)removeAllData {
     [self.fileManager removeItemAtPath:self.diskCachePath error:nil];
-    [self.fileManager createDirectoryAtPath:self.diskCachePath
-            withIntermediateDirectories:YES
-                             attributes:nil
-                                  error:NULL];
+    [self createDirectory];
+}
+
+- (void)createDirectory {
+  [self.fileManager createDirectoryAtPath:self.diskCachePath
+          withIntermediateDirectories:YES
+                           attributes:nil
+                                error:NULL];
+  
+  // disable iCloud backup
+  if (self.config.shouldDisableiCloud) {
+      // ignore iCloud backup resource value error
+      [[NSURL fileURLWithPath:self.diskCachePath isDirectory:YES] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
+  }
 }
 
 - (void)removeExpiredData {
@@ -285,6 +288,11 @@ static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDis
         }
         // New directory does not exist, rename directory
         [self.fileManager moveItemAtPath:srcPath toPath:dstPath error:nil];
+        // disable iCloud backup
+        if (self.config.shouldDisableiCloud) {
+            // ignore iCloud backup resource value error
+            [[NSURL fileURLWithPath:dstPath isDirectory:YES] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
+        }
     } else {
         // New directory exist, merge the files
         NSDirectoryEnumerator *dirEnumerator = [self.fileManager enumeratorAtPath:srcPath];

+ 14 - 2
Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.m

@@ -132,7 +132,13 @@
 #elif SD_UIKIT
             CGFloat screenScale = [UIScreen mainScreen].scale;
 #elif SD_MAC
-            CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor;
+            NSScreen *mainScreen = nil;
+            if (@available(macOS 10.12, *)) {
+                mainScreen = [NSScreen mainScreen];
+            } else {
+                mainScreen = [NSScreen screens].firstObject;
+            }
+            CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
 #endif
             self.scale = screenScale;
             self.opaque = NO;
@@ -166,7 +172,13 @@
 #elif SD_UIKIT
             CGFloat screenScale = [UIScreen mainScreen].scale;
 #elif SD_MAC
-            CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor;
+            NSScreen *mainScreen = nil;
+            if (@available(macOS 10.12, *)) {
+                mainScreen = [NSScreen mainScreen];
+            } else {
+                mainScreen = [NSScreen screens].firstObject;
+            }
+            CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
 #endif
             self.scale = screenScale;
             self.opaque = NO;

+ 2 - 9
Pods/SDWebImage/SDWebImage/Core/SDImageAPNGCoder.m

@@ -7,20 +7,13 @@
  */
 
 #import "SDImageAPNGCoder.h"
+#import "SDImageIOAnimatedCoderInternal.h"
 #if SD_MAC
 #import <CoreServices/CoreServices.h>
 #else
 #import <MobileCoreServices/MobileCoreServices.h>
 #endif
 
-// iOS 8 Image/IO framework binary does not contains these APNG constants, so we define them. Thanks Apple :)
-// We can not use runtime @available check for this issue, because it's a global symbol and should be loaded during launch time by dyld. So hack if the min deployment target version < iOS 9.0, whatever it running on iOS 9+ or not.
-#if (__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0)
-const CFStringRef kCGImagePropertyAPNGLoopCount = (__bridge CFStringRef)@"LoopCount";
-const CFStringRef kCGImagePropertyAPNGDelayTime = (__bridge CFStringRef)@"DelayTime";
-const CFStringRef kCGImagePropertyAPNGUnclampedDelayTime = (__bridge CFStringRef)@"UnclampedDelayTime";
-#endif
-
 @implementation SDImageAPNGCoder
 
 + (instancetype)sharedCoder {
@@ -39,7 +32,7 @@ const CFStringRef kCGImagePropertyAPNGUnclampedDelayTime = (__bridge CFStringRef
 }
 
 + (NSString *)imageUTType {
-    return (__bridge NSString *)kUTTypePNG;
+    return (__bridge NSString *)kSDUTTypePNG;
 }
 
 + (NSString *)dictionaryProperty {

+ 61 - 11
Pods/SDWebImage/SDWebImage/Core/SDImageCache.h

@@ -55,6 +55,23 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
     SDImageCacheMatchAnimatedImageClass = 1 << 7,
 };
 
+/**
+ *  A token associated with each cache query. Can be used to cancel a cache query
+ */
+@interface SDImageCacheToken : NSObject <SDWebImageOperation>
+
+/**
+ Cancel the current cache query.
+ */
+- (void)cancel;
+
+/**
+ The query's cache key.
+ */
+@property (nonatomic, strong, nullable, readonly) NSString *key;
+
+@end
+
 /**
  * SDImageCache maintains a memory cache and a disk cache. Disk cache write operations are performed
  * asynchronous so it doesn’t add unnecessary latency to the UI.
@@ -179,6 +196,17 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
             toDisk:(BOOL)toDisk
         completion:(nullable SDWebImageNoParamsBlock)completionBlock;
 
+/**
+ * Asynchronously store an image data into disk cache at the given key.
+ *
+ * @param imageData           The image data to store
+ * @param key             The unique image cache key, usually it's image absolute URL
+ * @param completionBlock A block executed after the operation is finished
+ */
+- (void)storeImageData:(nullable NSData *)imageData
+                forKey:(nullable NSString *)key
+            completion:(nullable SDWebImageNoParamsBlock)completionBlock;
+
 /**
  * Asynchronously store an image into memory and disk cache at the given key.
  *
@@ -198,7 +226,29 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
         completion:(nullable SDWebImageNoParamsBlock)completionBlock;
 
 /**
- * Synchronously store image into memory cache at the given key.
+ * Asynchronously store an image into memory and disk cache at the given key.
+ *
+ * @param image           The image to store
+ * @param imageData       The image data as returned by the server, this representation will be used for disk storage
+ *                        instead of converting the given image object into a storable/compressed image format in order
+ *                        to save quality and CPU
+ * @param key             The unique image cache key, usually it's image absolute URL
+ * @param options A mask to specify options to use for this store
+ * @param context The context options to use. Pass `.callbackQueue` to control callback queue
+ * @param cacheType The image store op cache type
+ * @param completionBlock A block executed after the operation is finished
+ * @note If no image data is provided and encode to disk, we will try to detect the image format (using either `sd_imageFormat` or `SDAnimatedImage` protocol method) and animation status, to choose the best matched format, including GIF, JPEG or PNG.
+ */
+- (void)storeImage:(nullable UIImage *)image
+         imageData:(nullable NSData *)imageData
+            forKey:(nullable NSString *)key
+           options:(SDWebImageOptions)options
+           context:(nullable SDWebImageContext *)context
+         cacheType:(SDImageCacheType)cacheType
+        completion:(nullable SDWebImageNoParamsBlock)completionBlock;
+
+/**
+ * Synchronously store an image into memory cache at the given key.
  *
  * @param image  The image to store
  * @param key    The unique image cache key, usually it's image absolute URL
@@ -207,7 +257,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
                     forKey:(nullable NSString *)key;
 
 /**
- * Synchronously store image data into disk cache at the given key.
+ * Synchronously store an image data into disk cache at the given key.
  *
  * @param imageData  The image data to store
  * @param key        The unique image cache key, usually it's image absolute URL
@@ -259,9 +309,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param key       The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`.
  * @param doneBlock The completion block. Will not get called if the operation is cancelled
  *
- * @return a NSOperation instance containing the cache op
+ * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled
  */
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
 
 /**
  * Asynchronously queries the cache with operation and call the completion when done.
@@ -270,9 +320,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param options   A mask to specify options to use for this cache query
  * @param doneBlock The completion block. Will not get called if the operation is cancelled
  *
- * @return a NSOperation instance containing the cache op
+ * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled
  */
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
 
 /**
  * Asynchronously queries the cache with operation and call the completion when done.
@@ -282,9 +332,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param context   A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
  * @param doneBlock The completion block. Will not get called if the operation is cancelled
  *
- * @return a NSOperation instance containing the cache op
+ * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancellederation, will callback immediately when cancelled
  */
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
 
 /**
  * Asynchronously queries the cache with operation and call the completion when done.
@@ -295,9 +345,9 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param queryCacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediately.
  * @param doneBlock The completion block. Will not get called if the operation is cancelled
  *
- * @return a NSOperation instance containing the cache op
+ * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled
  */
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;
 
 /**
  * Synchronously query the memory cache.
@@ -341,7 +391,7 @@ typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) {
  * @param context   A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
  * @return The image for the given key, or nil if not found.
  */
-- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context;;
+- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context;
 
 #pragma mark - Remove Ops
 

+ 299 - 166
Pods/SDWebImage/SDWebImage/Core/SDImageCache.m

@@ -14,6 +14,45 @@
 #import "UIImage+MemoryCacheCost.h"
 #import "UIImage+Metadata.h"
 #import "UIImage+ExtendedCacheData.h"
+#import "SDCallbackQueue.h"
+
+@interface SDImageCacheToken ()
+
+@property (nonatomic, strong, nullable, readwrite) NSString *key;
+@property (nonatomic, assign, getter=isCancelled) BOOL cancelled;
+@property (nonatomic, copy, nullable) SDImageCacheQueryCompletionBlock doneBlock;
+@property (nonatomic, strong, nullable) SDCallbackQueue *callbackQueue;
+
+@end
+
+@implementation SDImageCacheToken
+
+-(instancetype)initWithDoneBlock:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
+    self = [super init];
+    if (self) {
+        self.doneBlock = doneBlock;
+    }
+    return self;
+}
+
+- (void)cancel {
+    @synchronized (self) {
+        if (self.isCancelled) {
+            return;
+        }
+        self.cancelled = YES;
+        
+        SDImageCacheQueryCompletionBlock doneBlock = self.doneBlock;
+        self.doneBlock = nil;
+        if (doneBlock) {
+            [(self.callbackQueue ?: SDCallbackQueue.mainQueue) async:^{
+                doneBlock(nil, nil, SDImageCacheTypeNone);
+            }];
+        }
+    }
+}
+
+@end
 
 static NSString * _defaultDiskCacheDirectory;
 
@@ -24,7 +63,7 @@ static NSString * _defaultDiskCacheDirectory;
 @property (nonatomic, strong, readwrite, nonnull) id<SDDiskCache> diskCache;
 @property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config;
 @property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath;
-@property (nonatomic, strong, nullable) dispatch_queue_t ioQueue;
+@property (nonatomic, strong, nonnull) dispatch_queue_t ioQueue;
 
 @end
 
@@ -72,14 +111,16 @@ static NSString * _defaultDiskCacheDirectory;
     if ((self = [super init])) {
         NSAssert(ns, @"Cache namespace should not be nil");
         
-        // Create IO serial queue
-        _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);
-        
         if (!config) {
             config = SDImageCacheConfig.defaultCacheConfig;
         }
         _config = [config copy];
         
+        // Create IO queue
+        dispatch_queue_attr_t ioQueueAttributes = _config.ioQueueAttributes;
+        _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache.ioQueue", ioQueueAttributes);
+        NSAssert(_ioQueue, @"The IO queue should not be nil. Your configured `ioQueueAttributes` may be wrong");
+        
         // Init the memory cache
         NSAssert([config.memoryCacheClass conformsToProtocol:@protocol(SDMemoryCache)], @"Custom memory cache class must conform to `SDMemoryCache` protocol");
         _memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config];
@@ -158,14 +199,20 @@ static NSString * _defaultDiskCacheDirectory;
 - (void)storeImage:(nullable UIImage *)image
             forKey:(nullable NSString *)key
         completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    [self storeImage:image imageData:nil forKey:key toDisk:YES completion:completionBlock];
+    [self storeImage:image imageData:nil forKey:key options:0 context:nil cacheType:SDImageCacheTypeAll completion:completionBlock];
 }
 
 - (void)storeImage:(nullable UIImage *)image
             forKey:(nullable NSString *)key
             toDisk:(BOOL)toDisk
         completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    [self storeImage:image imageData:nil forKey:key toDisk:toDisk completion:completionBlock];
+    [self storeImage:image imageData:nil forKey:key options:0 context:nil cacheType:(toDisk ? SDImageCacheTypeAll : SDImageCacheTypeMemory) completion:completionBlock];
+}
+
+- (void)storeImageData:(nullable NSData *)imageData
+                forKey:(nullable NSString *)key
+            completion:(nullable SDWebImageNoParamsBlock)completionBlock {
+    [self storeImage:nil imageData:imageData forKey:key options:0 context:nil cacheType:SDImageCacheTypeAll completion:completionBlock];
 }
 
 - (void)storeImage:(nullable UIImage *)image
@@ -173,93 +220,108 @@ static NSString * _defaultDiskCacheDirectory;
             forKey:(nullable NSString *)key
             toDisk:(BOOL)toDisk
         completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    return [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:toDisk completion:completionBlock];
+    [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:(toDisk ? SDImageCacheTypeAll : SDImageCacheTypeMemory) completion:completionBlock];
 }
 
 - (void)storeImage:(nullable UIImage *)image
          imageData:(nullable NSData *)imageData
             forKey:(nullable NSString *)key
-          toMemory:(BOOL)toMemory
-            toDisk:(BOOL)toDisk
+           options:(SDWebImageOptions)options
+           context:(nullable SDWebImageContext *)context
+         cacheType:(SDImageCacheType)cacheType
         completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    if (!image || !key) {
+    if ((!image && !imageData) || !key) {
         if (completionBlock) {
             completionBlock();
         }
         return;
     }
+    BOOL toMemory = cacheType == SDImageCacheTypeMemory || cacheType == SDImageCacheTypeAll;
+    BOOL toDisk = cacheType == SDImageCacheTypeDisk || cacheType == SDImageCacheTypeAll;
     // if memory cache is enabled
-    if (toMemory && self.config.shouldCacheImagesInMemory) {
+    if (image && toMemory && self.config.shouldCacheImagesInMemory) {
         NSUInteger cost = image.sd_memoryCost;
         [self.memoryCache setObject:image forKey:key cost:cost];
     }
     
-    if (toDisk) {
-        dispatch_async(self.ioQueue, ^{
-            @autoreleasepool {
-                NSData *data = imageData;
-                if (!data && [image conformsToProtocol:@protocol(SDAnimatedImage)]) {
-                    // If image is custom animated image class, prefer its original animated data
-                    data = [((id<SDAnimatedImage>)image) animatedImageData];
-                }
-                if (!data && image) {
-                    // Check image's associated image format, may return .undefined
-                    SDImageFormat format = image.sd_imageFormat;
-                    if (format == SDImageFormatUndefined) {
-                        // If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
-                        if (image.sd_isAnimated) {
-                            format = SDImageFormatGIF;
-                        } else {
-                            // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
-                            if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
-                                format = SDImageFormatPNG;
-                            } else {
-                                format = SDImageFormatJPEG;
-                            }
-                        }
-                    }
-                    data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
+    if (!toDisk) {
+        if (completionBlock) {
+            completionBlock();
+        }
+        return;
+    }
+    NSData *data = imageData;
+    if (!data && [image respondsToSelector:@selector(animatedImageData)]) {
+        // If image is custom animated image class, prefer its original animated data
+        data = [((id<SDAnimatedImage>)image) animatedImageData];
+    }
+    SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
+    if (!data && image) {
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            // Check image's associated image format, may return .undefined
+            SDImageFormat format = image.sd_imageFormat;
+            if (format == SDImageFormatUndefined) {
+                // If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14)
+                if (image.sd_isAnimated) {
+                    format = SDImageFormatGIF;
+                } else {
+                    // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
+                    format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG;
                 }
+            }
+            NSData *data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:context[SDWebImageContextImageEncodeOptions]];
+            dispatch_async(self.ioQueue, ^{
                 [self _storeImageDataToDisk:data forKey:key];
-                if (image) {
-                    // Check extended data
-                    id extendedObject = image.sd_extendedObject;
-                    if ([extendedObject conformsToProtocol:@protocol(NSCoding)]) {
-                        NSData *extendedData;
-                        if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
-                            NSError *error;
-                            extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error];
-                            if (error) {
-                                NSLog(@"NSKeyedArchiver archive failed with error: %@", error);
-                            }
-                        } else {
-                            @try {
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-                                extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject];
-#pragma clang diagnostic pop
-                            } @catch (NSException *exception) {
-                                NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception);
-                            }
-                        }
-                        if (extendedData) {
-                            [self.diskCache setExtendedData:extendedData forKey:key];
-                        }
-                    }
+                [self _archivedDataWithImage:image forKey:key];
+                if (completionBlock) {
+                    [(queue ?: SDCallbackQueue.mainQueue) async:^{
+                        completionBlock();
+                    }];
                 }
-            }
-            
+            });
+        });
+    } else {
+        dispatch_async(self.ioQueue, ^{
+            [self _storeImageDataToDisk:data forKey:key];
+            [self _archivedDataWithImage:image forKey:key];
             if (completionBlock) {
-                dispatch_async(dispatch_get_main_queue(), ^{
+                [(queue ?: SDCallbackQueue.mainQueue) async:^{
                     completionBlock();
-                });
+                }];
             }
         });
+    }
+}
+
+- (void)_archivedDataWithImage:(UIImage *)image forKey:(NSString *)key {
+    if (!image || !key) {
+        return;
+    }
+    // Check extended data
+    id extendedObject = image.sd_extendedObject;
+    if (![extendedObject conformsToProtocol:@protocol(NSCoding)]) {
+        return;
+    }
+    NSData *extendedData;
+    if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
+        NSError *error;
+        extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error];
+        if (error) {
+            NSLog(@"NSKeyedArchiver archive failed with error: %@", error);
+        }
     } else {
-        if (completionBlock) {
-            completionBlock();
+        @try {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+            extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject];
+#pragma clang diagnostic pop
+        } @catch (NSException *exception) {
+            NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception);
         }
     }
+    if (extendedData) {
+        [self.diskCache setExtendedData:extendedData forKey:key];
+    }
 }
 
 - (void)storeImageToMemory:(UIImage *)image forKey:(NSString *)key {
@@ -357,9 +419,31 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context {
+    if (!key) {
+        return nil;
+    }
     NSData *data = [self diskImageDataForKey:key];
     UIImage *diskImage = [self diskImageForKey:key data:data options:options context:context];
-    if (diskImage && self.config.shouldCacheImagesInMemory) {
+    
+    BOOL shouldCacheToMomery = YES;
+    if (context[SDWebImageContextStoreCacheType]) {
+        SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
+        shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
+    }
+    CGSize thumbnailSize = CGSizeZero;
+    NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
+    if (thumbnailSizeValue != nil) {
+#if SD_MAC
+        thumbnailSize = thumbnailSizeValue.sizeValue;
+#else
+        thumbnailSize = thumbnailSizeValue.CGSizeValue;
+#endif
+    }
+    if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
+        // Query full size cache key which generate a thumbnail, should not write back to full size memory cache
+        shouldCacheToMomery = NO;
+    }
+    if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
         NSUInteger cost = diskImage.sd_memoryCost;
         [self.memoryCache setObject:diskImage forKey:key cost:cost];
     }
@@ -374,6 +458,27 @@ static NSString * _defaultDiskCacheDirectory;
 - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context {
     // First check the in-memory cache...
     UIImage *image = [self imageFromMemoryCacheForKey:key];
+    if (image) {
+        if (options & SDImageCacheDecodeFirstFrameOnly) {
+            // Ensure static image
+            if (image.sd_isAnimated) {
+#if SD_MAC
+                image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
+#else
+                image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
+#endif
+            }
+        } else if (options & SDImageCacheMatchAnimatedImageClass) {
+            // Check image class matching
+            Class animatedImageClass = image.class;
+            Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
+            if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) {
+                image = nil;
+            }
+        }
+    }
+    
+    // Since we don't need to query imageData, return image if exist
     if (image) {
         return image;
     }
@@ -405,62 +510,66 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 - (nullable UIImage *)diskImageForKey:(nullable NSString *)key {
+    if (!key) {
+        return nil;
+    }
     NSData *data = [self diskImageDataForKey:key];
-    return [self diskImageForKey:key data:data];
-}
-
-- (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data {
     return [self diskImageForKey:key data:data options:0 context:nil];
 }
 
 - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context {
-    if (data) {
-        UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context);
-        if (image) {
-            // Check extended data
-            NSData *extendedData = [self.diskCache extendedDataForKey:key];
-            if (extendedData) {
-                id extendedObject;
-                if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
-                    NSError *error;
-                    NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:extendedData error:&error];
-                    unarchiver.requiresSecureCoding = NO;
-                    extendedObject = [unarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error];
-                    if (error) {
-                        NSLog(@"NSKeyedUnarchiver unarchive failed with error: %@", error);
-                    }
-                } else {
-                    @try {
+    if (!data) {
+        return nil;
+    }
+    UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context);
+    [self _unarchiveObjectWithImage:image forKey:key];
+    return image;
+}
+
+- (void)_unarchiveObjectWithImage:(UIImage *)image forKey:(NSString *)key {
+    if (!image || !key) {
+        return;
+    }
+    // Check extended data
+    NSData *extendedData = [self.diskCache extendedDataForKey:key];
+    if (!extendedData) {
+        return;
+    }
+    id extendedObject;
+    if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
+        NSError *error;
+        NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:extendedData error:&error];
+        unarchiver.requiresSecureCoding = NO;
+        extendedObject = [unarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error];
+        if (error) {
+            NSLog(@"NSKeyedUnarchiver unarchive failed with error: %@", error);
+        }
+    } else {
+        @try {
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wdeprecated-declarations"
-                        extendedObject = [NSKeyedUnarchiver unarchiveObjectWithData:extendedData];
+            extendedObject = [NSKeyedUnarchiver unarchiveObjectWithData:extendedData];
 #pragma clang diagnostic pop
-                    } @catch (NSException *exception) {
-                        NSLog(@"NSKeyedUnarchiver unarchive failed with exception: %@", exception);
-                    }
-                }
-                image.sd_extendedObject = extendedObject;
-            }
+        } @catch (NSException *exception) {
+            NSLog(@"NSKeyedUnarchiver unarchive failed with exception: %@", exception);
         }
-        return image;
-    } else {
-        return nil;
     }
+    image.sd_extendedObject = extendedObject;
 }
 
-- (nullable NSOperation *)queryCacheOperationForKey:(NSString *)key done:(SDImageCacheQueryCompletionBlock)doneBlock {
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(NSString *)key done:(SDImageCacheQueryCompletionBlock)doneBlock {
     return [self queryCacheOperationForKey:key options:0 done:doneBlock];
 }
 
-- (nullable NSOperation *)queryCacheOperationForKey:(NSString *)key options:(SDImageCacheOptions)options done:(SDImageCacheQueryCompletionBlock)doneBlock {
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(NSString *)key options:(SDImageCacheOptions)options done:(SDImageCacheQueryCompletionBlock)doneBlock {
     return [self queryCacheOperationForKey:key options:options context:nil done:doneBlock];
 }
 
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
     return [self queryCacheOperationForKey:key options:options context:context cacheType:SDImageCacheTypeAll done:doneBlock];
 }
 
-- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
+- (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
     if (!key) {
         if (doneBlock) {
             doneBlock(nil, nil, SDImageCacheTypeNone);
@@ -484,8 +593,7 @@ static NSString * _defaultDiskCacheDirectory;
     if (image) {
         if (options & SDImageCacheDecodeFirstFrameOnly) {
             // Ensure static image
-            Class animatedImageClass = image.class;
-            if (image.sd_isAnimated || ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)])) {
+            if (image.sd_isAnimated) {
 #if SD_MAC
                 image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
 #else
@@ -511,57 +619,98 @@ static NSString * _defaultDiskCacheDirectory;
     }
     
     // Second check the disk cache...
-    NSOperation *operation = [NSOperation new];
+    SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue];
+    SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock];
+    operation.key = key;
+    operation.callbackQueue = queue;
     // Check whether we need to synchronously query disk
     // 1. in-memory cache hit & memoryDataSync
     // 2. in-memory cache miss & diskDataSync
     BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                 (!image && options & SDImageCacheQueryDiskDataSync));
-    void(^queryDiskBlock)(void) =  ^{
-        if (operation.isCancelled) {
-            if (doneBlock) {
-                doneBlock(nil, nil, SDImageCacheTypeNone);
+    NSData* (^queryDiskDataBlock)(void) = ^NSData* {
+        @synchronized (operation) {
+            if (operation.isCancelled) {
+                return nil;
             }
-            return;
         }
         
-        @autoreleasepool {
-            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
-            UIImage *diskImage;
-            if (image) {
-                // the image is from in-memory cache, but need image data
-                diskImage = image;
-            } else if (diskData) {
-                BOOL shouldCacheToMomery = YES;
-                if (context[SDWebImageContextStoreCacheType]) {
-                    SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
-                    shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
-                }
-                // decode image data only if in-memory cache missed
-                diskImage = [self diskImageForKey:key data:diskData options:options context:context];
-                if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
-                    NSUInteger cost = diskImage.sd_memoryCost;
-                    [self.memoryCache setObject:diskImage forKey:key cost:cost];
-                }
+        return [self diskImageDataBySearchingAllPathsForKey:key];
+    };
+    
+    UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) {
+        @synchronized (operation) {
+            if (operation.isCancelled) {
+                return nil;
             }
-            
-            if (doneBlock) {
-                if (shouldQueryDiskSync) {
-                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
-                } else {
-                    dispatch_async(dispatch_get_main_queue(), ^{
-                        doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
-                    });
-                }
+        }
+        
+        UIImage *diskImage;
+        if (image) {
+            // the image is from in-memory cache, but need image data
+            diskImage = image;
+        } else if (diskData) {
+            BOOL shouldCacheToMomery = YES;
+            if (context[SDWebImageContextStoreCacheType]) {
+                SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue];
+                shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory);
+            }
+            CGSize thumbnailSize = CGSizeZero;
+            NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
+            if (thumbnailSizeValue != nil) {
+        #if SD_MAC
+                thumbnailSize = thumbnailSizeValue.sizeValue;
+        #else
+                thumbnailSize = thumbnailSizeValue.CGSizeValue;
+        #endif
+            }
+            if (thumbnailSize.width > 0 && thumbnailSize.height > 0) {
+                // Query full size cache key which generate a thumbnail, should not write back to full size memory cache
+                shouldCacheToMomery = NO;
+            }
+            // decode image data only if in-memory cache missed
+            diskImage = [self diskImageForKey:key data:diskData options:options context:context];
+            if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
+                NSUInteger cost = diskImage.sd_memoryCost;
+                [self.memoryCache setObject:diskImage forKey:key cost:cost];
             }
         }
+        return diskImage;
     };
     
     // Query in ioQueue to keep IO-safe
     if (shouldQueryDiskSync) {
-        dispatch_sync(self.ioQueue, queryDiskBlock);
+        __block NSData* diskData;
+        __block UIImage* diskImage;
+        dispatch_sync(self.ioQueue, ^{
+            diskData = queryDiskDataBlock();
+            diskImage = queryDiskImageBlock(diskData);
+        });
+        if (doneBlock) {
+            doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
+        }
     } else {
-        dispatch_async(self.ioQueue, queryDiskBlock);
+        dispatch_async(self.ioQueue, ^{
+            NSData* diskData = queryDiskDataBlock();
+            UIImage* diskImage = queryDiskImageBlock(diskData);
+            @synchronized (operation) {
+                if (operation.isCancelled) {
+                    return;
+                }
+            }
+            if (doneBlock) {
+                [(queue ?: SDCallbackQueue.mainQueue) async:^{
+                    // Dispatch from IO queue to main queue need time, user may call cancel during the dispatch timing
+                    // This check is here to avoid double callback (one is from `SDImageCacheToken` in sync)
+                    @synchronized (operation) {
+                        if (operation.isCancelled) {
+                            return;
+                        }
+                    }
+                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
+                }];
+            }
+        });
     }
     
     return operation;
@@ -578,7 +727,7 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 - (void)removeImageForKey:(nullable NSString *)key fromMemory:(BOOL)fromMemory fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion {
-    if (key == nil) {
+    if (!key) {
         return;
     }
 
@@ -659,7 +808,14 @@ static NSString * _defaultDiskCacheDirectory;
 
 #if SD_UIKIT || SD_MAC
 - (void)applicationWillTerminate:(NSNotification *)notification {
-    [self deleteOldFilesWithCompletionBlock:nil];
+    // On iOS/macOS, the async opeartion to remove exipred data will be terminated quickly
+    // Try using the sync operation to ensure we reomve the exipred data
+    if (!self.config.shouldRemoveExpiredDataWhenTerminate) {
+        return;
+    }
+    dispatch_sync(self.ioQueue, ^{
+        [self.diskCache removeExpiredData];
+    });
 }
 #endif
 
@@ -757,30 +913,7 @@ static NSString * _defaultDiskCacheDirectory;
 }
 
 - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock {
-    switch (cacheType) {
-        case SDImageCacheTypeNone: {
-            [self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:NO completion:completionBlock];
-        }
-            break;
-        case SDImageCacheTypeMemory: {
-            [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:NO completion:completionBlock];
-        }
-            break;
-        case SDImageCacheTypeDisk: {
-            [self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:YES completion:completionBlock];
-        }
-            break;
-        case SDImageCacheTypeAll: {
-            [self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:YES completion:completionBlock];
-        }
-            break;
-        default: {
-            if (completionBlock) {
-                completionBlock();
-            }
-        }
-            break;
-    }
+    [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:cacheType completion:completionBlock];
 }
 
 - (void)removeImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock {

+ 17 - 1
Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.h

@@ -57,7 +57,8 @@ typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType) {
 /*
  * The option to control weak memory cache for images. When enable, `SDImageCache`'s memory cache will use a weak maptable to store the image at the same time when it stored to memory, and get removed at the same time.
  * However when memory warning is triggered, since the weak maptable does not hold a strong reference to image instance, even when the memory cache itself is purged, some images which are held strongly by UIImageViews or other live instances can be recovered again, to avoid later re-query from disk cache or network. This may be helpful for the case, for example, when app enter background and memory is purged, cause cell flashing after re-enter foreground.
- * Defaults to YES. You can change this option dynamically.
+ * When enabling this option, we will sync back the image from weak maptable to strong cache during next time top level `sd_setImage` function call.
+ * Defaults to NO (YES before 5.12.0 version). You can change this option dynamically.
  */
 @property (assign, nonatomic) BOOL shouldUseWeakMemoryCache;
 
@@ -67,6 +68,12 @@ typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType) {
  */
 @property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenEnterBackground;
 
+/**
+ * Whether or not to remove the expired disk data when application been terminated. This operation is processed in sync to ensure clean up.
+ * Defaults to YES.
+ */
+@property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenTerminate;
+
 /**
  * The reading options while reading cache from disk.
  * Defaults to 0. You can set this to `NSDataReadingMappedIfSafe` to improve performance.
@@ -120,6 +127,15 @@ typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType) {
  */
 @property (strong, nonatomic, nullable) NSFileManager *fileManager;
 
+/**
+ * The dispatch queue attr for ioQueue. You can config the QoS and concurrent/serial to internal IO queue. The ioQueue is used by SDImageCache to access read/write for disk data.
+ * Defaults we use `DISPATCH_QUEUE_SERIAL`(NULL), to use serial dispatch queue to ensure single access for disk data. It's safe but may be slow.
+ * @note You can override this to use `DISPATCH_QUEUE_CONCURRENT`, use concurrent queue.
+ * @warning **MAKE SURE** to keep `diskCacheWritingOptions` to use `NSDataWritingAtomic`, or concurrent queue may cause corrupted disk data (because multiple threads read/write same file without atomic is not IO-safe).
+ * @note This value does not support dynamic changes. Which means further modification on this value after cache initialized has no effect.
+ */
+@property (strong, nonatomic, nullable) dispatch_queue_attr_t ioQueueAttributes;
+
 /**
  * The custom memory cache class. Provided class instance must conform to `SDMemoryCache` protocol to allow usage.
  * Defaults to built-in `SDMemoryCache` class.

+ 6 - 1
Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.m

@@ -27,13 +27,16 @@ static const NSInteger kDefaultCacheMaxDiskAge = 60 * 60 * 24 * 7; // 1 week
     if (self = [super init]) {
         _shouldDisableiCloud = YES;
         _shouldCacheImagesInMemory = YES;
-        _shouldUseWeakMemoryCache = YES;
+        _shouldUseWeakMemoryCache = NO;
         _shouldRemoveExpiredDataWhenEnterBackground = YES;
+        _shouldRemoveExpiredDataWhenTerminate = YES;
         _diskCacheReadingOptions = 0;
         _diskCacheWritingOptions = NSDataWritingAtomic;
         _maxDiskAge = kDefaultCacheMaxDiskAge;
         _maxDiskSize = 0;
         _diskCacheExpireType = SDImageCacheConfigExpireTypeModificationDate;
+        _fileManager = nil;
+        _ioQueueAttributes = DISPATCH_QUEUE_SERIAL; // NULL
         _memoryCacheClass = [SDMemoryCache class];
         _diskCacheClass = [SDDiskCache class];
     }
@@ -46,6 +49,7 @@ static const NSInteger kDefaultCacheMaxDiskAge = 60 * 60 * 24 * 7; // 1 week
     config.shouldCacheImagesInMemory = self.shouldCacheImagesInMemory;
     config.shouldUseWeakMemoryCache = self.shouldUseWeakMemoryCache;
     config.shouldRemoveExpiredDataWhenEnterBackground = self.shouldRemoveExpiredDataWhenEnterBackground;
+    config.shouldRemoveExpiredDataWhenTerminate = self.shouldRemoveExpiredDataWhenTerminate;
     config.diskCacheReadingOptions = self.diskCacheReadingOptions;
     config.diskCacheWritingOptions = self.diskCacheWritingOptions;
     config.maxDiskAge = self.maxDiskAge;
@@ -54,6 +58,7 @@ static const NSInteger kDefaultCacheMaxDiskAge = 60 * 60 * 24 * 7; // 1 week
     config.maxMemoryCount = self.maxMemoryCount;
     config.diskCacheExpireType = self.diskCacheExpireType;
     config.fileManager = self.fileManager; // NSFileManager does not conform to NSCopying, just pass the reference
+    config.ioQueueAttributes = self.ioQueueAttributes; // Pass the reference
     config.memoryCacheClass = self.memoryCacheClass;
     config.diskCacheClass = self.diskCacheClass;
     

+ 42 - 6
Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.h

@@ -10,6 +10,7 @@
 #import "SDWebImageCompat.h"
 #import "SDWebImageOperation.h"
 #import "SDWebImageDefine.h"
+#import "SDImageCoder.h"
 
 /// Image Cache Type
 typedef NS_ENUM(NSInteger, SDImageCacheType) {
@@ -54,6 +55,18 @@ typedef void(^SDImageCacheContainsCompletionBlock)(SDImageCacheType containsCach
  */
 FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context);
 
+/// Get the decode options from the loading context options and cache key. This is the built-in translate between the web loading part to the decoding part (which does not depends on).
+/// @param context The context arg from the input
+/// @param options The options arg from the input
+/// @param cacheKey The image cache key from the input. Should not be nil
+FOUNDATION_EXPORT SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey);
+
+/// Set the decode options to the loading context options. This is the built-in translate between the web loading part from the decoding part (which does not depends on).
+/// @param mutableContext The context arg to override
+/// @param mutableOptions The options arg to override
+/// @param decodeOptions The image decoding options
+FOUNDATION_EXPORT void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableContext, SDWebImageOptions * _Nonnull mutableOptions, SDImageCoderOptions * _Nonnull decodeOptions);
+
 /**
  This is the image cache protocol to provide custom image cache for `SDWebImageManager`.
  Though the best practice to custom image cache, is to write your own class which conform `SDMemoryCache` or `SDDiskCache` protocol for `SDImageCache` class (See more on `SDImageCacheConfig.memoryCacheClass & SDImageCacheConfig.diskCacheClass`).
@@ -68,22 +81,23 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
 
  @param key The image cache key
  @param options A mask to specify options to use for this query
- @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
+ @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. Pass `.callbackQueue` to control callback queue
  @param completionBlock The completion block. Will not get called if the operation is cancelled
  @return The operation for this query
  */
 - (nullable id<SDWebImageOperation>)queryImageForKey:(nullable NSString *)key
                                              options:(SDWebImageOptions)options
                                              context:(nullable SDWebImageContext *)context
-                                          completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;
+                                          completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock API_DEPRECATED_WITH_REPLACEMENT("queryImageForKey:options:context:cacheType:completion:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
+@optional
 /**
  Query the cached image from image cache for given key. The operation can be used to cancel the query.
  If image is cached in memory, completion is called synchronously, else asynchronously and depends on the options arg (See `SDWebImageQueryDiskSync`)
 
  @param key The image cache key
  @param options A mask to specify options to use for this query
- @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
+ @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. Pass `.callbackQueue` to control callback queue
  @param cacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediately.
  @param completionBlock The completion block. Will not get called if the operation is cancelled
  @return The operation for this query
@@ -94,21 +108,43 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
                                            cacheType:(SDImageCacheType)cacheType
                                           completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;
 
+@required
+/**
+ Store the image into image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously.
+
+ @param image The image to store
+ @param imageData The image data to be used for disk storage
+ @param key The image cache key
+ @param cacheType The image store op cache type
+ @param completionBlock A block executed after the operation is finished
+ */
+- (void)storeImage:(nullable UIImage *)image
+         imageData:(nullable NSData *)imageData
+            forKey:(nullable NSString *)key
+         cacheType:(SDImageCacheType)cacheType
+        completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED_WITH_REPLACEMENT("storeImage:imageData:forKey:options:context:cacheType:completion:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
+
+@optional
 /**
  Store the image into image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously.
 
  @param image The image to store
  @param imageData The image data to be used for disk storage
  @param key The image cache key
+ @param options A mask to specify options to use for this store
+ @param context The context options to use. Pass `.callbackQueue` to control callback queue
  @param cacheType The image store op cache type
  @param completionBlock A block executed after the operation is finished
  */
 - (void)storeImage:(nullable UIImage *)image
          imageData:(nullable NSData *)imageData
             forKey:(nullable NSString *)key
+           options:(SDWebImageOptions)options
+           context:(nullable SDWebImageContext *)context
          cacheType:(SDImageCacheType)cacheType
         completion:(nullable SDWebImageNoParamsBlock)completionBlock;
 
+#pragma mark - Deprecated because SDWebImageManager does not use these APIs
 /**
  Remove the image from image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously.
 
@@ -118,7 +154,7 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
  */
 - (void)removeImageForKey:(nullable NSString *)key
                 cacheType:(SDImageCacheType)cacheType
-               completion:(nullable SDWebImageNoParamsBlock)completionBlock;
+               completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 /**
  Check if image cache contains the image for the given key (does not load the image). If image is cached in memory, completion is called synchronously, else asynchronously.
@@ -129,7 +165,7 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
  */
 - (void)containsImageForKey:(nullable NSString *)key
                   cacheType:(SDImageCacheType)cacheType
-                 completion:(nullable SDImageCacheContainsCompletionBlock)completionBlock;
+                 completion:(nullable SDImageCacheContainsCompletionBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 /**
  Clear all the cached images for image cache. If cache type is memory only, completion is called synchronously, else asynchronously.
@@ -138,6 +174,6 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonn
  @param completionBlock A block executed after the operation is finished
  */
 - (void)clearWithCacheType:(SDImageCacheType)cacheType
-                completion:(nullable SDWebImageNoParamsBlock)completionBlock;
+                completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 @end

+ 67 - 14
Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.m

@@ -13,8 +13,9 @@
 #import "UIImage+Metadata.h"
 #import "SDInternalMacros.h"
 
-UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
-    UIImage *image;
+#import <CoreServices/CoreServices.h>
+
+SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey) {
     BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
     NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
     CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
@@ -29,20 +30,72 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS
     if (context[SDWebImageContextImageThumbnailPixelSize]) {
         thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
     }
+    NSString *typeIdentifierHint = context[SDWebImageContextImageTypeIdentifierHint];
+    NSString *fileExtensionHint;
+    if (!typeIdentifierHint) {
+        // UTI has high priority
+        fileExtensionHint = cacheKey.pathExtension; // without dot
+        if (fileExtensionHint.length == 0) {
+            // Ignore file extension which is empty
+            fileExtensionHint = nil;
+        }
+    }
     
-    SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
+    // First check if user provided decode options
+    SDImageCoderMutableOptions *mutableCoderOptions;
+    if (context[SDWebImageContextImageDecodeOptions] != nil) {
+        mutableCoderOptions = [NSMutableDictionary dictionaryWithDictionary:context[SDWebImageContextImageDecodeOptions]];
+    } else {
+        mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:6];
+    }
+    
+    // Override individual options
     mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
     mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
     mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
     mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
-    mutableCoderOptions[SDImageCoderWebImageContext] = context;
-    SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
+    mutableCoderOptions[SDImageCoderDecodeTypeIdentifierHint] = typeIdentifierHint;
+    mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint;
     
-    // Grab the image coder
-    id<SDImageCoder> imageCoder;
-    if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) {
-        imageCoder = context[SDWebImageContextImageCoder];
+    return [mutableCoderOptions copy];
+}
+
+void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableContext, SDWebImageOptions * _Nonnull mutableOptions, SDImageCoderOptions * _Nonnull decodeOptions) {
+    if ([decodeOptions[SDImageCoderDecodeFirstFrameOnly] boolValue]) {
+        *mutableOptions |= SDWebImageDecodeFirstFrameOnly;
     } else {
+        *mutableOptions &= ~SDWebImageDecodeFirstFrameOnly;
+    }
+    
+    mutableContext[SDWebImageContextImageScaleFactor] = decodeOptions[SDImageCoderDecodeScaleFactor];
+    mutableContext[SDWebImageContextImagePreserveAspectRatio] = decodeOptions[SDImageCoderDecodePreserveAspectRatio];
+    mutableContext[SDWebImageContextImageThumbnailPixelSize] = decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
+    
+    NSString *typeIdentifierHint = decodeOptions[SDImageCoderDecodeTypeIdentifierHint];
+    if (!typeIdentifierHint) {
+        NSString *fileExtensionHint = decodeOptions[SDImageCoderDecodeFileExtensionHint];
+        if (fileExtensionHint) {
+            typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage);
+            // Ignore dynamic UTI
+            if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) {
+                typeIdentifierHint = nil;
+            }
+        }
+    }
+    mutableContext[SDWebImageContextImageTypeIdentifierHint] = typeIdentifierHint;
+}
+
+UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
+    NSCParameterAssert(imageData);
+    NSCParameterAssert(cacheKey);
+    UIImage *image;
+    SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey);
+    BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
+    CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue];
+    
+    // Grab the image coder
+    id<SDImageCoder> imageCoder = context[SDWebImageContextImageCoder];
+    if (!imageCoder) {
         imageCoder = [SDImageCodersManager sharedManager];
     }
     
@@ -69,16 +122,16 @@ UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSS
     }
     if (image) {
         BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
-        if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
-            // `SDAnimatedImage` do not decode
-            shouldDecode = NO;
-        } else if (image.sd_isAnimated) {
-            // animated image do not decode
+        BOOL lazyDecode = [coderOptions[SDImageCoderDecodeUseLazyDecoding] boolValue];
+        if (lazyDecode) {
+            // lazyDecode = NO means we should not forceDecode, highest priority
             shouldDecode = NO;
         }
         if (shouldDecode) {
             image = [SDImageCoderHelper decodedImageWithImage:image];
         }
+        // assign the decode options, to let manager check whether to re-decode if needed
+        image.sd_decodeOptions = coderOptions;
     }
     
     return image;

+ 26 - 23
Pods/SDWebImage/SDWebImage/Core/SDImageCachesManager.m

@@ -13,13 +13,12 @@
 
 @interface SDImageCachesManager ()
 
-@property (nonatomic, strong, nonnull) dispatch_semaphore_t cachesLock;
+@property (nonatomic, strong, nonnull) NSMutableArray<id<SDImageCache>> *imageCaches;
 
 @end
 
-@implementation SDImageCachesManager
-{
-    NSMutableArray<id<SDImageCache>> *_imageCaches;
+@implementation SDImageCachesManager {
+    SD_LOCK_DECLARE(_cachesLock);
 }
 
 + (SDImageCachesManager *)sharedManager {
@@ -41,25 +40,25 @@
         self.clearOperationPolicy = SDImageCachesManagerOperationPolicyConcurrent;
         // initialize with default image caches
         _imageCaches = [NSMutableArray arrayWithObject:[SDImageCache sharedImageCache]];
-        _cachesLock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_cachesLock);
     }
     return self;
 }
 
 - (NSArray<id<SDImageCache>> *)caches {
-    SD_LOCK(self.cachesLock);
+    SD_LOCK(_cachesLock);
     NSArray<id<SDImageCache>> *caches = [_imageCaches copy];
-    SD_UNLOCK(self.cachesLock);
+    SD_UNLOCK(_cachesLock);
     return caches;
 }
 
 - (void)setCaches:(NSArray<id<SDImageCache>> *)caches {
-    SD_LOCK(self.cachesLock);
+    SD_LOCK(_cachesLock);
     [_imageCaches removeAllObjects];
     if (caches.count) {
         [_imageCaches addObjectsFromArray:caches];
     }
-    SD_UNLOCK(self.cachesLock);
+    SD_UNLOCK(_cachesLock);
 }
 
 #pragma mark - Cache IO operations
@@ -68,18 +67,18 @@
     if (![cache conformsToProtocol:@protocol(SDImageCache)]) {
         return;
     }
-    SD_LOCK(self.cachesLock);
+    SD_LOCK(_cachesLock);
     [_imageCaches addObject:cache];
-    SD_UNLOCK(self.cachesLock);
+    SD_UNLOCK(_cachesLock);
 }
 
 - (void)removeCache:(id<SDImageCache>)cache {
     if (![cache conformsToProtocol:@protocol(SDImageCache)]) {
         return;
     }
-    SD_LOCK(self.cachesLock);
+    SD_LOCK(_cachesLock);
     [_imageCaches removeObject:cache];
-    SD_UNLOCK(self.cachesLock);
+    SD_UNLOCK(_cachesLock);
 }
 
 #pragma mark - SDImageCache
@@ -131,6 +130,10 @@
 }
 
 - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock {
+    [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:cacheType completion:completionBlock];
+}
+
+- (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock {
     if (!key) {
         return;
     }
@@ -139,28 +142,28 @@
     if (count == 0) {
         return;
     } else if (count == 1) {
-        [caches.firstObject storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
+        [caches.firstObject storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock];
         return;
     }
     switch (self.storeOperationPolicy) {
         case SDImageCachesManagerOperationPolicyHighestOnly: {
             id<SDImageCache> cache = caches.lastObject;
-            [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
+            [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock];
         }
             break;
         case SDImageCachesManagerOperationPolicyLowestOnly: {
             id<SDImageCache> cache = caches.firstObject;
-            [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock];
+            [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock];
         }
             break;
         case SDImageCachesManagerOperationPolicyConcurrent: {
             SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new];
             [operation beginWithTotalCount:caches.count];
-            [self concurrentStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
+            [self concurrentStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation];
         }
             break;
         case SDImageCachesManagerOperationPolicySerial: {
-            [self serialStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator];
+            [self serialStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator];
         }
             break;
         default:
@@ -316,11 +319,11 @@
     }
 }
 
-- (void)concurrentStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<SDImageCache>> *)enumerator operation:(SDImageCachesManagerOperation *)operation {
+- (void)concurrentStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<SDImageCache>> *)enumerator operation:(SDImageCachesManagerOperation *)operation {
     NSParameterAssert(enumerator);
     NSParameterAssert(operation);
     for (id<SDImageCache> cache in enumerator) {
-        [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:^{
+        [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:^{
             if (operation.isCancelled) {
                 // Cancelled
                 return;
@@ -463,7 +466,7 @@
     }];
 }
 
-- (void)serialStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<SDImageCache>> *)enumerator {
+- (void)serialStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator<id<SDImageCache>> *)enumerator {
     NSParameterAssert(enumerator);
     id<SDImageCache> cache = enumerator.nextObject;
     if (!cache) {
@@ -474,10 +477,10 @@
         return;
     }
     @weakify(self);
-    [cache storeImage:image imageData:imageData forKey:key cacheType:cacheType completion:^{
+    [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:^{
         @strongify(self);
         // Next
-        [self serialStoreImage:image imageData:imageData forKey:key cacheType:cacheType completion:completionBlock enumerator:enumerator];
+        [self serialStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:enumerator];
     }];
 }
 

+ 50 - 2
Pods/SDWebImage/SDWebImage/Core/SDImageCoder.h

@@ -9,6 +9,7 @@
 #import <Foundation/Foundation.h>
 #import "SDWebImageCompat.h"
 #import "NSData+ImageContentType.h"
+#import "SDImageFrame.h"
 
 typedef NSString * SDImageCoderOption NS_STRING_ENUM;
 typedef NSDictionary<SDImageCoderOption, id> SDImageCoderOptions;
@@ -44,6 +45,35 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAs
  */
 FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeThumbnailPixelSize;
 
+/**
+ A NSString value indicating the source image's file extension. Example: "jpg", "nef", "tif", don't prefix the dot
+ Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF
+ Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser
+ @note However, different UTType may share the same file extension, like `public.jpeg` and `public.jpeg-2000` both use `.jpg`. If you want detail control, use `TypeIdentifierHint` below
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFileExtensionHint;
+
+/**
+ A NSString value (UTI) indicating the source image's file extension. Example: "public.jpeg-2000", "com.nikon.raw-image", "public.tiff"
+ Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF
+ Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser
+ @note If you provide `TypeIdentifierHint`, the `FileExtensionHint` option above will be ignored (because UTType has high priority)
+ @note If you really don't want any hint which effect the image result, pass `NSNull.null` instead
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeTypeIdentifierHint;
+
+/**
+ A BOOL value indicating whether to use lazy-decoding. Defaults to NO on animated image coder, but defaults to YES on static image coder.
+ CGImageRef, this image object typically support lazy-decoding, via the `CGDataProviderCreateDirectAccess` or `CGDataProviderCreateSequential`
+ Which allows you to provide a lazy-called callback to access bitmap buffer, so that you can achieve lazy-decoding when consumer actually need bitmap buffer
+ UIKit on iOS use heavy on this and ImageIO codec prefers to lazy-decoding for common Hardware-Accelerate format like JPEG/PNG/HEIC
+ But however, the consumer may access bitmap buffer when running on main queue, like CoreAnimation layer render image. So this is a trade-off
+ You can force us to disable the lazy-decoding and always allocate bitmap buffer on RAM, but this may have higher ratio of OOM (out of memory)
+ @note The default value is NO for animated image coder (means `animatedImageFrameAtIndex:`)
+ @note The default value is YES for static image coder (means `decodedImageWithData:`)
+ @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDecoding;
 
 // These options are for image encoding
 /**
@@ -91,9 +121,11 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeEmbedThumb
  A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext)
  This option is ignored for all built-in coders and take no effect.
  But this may be useful for some custom coders, because some business logic may dependent on things other than image or image data information only.
+ Only the unknown context from top-level API (See SDWebImageDefine.h) may be passed in during image loading.
  See `SDWebImageContext` for more detailed information.
+ @warning Deprecated. This does nothing from 5.14.0. Use `SDWebImageContextImageDecodeOptions` to pass additional information in top-level API, and use `SDImageCoderOptions` to retrieve options from coder.
  */
-FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext API_DEPRECATED("The coder component will be seperated from Core subspec in the future. Update your code to not rely on this context option.", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));;
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext API_DEPRECATED("No longer supported. Use SDWebImageContextDecodeOptions in loader API to provide options. Use SDImageCoderOptions in coder API to retrieve options.", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0));
 
 #pragma mark - Coder
 /**
@@ -140,7 +172,8 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext
 
 /**
  Encode the image to image data.
- @note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames.
+ @note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames. But this consume time is not always reversible. In 5.15.0, we introduce `encodedDataWithFrames` API for better animated image encoding. Use that instead.
+ @note Which means, this just forward to `encodedDataWithFrames([SDImageFrame(image: image, duration: 0], image.sd_imageLoopCount))`
 
  @param image The image to be encoded
  @param format The image format to encode, you should note `SDImageFormatUndefined` format is also  possible
@@ -151,6 +184,21 @@ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext
                                    format:(SDImageFormat)format
                                   options:(nullable SDImageCoderOptions *)options;
 
+#pragma mark - Animated Encoding
+@optional
+/**
+ Encode the animated image frames to image data.
+
+ @param frames The animated image frames to be encoded, should be at least 1 element, or it will fallback to static image encode.
+ @param loopCount The final animated image loop count. 0 means infinity loop. This config ignore each frame's `sd_imageLoopCount`
+ @param format The image format to encode, you should note `SDImageFormatUndefined` format is also  possible
+ @param options A dictionary containing any encoding options. Pass @{SDImageCoderEncodeCompressionQuality: @(1)} to specify compression quality.
+ @return The encoded image data
+ */
+- (nullable NSData *)encodedDataWithFrames:(nonnull NSArray<SDImageFrame *>*)frames
+                                 loopCount:(NSUInteger)loopCount
+                                    format:(SDImageFormat)format
+                                   options:(nullable SDImageCoderOptions *)options;
 @end
 
 #pragma mark - Progressive Coder

+ 3 - 0
Pods/SDWebImage/SDWebImage/Core/SDImageCoder.m

@@ -12,6 +12,9 @@ SDImageCoderOption const SDImageCoderDecodeFirstFrameOnly = @"decodeFirstFrameOn
 SDImageCoderOption const SDImageCoderDecodeScaleFactor = @"decodeScaleFactor";
 SDImageCoderOption const SDImageCoderDecodePreserveAspectRatio = @"decodePreserveAspectRatio";
 SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnailPixelSize";
+SDImageCoderOption const SDImageCoderDecodeFileExtensionHint = @"decodeFileExtensionHint";
+SDImageCoderOption const SDImageCoderDecodeTypeIdentifierHint = @"decodeTypeIdentifierHint";
+SDImageCoderOption const SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding";
 
 SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly";
 SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality";

+ 27 - 1
Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.h

@@ -10,6 +10,15 @@
 #import "SDWebImageCompat.h"
 #import "SDImageFrame.h"
 
+typedef NS_ENUM(NSUInteger, SDImageCoderDecodeSolution) {
+    /// automatically choose the solution based on image format, hardware, OS version. This keep balance for compatibility and performance. Default after SDWebImage 5.13.0
+    SDImageCoderDecodeSolutionAutomatic,
+    /// always use CoreGraphics to draw on bitmap context and trigger decode. Best compatibility. Default before SDWebImage 5.13.0
+    SDImageCoderDecodeSolutionCoreGraphics,
+    /// available on iOS/tvOS 15+, use UIKit's new CGImageDecompressor/CMPhoto to decode. Best performance. If failed, will fallback to CoreGraphics as well
+    SDImageCoderDecodeSolutionUIKit
+};
+
 /**
  Provide some common helper methods for building the image decoder/encoder.
  */
@@ -76,6 +85,7 @@
 /**
  Create a scaled CGImage by the provided CGImage and size. This follows The Create Rule and you are response to call release after usage.
  It will detect whether the image size matching the scale size, if not, stretch the image to the target size.
+ @note If you need to keep aspect ratio, you can calculate the scale size by using `scaledSizeWithImageSize` first.
  
  @param cgImage The CGImage
  @param size The scale size in pixel.
@@ -83,8 +93,18 @@
  */
 + (CGImageRef _Nullable)CGImageCreateScaled:(_Nonnull CGImageRef)cgImage size:(CGSize)size CF_RETURNS_RETAINED;
 
+/** Scale the image size based on provided scale size, whether or not to preserve aspect ratio, whether or not to scale up.
+ @note For example, if you implements thumnail decoding, pass `shouldScaleUp` to NO to avoid the calculated size larger than image size.
+ 
+ @param imageSize The image size (in pixel or point defined by caller)
+ @param scaleSize The scale size (in pixel or point defined by caller)
+ @param preserveAspectRatio Whether or not to preserve aspect ratio
+ @param shouldScaleUp Whether or not to scale up (or scale down only)
+ */
++ (CGSize)scaledSizeWithImageSize:(CGSize)imageSize scaleSize:(CGSize)scaleSize preserveAspectRatio:(BOOL)preserveAspectRatio shouldScaleUp:(BOOL)shouldScaleUp;
+
 /**
- Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image
+ Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image. On iOS 15+, this may use `UIImage.preparingForDisplay()` to use CMPhoto for better performance than the old solution.
  @param image The image to be decoded
  @return The decoded image
  */
@@ -100,6 +120,12 @@
  */
 + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes;
 
+/**
+ Control the default force decode solution. Available solutions  in `SDImageCoderDecodeSolution`.
+ @note Defaults to `SDImageCoderDecodeSolutionAutomatic`, which prefers to use UIKit for JPEG/HEIF, and fallback on CoreGraphics. If you want control on your hand, set the other solution.
+ */
+@property (class, readwrite) SDImageCoderDecodeSolution defaultDecodeSolution;
+
 /**
  Control the default limit bytes to scale down largest images.
  This value must be larger than 4 Bytes (at least 1x1 pixel). Defaults to 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS.

+ 282 - 114
Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.m

@@ -15,12 +15,65 @@
 #import "SDAssociatedObject.h"
 #import "UIImage+Metadata.h"
 #import "SDInternalMacros.h"
+#import "SDGraphicsImageRenderer.h"
+#import "SDInternalMacros.h"
 #import <Accelerate/Accelerate.h>
 
 static inline size_t SDByteAlign(size_t size, size_t alignment) {
     return ((size + (alignment - 1)) / alignment) * alignment;
 }
 
+#if SD_UIKIT
+static inline UIImage *SDImageDecodeUIKit(UIImage *image) {
+    // See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay
+    // Need CGImage-based
+    if (@available(iOS 15, tvOS 15, *)) {
+        UIImage *decodedImage = [image imageByPreparingForDisplay];
+        if (decodedImage) {
+            SDImageCopyAssociatedObject(image, decodedImage);
+            decodedImage.sd_isDecoded = YES;
+            return decodedImage;
+        }
+    }
+    return nil;
+}
+
+static inline UIImage *SDImageDecodeAndScaleDownUIKit(UIImage *image, CGSize destResolution) {
+    // See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize
+    // Need CGImage-based
+    if (@available(iOS 15, tvOS 15, *)) {
+        // Calculate thumbnail point size
+        CGFloat scale = image.scale ?: 1;
+        CGSize thumbnailSize = CGSizeMake(destResolution.width / scale, destResolution.height / scale);
+        UIImage *decodedImage = [image imageByPreparingThumbnailOfSize:thumbnailSize];
+        if (decodedImage) {
+            SDImageCopyAssociatedObject(image, decodedImage);
+            decodedImage.sd_isDecoded = YES;
+            return decodedImage;
+        }
+    }
+    return nil;
+}
+
+static inline BOOL SDImageSupportsHardwareHEVCDecoder(void) {
+    static dispatch_once_t onceToken;
+    static BOOL supportsHardware = NO;
+    dispatch_once(&onceToken, ^{
+        SEL DeviceInfoSelector = SD_SEL_SPI(deviceInfoForKey:);
+        NSString *HEVCDecoder8bitSupported = @"N8lZxRgC7lfdRS3dRLn+Ag";
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+        if ([UIDevice.currentDevice respondsToSelector:DeviceInfoSelector] && [UIDevice.currentDevice performSelector:DeviceInfoSelector withObject:HEVCDecoder8bitSupported]) {
+            supportsHardware = YES;
+        }
+#pragma clang diagnostic pop
+    });
+    return supportsHardware;
+}
+#endif
+
+static SDImageCoderDecodeSolution kDefaultDecodeSolution = SDImageCoderDecodeSolutionAutomatic;
+
 static const size_t kBytesPerPixel = 4;
 static const size_t kBitsPerComponent = 8;
 
@@ -41,6 +94,13 @@ static CGFloat kDestImageLimitBytes = 30.f * kBytesPerMB;
 
 static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to overlap the seems where tiles meet.
 
+#if SD_MAC
+@interface SDAnimatedImageRep (Private)
+/// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`).
+@property (nonatomic, readwrite, weak) NSArray<SDImageFrame *> *frames;
+@end
+#endif
+
 @implementation SDImageCoderHelper
 
 + (UIImage *)animatedImageWithFrames:(NSArray<SDImageFrame *> *)frames {
@@ -57,12 +117,12 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         durations[i] = frames[i].duration * 1000;
     }
     NSUInteger const gcd = gcdArray(frameCount, durations);
-    __block NSUInteger totalDuration = 0;
+    __block NSTimeInterval totalDuration = 0;
     NSMutableArray<UIImage *> *animatedImages = [NSMutableArray arrayWithCapacity:frameCount];
     [frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) {
         UIImage *image = frame.image;
         NSUInteger duration = frame.duration * 1000;
-        totalDuration += duration;
+        totalDuration += frame.duration;
         NSUInteger repeatCount;
         if (gcd) {
             repeatCount = duration / gcd;
@@ -74,7 +134,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         }
     }];
     
-    animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration / 1000.f];
+    animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration];
     
 #else
     
@@ -88,13 +148,11 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     }
     
     for (size_t i = 0; i < frameCount; i++) {
-        @autoreleasepool {
-            SDImageFrame *frame = frames[i];
-            NSTimeInterval frameDuration = frame.duration;
-            CGImageRef frameImageRef = frame.image.CGImage;
-            NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
-            CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
-        }
+        SDImageFrame *frame = frames[i];
+        NSTimeInterval frameDuration = frame.duration;
+        CGImageRef frameImageRef = frame.image.CGImage;
+        NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}};
+        CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties);
     }
     // Finalize the destination.
     if (CGImageDestinationFinalize(imageDestination) == NO) {
@@ -108,6 +166,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData];
     NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
     imageRep.size = size;
+    imageRep.frames = frames; // Weak assign to avoid effect lazy semantic of NSBitmapImageRep
     animatedImage = [[NSImage alloc] initWithSize:size];
     [animatedImage addRepresentation:imageRep];
 #endif
@@ -120,7 +179,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         return nil;
     }
     
-    NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
+    NSMutableArray<SDImageFrame *> *frames;
     NSUInteger frameCount = 0;
     
 #if SD_UIKIT || SD_WATCH
@@ -129,13 +188,13 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     if (frameCount == 0) {
         return nil;
     }
+    frames = [NSMutableArray arrayWithCapacity:frameCount];
     
     NSTimeInterval avgDuration = animatedImage.duration / frameCount;
     if (avgDuration == 0) {
         avgDuration = 0.1; // if it's a animated image but no duration, set it to default 100ms (this do not have that 10ms limit like GIF or WebP to allow custom coder provide the limit)
     }
     
-    __block NSUInteger index = 0;
     __block NSUInteger repeatCount = 1;
     __block UIImage *previousImage = animatedImages.firstObject;
     [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
@@ -149,20 +208,25 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
             SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
             [frames addObject:frame];
             repeatCount = 1;
-            index++;
         }
         previousImage = image;
-        // last one
-        if (idx == frameCount - 1) {
-            SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
-            [frames addObject:frame];
-        }
     }];
+    // last one
+    SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount];
+    [frames addObject:frame];
     
 #else
     
     NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height);
     NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil];
+    // Check weak assigned frames firstly
+    if ([imageRep isKindOfClass:[SDAnimatedImageRep class]]) {
+        SDAnimatedImageRep *animatedImageRep = (SDAnimatedImageRep *)imageRep;
+        if (animatedImageRep.frames) {
+            return animatedImageRep.frames;
+        }
+    }
+    
     NSBitmapImageRep *bitmapImageRep;
     if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
         bitmapImageRep = (NSBitmapImageRep *)imageRep;
@@ -174,42 +238,27 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     if (frameCount == 0) {
         return nil;
     }
+    frames = [NSMutableArray arrayWithCapacity:frameCount];
     CGFloat scale = animatedImage.scale;
     
     for (size_t i = 0; i < frameCount; i++) {
-        @autoreleasepool {
-            // NSBitmapImageRep need to manually change frame. "Good taste" API
-            [bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)];
-            NSTimeInterval frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] doubleValue];
-            NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
-            SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
-            [frames addObject:frame];
-        }
+        // NSBitmapImageRep need to manually change frame. "Good taste" API
+        [bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)];
+        NSTimeInterval frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] doubleValue];
+        NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp];
+        SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration];
+        [frames addObject:frame];
     }
 #endif
     
-    return frames;
+    return [frames copy];
 }
 
 + (CGColorSpaceRef)colorSpaceGetDeviceRGB {
-#if SD_MAC
-    CGColorSpaceRef screenColorSpace = NSScreen.mainScreen.colorSpace.CGColorSpace;
-    if (screenColorSpace) {
-        return screenColorSpace;
-    }
-#endif
     static CGColorSpaceRef colorSpace;
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
-#if SD_UIKIT
-        if (@available(iOS 9.0, tvOS 9.0, *)) {
-            colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
-        } else {
-            colorSpace = CGColorSpaceCreateDeviceRGB();
-        }
-#else
-        colorSpace = CGColorSpaceCreateDeviceRGB();
-#endif
+        colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
     });
     return colorSpace;
 }
@@ -256,11 +305,18 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     }
     
     BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
-    // iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
-    // Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage`
-    // But since our build-in coders use this bitmapInfo, this can have a little performance benefit
-    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
-    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
+    // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
+    // Check #3330 for more detail about why this bitmap is choosen.
+    CGBitmapInfo bitmapInfo;
+    if (hasAlpha) {
+        // iPhone GPU prefer to use BGRA8888, see: https://forums.raywenderlich.com/t/why-mtlpixelformat-bgra8unorm/53489
+        // BGRA8888
+        bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
+    } else {
+        // BGR888 previously works on iOS 8~iOS 14, however, iOS 15+ will result a black image. FB9958017
+        // RGB888
+        bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
+    }
     CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
     if (!context) {
         return NULL;
@@ -293,9 +349,18 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         if (output_buffer.data) free(output_buffer.data);
     };
     BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
-    // iOS display alpha info (BGRA8888/BGRX8888)
-    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
-    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
+    // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
+    // Check #3330 for more detail about why this bitmap is choosen.
+    CGBitmapInfo bitmapInfo;
+    if (hasAlpha) {
+        // iPhone GPU prefer to use BGRA8888, see: https://forums.raywenderlich.com/t/why-mtlpixelformat-bgra8unorm/53489
+        // BGRA8888
+        bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
+    } else {
+        // BGR888 previously works on iOS 8~iOS 14, however, iOS 15+ will result a black image. FB9958017
+        // RGB888
+        bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
+    }
     vImage_CGImageFormat format = (vImage_CGImageFormat) {
         .bitsPerComponent = 8,
         .bitsPerPixel = 32,
@@ -303,7 +368,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         .bitmapInfo = bitmapInfo,
         .version = 0,
         .decode = NULL,
-        .renderingIntent = kCGRenderingIntentDefault,
+        .renderingIntent = CGImageGetRenderingIntent(cgImage)
     };
     
     vImage_Error a_ret = vImageBuffer_InitWithCGImage(&input_buffer, &format, NULL, cgImage, kvImageNoFlags);
@@ -326,21 +391,95 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     return outputImage;
 }
 
++ (CGSize)scaledSizeWithImageSize:(CGSize)imageSize scaleSize:(CGSize)scaleSize preserveAspectRatio:(BOOL)preserveAspectRatio shouldScaleUp:(BOOL)shouldScaleUp {
+    CGFloat width = imageSize.width;
+    CGFloat height = imageSize.height;
+    CGFloat resultWidth;
+    CGFloat resultHeight;
+    
+    if (width <= 0 || height <= 0 || scaleSize.width <= 0 || scaleSize.height <= 0) {
+        // Protect
+        resultWidth = width;
+        resultHeight = height;
+    } else {
+        // Scale to fit
+        if (preserveAspectRatio) {
+            CGFloat pixelRatio = width / height;
+            CGFloat scaleRatio = scaleSize.width / scaleSize.height;
+            if (pixelRatio > scaleRatio) {
+                resultWidth = scaleSize.width;
+                resultHeight = ceil(scaleSize.width / pixelRatio);
+            } else {
+                resultHeight = scaleSize.height;
+                resultWidth = ceil(scaleSize.height * pixelRatio);
+            }
+        } else {
+            // Stretch
+            resultWidth = scaleSize.width;
+            resultHeight = scaleSize.height;
+        }
+        if (!shouldScaleUp) {
+            // Scale down only
+            resultWidth = MIN(width, resultWidth);
+            resultHeight = MIN(height, resultHeight);
+        }
+    }
+    
+    return CGSizeMake(resultWidth, resultHeight);
+}
+
 + (UIImage *)decodedImageWithImage:(UIImage *)image {
     if (![self shouldDecodeImage:image]) {
         return image;
     }
     
-    CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
+    UIImage *decodedImage;
+    SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution;
+#if SD_UIKIT
+    if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) {
+        // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :(
+        SDImageFormat format = image.sd_imageFormat;
+        if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) {
+            decodedImage = SDImageDecodeUIKit(image);
+        } else if (format == SDImageFormatJPEG) {
+            decodedImage = SDImageDecodeUIKit(image);
+        }
+    } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) {
+        // Arbitrarily call CMPhoto
+        decodedImage = SDImageDecodeUIKit(image);
+    }
+    if (decodedImage) {
+        return decodedImage;
+    }
+#endif
+    
+    CGImageRef imageRef = image.CGImage;
     if (!imageRef) {
+        // Only decode for CGImage-based
         return image;
     }
+    
+    if (decodeSolution == SDImageCoderDecodeSolutionCoreGraphics) {
+        CGImageRef decodedImageRef = [self CGImageCreateDecoded:imageRef];
 #if SD_MAC
-    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
+        decodedImage = [[UIImage alloc] initWithCGImage:decodedImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
 #else
-    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
+        decodedImage = [[UIImage alloc] initWithCGImage:decodedImageRef scale:image.scale orientation:image.imageOrientation];
 #endif
-    CGImageRelease(imageRef);
+        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;
+        CGSize imageSize = image.size;
+        SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format];
+        decodedImage = [renderer imageWithActions:^(CGContextRef  _Nonnull context) {
+                [image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
+        }];
+    }
     SDImageCopyAssociatedObject(image, decodedImage);
     decodedImage.sd_isDecoded = YES;
     return decodedImage;
@@ -351,53 +490,83 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         return image;
     }
     
-    if (![self shouldScaleDownImage:image limitBytes:bytes]) {
-        return [self decodedImageWithImage:image];
-    }
-    
     CGFloat destTotalPixels;
     CGFloat tileTotalPixels;
     if (bytes == 0) {
-        bytes = kDestImageLimitBytes;
+        bytes = [self defaultScaleDownLimitBytes];
     }
+    bytes = MAX(bytes, kBytesPerPixel);
     destTotalPixels = bytes / kBytesPerPixel;
     tileTotalPixels = destTotalPixels / 3;
-    CGContextRef destContext;
+    
+    CGImageRef sourceImageRef = image.CGImage;
+    if (!sourceImageRef) {
+        // Only decode for CGImage-based
+        return image;
+    }
+    CGSize sourceResolution = CGSizeZero;
+    sourceResolution.width = CGImageGetWidth(sourceImageRef);
+    sourceResolution.height = CGImageGetHeight(sourceImageRef);
+    
+    if (![self shouldScaleDownImagePixelSize:sourceResolution limitBytes:bytes]) {
+        return [self decodedImageWithImage:image];
+    }
+    
+    CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
+    // Determine the scale ratio to apply to the input image
+    // that results in an output image of the defined size.
+    // see kDestImageSizeMB, and how it relates to destTotalPixels.
+    CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
+    CGSize destResolution = CGSizeZero;
+    destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale));
+    destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale));
+    
+    UIImage *decodedImage;
+#if SD_UIKIT
+    SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution;
+    if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) {
+        // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :(
+        SDImageFormat format = image.sd_imageFormat;
+        if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) {
+            decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
+        } else if (format == SDImageFormatJPEG) {
+            decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
+        }
+    } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) {
+        // Arbitrarily call CMPhoto
+        decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution);
+    }
+    if (decodedImage) {
+        return decodedImage;
+    }
+#endif
     
     // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
     // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
     @autoreleasepool {
-        CGImageRef sourceImageRef = image.CGImage;
-        
-        CGSize sourceResolution = CGSizeZero;
-        sourceResolution.width = CGImageGetWidth(sourceImageRef);
-        sourceResolution.height = CGImageGetHeight(sourceImageRef);
-        CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
-        // Determine the scale ratio to apply to the input image
-        // that results in an output image of the defined size.
-        // see kDestImageSizeMB, and how it relates to destTotalPixels.
-        CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels);
-        CGSize destResolution = CGSizeZero;
-        destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale));
-        destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale));
-        
         // device color space
         CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB];
         BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef];
-        // iOS display alpha info (BGRA8888/BGRX8888)
-        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
-        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
         
         // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
-        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipFirst
-        // to create bitmap graphics contexts without alpha info.
-        destContext = CGBitmapContextCreate(NULL,
-                                            destResolution.width,
-                                            destResolution.height,
-                                            kBitsPerComponent,
-                                            0,
-                                            colorspaceRef,
-                                            bitmapInfo);
+        // Check #3330 for more detail about why this bitmap is choosen.
+        CGBitmapInfo bitmapInfo;
+        if (hasAlpha) {
+            // iPhone GPU prefer to use BGRA8888, see: https://forums.raywenderlich.com/t/why-mtlpixelformat-bgra8unorm/53489
+            // BGRA8888
+            bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
+        } else {
+            // BGR888 previously works on iOS 8~iOS 14, however, iOS 15+ will result a black image. FB9958017
+            // RGB888
+            bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
+        }
+        CGContextRef destContext = CGBitmapContextCreate(NULL,
+                                                         destResolution.width,
+                                                         destResolution.height,
+                                                         kBitsPerComponent,
+                                                         0,
+                                                         colorspaceRef,
+                                                         bitmapInfo);
         
         if (destContext == NULL) {
             return image;
@@ -444,19 +613,17 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
         sourceTile.size.height += sourceSeemOverlap;
         destTile.size.height += kDestSeemOverlap;
         for( int y = 0; y < iterations; ++y ) {
-            @autoreleasepool {
-                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
-                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
-                sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
-                if( y == iterations - 1 && remainder ) {
-                    float dify = destTile.size.height;
-                    destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
-                    dify -= destTile.size.height;
-                    destTile.origin.y += dify;
-                }
-                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
-                CGImageRelease( sourceTileImageRef );
+            sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
+            destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
+            sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
+            if( y == iterations - 1 && remainder ) {
+                float dify = destTile.size.height;
+                destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
+                dify -= destTile.size.height;
+                destTile.origin.y = MIN(0, destTile.origin.y + dify);
             }
+            CGContextDrawImage( destContext, destTile, sourceTileImageRef );
+            CGImageRelease( sourceTileImageRef );
         }
         
         CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
@@ -465,20 +632,25 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
             return image;
         }
 #if SD_MAC
-        UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
+        decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp];
 #else
-        UIImage *destImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
+        decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
 #endif
         CGImageRelease(destImageRef);
-        if (destImage == nil) {
-            return image;
-        }
-        SDImageCopyAssociatedObject(image, destImage);
-        destImage.sd_isDecoded = YES;
-        return destImage;
+        SDImageCopyAssociatedObject(image, decodedImage);
+        decodedImage.sd_isDecoded = YES;
+        return decodedImage;
     }
 }
 
++ (SDImageCoderDecodeSolution)defaultDecodeSolution {
+    return kDefaultDecodeSolution;
+}
+
++ (void)setDefaultDecodeSolution:(SDImageCoderDecodeSolution)defaultDecodeSolution {
+    kDefaultDecodeSolution = defaultDecodeSolution;
+}
+
 + (NSUInteger)defaultScaleDownLimitBytes {
     return kDestImageLimitBytes;
 }
@@ -582,14 +754,10 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     return YES;
 }
 
-+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image limitBytes:(NSUInteger)bytes {
++ (BOOL)shouldScaleDownImagePixelSize:(CGSize)sourceResolution limitBytes:(NSUInteger)bytes {
     BOOL shouldScaleDown = YES;
     
-    CGImageRef sourceImageRef = image.CGImage;
-    CGSize sourceResolution = CGSizeZero;
-    sourceResolution.width = CGImageGetWidth(sourceImageRef);
-    sourceResolution.height = CGImageGetHeight(sourceImageRef);
-    float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
+    CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height;
     if (sourceTotalPixels <= 0) {
         return NO;
     }
@@ -599,7 +767,7 @@ static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to over
     }
     bytes = MAX(bytes, kBytesPerPixel);
     destTotalPixels = bytes / kBytesPerPixel;
-    float imageScale = destTotalPixels / sourceTotalPixels;
+    CGFloat imageScale = destTotalPixels / sourceTotalPixels;
     if (imageScale < 1) {
         shouldScaleDown = YES;
     } else {

+ 29 - 17
Pods/SDWebImage/SDWebImage/Core/SDImageCodersManager.m

@@ -15,13 +15,12 @@
 
 @interface SDImageCodersManager ()
 
-@property (nonatomic, strong, nonnull) dispatch_semaphore_t codersLock;
+@property (nonatomic, strong, nonnull) NSMutableArray<id<SDImageCoder>> *imageCoders;
 
 @end
 
-@implementation SDImageCodersManager
-{
-    NSMutableArray<id<SDImageCoder>> *_imageCoders;
+@implementation SDImageCodersManager {
+    SD_LOCK_DECLARE(_codersLock);
 }
 
 + (nonnull instancetype)sharedManager {
@@ -37,27 +36,25 @@
     if (self = [super init]) {
         // initialize with default coders
         _imageCoders = [NSMutableArray arrayWithArray:@[[SDImageIOCoder sharedCoder], [SDImageGIFCoder sharedCoder], [SDImageAPNGCoder sharedCoder]]];
-        _codersLock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_codersLock);
     }
     return self;
 }
 
-- (NSArray<id<SDImageCoder>> *)coders
-{
-    SD_LOCK(self.codersLock);
+- (NSArray<id<SDImageCoder>> *)coders {
+    SD_LOCK(_codersLock);
     NSArray<id<SDImageCoder>> *coders = [_imageCoders copy];
-    SD_UNLOCK(self.codersLock);
+    SD_UNLOCK(_codersLock);
     return coders;
 }
 
-- (void)setCoders:(NSArray<id<SDImageCoder>> *)coders
-{
-    SD_LOCK(self.codersLock);
+- (void)setCoders:(NSArray<id<SDImageCoder>> *)coders {
+    SD_LOCK(_codersLock);
     [_imageCoders removeAllObjects];
     if (coders.count) {
         [_imageCoders addObjectsFromArray:coders];
     }
-    SD_UNLOCK(self.codersLock);
+    SD_UNLOCK(_codersLock);
 }
 
 #pragma mark - Coder IO operations
@@ -66,18 +63,18 @@
     if (![coder conformsToProtocol:@protocol(SDImageCoder)]) {
         return;
     }
-    SD_LOCK(self.codersLock);
+    SD_LOCK(_codersLock);
     [_imageCoders addObject:coder];
-    SD_UNLOCK(self.codersLock);
+    SD_UNLOCK(_codersLock);
 }
 
 - (void)removeCoder:(nonnull id<SDImageCoder>)coder {
     if (![coder conformsToProtocol:@protocol(SDImageCoder)]) {
         return;
     }
-    SD_LOCK(self.codersLock);
+    SD_LOCK(_codersLock);
     [_imageCoders removeObject:coder];
-    SD_UNLOCK(self.codersLock);
+    SD_UNLOCK(_codersLock);
 }
 
 #pragma mark - SDImageCoder
@@ -130,4 +127,19 @@
     return nil;
 }
 
+- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
+    if (!frames || frames.count < 1) {
+        return nil;
+    }
+    NSArray<id<SDImageCoder>> *coders = self.coders;
+    for (id<SDImageCoder> coder in coders.reverseObjectEnumerator) {
+        if ([coder canEncodeToFormat:format]) {
+            if ([coder respondsToSelector:@selector(encodedDataWithFrames:loopCount:format:options:)]) {
+                return [coder encodedDataWithFrames:frames loopCount:loopCount format:format options:options];
+            }
+        }
+    }
+    return nil;
+}
+
 @end

+ 9 - 1
Pods/SDWebImage/SDWebImage/Core/SDImageFrame.h

@@ -24,6 +24,11 @@
  */
 @property (nonatomic, readonly, assign) NSTimeInterval duration;
 
+/// Create a frame instance with specify image and duration
+/// @param image current frame's image
+/// @param duration current frame's duration
+- (nonnull instancetype)initWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration;
+
 /**
  Create a frame instance with specify image and duration
 
@@ -31,6 +36,9 @@
  @param duration current frame's duration
  @return frame instance
  */
-+ (instancetype _Nonnull)frameWithImage:(UIImage * _Nonnull)image duration:(NSTimeInterval)duration;
++ (nonnull instancetype)frameWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration;
+
+- (nonnull instancetype)init NS_UNAVAILABLE;
++ (nonnull instancetype)new  NS_UNAVAILABLE;
 
 @end

+ 10 - 4
Pods/SDWebImage/SDWebImage/Core/SDImageFrame.m

@@ -17,11 +17,17 @@
 
 @implementation SDImageFrame
 
+- (instancetype)initWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
+    self = [super init];
+    if (self) {
+        _image = image;
+        _duration = duration;
+    }
+    return self;
+}
+
 + (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration {
-    SDImageFrame *frame = [[SDImageFrame alloc] init];
-    frame.image = image;
-    frame.duration = duration;
-    
+    SDImageFrame *frame = [[SDImageFrame alloc] initWithImage:image duration:duration];
     return frame;
 }
 

+ 2 - 1
Pods/SDWebImage/SDWebImage/Core/SDImageGIFCoder.m

@@ -7,6 +7,7 @@
  */
 
 #import "SDImageGIFCoder.h"
+#import "SDImageIOAnimatedCoderInternal.h"
 #if SD_MAC
 #import <CoreServices/CoreServices.h>
 #else
@@ -31,7 +32,7 @@
 }
 
 + (NSString *)imageUTType {
-    return (__bridge NSString *)kUTTypeGIF;
+    return (__bridge NSString *)kSDUTTypeGIF;
 }
 
 + (NSString *)dictionaryProperty {

+ 29 - 7
Pods/SDWebImage/SDWebImage/Core/SDImageGraphics.m

@@ -8,6 +8,7 @@
 
 #import "SDImageGraphics.h"
 #import "NSImage+Compatibility.h"
+#import "SDImageCoderHelper.h"
 #import "objc/runtime.h"
 
 #if SD_MAC
@@ -16,17 +17,32 @@ static void *kNSGraphicsContextScaleFactorKey;
 static CGContextRef SDCGContextCreateBitmapContext(CGSize size, BOOL opaque, CGFloat scale) {
     if (scale == 0) {
         // Match `UIGraphicsBeginImageContextWithOptions`, reset to the scale factor of the device’s main screen if scale is 0.
-        scale = [NSScreen mainScreen].backingScaleFactor;
+        NSScreen *mainScreen = nil;
+        if (@available(macOS 10.12, *)) {
+            mainScreen = [NSScreen mainScreen];
+        } else {
+            mainScreen = [NSScreen screens].firstObject;
+        }
+        scale = mainScreen.backingScaleFactor ?: 1.0f;
     }
     size_t width = ceil(size.width * scale);
     size_t height = ceil(size.height * scale);
     if (width < 1 || height < 1) return NULL;
     
-    //pre-multiplied BGRA for non-opaque, BGRX for opaque, 8-bits per component, as Apple's doc
-    CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
-    CGImageAlphaInfo alphaInfo = kCGBitmapByteOrder32Host | (opaque ? kCGImageAlphaNoneSkipFirst : kCGImageAlphaPremultipliedFirst);
-    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, space, kCGBitmapByteOrderDefault | alphaInfo);
-    CGColorSpaceRelease(space);
+    CGColorSpaceRef space = [SDImageCoderHelper colorSpaceGetDeviceRGB];
+    // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
+    // Check #3330 for more detail about why this bitmap is choosen.
+    CGBitmapInfo bitmapInfo;
+    if (!opaque) {
+        // iPhone GPU prefer to use BGRA8888, see: https://forums.raywenderlich.com/t/why-mtlpixelformat-bgra8unorm/53489
+        // BGRA8888
+        bitmapInfo = kCGBitmapByteOrder32Host | kCGImageAlphaPremultipliedFirst;
+    } else {
+        // BGR888 previously works on iOS 8~iOS 14, however, iOS 15+ will result a black image. FB9958017
+        // RGB888
+        bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast;
+    }
+    CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, space, bitmapInfo);
     if (!context) {
         return NULL;
     }
@@ -96,7 +112,13 @@ UIImage * SDGraphicsGetImageFromCurrentImageContext(void) {
     }
     if (!scale) {
         // reset to the scale factor of the device’s main screen if scale is 0.
-        scale = [NSScreen mainScreen].backingScaleFactor;
+        NSScreen *mainScreen = nil;
+        if (@available(macOS 10.12, *)) {
+            mainScreen = [NSScreen mainScreen];
+        } else {
+            mainScreen = [NSScreen screens].firstObject;
+        }
+        scale = mainScreen.backingScaleFactor ?: 1.0f;
     }
     NSImage *image = [[NSImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp];
     CGImageRelease(imageRef);

+ 0 - 3
Pods/SDWebImage/SDWebImage/Core/SDImageHEICCoder.m

@@ -18,8 +18,6 @@ static NSString * kSDCGImagePropertyHEICSUnclampedDelayTime = @"UnclampedDelayTi
 @implementation SDImageHEICCoder
 
 + (void)initialize {
-#if __IPHONE_13_0 || __TVOS_13_0 || __MAC_10_15 || __WATCHOS_6_0
-    // Xcode 11
     if (@available(iOS 13, tvOS 13, macOS 10.15, watchOS 6, *)) {
         // Use SDK instead of raw value
         kSDCGImagePropertyHEICSDictionary = (__bridge NSString *)kCGImagePropertyHEICSDictionary;
@@ -27,7 +25,6 @@ static NSString * kSDCGImagePropertyHEICSUnclampedDelayTime = @"UnclampedDelayTi
         kSDCGImagePropertyHEICSDelayTime = (__bridge NSString *)kCGImagePropertyHEICSDelayTime;
         kSDCGImagePropertyHEICSUnclampedDelayTime = (__bridge NSString *)kCGImagePropertyHEICSUnclampedDelayTime;
     }
-#endif
 }
 
 + (instancetype)sharedCoder {

+ 1 - 2
Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.h

@@ -7,7 +7,6 @@
 */
 
 #import <Foundation/Foundation.h>
-#import <ImageIO/ImageIO.h>
 #import "SDImageCoder.h"
 
 /**
@@ -24,7 +23,7 @@
  */
 @property (class, readonly) SDImageFormat imageFormat;
 /**
- The supported image format UTI Type. Such as `kUTTypeGIF`.
+ The supported image format UTI Type. Such as `kSDUTTypeGIF`.
  This can be used for cases when we can not detect `SDImageFormat. Such as progressive decoding's hint format `kCGImageSourceTypeIdentifierHint`.
  @note Subclass override.
  */

+ 235 - 80
Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.m

@@ -13,12 +13,40 @@
 #import "SDImageCoderHelper.h"
 #import "SDAnimatedImageRep.h"
 #import "UIImage+ForceDecode.h"
+#import "SDInternalMacros.h"
+
+#import <ImageIO/ImageIO.h>
+#import <CoreServices/CoreServices.h>
+
+#if SD_CHECK_CGIMAGE_RETAIN_SOURCE
+#import <dlfcn.h>
+
+// SPI to check thread safe during Example and Test
+static CGImageSourceRef (*SDCGImageGetImageSource)(CGImageRef);
+#endif
 
-// Specify DPI for vector format in CGImageSource, like PDF
-static NSString * kSDCGImageSourceRasterizationDPI = @"kCGImageSourceRasterizationDPI";
 // Specify File Size for lossy format encoding, like JPEG
 static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
 
+// This strip the un-wanted CGImageProperty, like the internal CGImageSourceRef in iOS 15+
+// However, CGImageCreateCopy still keep those CGImageProperty, not suit for our use case
+static CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) {
+    if (!image) return nil;
+    size_t width = CGImageGetWidth(image);
+    size_t height = CGImageGetHeight(image);
+    size_t bitsPerComponent = CGImageGetBitsPerComponent(image);
+    size_t bitsPerPixel = CGImageGetBitsPerPixel(image);
+    size_t bytesPerRow = CGImageGetBytesPerRow(image);
+    CGColorSpaceRef space = CGImageGetColorSpace(image);
+    CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(image);
+    CGDataProviderRef provider = CGImageGetDataProvider(image);
+    const CGFloat *decode = CGImageGetDecode(image);
+    bool shouldInterpolate = CGImageGetShouldInterpolate(image);
+    CGColorRenderingIntent intent = CGImageGetRenderingIntent(image);
+    CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, provider, decode, shouldInterpolate, intent);
+    return newImage;
+}
+
 @interface SDImageIOCoderFrame : NSObject
 
 @property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
@@ -32,6 +60,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
 @implementation SDImageIOAnimatedCoder {
     size_t _width, _height;
     CGImageSourceRef _imageSource;
+    BOOL _incremental;
+    SD_LOCK_DECLARE(_lock); // Lock only apply for incremental animation decoding
     NSData *_imageData;
     CGFloat _scale;
     NSUInteger _loopCount;
@@ -40,6 +70,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     BOOL _finished;
     BOOL _preserveAspectRatio;
     CGSize _thumbnailSize;
+    BOOL _lazyDecode;
 }
 
 - (void)dealloc
@@ -152,12 +183,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
 }
 
 + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source {
-    NSDictionary *options = @{
-        (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
-        (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
-    };
     NSTimeInterval frameDuration = 0.1;
-    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
+    CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, NULL);
     if (!cfFrameProperties) {
         return frameDuration;
     }
@@ -187,23 +214,30 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     return frameDuration;
 }
 
-+ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize options:(NSDictionary *)options {
-    // Some options need to pass to `CGImageSourceCopyPropertiesAtIndex` before `CGImageSourceCreateImageAtIndex`, or ImageIO will ignore them because they parse once :)
++ (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage {
+    // `animatedImage` means called from `SDAnimatedImageProvider.animatedImageFrameAtIndex`
+    NSDictionary *options;
+    if (animatedImage) {
+        if (!lazyDecode) {
+            options = @{
+                // image decoding and caching should happen at image creation time.
+                (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
+            };
+        } else {
+            options = @{
+                // image decoding will happen at rendering time
+                (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(NO),
+            };
+        }
+    }
     // Parse the image properties
-    NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, (__bridge CFDictionaryRef)options);
-    NSUInteger pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] unsignedIntegerValue];
-    NSUInteger pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] unsignedIntegerValue];
+    NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL);
+    CGFloat pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue];
+    CGFloat pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue];
     CGImagePropertyOrientation exifOrientation = (CGImagePropertyOrientation)[properties[(__bridge NSString *)kCGImagePropertyOrientation] unsignedIntegerValue];
     if (!exifOrientation) {
         exifOrientation = kCGImagePropertyOrientationUp;
     }
-    
-    CFStringRef uttype = CGImageSourceGetType(source);
-    // Check vector format
-    BOOL isVector = NO;
-    if ([NSData sd_imageFormatFromUTType:uttype] == SDImageFormatPDF) {
-        isVector = YES;
-    }
 
     NSMutableDictionary *decodingOptions;
     if (options) {
@@ -214,22 +248,6 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     CGImageRef imageRef;
     BOOL createFullImage = thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height);
     if (createFullImage) {
-        if (isVector) {
-            if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
-                // Provide the default pixel count for vector images, simply just use the screen size
-#if SD_WATCH
-                thumbnailSize = WKInterfaceDevice.currentDevice.screenBounds.size;
-#elif SD_UIKIT
-                thumbnailSize = UIScreen.mainScreen.bounds.size;
-#elif SD_MAC
-                thumbnailSize = NSScreen.mainScreen.frame.size;
-#endif
-            }
-            CGFloat maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
-            NSUInteger DPIPerPixel = 2;
-            NSUInteger rasterizationDPI = maxPixelSize * DPIPerPixel;
-            decodingOptions[kSDCGImageSourceRasterizationDPI] = @(rasterizationDPI);
-        }
         imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]);
     } else {
         decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio);
@@ -238,9 +256,9 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             CGFloat pixelRatio = pixelWidth / pixelHeight;
             CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height;
             if (pixelRatio > thumbnailRatio) {
-                maxPixelSize = thumbnailSize.width;
+                maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.width / pixelRatio);
             } else {
-                maxPixelSize = thumbnailSize.height;
+                maxPixelSize = MAX(thumbnailSize.height, thumbnailSize.height * pixelRatio);
             }
         } else {
             maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height);
@@ -252,6 +270,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     if (!imageRef) {
         return nil;
     }
+    BOOL isDecoded = NO;
     // Thumbnail image post-process
     if (!createFullImage) {
         if (preserveAspectRatio) {
@@ -260,8 +279,45 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         } else {
             // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size
             CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize];
-            CGImageRelease(imageRef);
-            imageRef = scaledImageRef;
+            if (scaledImageRef) {
+                CGImageRelease(imageRef);
+                imageRef = scaledImageRef;
+                isDecoded = YES;
+            }
+        }
+    }
+    // Check whether output CGImage is decoded
+    if (!lazyDecode) {
+        if (!isDecoded) {
+            // Use CoreGraphics to trigger immediately decode
+            CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef];
+            if (decodedImageRef) {
+                CGImageRelease(imageRef);
+                imageRef = decodedImageRef;
+                isDecoded = YES;
+            }
+        }
+    } else if (animatedImage) {
+        // iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273
+        if (@available(iOS 15, tvOS 15, *)) {
+            // User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef
+            // CGImageRef newImageRef = CGImageCreateCopy(imageRef); // This one does not strip the CGImageProperty
+            CGImageRef newImageRef = SDCGImageCreateCopy(imageRef);
+            if (newImageRef) {
+                CGImageRelease(imageRef);
+                imageRef = newImageRef;
+            }
+#if SD_CHECK_CGIMAGE_RETAIN_SOURCE
+            // Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+)
+            // If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check
+            static dispatch_once_t onceToken;
+            dispatch_once(&onceToken, ^{
+                SDCGImageGetImageSource = dlsym(RTLD_DEFAULT, "CGImageGetImageSource");
+            });
+            if (SDCGImageGetImageSource) {
+                NSCAssert(!SDCGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock");
+            }
+#endif
         }
     }
     
@@ -272,6 +328,8 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation];
 #endif
     CGImageRelease(imageRef);
+    image.sd_isDecoded = isDecoded;
+    
     return image;
 }
 
@@ -306,34 +364,68 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         preserveAspectRatio = preserveAspectRatioValue.boolValue;
     }
     
+    BOOL lazyDecode = YES; // Defaults YES for static image coder
+    NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+    if (lazyDecodeValue != nil) {
+        lazyDecode = lazyDecodeValue.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
     if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
         SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data];
-        NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
-        imageRep.size = size;
-        NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
-        [animatedImage addRepresentation:imageRep];
-        return animatedImage;
+        if (imageRep) {
+            NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale);
+            imageRep.size = size;
+            NSImage *animatedImage = [[NSImage alloc] initWithSize:size];
+            [animatedImage addRepresentation:imageRep];
+            animatedImage.sd_imageFormat = self.class.imageFormat;
+            return animatedImage;
+        }
     }
 #endif
     
-    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
+    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
+        NSString *fileExtensionHint = options[SDImageCoderDecodeFileExtensionHint];
+        if (fileExtensionHint) {
+            typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage);
+            // Ignore dynamic UTI
+            if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) {
+                typeIdentifierHint = nil;
+            }
+        }
+    } else if ([typeIdentifierHint isEqual:NSNull.null]) {
+        // Hack if user don't want to imply file extension
+        typeIdentifierHint = nil;
+    }
+    
+    NSDictionary *creatingOptions = nil;
+    if (typeIdentifierHint) {
+        creatingOptions = @{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : typeIdentifierHint};
+    }
+    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)creatingOptions);
+    if (!source) {
+        // Try again without UTType hint, the call site from user may provide the wrong UTType
+        source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
+    }
     if (!source) {
         return nil;
     }
+    
     size_t count = CGImageSourceGetCount(source);
     UIImage *animatedImage;
     
     BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue];
     if (decodeFirstFrame || count <= 1) {
-        animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
+        animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
     } else {
-        NSMutableArray<SDImageFrame *> *frames = [NSMutableArray array];
+        NSMutableArray<SDImageFrame *> *frames = [NSMutableArray arrayWithCapacity:count];
         
         for (size_t i = 0; i < count; i++) {
-            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
+            UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO];
             if (!image) {
                 continue;
             }
@@ -366,6 +458,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     if (self) {
         NSString *imageUTType = self.class.imageUTType;
         _imageSource = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : imageUTType});
+        _incremental = YES;
         CGFloat scale = 1;
         NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
         if (scaleFactor != nil) {
@@ -388,6 +481,13 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             preserveAspectRatio = preserveAspectRatioValue.boolValue;
         }
         _preserveAspectRatio = preserveAspectRatio;
+        BOOL lazyDecode = NO; // Defaults NO for animated image coder
+        NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+        if (lazyDecodeValue != nil) {
+            lazyDecode = lazyDecodeValue.boolValue;
+        }
+        _lazyDecode = lazyDecode;
+        SD_LOCK_INIT(_lock);
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
 #endif
@@ -396,6 +496,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
 }
 
 - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished {
+    NSCParameterAssert(_incremental);
     if (_finished) {
         return;
     }
@@ -409,11 +510,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished);
     
     if (_width + _height == 0) {
-        NSDictionary *options = @{
-            (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
-            (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
-        };
-        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, (__bridge CFDictionaryRef)options);
+        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL);
         if (properties) {
             CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
             if (val) CFNumberGetValue(val, kCFNumberLongType, &_height);
@@ -423,11 +520,14 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         }
     }
     
+    SD_LOCK(_lock);
     // For animated image progressive decoding because the frame count and duration may be changed.
     [self scanAndCheckFramesValidWithImageSource:_imageSource];
+    SD_UNLOCK(_lock);
 }
 
 - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
+    NSCParameterAssert(_incremental);
     UIImage *image;
     
     if (_width + _height > 0) {
@@ -437,7 +537,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         if (scaleFactor != nil) {
             scale = MAX([scaleFactor doubleValue], 1);
         }
-        image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:nil];
+        image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
         if (image) {
             image.sd_imageFormat = self.class.imageFormat;
         }
@@ -455,19 +555,31 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     if (!image) {
         return nil;
     }
-    CGImageRef imageRef = image.CGImage;
-    if (!imageRef) {
-        // Earily return, supports CGImage only
+    if (format != self.class.imageFormat) {
         return nil;
     }
     
-    if (format != self.class.imageFormat) {
+    NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
+    if (!frames || frames.count == 0) {
+        SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0];
+        frames = @[frame];
+    }
+    return [self encodedDataWithFrames:frames loopCount:image.sd_imageLoopCount format:format options:options];
+}
+
+- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
+    UIImage *image = frames.firstObject.image; // Primary image
+    if (!image) {
+        return nil;
+    }
+    CGImageRef imageRef = image.CGImage;
+    if (!imageRef) {
+        // Earily return, supports CGImage only
         return nil;
     }
     
     NSMutableData *imageData = [NSMutableData data];
     CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format];
-    NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
     
     // 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.
@@ -477,6 +589,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         return nil;
     }
     NSMutableDictionary *properties = [NSMutableDictionary dictionary];
+#if SD_UIKIT || SD_WATCH
+    CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation];
+#else
+    CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp;
+#endif
+    properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation);
     // Encoding Options
     double compressionQuality = 1;
     if (options[SDImageCoderEncodeCompressionQuality]) {
@@ -496,16 +614,18 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         maxPixelSize = maxPixelSizeValue.CGSizeValue;
 #endif
     }
-    NSUInteger pixelWidth = CGImageGetWidth(imageRef);
-    NSUInteger pixelHeight = CGImageGetHeight(imageRef);
+    CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef);
+    CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef);
     CGFloat finalPixelSize = 0;
-    if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > maxPixelSize.width && pixelHeight > maxPixelSize.height) {
+    BOOL encodeFullImage = maxPixelSize.width == 0 || maxPixelSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= maxPixelSize.width && pixelHeight <= maxPixelSize.height);
+    if (!encodeFullImage) {
+        // Thumbnail Encoding
         CGFloat pixelRatio = pixelWidth / pixelHeight;
         CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
         if (pixelRatio > maxPixelSizeRatio) {
-            finalPixelSize = maxPixelSize.width;
+            finalPixelSize = MAX(maxPixelSize.width, maxPixelSize.width / pixelRatio);
         } else {
-            finalPixelSize = maxPixelSize.height;
+            finalPixelSize = MAX(maxPixelSize.height, maxPixelSize.height * pixelRatio);
         }
         properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
     }
@@ -522,12 +642,11 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail);
     
     BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
-    if (encodeFirstFrame || frames.count == 0) {
+    if (encodeFirstFrame || frames.count <= 1) {
         // for static single images
         CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties);
     } else {
         // for animated images
-        NSUInteger loopCount = image.sd_imageLoopCount;
         NSDictionary *containerProperties = @{
             self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)}
         };
@@ -591,6 +710,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             preserveAspectRatio = preserveAspectRatioValue.boolValue;
         }
         _preserveAspectRatio = preserveAspectRatio;
+        BOOL lazyDecode = NO; // Defaults NO for animated image coder
+        NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+        if (lazyDecodeValue != nil) {
+            lazyDecode = lazyDecodeValue.boolValue;
+        }
+        _lazyDecode = lazyDecode;
         _imageSource = imageSource;
         _imageData = data;
 #if SD_UIKIT
@@ -606,17 +731,21 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     }
     NSUInteger frameCount = CGImageSourceGetCount(imageSource);
     NSUInteger loopCount = [self.class imageLoopCountWithSource:imageSource];
-    NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray array];
+    _loopCount = loopCount;
     
+    NSMutableArray<SDImageIOCoderFrame *> *frames = [NSMutableArray arrayWithCapacity:frameCount];
     for (size_t i = 0; i < frameCount; i++) {
         SDImageIOCoderFrame *frame = [[SDImageIOCoderFrame alloc] init];
         frame.index = i;
         frame.duration = [self.class frameDurationAtIndex:i source:imageSource];
         [frames addObject:frame];
     }
+    if (frames.count != frameCount) {
+        // frames not match, do not override current value
+        return NO;
+    }
     
     _frameCount = frameCount;
-    _loopCount = loopCount;
     _frames = [frames copy];
     
     return YES;
@@ -635,27 +764,53 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
 }
 
 - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
-    if (index >= _frameCount) {
-        return 0;
+    NSTimeInterval duration;
+    // Incremental Animation decoding may update frames when new bytes available
+    // Which should use lock to ensure frame count and frames match, ensure atomic logic
+    if (_incremental) {
+        SD_LOCK(_lock);
+        if (index >= _frames.count) {
+            SD_UNLOCK(_lock);
+            return 0;
+        }
+        duration = _frames[index].duration;
+        SD_UNLOCK(_lock);
+    } else {
+        if (index >= _frames.count) {
+            return 0;
+        }
+        duration = _frames[index].duration;
     }
-    return _frames[index].duration;
+    return duration;
 }
 
 - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
-    if (index >= _frameCount) {
-        return nil;
+    UIImage *image;
+    // Incremental Animation decoding may update frames when new bytes available
+    // Which should use lock to ensure frame count and frames match, ensure atomic logic
+    if (_incremental) {
+        SD_LOCK(_lock);
+        if (index >= _frames.count) {
+            SD_UNLOCK(_lock);
+            return nil;
+        }
+        image = [self safeAnimatedImageFrameAtIndex:index];
+        SD_UNLOCK(_lock);
+    } else {
+        if (index >= _frames.count) {
+            return nil;
+        }
+        image = [self safeAnimatedImageFrameAtIndex:index];
     }
-    // Animated Image should not use the CGContext solution to force decode. Prefers to use Image/IO built in method, which is safer and memory friendly, see https://github.com/SDWebImage/SDWebImage/issues/2961
-    NSDictionary *options = @{
-        (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES),
-        (__bridge NSString *)kCGImageSourceShouldCache : @(YES) // Always cache to reduce CPU usage
-    };
-    UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:options];
+    return image;
+}
+
+- (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
+    UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:YES];
     if (!image) {
         return nil;
     }
     image.sd_imageFormat = self.class.imageFormat;
-    image.sd_isDecoded = YES;;
     return image;
 }
 

+ 142 - 14
Pods/SDWebImage/SDWebImage/Core/SDImageIOCoder.m

@@ -9,10 +9,13 @@
 #import "SDImageIOCoder.h"
 #import "SDImageCoderHelper.h"
 #import "NSImage+Compatibility.h"
-#import <ImageIO/ImageIO.h>
 #import "UIImage+Metadata.h"
+#import "SDImageGraphics.h"
 #import "SDImageIOAnimatedCoderInternal.h"
 
+#import <ImageIO/ImageIO.h>
+#import <CoreServices/CoreServices.h>
+
 // Specify File Size for lossy format encoding, like JPEG
 static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize";
 
@@ -24,6 +27,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     BOOL _finished;
     BOOL _preserveAspectRatio;
     CGSize _thumbnailSize;
+    BOOL _lazyDecode;
 }
 
 - (void)dealloc {
@@ -52,6 +56,67 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
     return coder;
 }
 
+#pragma mark - Bitmap PDF representation
++ (UIImage *)createBitmapPDFWithData:(nonnull NSData *)data pageNumber:(NSUInteger)pageNumber targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio {
+    NSParameterAssert(data);
+    UIImage *image;
+    
+    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data);
+    if (!provider) {
+        return nil;
+    }
+    CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider);
+    CGDataProviderRelease(provider);
+    if (!document) {
+        return nil;
+    }
+    
+    // `CGPDFDocumentGetPage` page number is 1-indexed.
+    CGPDFPageRef page = CGPDFDocumentGetPage(document, pageNumber + 1);
+    if (!page) {
+        CGPDFDocumentRelease(document);
+        return nil;
+    }
+    
+    CGPDFBox box = kCGPDFMediaBox;
+    CGRect rect = CGPDFPageGetBoxRect(page, box);
+    CGRect targetRect = rect;
+    if (!CGSizeEqualToSize(targetSize, CGSizeZero)) {
+        targetRect = CGRectMake(0, 0, targetSize.width, targetSize.height);
+    }
+    
+    CGFloat xRatio = targetRect.size.width / rect.size.width;
+    CGFloat yRatio = targetRect.size.height / rect.size.height;
+    CGFloat xScale = preserveAspectRatio ? MIN(xRatio, yRatio) : xRatio;
+    CGFloat yScale = preserveAspectRatio ? MIN(xRatio, yRatio) : yRatio;
+    
+    // `CGPDFPageGetDrawingTransform` will only scale down, but not scale up, so we need calculate the actual scale again
+    CGRect drawRect = CGRectMake( 0, 0, targetRect.size.width / xScale, targetRect.size.height / yScale);
+    CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScale, yScale);
+    CGAffineTransform transform = CGPDFPageGetDrawingTransform(page, box, drawRect, 0, preserveAspectRatio);
+    
+    SDGraphicsBeginImageContextWithOptions(targetRect.size, NO, 0);
+    CGContextRef context = SDGraphicsGetCurrentContext();
+    
+#if SD_UIKIT || SD_WATCH
+    // Core Graphics coordinate system use the bottom-left, UIKit use the flipped one
+    CGContextTranslateCTM(context, 0, targetRect.size.height);
+    CGContextScaleCTM(context, 1, -1);
+#endif
+    
+    CGContextConcatCTM(context, scaleTransform);
+    CGContextConcatCTM(context, transform);
+    
+    CGContextDrawPDFPage(context, page);
+    
+    image = SDGraphicsGetImageFromCurrentImageContext();
+    SDGraphicsEndImageContext();
+    
+    CGPDFDocumentRelease(document);
+    
+    return image;
+}
+
 #pragma mark - Decode
 - (BOOL)canDecodeFromData:(nullable NSData *)data {
     return YES;
@@ -83,18 +148,73 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         preserveAspectRatio = preserveAspectRatioValue.boolValue;
     }
     
-    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
+    // Check vector format
+    if ([NSData sd_imageFormatForImageData:data] == SDImageFormatPDF) {
+        // History before iOS 16, ImageIO can decode PDF with rasterization size, but can't ever :(
+        // So, use CoreGraphics to decode PDF (copy code from SDWebImagePDFCoder, may do refactor in the future)
+        UIImage *image;
+        NSUInteger pageNumber = 0; // Still use first page, may added options is user want
+#if SD_MAC
+        // If don't use thumbnail, prefers the built-in generation of vector image
+        // macOS's `NSImage` supports PDF built-in rendering
+        if (thumbnailSize.width == 0 || thumbnailSize.height == 0) {
+            NSPDFImageRep *imageRep = [[NSPDFImageRep alloc] initWithData:data];
+            if (imageRep) {
+                imageRep.currentPage = pageNumber;
+                image = [[NSImage alloc] initWithSize:imageRep.size];
+                [image addRepresentation:imageRep];
+                image.sd_imageFormat = SDImageFormatPDF;
+                return image;
+            }
+        }
+#endif
+        image = [self.class createBitmapPDFWithData:data pageNumber:pageNumber targetSize:thumbnailSize preserveAspectRatio:preserveAspectRatio];
+        image.sd_imageFormat = SDImageFormatPDF;
+        return image;
+    }
+    
+    BOOL lazyDecode = YES; // Defaults YES for static image coder
+    NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+    if (lazyDecodeValue != nil) {
+        lazyDecode = lazyDecodeValue.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
+        NSString *fileExtensionHint = options[SDImageCoderDecodeFileExtensionHint];
+        if (fileExtensionHint) {
+            typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage);
+            // Ignore dynamic UTI
+            if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) {
+                typeIdentifierHint = nil;
+            }
+        }
+    } else if ([typeIdentifierHint isEqual:NSNull.null]) {
+        // Hack if user don't want to imply file extension
+        typeIdentifierHint = nil;
+    }
+    
+    NSDictionary *creatingOptions = nil;
+    if (typeIdentifierHint) {
+        creatingOptions = @{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : typeIdentifierHint};
+    }
+    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)creatingOptions);
+    if (!source) {
+        // Try again without UTType hint, the call site from user may provide the wrong UTType
+        source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil);
+    }
     if (!source) {
         return nil;
     }
     
-    UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize options:nil];
+    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];
     CFRelease(source);
-    if (!image) {
-        return nil;
-    }
     
-    image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
+    image.sd_imageFormat = imageFormat;
     return image;
 }
 
@@ -130,6 +250,12 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
             preserveAspectRatio = preserveAspectRatioValue.boolValue;
         }
         _preserveAspectRatio = preserveAspectRatio;
+        BOOL lazyDecode = YES; // Defaults YES for static image coder
+        NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding];
+        if (lazyDecodeValue != nil) {
+            lazyDecode = lazyDecodeValue.boolValue;
+        }
+        _lazyDecode = lazyDecode;
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
 #endif
@@ -180,7 +306,7 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         if (scaleFactor != nil) {
             scale = MAX([scaleFactor doubleValue], 1);
         }
-        image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize options:nil];
+        image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO];
         if (image) {
             CFStringRef uttype = CGImageSourceGetType(_imageSource);
             image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype];
@@ -250,16 +376,18 @@ static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestination
         maxPixelSize = maxPixelSizeValue.CGSizeValue;
 #endif
     }
-    NSUInteger pixelWidth = CGImageGetWidth(imageRef);
-    NSUInteger pixelHeight = CGImageGetHeight(imageRef);
-    if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && pixelWidth > maxPixelSize.width && pixelHeight > maxPixelSize.height) {
+    CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef);
+    CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef);
+    CGFloat finalPixelSize = 0;
+    BOOL encodeFullImage = maxPixelSize.width == 0 || maxPixelSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= maxPixelSize.width && pixelHeight <= maxPixelSize.height);
+    if (!encodeFullImage) {
+        // Thumbnail Encoding
         CGFloat pixelRatio = pixelWidth / pixelHeight;
         CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height;
-        CGFloat finalPixelSize;
         if (pixelRatio > maxPixelSizeRatio) {
-            finalPixelSize = maxPixelSize.width;
+            finalPixelSize = MAX(maxPixelSize.width, maxPixelSize.width / pixelRatio);
         } else {
-            finalPixelSize = maxPixelSize.height;
+            finalPixelSize = MAX(maxPixelSize.height, maxPixelSize.height * pixelRatio);
         }
         properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize);
     }

+ 47 - 2
Pods/SDWebImage/SDWebImage/Core/SDImageLoader.h

@@ -9,6 +9,7 @@
 #import "SDWebImageCompat.h"
 #import "SDWebImageDefine.h"
 #import "SDWebImageOperation.h"
+#import "SDImageCoder.h"
 
 typedef void(^SDImageLoaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL);
 typedef void(^SDImageLoaderCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished);
@@ -50,6 +51,18 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Non
  */
 FOUNDATION_EXPORT UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, BOOL finished,  id<SDWebImageOperation> _Nonnull operation, SDWebImageOptions options, SDWebImageContext * _Nullable context);
 
+/**
+ This function get the progressive decoder for current loading operation. If no progressive decoding is happended or decoder is not able to construct, return nil.
+ @return The progressive decoder associated with the loading operation.
+ */
+FOUNDATION_EXPORT id<SDProgressiveImageCoder> _Nullable SDImageLoaderGetProgressiveCoder(id<SDWebImageOperation> _Nonnull operation);
+
+/**
+ This function set the progressive decoder for current loading operation. If no progressive decoding is happended, pass nil.
+ @param operation The loading operation to associate the progerssive decoder.
+ */
+FOUNDATION_EXPORT void SDImageLoaderSetProgressiveCoder(id<SDWebImageOperation> _Nonnull operation, id<SDProgressiveImageCoder> _Nullable progressiveCoder);
+
 #pragma mark - SDImageLoader
 
 /**
@@ -60,6 +73,7 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NS
 */
 @protocol SDImageLoader <NSObject>
 
+@required
 /**
  Whether current image loader supports to load the provide image URL.
  This will be checked every time a new image request come for loader. If this return NO, we will mark this image load as failed. If return YES, we will start to call `requestImageWithURL:options:context:progress:completed:`.
@@ -67,8 +81,23 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NS
  @param url The image URL to be loaded.
  @return YES to continue download, NO to stop download.
  */
-- (BOOL)canRequestImageForURL:(nullable NSURL *)url;
+- (BOOL)canRequestImageForURL:(nullable NSURL *)url API_DEPRECATED_WITH_REPLACEMENT("canRequestImageForURL:options:context:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
+
+@optional
+/**
+ Whether current image loader supports to load the provide image URL, with associated options and context.
+ This will be checked every time a new image request come for loader. If this return NO, we will mark this image load as failed. If return YES, we will start to call `requestImageWithURL:options:context:progress:completed:`.
+
+ @param url The image URL to be loaded.
+ @param options A mask to specify options to use for this request
+ @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
+ @return YES to continue download, NO to stop download.
+ */
+- (BOOL)canRequestImageForURL:(nullable NSURL *)url
+                      options:(SDWebImageOptions)options
+                      context:(nullable SDWebImageContext *)context;
 
+@required
 /**
  Load the image and image data with the given URL and return the image data. You're responsible for producing the image instance.
 
@@ -96,6 +125,22 @@ FOUNDATION_EXPORT UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NS
  @return Whether to block this url or not. Return YES to mark this URL as failed.
  */
 - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url
-                              error:(nonnull NSError *)error;
+                              error:(nonnull NSError *)error API_DEPRECATED_WITH_REPLACEMENT("shouldBlockFailedURLWithURL:error:options:context:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
+
+@optional
+/**
+ Whether the error from image loader should be marked indeed un-recoverable or not, with associated options and context.
+ If this return YES, failed URL which does not using `SDWebImageRetryFailed` will be blocked into black list. Else not.
+
+ @param url The URL represent the image. Note this may not be a HTTP URL
+ @param error The URL's loading error, from previous `requestImageWithURL:options:context:progress:completed:` completedBlock's error.
+ @param options A mask to specify options to use for this request
+ @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
+ @return Whether to block this url or not. Return YES to mark this URL as failed.
+ */
+- (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url
+                              error:(nonnull NSError *)error
+                            options:(SDWebImageOptions)options
+                            context:(nullable SDWebImageContext *)context;
 
 @end

+ 35 - 65
Pods/SDWebImage/SDWebImage/Core/SDImageLoader.m

@@ -13,10 +13,23 @@
 #import "SDAnimatedImage.h"
 #import "UIImage+Metadata.h"
 #import "SDInternalMacros.h"
+#import "SDImageCacheDefine.h"
 #import "objc/runtime.h"
 
+SDWebImageContextOption const SDWebImageContextLoaderCachedImage = @"loaderCachedImage";
+
 static void * SDImageLoaderProgressiveCoderKey = &SDImageLoaderProgressiveCoderKey;
 
+id<SDProgressiveImageCoder> SDImageLoaderGetProgressiveCoder(id<SDWebImageOperation> operation) {
+    NSCParameterAssert(operation);
+    return objc_getAssociatedObject(operation, SDImageLoaderProgressiveCoderKey);
+}
+
+void SDImageLoaderSetProgressiveCoder(id<SDWebImageOperation> operation, id<SDProgressiveImageCoder> progressiveCoder) {
+    NSCParameterAssert(operation);
+    objc_setAssociatedObject(operation, SDImageLoaderProgressiveCoderKey, progressiveCoder, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
 UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, SDWebImageOptions options, SDWebImageContext * _Nullable context) {
     NSCParameterAssert(imageData);
     NSCParameterAssert(imageURL);
@@ -29,34 +42,13 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS
     } else {
         cacheKey = imageURL.absoluteString;
     }
+    SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey);
     BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
-    NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
-    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
-    NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio];
-    NSValue *thumbnailSizeValue;
-    BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
-    if (shouldScaleDown) {
-        CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
-        CGFloat dimension = ceil(sqrt(thumbnailPixels));
-        thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
-    }
-    if (context[SDWebImageContextImageThumbnailPixelSize]) {
-        thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
-    }
-    
-    SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
-    mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
-    mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
-    mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
-    mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
-    mutableCoderOptions[SDImageCoderWebImageContext] = context;
-    SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
+    CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue];
     
     // Grab the image coder
-    id<SDImageCoder> imageCoder;
-    if ([context[SDWebImageContextImageCoder] conformsToProtocol:@protocol(SDImageCoder)]) {
-        imageCoder = context[SDWebImageContextImageCoder];
-    } else {
+    id<SDImageCoder> imageCoder = context[SDWebImageContextImageCoder];
+    if (!imageCoder) {
         imageCoder = [SDImageCodersManager sharedManager];
     }
     
@@ -83,17 +75,16 @@ UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NS
     }
     if (image) {
         BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
-        if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
-            // `SDAnimatedImage` do not decode
-            shouldDecode = NO;
-        } else if (image.sd_isAnimated) {
-            // animated image do not decode
+        BOOL lazyDecode = [coderOptions[SDImageCoderDecodeUseLazyDecoding] boolValue];
+        if (lazyDecode) {
+            // lazyDecode = NO means we should not forceDecode, highest priority
             shouldDecode = NO;
         }
-        
         if (shouldDecode) {
             image = [SDImageCoderHelper decodedImageWithImage:image];
         }
+        // assign the decode options, to let manager check whether to re-decode if needed
+        image.sd_decodeOptions = coderOptions;
     }
     
     return image;
@@ -112,35 +103,16 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
     } else {
         cacheKey = imageURL.absoluteString;
     }
+    SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey);
     BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly);
-    NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor];
-    CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey);
-    NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio];
-    NSValue *thumbnailSizeValue;
-    BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages);
-    if (shouldScaleDown) {
-        CGFloat thumbnailPixels = SDImageCoderHelper.defaultScaleDownLimitBytes / 4;
-        CGFloat dimension = ceil(sqrt(thumbnailPixels));
-        thumbnailSizeValue = @(CGSizeMake(dimension, dimension));
-    }
-    if (context[SDWebImageContextImageThumbnailPixelSize]) {
-        thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize];
-    }
-    
-    SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:2];
-    mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame);
-    mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale);
-    mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue;
-    mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue;
-    mutableCoderOptions[SDImageCoderWebImageContext] = context;
-    SDImageCoderOptions *coderOptions = [mutableCoderOptions copy];
+    CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue];
     
     // Grab the progressive image coder
-    id<SDProgressiveImageCoder> progressiveCoder = objc_getAssociatedObject(operation, SDImageLoaderProgressiveCoderKey);
+    id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(operation);
     if (!progressiveCoder) {
         id<SDProgressiveImageCoder> imageCoder = context[SDWebImageContextImageCoder];
         // Check the progressive coder if provided
-        if ([imageCoder conformsToProtocol:@protocol(SDProgressiveImageCoder)]) {
+        if ([imageCoder respondsToSelector:@selector(initIncrementalWithOptions:)]) {
             progressiveCoder = [[[imageCoder class] alloc] initIncrementalWithOptions:coderOptions];
         } else {
             // We need to create a new instance for progressive decoding to avoid conflicts
@@ -152,7 +124,7 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
                 }
             }
         }
-        objc_setAssociatedObject(operation, SDImageLoaderProgressiveCoderKey, progressiveCoder, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+        SDImageLoaderSetProgressiveCoder(operation, progressiveCoder);
     }
     // If we can't find any progressive coder, disable progressive download
     if (!progressiveCoder) {
@@ -163,7 +135,7 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
     if (!decodeFirstFrame) {
         // check whether we should use `SDAnimatedImage`
         Class animatedImageClass = context[SDWebImageContextAnimatedImageClass];
-        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)] && [progressiveCoder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) {
+        if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)] && [progressiveCoder respondsToSelector:@selector(animatedImageFrameAtIndex:)]) {
             image = [[animatedImageClass alloc] initWithAnimatedCoder:(id<SDAnimatedImageCoder>)progressiveCoder scale:scale];
             if (image) {
                 // Progressive decoding does not preload frames
@@ -180,21 +152,19 @@ UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull im
     }
     if (image) {
         BOOL shouldDecode = !SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage);
-        if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) {
-            // `SDAnimatedImage` do not decode
-            shouldDecode = NO;
-        } else if (image.sd_isAnimated) {
-            // animated image do not decode
+        BOOL lazyDecode = [coderOptions[SDImageCoderDecodeUseLazyDecoding] boolValue];
+        if (lazyDecode) {
+            // lazyDecode = NO means we should not forceDecode, highest priority
             shouldDecode = NO;
         }
         if (shouldDecode) {
             image = [SDImageCoderHelper decodedImageWithImage:image];
         }
-        // mark the image as progressive (completionBlock one are not mark as progressive)
-        image.sd_isIncremental = YES;
+        // assign the decode options, to let manager check whether to re-decode if needed
+        image.sd_decodeOptions = coderOptions;
+        // mark the image as progressive (completed one are not mark as progressive)
+        image.sd_isIncremental = !finished;
     }
     
     return image;
 }
-
-SDWebImageContextOption const SDWebImageContextLoaderCachedImage = @"loaderCachedImage";

+ 24 - 15
Pods/SDWebImage/SDWebImage/Core/SDImageLoadersManager.m

@@ -12,13 +12,12 @@
 
 @interface SDImageLoadersManager ()
 
-@property (nonatomic, strong, nonnull) dispatch_semaphore_t loadersLock;
+@property (nonatomic, strong, nonnull) NSMutableArray<id<SDImageLoader>> *imageLoaders;
 
 @end
 
-@implementation SDImageLoadersManager
-{
-    NSMutableArray<id<SDImageLoader>>* _imageLoaders;
+@implementation SDImageLoadersManager {
+    SD_LOCK_DECLARE(_loadersLock);
 }
 
 + (SDImageLoadersManager *)sharedManager {
@@ -35,25 +34,25 @@
     if (self) {
         // initialize with default image loaders
         _imageLoaders = [NSMutableArray arrayWithObject:[SDWebImageDownloader sharedDownloader]];
-        _loadersLock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_loadersLock);
     }
     return self;
 }
 
 - (NSArray<id<SDImageLoader>> *)loaders {
-    SD_LOCK(self.loadersLock);
+    SD_LOCK(_loadersLock);
     NSArray<id<SDImageLoader>>* loaders = [_imageLoaders copy];
-    SD_UNLOCK(self.loadersLock);
+    SD_UNLOCK(_loadersLock);
     return loaders;
 }
 
 - (void)setLoaders:(NSArray<id<SDImageLoader>> *)loaders {
-    SD_LOCK(self.loadersLock);
+    SD_LOCK(_loadersLock);
     [_imageLoaders removeAllObjects];
     if (loaders.count) {
         [_imageLoaders addObjectsFromArray:loaders];
     }
-    SD_UNLOCK(self.loadersLock);
+    SD_UNLOCK(_loadersLock);
 }
 
 #pragma mark - Loader Property
@@ -62,27 +61,37 @@
     if (![loader conformsToProtocol:@protocol(SDImageLoader)]) {
         return;
     }
-    SD_LOCK(self.loadersLock);
+    SD_LOCK(_loadersLock);
     [_imageLoaders addObject:loader];
-    SD_UNLOCK(self.loadersLock);
+    SD_UNLOCK(_loadersLock);
 }
 
 - (void)removeLoader:(id<SDImageLoader>)loader {
     if (![loader conformsToProtocol:@protocol(SDImageLoader)]) {
         return;
     }
-    SD_LOCK(self.loadersLock);
+    SD_LOCK(_loadersLock);
     [_imageLoaders removeObject:loader];
-    SD_UNLOCK(self.loadersLock);
+    SD_UNLOCK(_loadersLock);
 }
 
 #pragma mark - SDImageLoader
 
 - (BOOL)canRequestImageForURL:(nullable NSURL *)url {
+    return [self canRequestImageForURL:url options:0 context:nil];
+}
+
+- (BOOL)canRequestImageForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context {
     NSArray<id<SDImageLoader>> *loaders = self.loaders;
     for (id<SDImageLoader> loader in loaders.reverseObjectEnumerator) {
-        if ([loader canRequestImageForURL:url]) {
-            return YES;
+        if ([loader respondsToSelector:@selector(canRequestImageForURL:options:context:)]) {
+            if ([loader canRequestImageForURL:url options:options context:context]) {
+                return YES;
+            }
+        } else {
+            if ([loader canRequestImageForURL:url]) {
+                return YES;
+            }
         }
     }
     return NO;

+ 14 - 11
Pods/SDWebImage/SDWebImage/Core/SDMemoryCache.m

@@ -13,12 +13,15 @@
 
 static void * SDMemoryCacheContext = &SDMemoryCacheContext;
 
-@interface SDMemoryCache <KeyType, ObjectType> ()
+@interface SDMemoryCache <KeyType, ObjectType> () {
+#if SD_UIKIT
+    SD_LOCK_DECLARE(_weakCacheLock); // a lock to keep the access to `weakCache` thread-safe
+#endif
+}
 
 @property (nonatomic, strong, nullable) SDImageCacheConfig *config;
 #if SD_UIKIT
 @property (nonatomic, strong, nonnull) NSMapTable<KeyType, ObjectType> *weakCache; // strong-weak cache
-@property (nonatomic, strong, nonnull) dispatch_semaphore_t weakCacheLock; // a lock to keep the access to `weakCache` thread-safe
 #endif
 @end
 
@@ -61,7 +64,7 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
 
 #if SD_UIKIT
     self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
-    self.weakCacheLock = dispatch_semaphore_create(1);
+    SD_LOCK_INIT(_weakCacheLock);
 
     [[NSNotificationCenter defaultCenter] addObserver:self
                                              selector:@selector(didReceiveMemoryWarning:)
@@ -85,9 +88,9 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
     }
     if (key && obj) {
         // Store weak cache
-        SD_LOCK(self.weakCacheLock);
+        SD_LOCK(_weakCacheLock);
         [self.weakCache setObject:obj forKey:key];
-        SD_UNLOCK(self.weakCacheLock);
+        SD_UNLOCK(_weakCacheLock);
     }
 }
 
@@ -98,9 +101,9 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
     }
     if (key && !obj) {
         // Check weak cache
-        SD_LOCK(self.weakCacheLock);
+        SD_LOCK(_weakCacheLock);
         obj = [self.weakCache objectForKey:key];
-        SD_UNLOCK(self.weakCacheLock);
+        SD_UNLOCK(_weakCacheLock);
         if (obj) {
             // Sync cache
             NSUInteger cost = 0;
@@ -120,9 +123,9 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
     }
     if (key) {
         // Remove weak cache
-        SD_LOCK(self.weakCacheLock);
+        SD_LOCK(_weakCacheLock);
         [self.weakCache removeObjectForKey:key];
-        SD_UNLOCK(self.weakCacheLock);
+        SD_UNLOCK(_weakCacheLock);
     }
 }
 
@@ -132,9 +135,9 @@ static void * SDMemoryCacheContext = &SDMemoryCacheContext;
         return;
     }
     // Manually remove should also remove weak cache
-    SD_LOCK(self.weakCacheLock);
+    SD_LOCK(_weakCacheLock);
     [self.weakCache removeAllObjects];
-    SD_UNLOCK(self.weakCacheLock);
+    SD_UNLOCK(_weakCacheLock);
 }
 #endif
 

+ 1 - 1
Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheSerializer.h

@@ -19,7 +19,7 @@ typedef NSData * _Nullable(^SDWebImageCacheSerializerBlock)(UIImage * _Nonnull i
 
 /// Provide the image data associated to the image and store to disk cache
 /// @param image The loaded image
-/// @param data The original loaded image data
+/// @param data The original loaded image data. May be nil when image is transformed (UIImage.sd_isTransformed = YES)
 /// @param imageURL The image URL
 - (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL;
 

+ 53 - 4
Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.h

@@ -105,6 +105,8 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
     /**
      * By default, placeholder images are loaded while the image is loading. This flag will delay the loading
      * of the placeholder image until after the image has finished loading.
+     * @note This is used to treate placeholder as an **Error Placeholder** but not **Loading Placeholder** by defaults. if the image loading is cancelled or error, the placeholder will be always set.
+     * @note Therefore, if you want both **Error Placeholder** and **Loading Placeholder** exist, use `SDWebImageAvoidAutoSetImage` to manually set the two placeholders and final loaded image by your hand depends on loading result.
      */
     SDWebImageDelayPlaceholder = 1 << 8,
     
@@ -167,7 +169,8 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
     
     /**
      * By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation.
-     * However, this process may increase the memory usage as well. If you are experiencing a issue due to excessive memory consumption, This flag can prevent decode the image.
+     * However, this process may increase the memory usage as well. If you are experiencing an issue due to excessive memory consumption, This flag can prevent decode the image.
+     * @note 5.14.0 introduce `SDImageCoderDecodeUseLazyDecoding`, use that for better control from codec, instead of post-processing. Which acts the similar like this option but works for SDAnimatedImage as well (this one does not)
      */
     SDWebImageAvoidDecodeImage = 1 << 18,
     
@@ -205,7 +208,7 @@ typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
 };
 
 
-#pragma mark - Context Options
+#pragma mark - Manager Context Options
 
 /**
  A String to be used as the operation key for view category to store the image load operation. This is used for view instance which supports different image loading process. If nil, will use the class name as operation key. (NSString *)
@@ -218,6 +221,15 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextSetIma
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomManager API_DEPRECATED("Use individual context option like .imageCache, .imageLoader and .imageTransformer instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
+/**
+ A `SDCallbackQueue` instance which controls the `Cache`/`Manager`/`Loader`'s callback queue for their completionBlock.
+ This is useful for user who call these 3 components in non-main queue and want to avoid callback in main queue.
+ @note For UI callback (`sd_setImageWithURL`), we will still use main queue to dispatch, means if you specify a global queue, it will enqueue from the global queue to main queue.
+ @note This does not effect the components' working queue (for example, `Cache` still query disk on internal ioQueue, `Loader` still do network on URLSessionConfiguration.delegateQueue), change those config if you need.
+ Defaults to nil. Which means main queue.
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCallbackQueue;
+
 /**
  A id<SDImageCache> instance which conforms to `SDImageCache` protocol. It's used to override the image manager's cache during the image loading pipeline.
  In other word, if you just want to specify a custom cache during image loading, you don't need to re-create a dummy SDWebImageManager instance with the cache. If not provided, use the image manager's cache (id<SDImageCache>)
@@ -239,9 +251,20 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageC
 
 /**
  A id<SDImageTransformer> instance which conforms `SDImageTransformer` protocol. It's used for image transform after the image load finished and store the transformed image to cache. If you provide one, it will ignore the `transformer` in manager and use provided one instead. If you pass NSNull, the transformer feature will be disabled. (id<SDImageTransformer>)
+ @note When this value is used, we will trigger image transform after downloaded, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTransformer;
 
+#pragma mark - Image Decoder Context Options
+
+/**
+ A Dictionary (SDImageCoderOptions) value, which pass the extra decoding options to the SDImageCoder. Introduced in SDWebImage 5.14.0
+ You can pass additional decoding related options to the decoder, extensible and control by you. And pay attention this dictionary may be retained by decoded image via `UIImage.sd_decodeOptions` 
+ This context option replace the deprecated `SDImageCoderWebImageContext`, which may cause retain cycle (cache -> image -> options -> context -> cache)
+ @note There are already individual options below like `.imageScaleFactor`, `.imagePreserveAspectRatio`, each of individual options will override the same filed for this dictionary.
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageDecodeOptions;
+
 /**
  A CGFloat raw value which specify the image scale factor. The number should be greater than or equal to 1.0. If not provide or the number is invalid, we will use the cache key to specify the scale factor. (NSNumber)
  */
@@ -257,9 +280,28 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageP
  A CGSize raw value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.imagePreserveAspectRatio`) the value size.
  @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both.
  Defaults to CGSizeZero, which means no thumbnail generation at all. (NSValue)
+ @note When this value is used, we will trigger thumbnail decoding for url, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize;
 
+/**
+ A NSString value (UTI) indicating the source image's file extension. Example: "public.jpeg-2000", "com.nikon.raw-image", "public.tiff"
+ Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF
+ Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser
+ @note If you don't provide this option, we will use the `URL.path` as file extension to calculate the UTI hint
+ @note If you really don't want any hint which effect the image result, pass `NSNull.null` instead
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTypeIdentifierHint;
+
+#pragma mark - Cache Context Options
+
+/**
+ A Dictionary (SDImageCoderOptions) value, which pass the extra encode options to the SDImageCoder. Introduced in SDWebImage 5.15.0
+ You can pass encode options like `compressionQuality`, `maxFileSize`, `maxPixelSize` to control the encoding related thing, this is used inside `SDImageCache` during store logic.
+ @note For developer who use custom cache protocol (not SDImageCache instance), they need to upgrade and use these options for encoding.
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageEncodeOptions;
+
 /**
  A SDImageCacheType raw value which specify the source of cache to query. Specify `SDImageCacheTypeDisk` to query from disk cache only; `SDImageCacheTypeMemory` to query from memory only. And `SDImageCacheTypeAll` to query from both memory cache and disk cache. Specify `SDImageCacheTypeNone` is invalid and totally ignore the cache query.
  If not provide or the value is invalid, we will use `SDImageCacheTypeAll`. (NSNumber)
@@ -275,24 +317,31 @@ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextStoreC
 
 /**
  The same behavior like `SDWebImageContextQueryCacheType`, but control the query cache type for the original image when you use image transformer feature. This allows the detail control of cache query for these two images. For example, if you want to query the transformed image from both memory/disk cache, query the original image from disk cache only, use `[.queryCacheType : .all, .originalQueryCacheType : .disk]`
- If not provide or the value is invalid, we will use `SDImageCacheTypeNone`, which does not query the original image from cache. (NSNumber)
+ If not provide or the value is invalid, we will use `SDImageCacheTypeDisk`, which query the original full image data from disk cache after transformed image cache miss. This is suitable for most common cases to avoid re-downloading the full data for different transform variants. (NSNumber)
  @note Which means, if you set this value to not be `.none`, we will query the original image from cache, then do transform with transformer, instead of actual downloading, which can save bandwidth usage.
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextOriginalQueryCacheType;
 
 /**
  The same behavior like `SDWebImageContextStoreCacheType`, but control the store cache type for the original image when you use image transformer feature. This allows the detail control of cache storage for these two images. For example, if you want to store the transformed image into both memory/disk cache, store the original image into disk cache only, use `[.storeCacheType : .all, .originalStoreCacheType : .disk]`
- If not provide or the value is invalid, we will use `SDImageCacheTypeNone`, which does not store the original image into cache. (NSNumber)
+ If not provide or the value is invalid, we will use `SDImageCacheTypeDisk`, which store the original full image data into disk cache after storing the transformed image. This is suitable for most common cases to avoid re-downloading the full data for different transform variants. (NSNumber)
  @note This only store the original image, if you want to use the original image without downloading in next query, specify `SDWebImageContextOriginalQueryCacheType` as well.
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextOriginalStoreCacheType;
 
+/**
+ A id<SDImageCache> instance which conforms to `SDImageCache` protocol. It's used to control the cache for original image when using the transformer. If you provide one, the original image (full size image) will query and write from that cache instance instead, the transformed image will query and write from the default `SDWebImageContextImageCache` instead. (id<SDImageCache>)
+ */
+FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextOriginalImageCache;
+
 /**
  A Class object which the instance is a `UIImage/NSImage` subclass and adopt `SDAnimatedImage` protocol. We will call `initWithData:scale:options:` to create the instance (or `initWithAnimatedCoder:scale:` when using progressive download) . If the instance create failed, fallback to normal `UIImage/NSImage`.
  This can be used to improve animated images rendering performance (especially memory usage on big animated images) with `SDAnimatedImageView` (Class).
  */
 FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextAnimatedImageClass;
 
+#pragma mark - Download Context Options
+
 /**
  A id<SDWebImageDownloaderRequestModifier> instance to modify the image download request. It's used for downloader to modify the original request from URL and options. If you provide one, it will ignore the `requestModifier` in downloader and use provided one instead. (id<SDWebImageDownloaderRequestModifier>)
  */

+ 16 - 4
Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.m

@@ -13,7 +13,7 @@
 
 #pragma mark - Image scale
 
-static inline NSArray<NSNumber *> * _Nonnull SDImageScaleFactors() {
+static inline NSArray<NSNumber *> * _Nonnull SDImageScaleFactors(void) {
     return @[@2, @3];
 }
 
@@ -28,7 +28,13 @@ inline CGFloat SDImageScaleFactorForKey(NSString * _Nullable key) {
 #elif SD_UIKIT
     if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)])
 #elif SD_MAC
-    if ([[NSScreen mainScreen] respondsToSelector:@selector(backingScaleFactor)])
+    NSScreen *mainScreen = nil;
+    if (@available(macOS 10.12, *)) {
+        mainScreen = [NSScreen mainScreen];
+    } else {
+        mainScreen = [NSScreen screens].firstObject;
+    }
+    if ([mainScreen respondsToSelector:@selector(backingScaleFactor)])
 #endif
     {
         // a@2x.png -> 8
@@ -79,9 +85,10 @@ inline UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage *
         UIImage *animatedImage;
 #if SD_UIKIT || SD_WATCH
         // `UIAnimatedImage` images share the same size and scale.
-        NSMutableArray<UIImage *> *scaledImages = [NSMutableArray array];
+        NSArray<UIImage *> *images = image.images;
+        NSMutableArray<UIImage *> *scaledImages = [NSMutableArray arrayWithCapacity:images.count];
         
-        for (UIImage *tempImage in image.images) {
+        for (UIImage *tempImage in images) {
             UIImage *tempScaledImage = [[UIImage alloc] initWithCGImage:tempImage.CGImage scale:scale orientation:tempImage.imageOrientation];
             [scaledImages addObject:tempScaledImage];
         }
@@ -120,17 +127,22 @@ inline UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage *
 
 SDWebImageContextOption const SDWebImageContextSetImageOperationKey = @"setImageOperationKey";
 SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager";
+SDWebImageContextOption const SDWebImageContextCallbackQueue = @"callbackQueue";
 SDWebImageContextOption const SDWebImageContextImageCache = @"imageCache";
 SDWebImageContextOption const SDWebImageContextImageLoader = @"imageLoader";
 SDWebImageContextOption const SDWebImageContextImageCoder = @"imageCoder";
 SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransformer";
+SDWebImageContextOption const SDWebImageContextImageDecodeOptions = @"imageDecodeOptions";
 SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor";
 SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio";
 SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize";
+SDWebImageContextOption const SDWebImageContextImageTypeIdentifierHint = @"imageTypeIdentifierHint";
+SDWebImageContextOption const SDWebImageContextImageEncodeOptions = @"imageEncodeOptions";
 SDWebImageContextOption const SDWebImageContextQueryCacheType = @"queryCacheType";
 SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType";
 SDWebImageContextOption const SDWebImageContextOriginalQueryCacheType = @"originalQueryCacheType";
 SDWebImageContextOption const SDWebImageContextOriginalStoreCacheType = @"originalStoreCacheType";
+SDWebImageContextOption const SDWebImageContextOriginalImageCache = @"originalImageCache";
 SDWebImageContextOption const SDWebImageContextAnimatedImageClass = @"animatedImageClass";
 SDWebImageContextOption const SDWebImageContextDownloadRequestModifier = @"downloadRequestModifier";
 SDWebImageContextOption const SDWebImageContextDownloadResponseModifier = @"downloadResponseModifier";

+ 1 - 1
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.h

@@ -157,7 +157,7 @@ typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock;
 
 /**
  * Set the response modifier to modify the original download response during image load.
- * This request modifier method will be called for each downloading image response. Return the original response means no modification. Return nil will mark current download as cancelled.
+ * This response modifier method will be called for each downloading image response. Return the original response means no modification. Return nil will mark current download as cancelled.
  * Defaults to nil, means does not modify the original download response.
  * @note If you want to modify single response, consider using `SDWebImageContextDownloadResponseModifier` context option.
  */

+ 97 - 36
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.m

@@ -10,7 +10,10 @@
 #import "SDWebImageDownloaderConfig.h"
 #import "SDWebImageDownloaderOperation.h"
 #import "SDWebImageError.h"
+#import "SDWebImageCacheKeyFilter.h"
+#import "SDImageCacheDefine.h"
 #import "SDInternalMacros.h"
+#import "objc/runtime.h"
 
 NSNotificationName const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification";
 NSNotificationName const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification";
@@ -18,6 +21,22 @@ NSNotificationName const SDWebImageDownloadStopNotification = @"SDWebImageDownlo
 NSNotificationName const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification";
 
 static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
+static void * SDWebImageDownloaderOperationKey = &SDWebImageDownloaderOperationKey;
+
+BOOL SDWebImageDownloaderOperationGetCompleted(id<SDWebImageDownloaderOperation> operation) {
+    NSCParameterAssert(operation);
+    NSNumber *value = objc_getAssociatedObject(operation, SDWebImageDownloaderOperationKey);
+    if (value != nil) {
+        return value.boolValue;
+    } else {
+        return NO;
+    }
+}
+
+void SDWebImageDownloaderOperationSetCompleted(id<SDWebImageDownloaderOperation> operation, BOOL isCompleted) {
+    NSCParameterAssert(operation);
+    objc_setAssociatedObject(operation, SDWebImageDownloaderOperationKey, @(isCompleted), OBJC_ASSOCIATION_RETAIN);
+}
 
 @interface SDWebImageDownloadToken ()
 
@@ -40,15 +59,16 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
 @property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
 @property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, NSOperation<SDWebImageDownloaderOperation> *> *URLOperations;
 @property (strong, nonatomic, nullable) NSMutableDictionary<NSString *, NSString *> *HTTPHeaders;
-@property (strong, nonatomic, nonnull) dispatch_semaphore_t HTTPHeadersLock; // A lock to keep the access to `HTTPHeaders` thread-safe
-@property (strong, nonatomic, nonnull) dispatch_semaphore_t operationsLock; // A lock to keep the access to `URLOperations` thread-safe
 
 // The session in which data tasks will run
 @property (strong, nonatomic) NSURLSession *session;
 
 @end
 
-@implementation SDWebImageDownloader
+@implementation SDWebImageDownloader {
+    SD_LOCK_DECLARE(_HTTPHeadersLock); // A lock to keep the access to `HTTPHeaders` thread-safe
+    SD_LOCK_DECLARE(_operationsLock); // A lock to keep the access to `URLOperations` thread-safe
+}
 
 + (void)initialize {
     // Bind SDNetworkActivityIndicator if available (download it here: http://github.com/rs/SDNetworkActivityIndicator )
@@ -96,7 +116,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
         [_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
         _downloadQueue = [NSOperationQueue new];
         _downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
-        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
+        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader.downloadQueue";
         _URLOperations = [NSMutableDictionary new];
         NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary];
         NSString *userAgent = nil;
@@ -120,8 +140,8 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
         }
         headerDictionary[@"Accept"] = @"image/*,*/*;q=0.8";
         _HTTPHeaders = headerDictionary;
-        _HTTPHeadersLock = dispatch_semaphore_create(1);
-        _operationsLock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_HTTPHeadersLock);
+        SD_LOCK_INIT(_operationsLock);
         NSURLSessionConfiguration *sessionConfiguration = _config.sessionConfiguration;
         if (!sessionConfiguration) {
             sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
@@ -139,11 +159,12 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
 }
 
 - (void)dealloc {
-    [self.session invalidateAndCancel];
-    self.session = nil;
-    
     [self.downloadQueue cancelAllOperations];
     [self.config removeObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) context:SDWebImageDownloaderContext];
+    
+    // Invalide the URLSession after all operations been cancelled
+    [self.session invalidateAndCancel];
+    self.session = nil;
 }
 
 - (void)invalidateSessionAndCancel:(BOOL)cancelPendingOperations {
@@ -161,18 +182,18 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
     if (!field) {
         return;
     }
-    SD_LOCK(self.HTTPHeadersLock);
+    SD_LOCK(_HTTPHeadersLock);
     [self.HTTPHeaders setValue:value forKey:field];
-    SD_UNLOCK(self.HTTPHeadersLock);
+    SD_UNLOCK(_HTTPHeadersLock);
 }
 
 - (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field {
     if (!field) {
         return nil;
     }
-    SD_LOCK(self.HTTPHeadersLock);
+    SD_LOCK(_HTTPHeadersLock);
     NSString *value = [self.HTTPHeaders objectForKey:field];
-    SD_UNLOCK(self.HTTPHeadersLock);
+    SD_UNLOCK(_HTTPHeadersLock);
     return value;
 }
 
@@ -202,14 +223,31 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
         return nil;
     }
     
-    SD_LOCK(self.operationsLock);
     id downloadOperationCancelToken;
+    // When different thumbnail size download with same url, we need to make sure each callback called with desired size
+    id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
+    NSString *cacheKey;
+    if (cacheKeyFilter) {
+        cacheKey = [cacheKeyFilter cacheKeyForURL:url];
+    } else {
+        cacheKey = url.absoluteString;
+    }
+    SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey);
+    SD_LOCK(_operationsLock);
     NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
     // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`.
-    if (!operation || operation.isFinished || operation.isCancelled) {
+    BOOL shouldNotReuseOperation;
+    if (operation) {
+        @synchronized (operation) {
+            shouldNotReuseOperation = operation.isFinished || operation.isCancelled || SDWebImageDownloaderOperationGetCompleted(operation);
+        }
+    } else {
+        shouldNotReuseOperation = YES;
+    }
+    if (shouldNotReuseOperation) {
         operation = [self createDownloaderOperationWithUrl:url options:options context:context];
         if (!operation) {
-            SD_UNLOCK(self.operationsLock);
+            SD_UNLOCK(_operationsLock);
             if (completedBlock) {
                 NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
                 completedBlock(nil, nil, error, YES);
@@ -222,13 +260,13 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
             if (!self) {
                 return;
             }
-            SD_LOCK(self.operationsLock);
+            SD_LOCK(self->_operationsLock);
             [self.URLOperations removeObjectForKey:url];
-            SD_UNLOCK(self.operationsLock);
+            SD_UNLOCK(self->_operationsLock);
         };
-        self.URLOperations[url] = operation;
+        [self.URLOperations setObject:operation forKey:url];
         // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers.
-        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
+        downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
         // Add operation to operation queue only after all configuration done according to Apple's doc.
         // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
         [self.downloadQueue addOperation:operation];
@@ -236,19 +274,10 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
         // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue)
         // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes.
         @synchronized (operation) {
-            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
-        }
-        if (!operation.isExecuting) {
-            if (options & SDWebImageDownloaderHighPriority) {
-                operation.queuePriority = NSOperationQueuePriorityHigh;
-            } else if (options & SDWebImageDownloaderLowPriority) {
-                operation.queuePriority = NSOperationQueuePriorityLow;
-            } else {
-                operation.queuePriority = NSOperationQueuePriorityNormal;
-            }
+            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions];
         }
     }
-    SD_UNLOCK(self.operationsLock);
+    SD_UNLOCK(_operationsLock);
     
     SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
     token.url = url;
@@ -258,6 +287,18 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
     return token;
 }
 
+#pragma mark Helper methods
++ (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions {
+    SDWebImageOptions options = 0;
+    if (downloadOptions & SDWebImageDownloaderScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages;
+    if (downloadOptions & SDWebImageDownloaderDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly;
+    if (downloadOptions & SDWebImageDownloaderPreloadAllFrames) options |= SDWebImagePreloadAllFrames;
+    if (downloadOptions & SDWebImageDownloaderAvoidDecodeImage) options |= SDWebImageAvoidDecodeImage;
+    if (downloadOptions & SDWebImageDownloaderMatchAnimatedImageClass) options |= SDWebImageMatchAnimatedImageClass;
+    
+    return options;
+}
+
 - (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url
                                                                                   options:(SDWebImageDownloaderOptions)options
                                                                                   context:(nullable SDWebImageContext *)context {
@@ -271,9 +312,9 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
     NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
     mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
     mutableRequest.HTTPShouldUsePipelining = YES;
-    SD_LOCK(self.HTTPHeadersLock);
+    SD_LOCK(_HTTPHeadersLock);
     mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
-    SD_UNLOCK(self.HTTPHeadersLock);
+    SD_UNLOCK(_HTTPHeadersLock);
     
     // Context Option
     SDWebImageMutableContext *mutableContext;
@@ -328,9 +369,7 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
     
     // Operation Class
     Class operationClass = self.config.operationClass;
-    if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
-        // Custom operation class
-    } else {
+    if (!operationClass) {
         operationClass = [SDWebImageDownloaderOperation class];
     }
     NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
@@ -347,6 +386,14 @@ static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext;
         operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
     }
     
+    if ([operation respondsToSelector:@selector(setAcceptableStatusCodes:)]) {
+        operation.acceptableStatusCodes = self.config.acceptableStatusCodes;
+    }
+    
+    if ([operation respondsToSelector:@selector(setAcceptableContentTypes:)]) {
+        operation.acceptableContentTypes = self.config.acceptableContentTypes;
+    }
+    
     if (options & SDWebImageDownloaderHighPriority) {
         operation.queuePriority = NSOperationQueuePriorityHigh;
     } else if (options & SDWebImageDownloaderLowPriority) {
@@ -468,6 +515,12 @@ didReceiveResponse:(NSURLResponse *)response
     
     // Identify the operation that runs this task and pass it the delegate method
     NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
+    if (dataOperation) {
+        @synchronized (dataOperation) {
+            // Mark the downloader operation `isCompleted = YES`, no longer re-use this operation when new request comes in
+            SDWebImageDownloaderOperationSetCompleted(dataOperation, YES);
+        }
+    }
     if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) {
         [dataOperation URLSession:session task:task didCompleteWithError:error];
     }
@@ -561,6 +614,10 @@ didReceiveResponse:(NSURLResponse *)response
 @implementation SDWebImageDownloader (SDImageLoader)
 
 - (BOOL)canRequestImageForURL:(NSURL *)url {
+    return [self canRequestImageForURL:url options:0 context:nil];
+}
+
+- (BOOL)canRequestImageForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context {
     if (!url) {
         return NO;
     }
@@ -596,6 +653,10 @@ didReceiveResponse:(NSURLResponse *)response
 }
 
 - (BOOL)shouldBlockFailedURLWithURL:(NSURL *)url error:(NSError *)error {
+    return [self shouldBlockFailedURLWithURL:url error:error options:0 context:nil];
+}
+
+- (BOOL)shouldBlockFailedURLWithURL:(NSURL *)url error:(NSError *)error options:(SDWebImageOptions)options context:(SDWebImageContext *)context {
     BOOL shouldBlockFailedURL;
     // Filter the error domain and check error codes
     if ([error.domain isEqualToString:SDWebImageErrorDomain]) {

+ 15 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.h

@@ -95,4 +95,19 @@ typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
  */
 @property (nonatomic, copy, nullable) NSString *password;
 
+/**
+ * Set the acceptable HTTP Response status code. The status code which beyond the range will mark the download operation failed.
+ * For example, if we config [200, 400) but server response is 503, the download will fail with error code `SDWebImageErrorInvalidDownloadStatusCode`.
+ * Defaults to [200,400). Nil means no validation at all.
+ */
+@property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes;
+
+/**
+ * Set the acceptable HTTP Response content type. The content type beyond the set will mark the download operation failed.
+ * For example, if we config ["image/png"] but server response is "application/json", the download will fail with error code `SDWebImageErrorInvalidDownloadContentType`.
+ * Normally you don't need this for image format detection because we use image's data file signature magic bytes: https://en.wikipedia.org/wiki/List_of_file_signatures
+ * Defaults to nil. Nil means no validation at all.
+ */
+@property (nonatomic, copy, nullable) NSSet<NSString *> *acceptableContentTypes;
+
 @end

+ 11 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.m

@@ -7,6 +7,7 @@
  */
 
 #import "SDWebImageDownloaderConfig.h"
+#import "SDWebImageDownloaderOperation.h"
 
 static SDWebImageDownloaderConfig * _defaultDownloaderConfig;
 
@@ -26,6 +27,7 @@ static SDWebImageDownloaderConfig * _defaultDownloaderConfig;
         _maxConcurrentDownloads = 6;
         _downloadTimeout = 15.0;
         _executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
+        _acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)];
     }
     return self;
 }
@@ -41,9 +43,18 @@ static SDWebImageDownloaderConfig * _defaultDownloaderConfig;
     config.urlCredential = self.urlCredential;
     config.username = self.username;
     config.password = self.password;
+    config.acceptableStatusCodes = self.acceptableStatusCodes;
+    config.acceptableContentTypes = self.acceptableContentTypes;
     
     return config;
 }
 
+- (void)setOperationClass:(Class)operationClass {
+    if (operationClass) {
+        NSAssert([operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)], @"Custom downloader operation class must subclass NSOperation and conform to `SDWebImageDownloaderOperation` protocol");
+    }
+    _operationClass = operationClass;
+}
+
 
 @end

+ 39 - 1
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.h

@@ -29,6 +29,10 @@
 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
 
+- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
+                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
+                        decodeOptions:(nullable SDImageCoderOptions *)decodeOptions;
+
 - (BOOL)cancel:(nullable id)token;
 
 @property (strong, nonatomic, readonly, nullable) NSURLRequest *request;
@@ -37,8 +41,12 @@
 @optional
 @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask;
 @property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
+
+// These operation-level config was inherited from downloader. See `SDWebImageDownloaderConfig` for documentation.
 @property (strong, nonatomic, nullable) NSURLCredential *credential;
 @property (assign, nonatomic) double minimumProgressInterval;
+@property (copy, nonatomic, nullable) NSIndexSet *acceptableStatusCodes;
+@property (copy, nonatomic, nullable) NSSet<NSString *> *acceptableContentTypes;
 
 @end
 
@@ -85,6 +93,21 @@
  */
 @property (assign, nonatomic) double minimumProgressInterval;
 
+/**
+ * Set the acceptable HTTP Response status code. The status code which beyond the range will mark the download operation failed.
+ * For example, if we config [200, 400) but server response is 503, the download will fail with error code `SDWebImageErrorInvalidDownloadStatusCode`.
+ * Defaults to [200,400). Nil means no validation at all.
+ */
+@property (copy, nonatomic, nullable) NSIndexSet *acceptableStatusCodes;
+
+/**
+ * Set the acceptable HTTP Response content type. The content type beyond the set will mark the download operation failed.
+ * For example, if we config ["image/png"] but server response is "application/json", the download will fail with error code `SDWebImageErrorInvalidDownloadContentType`.
+ * Normally you don't need this for image format detection because we use image's data file signature magic bytes: https://en.wikipedia.org/wiki/List_of_file_signatures
+ * Defaults to nil. Nil means no validation at all.
+ */
+@property (copy, nonatomic, nullable) NSSet<NSString *> *acceptableContentTypes;
+
 /**
  * The options for the receiver.
  */
@@ -128,7 +151,7 @@
                                 context:(nullable SDWebImageContext *)context NS_DESIGNATED_INITIALIZER;
 
 /**
- *  Adds handlers for progress and completion. Returns a tokent that can be passed to -cancel: to cancel this set of
+ *  Adds handlers for progress and completion. Returns a token that can be passed to -cancel: to cancel this set of
  *  callbacks.
  *
  *  @param progressBlock  the block executed when a new chunk of data arrives.
@@ -141,6 +164,21 @@
 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
 
+/**
+ *  Adds handlers for progress and completion, and optional decode options (which need another image other than the initial one). Returns a token that can be passed to -cancel: to cancel this set of
+ *  callbacks.
+ *
+ *  @param progressBlock  the block executed when a new chunk of data arrives.
+ *                        @note the progress block is executed on a background queue
+ *  @param completedBlock the block executed when the download is done.
+ *                        @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue
+ *  @param decodeOptions The optional decode options, used when in thumbnail decoding for current completion block callback. For example, request <url1, {thumbnail: 100x100}> and then <url1, {thumbnail: 200x200}>, we may callback these two completion block with different size.
+ *  @return the token to use to cancel this set of handlers
+ */
+- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
+                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
+                        decodeOptions:(nullable SDImageCoderOptions *)decodeOptions;
+
 /**
  *  Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled.
  *

+ 255 - 90
Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.m

@@ -11,22 +11,43 @@
 #import "SDInternalMacros.h"
 #import "SDWebImageDownloaderResponseModifier.h"
 #import "SDWebImageDownloaderDecryptor.h"
+#import "SDImageCacheDefine.h"
+#import "SDCallbackQueue.h"
 
-// iOS 8 Foundation.framework extern these symbol but the define is in CFNetwork.framework. We just fix this without import CFNetwork.framework
-#if ((__IPHONE_OS_VERSION_MIN_REQUIRED && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0) || (__MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_11))
-const float NSURLSessionTaskPriorityHigh = 0.75;
-const float NSURLSessionTaskPriorityDefault = 0.5;
-const float NSURLSessionTaskPriorityLow = 0.25;
-#endif
+BOOL SDWebImageDownloaderOperationGetCompleted(id<SDWebImageDownloaderOperation> operation); // Private currently, mark open if needed
+
+// A handler to represent individual request
+@interface SDWebImageDownloaderOperationToken : NSObject
+
+@property (nonatomic, copy, nullable) SDWebImageDownloaderCompletedBlock completedBlock;
+@property (nonatomic, copy, nullable) SDWebImageDownloaderProgressBlock progressBlock;
+@property (nonatomic, copy, nullable) SDImageCoderOptions *decodeOptions;
+
+@end
 
-static NSString *const kProgressCallbackKey = @"progress";
-static NSString *const kCompletedCallbackKey = @"completed";
+@implementation SDWebImageDownloaderOperationToken
 
-typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
+- (BOOL)isEqual:(id)other {
+    if (nil == other) {
+      return NO;
+    }
+    if (self == other) {
+      return YES;
+    }
+    if (![other isKindOfClass:[self class]]) {
+      return NO;
+    }
+    SDWebImageDownloaderOperationToken *object = (SDWebImageDownloaderOperationToken *)other;
+    // warn: only compare decodeOptions, ignore pointer, use `removeObjectIdenticalTo`
+    BOOL result = [self.decodeOptions isEqualToDictionary:object.decodeOptions];
+    return result;
+}
+
+@end
 
 @interface SDWebImageDownloaderOperation ()
 
-@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;
+@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageDownloaderOperationToken *> *callbackTokens;
 
 @property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options;
 @property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context;
@@ -55,6 +76,8 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
 @property (strong, nonatomic, readwrite, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
 
 @property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue; // the serial operation queue to do image decoding
+
+@property (strong, nonatomic, nonnull) NSMapTable<SDImageCoderOptions *, UIImage *> *imageMap; // each variant of image is weak-referenced to avoid too many re-decode during downloading
 #if SD_UIKIT
 @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
 #endif
@@ -82,15 +105,17 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
         _request = [request copy];
         _options = options;
         _context = [context copy];
-        _callbackBlocks = [NSMutableArray new];
+        _callbackTokens = [NSMutableArray new];
         _responseModifier = context[SDWebImageContextDownloadResponseModifier];
         _decryptor = context[SDWebImageContextDownloadDecryptor];
         _executing = NO;
         _finished = NO;
         _expectedSize = 0;
         _unownedSession = session;
-        _coderQueue = [NSOperationQueue new];
+        _coderQueue = [[NSOperationQueue alloc] init];
         _coderQueue.maxConcurrentOperationCount = 1;
+        _coderQueue.name = @"com.hackemist.SDWebImageDownloaderOperation.coderQueue";
+        _imageMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1];
 #if SD_UIKIT
         _backgroundTaskId = UIBackgroundTaskInvalid;
 #endif
@@ -100,33 +125,31 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
 
 - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                             completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
-    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
-    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
-    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
-    @synchronized (self) {
-        [self.callbackBlocks addObject:callbacks];
-    }
-    return callbacks;
+    return [self addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:nil];
 }
 
-- (nullable NSArray<id> *)callbacksForKey:(NSString *)key {
-    NSMutableArray<id> *callbacks;
+- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
+                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock
+                        decodeOptions:(nullable SDImageCoderOptions *)decodeOptions {
+    if (!completedBlock && !progressBlock && !decodeOptions) return nil;
+    SDWebImageDownloaderOperationToken *token = [SDWebImageDownloaderOperationToken new];
+    token.completedBlock = completedBlock;
+    token.progressBlock = progressBlock;
+    token.decodeOptions = decodeOptions;
     @synchronized (self) {
-        callbacks = [[self.callbackBlocks valueForKey:key] mutableCopy];
+        [self.callbackTokens addObject:token];
     }
-    // We need to remove [NSNull null] because there might not always be a progress block for each callback
-    [callbacks removeObjectIdenticalTo:[NSNull null]];
-    return [callbacks copy]; // strip mutability here
+    
+    return token;
 }
 
 - (BOOL)cancel:(nullable id)token {
-    if (!token) return NO;
+    if (![token isKindOfClass:SDWebImageDownloaderOperationToken.class]) return NO;
     
     BOOL shouldCancel = NO;
     @synchronized (self) {
-        NSMutableArray *tempCallbackBlocks = [self.callbackBlocks mutableCopy];
-        [tempCallbackBlocks removeObjectIdenticalTo:token];
-        if (tempCallbackBlocks.count == 0) {
+        NSArray *tokens = self.callbackTokens;
+        if (tokens.count == 1 && [tokens indexOfObjectIdenticalTo:token] != NSNotFound) {
             shouldCancel = YES;
         }
     }
@@ -136,14 +159,9 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
     } else {
         // Only callback this token's completion block
         @synchronized (self) {
-            [self.callbackBlocks removeObjectIdenticalTo:token];
+            [self.callbackTokens removeObjectIdenticalTo:token];
         }
-        SDWebImageDownloaderCompletedBlock completedBlock = [token valueForKey:kCompletedCallbackKey];
-        dispatch_main_async_safe(^{
-            if (completedBlock) {
-                completedBlock(nil, nil, [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}], YES);
-            }
-        });
+        [self callCompletionBlockWithToken:token image:nil imageData:nil error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] finished:YES];
     }
     return shouldCancel;
 }
@@ -151,7 +169,7 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
 - (void)start {
     @synchronized (self) {
         if (self.isCancelled) {
-            self.finished = YES;
+            if (!self.isFinished) self.finished = YES;
             // Operation cancelled by user before sending the request
             [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]];
             [self reset];
@@ -198,9 +216,17 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
             }
             if (cachedResponse) {
                 self.cachedData = cachedResponse.data;
+                self.response = cachedResponse.response;
             }
         }
         
+        if (!session.delegate) {
+            // Session been invalid and has no delegate at all
+            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Session delegate is nil and invalid"}]];
+            [self reset];
+            return;
+        }
+        
         self.dataTask = [session dataTaskWithRequest:self.request];
         self.executing = YES;
     }
@@ -208,25 +234,29 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
     if (self.dataTask) {
         if (self.options & SDWebImageDownloaderHighPriority) {
             self.dataTask.priority = NSURLSessionTaskPriorityHigh;
-            self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
         } else if (self.options & SDWebImageDownloaderLowPriority) {
             self.dataTask.priority = NSURLSessionTaskPriorityLow;
-            self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
         } else {
             self.dataTask.priority = NSURLSessionTaskPriorityDefault;
-            self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
         }
         [self.dataTask resume];
-        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
-            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
+        NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+        @synchronized (self) {
+            tokens = [self.callbackTokens copy];
+        }
+        for (SDWebImageDownloaderOperationToken *token in tokens) {
+            if (token.progressBlock) {
+                token.progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
+            }
         }
         __block typeof(self) strongSelf = self;
         dispatch_async(dispatch_get_main_queue(), ^{
             [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
         });
     } else {
+        if (!self.isFinished) self.finished = YES;
         [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
-        [self done];
+        [self reset];
     }
 }
 
@@ -239,22 +269,28 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
 - (void)cancelInternal {
     if (self.isFinished) return;
     [super cancel];
+    
+    __block typeof(self) strongSelf = self;
+    dispatch_async(dispatch_get_main_queue(), ^{
+        [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
+    });
 
     if (self.dataTask) {
+        // Cancel the URLSession, `URLSession:task:didCompleteWithError:` delegate callback will be ignored
         [self.dataTask cancel];
-        __block typeof(self) strongSelf = self;
-        dispatch_async(dispatch_get_main_queue(), ^{
-            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
-        });
-
-        // As we cancelled the task, its callback won't be called and thus won't
-        // maintain the isFinished and isExecuting flags.
+        self.dataTask = nil;
+    }
+    
+    // NSOperation disallow setFinished=YES **before** operation's start method been called
+    // We check for the initialized status, which is isExecuting == NO && isFinished = NO
+    // Ony update for non-intialized status, which is !(isExecuting == NO && isFinished = NO), or if (self.isExecuting || self.isFinished) {...}
+    if (self.isExecuting || self.isFinished) {
         if (self.isExecuting) self.executing = NO;
         if (!self.isFinished) self.finished = YES;
-    } else {
-        // Operation cancelled by user during sending the request
-        [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}]];
     }
+    
+    // Operation cancelled by user during sending the request
+    [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}]];
 
     [self reset];
 }
@@ -267,7 +303,7 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
 
 - (void)reset {
     @synchronized (self) {
-        [self.callbackBlocks removeAllObjects];
+        [self.callbackTokens removeAllObjects];
         self.dataTask = nil;
         
         if (self.ownedSession) {
@@ -298,7 +334,7 @@ typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;
     [self didChangeValueForKey:@"isExecuting"];
 }
 
-- (BOOL)isConcurrent {
+- (BOOL)isAsynchronous {
     return YES;
 }
 
@@ -316,7 +352,9 @@ didReceiveResponse:(NSURLResponse *)response
         response = [self.responseModifier modifiedResponseWithResponse:response];
         if (!response) {
             valid = NO;
-            self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response is nil"}];
+            self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain
+                                                     code:SDWebImageErrorInvalidDownloadResponse
+                                                 userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response is nil"}];
         }
     }
     
@@ -325,23 +363,53 @@ didReceiveResponse:(NSURLResponse *)response
     self.expectedSize = expected;
     self.response = response;
     
-    NSInteger statusCode = [response respondsToSelector:@selector(statusCode)] ? ((NSHTTPURLResponse *)response).statusCode : 200;
-    // Status code should between [200,400)
-    BOOL statusCodeValid = statusCode >= 200 && statusCode < 400;
+    // Check status code valid (defaults [200,400))
+    NSInteger statusCode = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0;
+    BOOL statusCodeValid = YES;
+    if (valid && statusCode > 0 && self.acceptableStatusCodes) {
+        statusCodeValid = [self.acceptableStatusCodes containsIndex:statusCode];
+    }
     if (!statusCodeValid) {
         valid = NO;
-        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response status code is not in 200-400", SDWebImageErrorDownloadStatusCodeKey : @(statusCode)}];
+        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain
+                                                 code:SDWebImageErrorInvalidDownloadStatusCode
+                                             userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Download marked as failed because of invalid response status code %ld", (long)statusCode],
+                                                        SDWebImageErrorDownloadStatusCodeKey : @(statusCode),
+                                                        SDWebImageErrorDownloadResponseKey : response}];
+    }
+    // Check content type valid (defaults nil)
+    NSString *contentType = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).MIMEType : nil;
+    BOOL contentTypeValid = YES;
+    if (valid && contentType.length > 0 && self.acceptableContentTypes) {
+        contentTypeValid = [self.acceptableContentTypes containsObject:contentType];
+    }
+    if (!contentTypeValid) {
+        valid = NO;
+        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain
+                                                 code:SDWebImageErrorInvalidDownloadContentType
+                                             userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Download marked as failed because of invalid response content type %@", contentType],
+                                                        SDWebImageErrorDownloadContentTypeKey : contentType,
+                                                        SDWebImageErrorDownloadResponseKey : response}];
     }
     //'304 Not Modified' is an exceptional one
     //URLSession current behavior will return 200 status code when the server respond 304 and URLCache hit. But this is not a standard behavior and we just add a check
-    if (statusCode == 304 && !self.cachedData) {
+    if (valid && statusCode == 304 && !self.cachedData) {
         valid = NO;
-        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Download response status code is 304 not modified and ignored"}];
+        self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain
+                                                 code:SDWebImageErrorCacheNotModified
+                                             userInfo:@{NSLocalizedDescriptionKey: @"Download response status code is 304 not modified and ignored",
+                                                        SDWebImageErrorDownloadResponseKey : response}];
     }
     
     if (valid) {
-        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
-            progressBlock(0, expected, self.request.URL);
+        NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+        @synchronized (self) {
+            tokens = [self.callbackTokens copy];
+        }
+        for (SDWebImageDownloaderOperationToken *token in tokens) {
+            if (token.progressBlock) {
+                token.progressBlock(0, expected, self.request.URL);
+            }
         }
     } else {
         // Status code invalid and marked as cancelled. Do not call `[self.dataTask cancel]` which may mass up URLSession life cycle
@@ -364,10 +432,16 @@ didReceiveResponse:(NSURLResponse *)response
     [self.imageData appendData:data];
     
     self.receivedSize = self.imageData.length;
+    NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+    @synchronized (self) {
+        tokens = [self.callbackTokens copy];
+    }
     if (self.expectedSize == 0) {
         // Unknown expectedSize, immediately call progressBlock and return
-        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
-            progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
+        for (SDWebImageDownloaderOperationToken *token in tokens) {
+            if (token.progressBlock) {
+                token.progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
+            }
         }
         return;
     }
@@ -386,15 +460,29 @@ didReceiveResponse:(NSURLResponse *)response
     
     // Using data decryptor will disable the progressive decoding, since there are no support for progressive decrypt
     BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor;
-    if (supportProgressive) {
+    // When multiple thumbnail decoding use different size, this progressive decoding will cause issue because each callback assume called with different size's image, can not share the same decoding part
+    // We currently only pick the first thumbnail size, see #3423 talks
+    // Progressive decoding Only decode partial image, full image in `URLSession:task:didCompleteWithError:`
+    if (supportProgressive && !finished) {
         // Get the image data
-        NSData *imageData = [self.imageData copy];
+        NSData *imageData = self.imageData;
         
         // keep maximum one progressive decode process during download
         if (self.coderQueue.operationCount == 0) {
             // NSOperation have autoreleasepool, don't need to create extra one
+            @weakify(self);
             [self.coderQueue addOperationWithBlock:^{
-                UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, finished, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
+                @strongify(self);
+                if (!self) {
+                    return;
+                }
+                // When cancelled or transfer finished (`didCompleteWithError`), cancel the progress callback, only completed block is called and enough
+                @synchronized (self) {
+                    if (self.isCancelled || SDWebImageDownloaderOperationGetCompleted(self)) {
+                        return;
+                    }
+                }
+                UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
                 if (image) {
                     // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding.
                     
@@ -404,8 +492,10 @@ didReceiveResponse:(NSURLResponse *)response
         }
     }
     
-    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
-        progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
+    for (SDWebImageDownloaderOperationToken *token in tokens) {
+        if (token.progressBlock) {
+            token.progressBlock(self.receivedSize, self.expectedSize, self.request.URL);
+        }
     }
 }
 
@@ -431,7 +521,9 @@ didReceiveResponse:(NSURLResponse *)response
     // If we already cancel the operation or anything mark the operation finished, don't callback twice
     if (self.isFinished) return;
     
-    @synchronized(self) {
+    NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+    @synchronized (self) {
+        tokens = [self.callbackTokens copy];
         self.dataTask = nil;
         __block typeof(self) strongSelf = self;
         dispatch_async(dispatch_get_main_queue(), ^{
@@ -451,8 +543,8 @@ didReceiveResponse:(NSURLResponse *)response
         [self callCompletionBlocksWithError:error];
         [self done];
     } else {
-        if ([self callbacksForKey:kCompletedCallbackKey].count > 0) {
-            NSData *imageData = [self.imageData copy];
+        if (tokens.count > 0) {
+            NSData *imageData = self.imageData;
             self.imageData = nil;
             // data decryptor
             if (imageData && self.decryptor) {
@@ -463,24 +555,74 @@ didReceiveResponse:(NSURLResponse *)response
                  *  then we should check if the cached data is equal to image data
                  */
                 if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
-                    self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored"}];
+                    self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain
+                                                             code:SDWebImageErrorCacheNotModified
+                                                         userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored",
+                                                                    SDWebImageErrorDownloadResponseKey : self.response}];
                     // call completion block with not modified error
                     [self callCompletionBlocksWithError:self.responseError];
                     [self done];
                 } else {
                     // decode the image in coder queue, cancel all previous decoding process
                     [self.coderQueue cancelAllOperations];
-                    [self.coderQueue addOperationWithBlock:^{
-                        UIImage *image = SDImageLoaderDecodeImageData(imageData, self.request.URL, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context);
-                        CGSize imageSize = image.size;
-                        if (imageSize.width == 0 || imageSize.height == 0) {
-                            NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
-                            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}]];
-                        } else {
-                            [self callCompletionBlocksWithImage:image imageData:imageData error:nil finished:YES];
+                    @weakify(self);
+                    for (SDWebImageDownloaderOperationToken *token in tokens) {
+                        [self.coderQueue addOperationWithBlock:^{
+                            @strongify(self);
+                            if (!self) {
+                                return;
+                            }
+                            UIImage *image;
+                            // check if we already decode this variant of image for current callback
+                            if (token.decodeOptions) {
+                                image = [self.imageMap objectForKey:token.decodeOptions];
+                            }
+                            if (!image) {
+                                // check if we already use progressive decoding, use that to produce faster decoding
+                                id<SDProgressiveImageCoder> progressiveCoder = SDImageLoaderGetProgressiveCoder(self);
+                                SDWebImageOptions options = [[self class] imageOptionsFromDownloaderOptions:self.options];
+                                SDWebImageContext *context;
+                                if (token.decodeOptions) {
+                                    SDWebImageMutableContext *mutableContext = [NSMutableDictionary dictionaryWithDictionary:self.context];
+                                    SDSetDecodeOptionsToContext(mutableContext, &options, token.decodeOptions);
+                                    context = [mutableContext copy];
+                                } else {
+                                    context = self.context;
+                                }
+                                if (progressiveCoder) {
+                                    image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, YES, self, options, context);
+                                } else {
+                                    image = SDImageLoaderDecodeImageData(imageData, self.request.URL, options, context);
+                                }
+                                if (image && token.decodeOptions) {
+                                    [self.imageMap setObject:image forKey:token.decodeOptions];
+                                }
+                            }
+                            CGSize imageSize = image.size;
+                            if (imageSize.width == 0 || imageSize.height == 0) {
+                                NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels";
+                                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}];
+                                [self callCompletionBlockWithToken:token image:nil imageData:nil error:error finished:YES];
+                            } else {
+                                [self callCompletionBlockWithToken:token image:image imageData:imageData error:nil finished:YES];
+                            }
+                        }];
+                    }
+                    // call [self done] after all completed block was dispatched
+                    dispatch_block_t doneBlock = ^{
+                        @strongify(self);
+                        if (!self) {
+                            return;
                         }
                         [self done];
-                    }];
+                    };
+                    if (@available(iOS 13, tvOS 13, macOS 10.15, watchOS 6, *)) {
+                        // seems faster than `addOperationWithBlock`
+                        [self.coderQueue addBarrierBlock:doneBlock];
+                    } else {
+                        // serial queue, this does the same effect in semantics
+                        [self.coderQueue addOperationWithBlock:doneBlock];
+                    }
                 }
             } else {
                 [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]];
@@ -510,7 +652,9 @@ didReceiveResponse:(NSURLResponse *)response
                 credential = self.credential;
                 disposition = NSURLSessionAuthChallengeUseCredential;
             } else {
-                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
+                // Web Server like Nginx can set `ssl_verify_client` to optional but not always on
+                // We'd better use default handling here
+                disposition = NSURLSessionAuthChallengePerformDefaultHandling;
             }
         } else {
             disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
@@ -550,12 +694,33 @@ didReceiveResponse:(NSURLResponse *)response
                             imageData:(nullable NSData *)imageData
                                 error:(nullable NSError *)error
                              finished:(BOOL)finished {
-    NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
-    dispatch_main_async_safe(^{
-        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
-            completedBlock(image, imageData, error, finished);
+    NSArray<SDWebImageDownloaderOperationToken *> *tokens;
+    @synchronized (self) {
+        tokens = [self.callbackTokens copy];
+    }
+    for (SDWebImageDownloaderOperationToken *token in tokens) {
+        SDWebImageDownloaderCompletedBlock completedBlock = token.completedBlock;
+        if (completedBlock) {
+            SDCallbackQueue *queue = self.context[SDWebImageContextCallbackQueue];
+            [(queue ?: SDCallbackQueue.mainQueue) async:^{
+                completedBlock(image, imageData, error, finished);
+            }];
         }
-    });
+    }
+}
+
+- (void)callCompletionBlockWithToken:(nonnull SDWebImageDownloaderOperationToken *)token
+                               image:(nullable UIImage *)image
+                           imageData:(nullable NSData *)imageData
+                               error:(nullable NSError *)error
+                            finished:(BOOL)finished {
+    SDWebImageDownloaderCompletedBlock completedBlock = token.completedBlock;
+    if (completedBlock) {
+        SDCallbackQueue *queue = self.context[SDWebImageContextCallbackQueue];
+        [(queue ?: SDCallbackQueue.mainQueue) async:^{
+            completedBlock(image, imageData, error, finished);
+        }];
+    }
 }
 
 @end

+ 5 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageError.h

@@ -11,8 +11,12 @@
 
 FOUNDATION_EXPORT NSErrorDomain const _Nonnull SDWebImageErrorDomain;
 
+/// The response instance for invalid download response (NSURLResponse *)
+FOUNDATION_EXPORT NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadResponseKey;
 /// The HTTP status code for invalid download response (NSNumber *)
 FOUNDATION_EXPORT NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadStatusCodeKey;
+/// The HTTP MIME content type for invalid download response (NSString *)
+FOUNDATION_EXPORT NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadContentTypeKey;
 
 /// SDWebImage error domain and codes
 typedef NS_ERROR_ENUM(SDWebImageErrorDomain, SDWebImageError) {
@@ -24,4 +28,5 @@ typedef NS_ERROR_ENUM(SDWebImageErrorDomain, SDWebImageError) {
     SDWebImageErrorInvalidDownloadStatusCode = 2001, // The image download response a invalid status code. You can check the status code in error's userInfo under `SDWebImageErrorDownloadStatusCodeKey`
     SDWebImageErrorCancelled = 2002, // The image loading operation is cancelled before finished, during either async disk cache query, or waiting before actual network request. For actual network request error, check `NSURLErrorDomain` error domain and code.
     SDWebImageErrorInvalidDownloadResponse = 2003, // When using response modifier, the modified download response is nil and marked as failed.
+    SDWebImageErrorInvalidDownloadContentType = 2004, // The image download response a invalid content type. You can check the MIME content type in error's userInfo under `SDWebImageErrorDownloadContentTypeKey`
 };

+ 3 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageError.m

@@ -10,4 +10,7 @@
 #import "SDWebImageError.h"
 
 NSErrorDomain const _Nonnull SDWebImageErrorDomain = @"SDWebImageErrorDomain";
+
+NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadResponseKey = @"SDWebImageErrorDownloadResponseKey";
 NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadStatusCodeKey = @"SDWebImageErrorDownloadStatusCodeKey";
+NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadContentTypeKey = @"SDWebImageErrorDownloadContentTypeKey";

+ 1 - 10
Pods/SDWebImage/SDWebImage/Core/SDWebImageIndicator.m

@@ -12,16 +12,7 @@
 
 #if SD_MAC
 #import <QuartzCore/QuartzCore.h>
-#endif
-
-#if SD_UIKIT
-#if __IPHONE_13_0 || __TVOS_13_0 || __MAC_10_15
-// Xcode 11
-#else
-// Supports Xcode 10 users, for those users, define these enum
-static NSInteger UIActivityIndicatorViewStyleMedium = 100;
-static NSInteger UIActivityIndicatorViewStyleLarge = 101;
-#endif
+#import <CoreImage/CIFilter.h>
 #endif
 
 #pragma mark - Activity Indicator

+ 3 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.h

@@ -29,6 +29,9 @@ typedef void(^SDInternalCompletionBlock)(UIImage * _Nullable image, NSData * _Nu
  */
 - (void)cancel;
 
+/// Whether the operation has been cancelled.
+@property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled;
+
 /**
  The cache operation from the image cache query
  */

+ 278 - 196
Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.m

@@ -13,6 +13,7 @@
 #import "SDAssociatedObject.h"
 #import "SDWebImageError.h"
 #import "SDInternalMacros.h"
+#import "SDCallbackQueue.h"
 
 static id<SDImageCache> _defaultImageCache;
 static id<SDImageLoader> _defaultImageLoader;
@@ -26,14 +27,15 @@ static id<SDImageLoader> _defaultImageLoader;
 
 @end
 
-@interface SDWebImageManager ()
+@interface SDWebImageManager () {
+    SD_LOCK_DECLARE(_failedURLsLock); // a lock to keep the access to `failedURLs` thread-safe
+    SD_LOCK_DECLARE(_runningOperationsLock); // a lock to keep the access to `runningOperations` thread-safe
+}
 
 @property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;
 @property (strong, nonatomic, readwrite, nonnull) id<SDImageLoader> imageLoader;
 @property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
-@property (strong, nonatomic, nonnull) dispatch_semaphore_t failedURLsLock; // a lock to keep the access to `failedURLs` thread-safe
 @property (strong, nonatomic, nonnull) NSMutableSet<SDWebImageCombinedOperation *> *runningOperations;
-@property (strong, nonatomic, nonnull) dispatch_semaphore_t runningOperationsLock; // a lock to keep the access to `runningOperations` thread-safe
 
 @end
 
@@ -87,9 +89,9 @@ static id<SDImageLoader> _defaultImageLoader;
         _imageCache = cache;
         _imageLoader = loader;
         _failedURLs = [NSMutableSet new];
-        _failedURLsLock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_failedURLsLock);
         _runningOperations = [NSMutableSet new];
-        _runningOperationsLock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_runningOperationsLock);
     }
     return self;
 }
@@ -111,6 +113,26 @@ static id<SDImageLoader> _defaultImageLoader;
     return key;
 }
 
+- (nullable NSString *)originalCacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context {
+    if (!url) {
+        return @"";
+    }
+    
+    NSString *key;
+    // Cache Key Filter
+    id<SDWebImageCacheKeyFilter> cacheKeyFilter = self.cacheKeyFilter;
+    if (context[SDWebImageContextCacheKeyFilter]) {
+        cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
+    }
+    if (cacheKeyFilter) {
+        key = [cacheKeyFilter cacheKeyForURL:url];
+    } else {
+        key = url.absoluteString;
+    }
+    
+    return key;
+}
+
 - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context {
     if (!url) {
         return @"";
@@ -149,7 +171,7 @@ static id<SDImageLoader> _defaultImageLoader;
     id<SDImageTransformer> transformer = self.transformer;
     if (context[SDWebImageContextImageTransformer]) {
         transformer = context[SDWebImageContextImageTransformer];
-        if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
+        if ([transformer isEqual:NSNull.null]) {
             transformer = nil;
         }
     }
@@ -188,43 +210,55 @@ static id<SDImageLoader> _defaultImageLoader;
 
     BOOL isFailedUrl = NO;
     if (url) {
-        SD_LOCK(self.failedURLsLock);
+        SD_LOCK(_failedURLsLock);
         isFailedUrl = [self.failedURLs containsObject:url];
-        SD_UNLOCK(self.failedURLsLock);
+        SD_UNLOCK(_failedURLsLock);
     }
+    
+    // Preprocess the options and context arg to decide the final the result for manager
+    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
 
     if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
         NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
         NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
-        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
+        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] queue:result.context[SDWebImageContextCallbackQueue] url:url];
         return operation;
     }
 
-    SD_LOCK(self.runningOperationsLock);
+    SD_LOCK(_runningOperationsLock);
     [self.runningOperations addObject:operation];
-    SD_UNLOCK(self.runningOperationsLock);
+    SD_UNLOCK(_runningOperationsLock);
     
-    // Preprocess the options and context arg to decide the final the result for manager
-    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
+    // Start the entry to load image from cache, the longest steps are below
+    // Steps without transformer:
+    // 1. query image from cache, miss
+    // 2. download data and image
+    // 3. store image to cache
     
-    // Start the entry to load image from cache
+    // Steps with transformer:
+    // 1. query transformed image from cache, miss
+    // 2. query original image from cache, miss
+    // 3. download data and image
+    // 4. do transform in CPU
+    // 5. store original image to cache
+    // 6. store transformed image to cache
     [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
 
     return operation;
 }
 
 - (void)cancelAll {
-    SD_LOCK(self.runningOperationsLock);
+    SD_LOCK(_runningOperationsLock);
     NSSet<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
-    SD_UNLOCK(self.runningOperationsLock);
+    SD_UNLOCK(_runningOperationsLock);
     [copiedOperations makeObjectsPerformSelector:@selector(cancel)]; // This will call `safelyRemoveOperationFromRunning:` and remove from the array
 }
 
 - (BOOL)isRunning {
     BOOL isRunning = NO;
-    SD_LOCK(self.runningOperationsLock);
+    SD_LOCK(_runningOperationsLock);
     isRunning = (self.runningOperations.count > 0);
-    SD_UNLOCK(self.runningOperationsLock);
+    SD_UNLOCK(_runningOperationsLock);
     return isRunning;
 }
 
@@ -232,15 +266,15 @@ static id<SDImageLoader> _defaultImageLoader;
     if (!url) {
         return;
     }
-    SD_LOCK(self.failedURLsLock);
+    SD_LOCK(_failedURLsLock);
     [self.failedURLs removeObject:url];
-    SD_UNLOCK(self.failedURLsLock);
+    SD_UNLOCK(_failedURLsLock);
 }
 
 - (void)removeAllFailedURLs {
-    SD_LOCK(self.failedURLsLock);
+    SD_LOCK(_failedURLsLock);
     [self.failedURLs removeAllObjects];
-    SD_UNLOCK(self.failedURLsLock);
+    SD_UNLOCK(_failedURLsLock);
 }
 
 #pragma mark - Private
@@ -253,13 +287,10 @@ static id<SDImageLoader> _defaultImageLoader;
                             progress:(nullable SDImageLoaderProgressBlock)progressBlock
                            completed:(nullable SDInternalCompletionBlock)completedBlock {
     // Grab the image cache to use
-    id<SDImageCache> imageCache;
-    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
-        imageCache = context[SDWebImageContextImageCache];
-    } else {
+    id<SDImageCache> imageCache = context[SDWebImageContextImageCache];
+    if (!imageCache) {
         imageCache = self.imageCache;
     }
-    
     // Get the query cache type
     SDImageCacheType queryCacheType = SDImageCacheTypeAll;
     if (context[SDWebImageContextQueryCacheType]) {
@@ -269,21 +300,26 @@ static id<SDImageLoader> _defaultImageLoader;
     // Check whether we should query cache
     BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
     if (shouldQueryCache) {
+        // transformed cache key
         NSString *key = [self cacheKeyForURL:url context:context];
         @weakify(operation);
         operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
             @strongify(operation);
             if (!operation || operation.isCancelled) {
                 // Image combined operation cancelled by user
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url];
                 [self safelyRemoveOperationFromRunning:operation];
                 return;
-            } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
-                // Have a chance to query original cache instead of downloading
-                [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
-                return;
+            } else if (!cachedImage) {
+                NSString *originKey = [self originalCacheKeyForURL:url context:context];
+                BOOL mayInOriginalCache = ![key isEqualToString:originKey];
+                // Have a chance to query original cache instead of downloading, then applying transform
+                // Thumbnail decoding is done inside SDImageCache's decoding part, which does not need post processing for transform
+                if (mayInOriginalCache) {
+                    [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
+                    return;
+                }
             }
-            
             // Continue download process
             [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
         }];
@@ -300,16 +336,17 @@ static id<SDImageLoader> _defaultImageLoader;
                                      context:(nullable SDWebImageContext *)context
                                     progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                    completed:(nullable SDInternalCompletionBlock)completedBlock {
-    // Grab the image cache to use
-    id<SDImageCache> imageCache;
-    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
+    // Grab the image cache to use, choose standalone original cache firstly
+    id<SDImageCache> imageCache = context[SDWebImageContextOriginalImageCache];
+    if (!imageCache) {
+        // if no standalone cache available, use default cache
         imageCache = context[SDWebImageContextImageCache];
-    } else {
-        imageCache = self.imageCache;
+        if (!imageCache) {
+            imageCache = self.imageCache;
+        }
     }
-    
     // Get the original query cache type
-    SDImageCacheType originalQueryCacheType = SDImageCacheTypeNone;
+    SDImageCacheType originalQueryCacheType = SDImageCacheTypeDisk;
     if (context[SDWebImageContextOriginalQueryCacheType]) {
         originalQueryCacheType = [context[SDWebImageContextOriginalQueryCacheType] integerValue];
     }
@@ -317,42 +354,30 @@ static id<SDImageLoader> _defaultImageLoader;
     // Check whether we should query original cache
     BOOL shouldQueryOriginalCache = (originalQueryCacheType != SDImageCacheTypeNone);
     if (shouldQueryOriginalCache) {
-        // Change originContext to mutable
-        SDWebImageMutableContext * __block originContext;
-        if (context) {
-            originContext = [context mutableCopy];
-        } else {
-            originContext = [NSMutableDictionary dictionary];
-        }
-        
-        // Disable transformer for cache key generation
-        id<SDImageTransformer> transformer = originContext[SDWebImageContextImageTransformer];
-        originContext[SDWebImageContextImageTransformer] = [NSNull null];
-        
-        NSString *key = [self cacheKeyForURL:url context:originContext];
+        // Get original cache key generation without transformer
+        NSString *key = [self originalCacheKeyForURL:url context:context];
         @weakify(operation);
         operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:originalQueryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
             @strongify(operation);
             if (!operation || operation.isCancelled) {
                 // Image combined operation cancelled by user
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url];
                 [self safelyRemoveOperationFromRunning:operation];
                 return;
+            } else if (!cachedImage) {
+                // Original image cache miss. Continue download process
+                [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
+                return;
             }
-            
-            // Add original transformer
-            if (transformer) {
-                originContext[SDWebImageContextImageTransformer] = transformer;
-            }
-            
-            // Use the store cache process instead of downloading, and ignore .refreshCached option for now
-            [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:cachedImage downloadedData:cachedData finished:YES progress:progressBlock completed:completedBlock];
+                        
+            // Skip downloading and continue transform process, and ignore .refreshCached option for now
+            [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:cachedImage originalData:cachedData cacheType:cacheType finished:YES completed:completedBlock];
             
             [self safelyRemoveOperationFromRunning:operation];
         }];
     } else {
         // Continue download process
-        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:originalQueryCacheType progress:progressBlock completed:completedBlock];
+        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
     }
 }
 
@@ -366,11 +391,14 @@ static id<SDImageLoader> _defaultImageLoader;
                               cacheType:(SDImageCacheType)cacheType
                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
                               completed:(nullable SDInternalCompletionBlock)completedBlock {
+    // Mark the cache operation end
+    @synchronized (operation) {
+        operation.cacheOperation = nil;
+    }
+    
     // Grab the image loader to use
-    id<SDImageLoader> imageLoader;
-    if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
-        imageLoader = context[SDWebImageContextImageLoader];
-    } else {
+    id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];
+    if (!imageLoader) {
         imageLoader = self.imageLoader;
     }
     
@@ -378,12 +406,16 @@ static id<SDImageLoader> _defaultImageLoader;
     BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
     shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
     shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
-    shouldDownload &= [imageLoader canRequestImageForURL:url];
+    if ([imageLoader respondsToSelector:@selector(canRequestImageForURL:options:context:)]) {
+        shouldDownload &= [imageLoader canRequestImageForURL:url options:options context:context];
+    } else {
+        shouldDownload &= [imageLoader canRequestImageForURL:url];
+    }
     if (shouldDownload) {
         if (cachedImage && options & SDWebImageRefreshCached) {
             // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
             // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
-            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
+            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
             // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image.
             SDWebImageMutableContext *mutableContext;
             if (context) {
@@ -400,29 +432,29 @@ static id<SDImageLoader> _defaultImageLoader;
             @strongify(operation);
             if (!operation || operation.isCancelled) {
                 // Image combined operation cancelled by user
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] queue:context[SDWebImageContextCallbackQueue] url:url];
             } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
                 // Image refresh hit the NSURLCache cache, do not call the completion block
             } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) {
                 // Download operation cancelled by user before sending the request, don't block failed URL
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];
             } else if (error) {
-                [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
+                [self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url];
                 BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context];
                 
                 if (shouldBlockFailedURL) {
-                    SD_LOCK(self.failedURLsLock);
+                    SD_LOCK(self->_failedURLsLock);
                     [self.failedURLs addObject:url];
-                    SD_UNLOCK(self.failedURLsLock);
+                    SD_UNLOCK(self->_failedURLsLock);
                 }
             } else {
                 if ((options & SDWebImageRetryFailed)) {
-                    SD_LOCK(self.failedURLsLock);
+                    SD_LOCK(self->_failedURLsLock);
                     [self.failedURLs removeObject:url];
-                    SD_UNLOCK(self.failedURLsLock);
+                    SD_UNLOCK(self->_failedURLsLock);
                 }
-                // Continue store cache process
-                [self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
+                // Continue transform process
+                [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock];
             }
             
             if (finished) {
@@ -430,127 +462,152 @@ static id<SDImageLoader> _defaultImageLoader;
             }
         }];
     } else if (cachedImage) {
-        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
+        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
         [self safelyRemoveOperationFromRunning:operation];
     } else {
         // Image not in cache and download disallowed by delegate
-        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
+        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES queue:context[SDWebImageContextCallbackQueue] url:url];
         [self safelyRemoveOperationFromRunning:operation];
     }
 }
 
-// Store cache process
-- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
-                                      url:(nonnull NSURL *)url
-                                  options:(SDWebImageOptions)options
-                                  context:(SDWebImageContext *)context
-                          downloadedImage:(nullable UIImage *)downloadedImage
-                           downloadedData:(nullable NSData *)downloadedData
-                                 finished:(BOOL)finished
-                                 progress:(nullable SDImageLoaderProgressBlock)progressBlock
-                                completed:(nullable SDInternalCompletionBlock)completedBlock {
-    // the target image store cache type
-    SDImageCacheType storeCacheType = SDImageCacheTypeAll;
-    if (context[SDWebImageContextStoreCacheType]) {
-        storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
+// Transform process
+- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
+                                     url:(nonnull NSURL *)url
+                                 options:(SDWebImageOptions)options
+                                 context:(SDWebImageContext *)context
+                           originalImage:(nullable UIImage *)originalImage
+                            originalData:(nullable NSData *)originalData
+                               cacheType:(SDImageCacheType)cacheType
+                                finished:(BOOL)finished
+                               completed:(nullable SDInternalCompletionBlock)completedBlock {
+    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
+    if ([transformer isEqual:NSNull.null]) {
+        transformer = nil;
+    }
+    // transformer check
+    BOOL shouldTransformImage = originalImage && transformer;
+    shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
+    shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage));
+    // thumbnail check
+    BOOL isThumbnail = originalImage.sd_isThumbnail;
+    NSData *cacheData = originalData;
+    UIImage *cacheImage = originalImage;
+    if (isThumbnail) {
+        cacheData = nil; // thumbnail don't store full size data
+        originalImage = nil; // thumbnail don't have full size image
+    }
+    
+    if (shouldTransformImage) {
+        // transformed cache key
+        NSString *key = [self cacheKeyForURL:url context:context];
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            // Case that transformer on thumbnail, which this time need full pixel image
+            UIImage *transformedImage = [transformer transformedImageWithImage:cacheImage forKey:key];
+            if (transformedImage) {
+                transformedImage.sd_isTransformed = YES;
+                [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:transformedImage originalData:originalData cacheData:nil cacheType:cacheType finished:finished completed:completedBlock];
+            } else {
+                [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:cacheImage originalData:originalData cacheData:cacheData cacheType:cacheType finished:finished completed:completedBlock];
+            }
+        });
+    } else {
+        [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:cacheImage originalData:originalData cacheData:cacheData cacheType:cacheType finished:finished completed:completedBlock];
+    }
+}
+
+// Store origin cache process
+- (void)callStoreOriginCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
+                                            url:(nonnull NSURL *)url
+                                        options:(SDWebImageOptions)options
+                                        context:(SDWebImageContext *)context
+                                  originalImage:(nullable UIImage *)originalImage
+                                     cacheImage:(nullable UIImage *)cacheImage
+                                   originalData:(nullable NSData *)originalData
+                                      cacheData:(nullable NSData *)cacheData
+                                      cacheType:(SDImageCacheType)cacheType
+                                       finished:(BOOL)finished
+                                      completed:(nullable SDInternalCompletionBlock)completedBlock {
+    // Grab the image cache to use, choose standalone original cache firstly
+    id<SDImageCache> imageCache = context[SDWebImageContextOriginalImageCache];
+    if (!imageCache) {
+        // if no standalone cache available, use default cache
+        imageCache = context[SDWebImageContextImageCache];
+        if (!imageCache) {
+            imageCache = self.imageCache;
+        }
     }
     // the original store image cache type
-    SDImageCacheType originalStoreCacheType = SDImageCacheTypeNone;
+    SDImageCacheType originalStoreCacheType = SDImageCacheTypeDisk;
     if (context[SDWebImageContextOriginalStoreCacheType]) {
         originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue];
     }
-    // origin cache key
-    SDWebImageMutableContext *originContext = [context mutableCopy];
-    // disable transformer for cache key generation
-    originContext[SDWebImageContextImageTransformer] = [NSNull null];
-    NSString *key = [self cacheKeyForURL:url context:originContext];
-    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
-    if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
-        transformer = nil;
-    }
     id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
     
-    BOOL shouldTransformImage = downloadedImage && transformer;
-    shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
-    shouldTransformImage = shouldTransformImage && (!downloadedImage.sd_isVector || (options & SDWebImageTransformVectorImage));
-    BOOL shouldCacheOriginal = downloadedImage && finished;
+    // If the original cacheType is disk, since we don't need to store the original data again
+    // Strip the disk from the originalStoreCacheType
+    if (cacheType == SDImageCacheTypeDisk) {
+        if (originalStoreCacheType == SDImageCacheTypeDisk) originalStoreCacheType = SDImageCacheTypeNone;
+        if (originalStoreCacheType == SDImageCacheTypeAll) originalStoreCacheType = SDImageCacheTypeMemory;
+    }
     
-    // if available, store original image to cache
-    if (shouldCacheOriginal) {
-        // normally use the store cache type, but if target image is transformed, use original store cache type instead
-        SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
-        if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
-            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
-                @autoreleasepool {
-                    NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
-                    [self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
-                        // Continue transform process
-                        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
-                    }];
-                }
-            });
-        } else {
-            [self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
-                // Continue transform process
-                [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
+    // Get original cache key generation without transformer
+    NSString *key = [self originalCacheKeyForURL:url context:context];
+    if (finished && cacheSerializer && (originalStoreCacheType == SDImageCacheTypeDisk || originalStoreCacheType == SDImageCacheTypeAll)) {
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            NSData *newOriginalData = [cacheSerializer cacheDataWithImage:originalImage originalData:originalData imageURL:url];
+            // Store original image and data
+            [self storeImage:originalImage imageData:newOriginalData forKey:key options:options context:context imageCache:imageCache cacheType:originalStoreCacheType finished:finished completion:^{
+                // Continue store cache process, transformed data is nil
+                [self callStoreCacheProcessForOperation:operation url:url options:options context:context image:cacheImage data:cacheData cacheType:cacheType finished:finished completed:completedBlock];
             }];
-        }
+        });
     } else {
-        // Continue transform process
-        [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
+        // Store original image and data
+        [self storeImage:originalImage imageData:originalData forKey:key options:options context:context imageCache:imageCache cacheType:originalStoreCacheType finished:finished completion:^{
+            // Continue store cache process, transformed data is nil
+            [self callStoreCacheProcessForOperation:operation url:url options:options context:context image:cacheImage data:cacheData cacheType:cacheType finished:finished completed:completedBlock];
+        }];
     }
 }
 
-// Transform process
-- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
-                                     url:(nonnull NSURL *)url
-                                 options:(SDWebImageOptions)options
-                                 context:(SDWebImageContext *)context
-                           originalImage:(nullable UIImage *)originalImage
-                            originalData:(nullable NSData *)originalData
-                                finished:(BOOL)finished
-                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
-                               completed:(nullable SDInternalCompletionBlock)completedBlock {
+// Store normal cache process
+- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
+                                      url:(nonnull NSURL *)url
+                                  options:(SDWebImageOptions)options
+                                  context:(SDWebImageContext *)context
+                                    image:(nullable UIImage *)image
+                                     data:(nullable NSData *)data
+                                cacheType:(SDImageCacheType)cacheType
+                                 finished:(BOOL)finished
+                                completed:(nullable SDInternalCompletionBlock)completedBlock {
+    // Grab the image cache to use
+    id<SDImageCache> imageCache = context[SDWebImageContextImageCache];
+    if (!imageCache) {
+        imageCache = self.imageCache;
+    }
     // the target image store cache type
     SDImageCacheType storeCacheType = SDImageCacheTypeAll;
     if (context[SDWebImageContextStoreCacheType]) {
         storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
     }
-    // transformed cache key
-    NSString *key = [self cacheKeyForURL:url context:context];
-    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
-    if (![transformer conformsToProtocol:@protocol(SDImageTransformer)]) {
-        transformer = nil;
-    }
     id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
     
-    BOOL shouldTransformImage = originalImage && transformer;
-    shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage));
-    shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage));
-    // if available, store transformed image to cache
-    if (shouldTransformImage) {
+    // transformed cache key
+    NSString *key = [self cacheKeyForURL:url context:context];
+    if (finished && cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
         dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
-            @autoreleasepool {
-                UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
-                if (transformedImage && finished) {
-                    BOOL imageWasTransformed = ![transformedImage isEqual:originalImage];
-                    NSData *cacheData;
-                    // pass nil if the image was transformed, so we can recalculate the data from the image
-                    if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
-                        cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : originalData) imageURL:url];
-                    } else {
-                        cacheData = (imageWasTransformed ? nil : originalData);
-                    }
-                    [self storeImage:transformedImage imageData:cacheData forKey:key cacheType:storeCacheType options:options context:context completion:^{
-                        [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
-                    }];
-                } else {
-                    [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
-                }
-            }
+            NSData *newData = [cacheSerializer cacheDataWithImage:image originalData:data imageURL:url];
+            // Store image and data
+            [self storeImage:image imageData:newData forKey:key options:options context:context imageCache:imageCache cacheType:storeCacheType finished:finished completion:^{
+                [self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished queue:context[SDWebImageContextCallbackQueue] url:url];
+            }];
         });
     } else {
-        [self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
+        // Store image and data
+        [self storeImage:image imageData:data forKey:key options:options context:context imageCache:imageCache cacheType:storeCacheType finished:finished completion:^{
+            [self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished queue:context[SDWebImageContextCallbackQueue] url:url];
+        }];
     }
 }
 
@@ -560,33 +617,46 @@ static id<SDImageLoader> _defaultImageLoader;
     if (!operation) {
         return;
     }
-    SD_LOCK(self.runningOperationsLock);
+    SD_LOCK(_runningOperationsLock);
     [self.runningOperations removeObject:operation];
-    SD_UNLOCK(self.runningOperationsLock);
+    SD_UNLOCK(_runningOperationsLock);
 }
 
 - (void)storeImage:(nullable UIImage *)image
          imageData:(nullable NSData *)data
             forKey:(nullable NSString *)key
-         cacheType:(SDImageCacheType)cacheType
            options:(SDWebImageOptions)options
            context:(nullable SDWebImageContext *)context
+        imageCache:(nonnull id<SDImageCache>)imageCache
+         cacheType:(SDImageCacheType)cacheType
+          finished:(BOOL)finished
         completion:(nullable SDWebImageNoParamsBlock)completion {
-    id<SDImageCache> imageCache;
-    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
-        imageCache = context[SDWebImageContextImageCache];
-    } else {
-        imageCache = self.imageCache;
-    }
     BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache);
+    // Ignore progressive data cache
+    if (!finished) {
+        if (completion) {
+            completion();
+        }
+        return;
+    }
     // Check whether we should wait the store cache finished. If not, callback immediately
-    [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{
-        if (waitStoreCache) {
-            if (completion) {
-                completion();
+    if ([imageCache respondsToSelector:@selector(storeImage:imageData:forKey:options:context:cacheType:completion:)]) {
+        [imageCache storeImage:image imageData:data forKey:key options:options context:context cacheType:cacheType completion:^{
+            if (waitStoreCache) {
+                if (completion) {
+                    completion();
+                }
             }
-        }
-    }];
+        }];
+    } else {
+        [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{
+            if (waitStoreCache) {
+                if (completion) {
+                    completion();
+                }
+            }
+        }];
+    }
     if (!waitStoreCache) {
         if (completion) {
             completion();
@@ -597,8 +667,9 @@ static id<SDImageLoader> _defaultImageLoader;
 - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
                              completion:(nullable SDInternalCompletionBlock)completionBlock
                                   error:(nullable NSError *)error
+                                  queue:(nullable SDCallbackQueue *)queue
                                     url:(nullable NSURL *)url {
-    [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url];
+    [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES queue:queue url:url];
 }
 
 - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
@@ -608,22 +679,21 @@ static id<SDImageLoader> _defaultImageLoader;
                                   error:(nullable NSError *)error
                               cacheType:(SDImageCacheType)cacheType
                                finished:(BOOL)finished
+                                  queue:(nullable SDCallbackQueue *)queue
                                     url:(nullable NSURL *)url {
-    dispatch_main_async_safe(^{
-        if (completionBlock) {
+    if (completionBlock) {
+        [(queue ?: SDCallbackQueue.mainQueue) async:^{
             completionBlock(image, data, error, cacheType, finished, url);
-        }
-    });
+        }];
+    }
 }
 
 - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url
                               error:(nonnull NSError *)error
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context {
-    id<SDImageLoader> imageLoader;
-    if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
-        imageLoader = context[SDWebImageContextImageLoader];
-    } else {
+    id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader];
+    if (!imageLoader) {
         imageLoader = self.imageLoader;
     }
     // Check whether we should block failed url
@@ -631,7 +701,11 @@ static id<SDImageLoader> _defaultImageLoader;
     if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
         shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
     } else {
-        shouldBlockFailedURL = [imageLoader shouldBlockFailedURLWithURL:url error:error];
+        if ([imageLoader respondsToSelector:@selector(shouldBlockFailedURLWithURL:error:options:context:)]) {
+            shouldBlockFailedURL = [imageLoader shouldBlockFailedURLWithURL:url error:error options:options context:context];
+        } else {
+            shouldBlockFailedURL = [imageLoader shouldBlockFailedURLWithURL:url error:error];
+        }
     }
     
     return shouldBlockFailedURL;
@@ -681,12 +755,20 @@ static id<SDImageLoader> _defaultImageLoader;
 
 @implementation SDWebImageCombinedOperation
 
+- (BOOL)isCancelled {
+    // Need recursive lock (user's cancel block may check isCancelled), do not use SD_LOCK
+    @synchronized (self) {
+        return _cancelled;
+    }
+}
+
 - (void)cancel {
+    // Need recursive lock (user's cancel block may check isCancelled), do not use SD_LOCK
     @synchronized(self) {
-        if (self.isCancelled) {
+        if (_cancelled) {
             return;
         }
-        self.cancelled = YES;
+        _cancelled = YES;
         if (self.cacheOperation) {
             [self.cacheOperation cancel];
             self.cacheOperation = nil;

+ 6 - 0
Pods/SDWebImage/SDWebImage/Core/SDWebImageOperation.h

@@ -11,8 +11,14 @@
 /// A protocol represents cancelable operation.
 @protocol SDWebImageOperation <NSObject>
 
+/// Cancel the operation
 - (void)cancel;
 
+@optional
+
+/// Whether the operation has been cancelled.
+@property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled;
+
 @end
 
 /// NSOperation conform to `SDWebImageOperation`.

+ 29 - 4
Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.h

@@ -76,20 +76,23 @@ typedef void(^SDWebImagePrefetcherCompletionBlock)(NSUInteger noOfFinishedUrls,
 
 /**
  * The options for prefetcher. Defaults to SDWebImageLowPriority.
+ * @deprecated Prefetcher is designed to be used shared and should not effect others. So in 5.15.0 we added API  `prefetchURLs:options:context:`. If you want global control, try to use `SDWebImageOptionsProcessor` in manager level.
  */
-@property (nonatomic, assign) SDWebImageOptions options;
+@property (nonatomic, assign) SDWebImageOptions options API_DEPRECATED("Use individual prefetch options param instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 /**
  * The context for prefetcher. Defaults to nil.
+ * @deprecated Prefetcher is designed to be used shared and should not effect others. So in 5.15.0 we added API  `prefetchURLs:options:context:`. If you want global control, try to use `SDWebImageOptionsProcessor` in `SDWebImageManager.optionsProcessor`.
  */
-@property (nonatomic, copy, nullable) SDWebImageContext *context;
+@property (nonatomic, copy, nullable) SDWebImageContext *context API_DEPRECATED("Use individual prefetch context param instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED));
 
 /**
  * Queue options for prefetcher when call the progressBlock, completionBlock and delegate methods. Defaults to Main Queue.
- * @note The call is asynchronously to avoid blocking target queue.
+ * @deprecated 5.15.0 introduce SDCallbackQueue, use that is preferred and has higher priority. The set/get to this property will translate to that instead.
+ * @note The call is asynchronously to avoid blocking target queue. (see SDCallbackPolicyDispatch)
  * @note The delegate queue should be set before any prefetching start and may not be changed during prefetching to avoid thread-safe problem.
  */
-@property (strong, nonatomic, nonnull) dispatch_queue_t delegateQueue;
+@property (strong, nonatomic, nonnull) dispatch_queue_t delegateQueue API_DEPRECATED("Use SDWebImageContextCallbackQueue context param instead, see SDCallbackQueue", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0));
 
 /**
  * The delegate for the prefetcher. Defaults to nil.
@@ -134,6 +137,28 @@ typedef void(^SDWebImagePrefetcherCompletionBlock)(NSUInteger noOfFinishedUrls,
                                           progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
                                          completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
 
+/**
+ * Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching. It based on the image manager so the image may from the cache and network according to the `options` property.
+ * Prefetching is separate to each other, which means the progressBlock and completionBlock you provide is bind to the prefetching for the list of urls.
+ * Attention that call this will not cancel previous fetched urls. You should keep the token return by this to cancel or cancel all the prefetch.
+ *
+ * @param urls            list of URLs to prefetch
+ * @param options         The options to use when downloading the image. @see SDWebImageOptions for the possible values.
+ * @param context         A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold.
+ * @param progressBlock   block to be called when progress updates;
+ *                        first parameter is the number of completed (successful or not) requests,
+ *                        second parameter is the total number of images originally requested to be prefetched
+ * @param completionBlock block to be called when the current prefetching is completed
+ *                        first param is the number of completed (successful or not) requests,
+ *                        second parameter is the number of skipped requests
+ * @return the token to cancel the current prefetching.
+ */
+- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
+                                           options:(SDWebImageOptions)options
+                                           context:(nullable SDWebImageContext *)context
+                                          progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
+                                         completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
+
 /**
  * Remove and cancel all the prefeching for the prefetcher.
  */

+ 83 - 47
Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.m

@@ -8,9 +8,16 @@
 
 #import "SDWebImagePrefetcher.h"
 #import "SDAsyncBlockOperation.h"
+#import "SDCallbackQueue.h"
 #import "SDInternalMacros.h"
 #import <stdatomic.h>
 
+@interface SDCallbackQueue ()
+
+@property (nonatomic, strong, nonnull) dispatch_queue_t queue;
+
+@end
+
 @interface SDWebImagePrefetchToken () {
     @public
     // Though current implementation, `SDWebImageManager` completion block is always on main queue. But however, there is no guarantee in docs. And we may introduce config to specify custom queue in the future.
@@ -22,14 +29,16 @@
     unsigned long _totalCount;
     
     // Used to ensure NSPointerArray thread safe
-    dispatch_semaphore_t _prefetchOperationsLock;
-    dispatch_semaphore_t _loadOperationsLock;
+    SD_LOCK_DECLARE(_prefetchOperationsLock);
+    SD_LOCK_DECLARE(_loadOperationsLock);
 }
 
 @property (nonatomic, copy, readwrite) NSArray<NSURL *> *urls;
 @property (nonatomic, strong) NSPointerArray *loadOperations;
 @property (nonatomic, strong) NSPointerArray *prefetchOperations;
 @property (nonatomic, weak) SDWebImagePrefetcher *prefetcher;
+@property (nonatomic, assign) SDWebImageOptions options;
+@property (nonatomic, copy, nullable) SDWebImageContext *context;
 @property (nonatomic, copy, nullable) SDWebImagePrefetcherCompletionBlock completionBlock;
 @property (nonatomic, copy, nullable) SDWebImagePrefetcherProgressBlock progressBlock;
 
@@ -40,6 +49,7 @@
 @property (strong, nonatomic, nonnull) SDWebImageManager *manager;
 @property (strong, atomic, nonnull) NSMutableSet<SDWebImagePrefetchToken *> *runningTokens;
 @property (strong, nonatomic, nonnull) NSOperationQueue *prefetchQueue;
+@property (strong, nonatomic, nullable) SDCallbackQueue *callbackQueue;
 
 @end
 
@@ -63,7 +73,6 @@
         _manager = manager;
         _runningTokens = [NSMutableSet set];
         _options = SDWebImageLowPriority;
-        _delegateQueue = dispatch_get_main_queue();
         _prefetchQueue = [NSOperationQueue new];
         self.maxConcurrentPrefetchCount = 3;
     }
@@ -78,6 +87,17 @@
     return self.prefetchQueue.maxConcurrentOperationCount;
 }
 
+- (void)setDelegateQueue:(dispatch_queue_t)delegateQueue {
+    // Deprecate and translate to SDCallbackQueue
+    _callbackQueue = [[SDCallbackQueue alloc] initWithDispatchQueue:delegateQueue];
+    _callbackQueue.policy = SDCallbackPolicyDispatch;
+}
+
+- (dispatch_queue_t)delegateQueue {
+    // Deprecate and translate to SDCallbackQueue
+    return (_callbackQueue ?: SDCallbackQueue.mainQueue).queue;
+}
+
 #pragma mark - Prefetch
 - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls {
     return [self prefetchURLs:urls progress:nil completed:nil];
@@ -86,6 +106,14 @@
 - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
                                           progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
                                          completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock {
+    return [self prefetchURLs:urls options:self.options context:self.context progress:progressBlock completed:completionBlock];
+}
+
+- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
+                                           options:(SDWebImageOptions)options
+                                           context:(nullable SDWebImageContext *)context
+                                          progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
+                                         completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock {
     if (!urls || urls.count == 0) {
         if (completionBlock) {
             completionBlock(0, 0);
@@ -95,6 +123,8 @@
     SDWebImagePrefetchToken *token = [SDWebImagePrefetchToken new];
     token.prefetcher = self;
     token.urls = urls;
+    token.options = options;
+    token.context = context;
     token->_skippedCount = 0;
     token->_finishedCount = 0;
     token->_totalCount = token.urls.count;
@@ -111,49 +141,47 @@
 
 - (void)startPrefetchWithToken:(SDWebImagePrefetchToken * _Nonnull)token {
     for (NSURL *url in token.urls) {
-        @autoreleasepool {
-            @weakify(self);
-            SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) {
+        @weakify(self);
+        SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) {
+            @strongify(self);
+            if (!self || asyncOperation.isCancelled) {
+                return;
+            }
+            id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:token.options context:token.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
                 @strongify(self);
-                if (!self || asyncOperation.isCancelled) {
+                if (!self) {
                     return;
                 }
-                id<SDWebImageOperation> operation = [self.manager loadImageWithURL:url options:self.options context:self.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) {
-                    @strongify(self);
-                    if (!self) {
-                        return;
-                    }
-                    if (!finished) {
-                        return;
-                    }
-                    atomic_fetch_add_explicit(&(token->_finishedCount), 1, memory_order_relaxed);
-                    if (error) {
-                        // Add last failed
-                        atomic_fetch_add_explicit(&(token->_skippedCount), 1, memory_order_relaxed);
-                    }
-                    
-                    // Current operation finished
-                    [self callProgressBlockForToken:token imageURL:imageURL];
-                    
-                    if (atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed) == token->_totalCount) {
-                        // All finished
-                        if (!atomic_flag_test_and_set_explicit(&(token->_isAllFinished), memory_order_relaxed)) {
-                            [self callCompletionBlockForToken:token];
-                            [self removeRunningToken:token];
-                        }
+                if (!finished) {
+                    return;
+                }
+                atomic_fetch_add_explicit(&(token->_finishedCount), 1, memory_order_relaxed);
+                if (error) {
+                    // Add last failed
+                    atomic_fetch_add_explicit(&(token->_skippedCount), 1, memory_order_relaxed);
+                }
+                
+                // Current operation finished
+                [self callProgressBlockForToken:token imageURL:imageURL];
+                
+                if (atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed) == token->_totalCount) {
+                    // All finished
+                    if (!atomic_flag_test_and_set_explicit(&(token->_isAllFinished), memory_order_relaxed)) {
+                        [self callCompletionBlockForToken:token];
+                        [self removeRunningToken:token];
                     }
-                    [asyncOperation complete];
-                }];
-                NSAssert(operation != nil, @"Operation should not be nil, [SDWebImageManager loadImageWithURL:options:context:progress:completed:] break prefetch logic");
-                SD_LOCK(token->_loadOperationsLock);
-                [token.loadOperations addPointer:(__bridge void *)operation];
-                SD_UNLOCK(token->_loadOperationsLock);
+                }
+                [asyncOperation complete];
             }];
-            SD_LOCK(token->_prefetchOperationsLock);
-            [token.prefetchOperations addPointer:(__bridge void *)prefetchOperation];
-            SD_UNLOCK(token->_prefetchOperationsLock);
-            [self.prefetchQueue addOperation:prefetchOperation];
-        }
+            NSAssert(operation != nil, @"Operation should not be nil, [SDWebImageManager loadImageWithURL:options:context:progress:completed:] break prefetch logic");
+            SD_LOCK(token->_loadOperationsLock);
+            [token.loadOperations addPointer:(__bridge void *)operation];
+            SD_UNLOCK(token->_loadOperationsLock);
+        }];
+        SD_LOCK(token->_prefetchOperationsLock);
+        [token.prefetchOperations addPointer:(__bridge void *)prefetchOperation];
+        SD_UNLOCK(token->_prefetchOperationsLock);
+        [self.prefetchQueue addOperation:prefetchOperation];
     }
 }
 
@@ -175,14 +203,18 @@
     NSUInteger tokenTotalCount = [self tokenTotalCount];
     NSUInteger finishedCount = atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed);
     NSUInteger totalCount = token->_totalCount;
-    dispatch_async(self.delegateQueue, ^{
+    SDCallbackQueue *queue = token.context[SDWebImageContextCallbackQueue];
+    if (!queue) {
+        queue = self.callbackQueue;
+    }
+    [(queue ?: SDCallbackQueue.mainQueue) async:^{
         if (shouldCallDelegate) {
             [self.delegate imagePrefetcher:self didPrefetchURL:url finishedCount:tokenFinishedCount totalCount:tokenTotalCount];
         }
         if (token.progressBlock) {
             token.progressBlock(finishedCount, totalCount);
         }
-    });
+    }];
 }
 
 - (void)callCompletionBlockForToken:(SDWebImagePrefetchToken *)token {
@@ -194,14 +226,18 @@
     NSUInteger tokenSkippedCount = [self tokenSkippedCount];
     NSUInteger finishedCount = atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed);
     NSUInteger skippedCount = atomic_load_explicit(&(token->_skippedCount), memory_order_relaxed);
-    dispatch_async(self.delegateQueue, ^{
+    SDCallbackQueue *queue = token.context[SDWebImageContextCallbackQueue];
+    if (!queue) {
+        queue = self.callbackQueue;
+    }
+    [(queue ?: SDCallbackQueue.mainQueue) async:^{
         if (shoulCallDelegate) {
             [self.delegate imagePrefetcher:self didFinishWithTotalCount:tokenTotalCount skippedCount:tokenSkippedCount];
         }
         if (token.completionBlock) {
             token.completionBlock(finishedCount, skippedCount);
         }
-    });
+    }];
 }
 
 #pragma mark - Helper
@@ -268,8 +304,8 @@
 - (instancetype)init {
     self = [super init];
     if (self) {
-        _prefetchOperationsLock = dispatch_semaphore_create(1);
-        _loadOperationsLock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_prefetchOperationsLock);
+        SD_LOCK_INIT(_loadOperationsLock);
     }
     return self;
 }

+ 19 - 1
Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.m

@@ -9,12 +9,30 @@
 #import "UIImage+ForceDecode.h"
 #import "SDImageCoderHelper.h"
 #import "objc/runtime.h"
+#import "NSImage+Compatibility.h"
 
 @implementation UIImage (ForceDecode)
 
 - (BOOL)sd_isDecoded {
     NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isDecoded));
-    return value.boolValue;
+    if (value != nil) {
+        return value.boolValue;
+    } else {
+        // Assume only CGImage based can use lazy decoding
+        CGImageRef cgImage = self.CGImage;
+        if (cgImage) {
+            CFStringRef uttype = CGImageGetUTType(self.CGImage);
+            if (uttype) {
+                // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier`
+                return NO;
+            } else {
+                // Thumbnail or CGBitmapContext drawn image
+                return YES;
+            }
+        }
+    }
+    // Assume others as non-decoded
+    return NO;
 }
 
 - (void)setSd_isDecoded:(BOOL)sd_isDecoded {

+ 2 - 1
Pods/SDWebImage/SDWebImage/Core/UIImage+MemoryCacheCost.m

@@ -20,7 +20,8 @@ FOUNDATION_STATIC_INLINE NSUInteger SDMemoryCacheCostForImage(UIImage *image) {
 #if SD_MAC
     frameCount = 1;
 #elif SD_UIKIT || SD_WATCH
-    frameCount = image.images.count > 0 ? image.images.count : 1;
+    // Filter the same frame in `_UIAnimatedImage`.
+    frameCount = image.images.count > 1 ? [NSSet setWithArray:image.images].count : 1;
 #endif
     NSUInteger cost = bytesPerFrame * frameCount;
     return cost;

+ 35 - 3
Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.h

@@ -8,6 +8,7 @@
 
 #import "SDWebImageCompat.h"
 #import "NSData+ImageContentType.h"
+#import "SDImageCoder.h"
 
 /**
  UIImage category for image metadata, including animation, loop count, format, incremental, etc.
@@ -20,12 +21,23 @@
  * For animated image format, 0 means infinite looping.
  * Note that because of the limitations of categories this property can get out of sync if you create another instance with CGImage or other methods.
  * AppKit:
- * NSImage currently only support animated via GIF imageRep unlike UIImage.
- * The getter of this property will get the loop count from GIF imageRep
- * The setter of this property will set the loop count from GIF imageRep
+ * NSImage currently only support animated via `NSBitmapImageRep`(GIF) or `SDAnimatedImageRep`(APNG/GIF/WebP) unlike UIImage.
+ * The getter of this property will get the loop count from animated imageRep
+ * The setter of this property will set the loop count from animated imageRep
  */
 @property (nonatomic, assign) NSUInteger sd_imageLoopCount;
 
+/**
+ * UIKit:
+ * Returns the `images`'s count by unapply the patch for the different frame durations. Which matches the real visible frame count when displaying on UIImageView.
+ * See more in `SDImageCoderHelper.animatedImageWithFrames`.
+ * Returns 1 for static image.
+ * AppKit:
+ * Returns the underlaying `NSBitmapImageRep` or `SDAnimatedImageRep` frame count.
+ * Returns 1 for static image.
+ */
+@property (nonatomic, assign, readonly) NSUInteger sd_imageFrameCount;
+
 /**
  * UIKit:
  * Check the `images` array property.
@@ -54,4 +66,24 @@
  */
 @property (nonatomic, assign) BOOL sd_isIncremental;
 
+/**
+ A bool value indicating that the image is transformed from original image, so the image data may not always match original download one.
+ */
+@property (nonatomic, assign) BOOL sd_isTransformed;
+
+/**
+ A bool value indicating that the image is using thumbnail decode with smaller size, so the image data may not always match original download one.
+ @note This just check `sd_decodeOptions[.decodeThumbnailPixelSize] > CGSize.zero`
+ */
+@property (nonatomic, assign, readonly) BOOL sd_isThumbnail;
+
+/**
+ A dictionary value contains the decode options when decoded from SDWebImage loading system (say, `SDImageCacheDecodeImageData/SDImageLoaderDecode[Progressive]ImageData`)
+ It may not always available and only image decoding related options will be saved. (including [.decodeScaleFactor, .decodeThumbnailPixelSize, .decodePreserveAspectRatio, .decodeFirstFrameOnly])
+ @note This is used to identify and check the image is from thumbnail decoding, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key)
+ @warning You should not store object inside which keep strong reference to image itself, which will cause retain cycle.
+ @warning This API exist only because of current SDWebImageDownloader bad design which does not callback the context we call it. There will be refactor in future (API break), use with caution.
+ */
+@property (nonatomic, copy) SDImageCoderOptions *sd_decodeOptions;
+
 @end

+ 75 - 4
Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.m

@@ -29,6 +29,32 @@
     objc_setAssociatedObject(self, @selector(sd_imageLoopCount), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 }
 
+- (NSUInteger)sd_imageFrameCount {
+    NSArray<UIImage *> *animatedImages = self.images;
+    if (!animatedImages || animatedImages.count <= 1) {
+        return 1;
+    }
+    NSNumber *value = objc_getAssociatedObject(self, @selector(sd_imageFrameCount));
+    if ([value isKindOfClass:[NSNumber class]]) {
+        return [value unsignedIntegerValue];
+    }
+    __block NSUInteger frameCount = 1;
+    __block UIImage *previousImage = animatedImages.firstObject;
+    [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) {
+        // ignore first
+        if (idx == 0) {
+            return;
+        }
+        if (![image isEqual:previousImage]) {
+            frameCount++;
+        }
+        previousImage = image;
+    }];
+    objc_setAssociatedObject(self, @selector(sd_imageFrameCount), @(frameCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+    
+    return frameCount;
+}
+
 - (BOOL)sd_isAnimated {
     return (self.images != nil);
 }
@@ -87,6 +113,19 @@
     }
 }
 
+- (NSUInteger)sd_imageFrameCount {
+    NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height);
+    NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil];
+    NSBitmapImageRep *bitmapImageRep;
+    if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) {
+        bitmapImageRep = (NSBitmapImageRep *)imageRep;
+    }
+    if (bitmapImageRep) {
+        return [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue];
+    }
+    return 1;
+}
+
 - (BOOL)sd_isAnimated {
     BOOL isAnimated = NO;
     NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height);
@@ -127,10 +166,8 @@
         return imageFormat;
     }
     // Check CGImage's UTType, may return nil for non-Image/IO based image
-    if (@available(iOS 9.0, tvOS 9.0, macOS 10.11, watchOS 2.0, *)) {
-        CFStringRef uttype = CGImageGetUTType(self.CGImage);
-        imageFormat = [NSData sd_imageFormatFromUTType:uttype];
-    }
+    CFStringRef uttype = CGImageGetUTType(self.CGImage);
+    imageFormat = [NSData sd_imageFormatFromUTType:uttype];
     return imageFormat;
 }
 
@@ -147,4 +184,38 @@
     return value.boolValue;
 }
 
+- (void)setSd_isTransformed:(BOOL)sd_isTransformed {
+    objc_setAssociatedObject(self, @selector(sd_isTransformed), @(sd_isTransformed), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+}
+
+- (BOOL)sd_isTransformed {
+    NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isTransformed));
+    return value.boolValue;
+}
+
+- (void)setSd_decodeOptions:(SDImageCoderOptions *)sd_decodeOptions {
+    objc_setAssociatedObject(self, @selector(sd_decodeOptions), sd_decodeOptions, OBJC_ASSOCIATION_COPY_NONATOMIC);
+}
+
+-(BOOL)sd_isThumbnail {
+    CGSize thumbnailSize = CGSizeZero;
+    NSValue *thumbnailSizeValue = self.sd_decodeOptions[SDImageCoderDecodeThumbnailPixelSize];
+    if (thumbnailSizeValue != nil) {
+    #if SD_MAC
+        thumbnailSize = thumbnailSizeValue.sizeValue;
+    #else
+        thumbnailSize = thumbnailSizeValue.CGSizeValue;
+    #endif
+    }
+    return thumbnailSize.width > 0 && thumbnailSize.height > 0;
+}
+
+- (SDImageCoderOptions *)sd_decodeOptions {
+    SDImageCoderOptions *value = objc_getAssociatedObject(self, @selector(sd_decodeOptions));
+    if ([value isKindOfClass:NSDictionary.class]) {
+        return value;
+    }
+    return nil;
+}
+
 @end

+ 138 - 34
Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.m

@@ -57,7 +57,89 @@ static inline CGRect SDCGRectFitWithScaleMode(CGRect rect, CGSize size, SDImageS
     return rect;
 }
 
-static inline UIColor * SDGetColorFromPixel(Pixel_8888 pixel, CGBitmapInfo bitmapInfo) {
+static inline UIColor * SDGetColorFromGrayscale(Pixel_88 pixel, CGBitmapInfo bitmapInfo) {
+    // Get alpha info, byteOrder info
+    CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
+    CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
+    CGFloat w = 0, a = 1;
+    
+    BOOL byteOrderNormal = NO;
+    switch (byteOrderInfo) {
+        case kCGBitmapByteOrderDefault: {
+            byteOrderNormal = YES;
+        } break;
+        case kCGBitmapByteOrder32Little: {
+        } break;
+        case kCGBitmapByteOrder32Big: {
+            byteOrderNormal = YES;
+        } break;
+        default: break;
+    }
+    switch (alphaInfo) {
+        case kCGImageAlphaPremultipliedFirst:
+        case kCGImageAlphaFirst: {
+            if (byteOrderNormal) {
+                // AW
+                a = pixel[0] / 255.0;
+                w = pixel[1] / 255.0;
+            } else {
+                // WA
+                w = pixel[0] / 255.0;
+                a = pixel[1] / 255.0;
+            }
+        }
+            break;
+        case kCGImageAlphaPremultipliedLast:
+        case kCGImageAlphaLast: {
+            if (byteOrderNormal) {
+                // WA
+                w = pixel[0] / 255.0;
+                a = pixel[1] / 255.0;
+            } else {
+                // AW
+                a = pixel[0] / 255.0;
+                w = pixel[1] / 255.0;
+            }
+        }
+            break;
+        case kCGImageAlphaNone: {
+            // W
+            w = pixel[0] / 255.0;
+        }
+            break;
+        case kCGImageAlphaNoneSkipLast: {
+            if (byteOrderNormal) {
+                // WX
+                w = pixel[0] / 255.0;
+            } else {
+                // XW
+                a = pixel[1] / 255.0;
+            }
+        }
+            break;
+        case kCGImageAlphaNoneSkipFirst: {
+            if (byteOrderNormal) {
+                // XW
+                a = pixel[1] / 255.0;
+            } else {
+                // WX
+                a = pixel[0] / 255.0;
+            }
+        }
+            break;
+        case kCGImageAlphaOnly: {
+            // A
+            a = pixel[0] / 255.0;
+        }
+            break;
+        default:
+            break;
+    }
+    
+    return [UIColor colorWithWhite:w alpha:a];
+}
+
+static inline UIColor * SDGetColorFromRGBA(Pixel_8888 pixel, CGBitmapInfo bitmapInfo) {
     // Get alpha info, byteOrder info
     CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
     CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
@@ -470,18 +552,34 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
     size_t components = CGImageGetBitsPerPixel(imageRef) / CGImageGetBitsPerComponent(imageRef);
     CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
     
-    CFRange range = CFRangeMake(bytesPerRow * point.y + components * point.x, 4);
+    CFRange range = CFRangeMake(bytesPerRow * point.y + components * point.x, components);
     if (CFDataGetLength(data) < range.location + range.length) {
         CFRelease(data);
         CGImageRelease(imageRef);
         return nil;
     }
-    Pixel_8888 pixel = {0};
-    CFDataGetBytes(data, range, pixel);
-    CFRelease(data);
-    CGImageRelease(imageRef);
-    // Convert to color
-    return SDGetColorFromPixel(pixel, bitmapInfo);
+    // greyscale
+    if (components == 2) {
+        Pixel_88 pixel = {0};
+        CFDataGetBytes(data, range, pixel);
+        CFRelease(data);
+        CGImageRelease(imageRef);
+        // Convert to color
+        return SDGetColorFromGrayscale(pixel, bitmapInfo);
+    } else if (components == 3 || components == 4) {
+        // RGB/RGBA
+        Pixel_8888 pixel = {0};
+        CFDataGetBytes(data, range, pixel);
+        CFRelease(data);
+        CGImageRelease(imageRef);
+        // Convert to color
+        return SDGetColorFromRGBA(pixel, bitmapInfo);
+    } else {
+        NSLog(@"Unsupported components: %zu", components);
+        CFRelease(data);
+        CGImageRelease(imageRef);
+        return nil;
+    }
 }
 
 - (nullable NSArray<UIColor *> *)sd_colorsWithRect:(CGRect)rect {
@@ -539,17 +637,32 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
     // Convert to color
     CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
     NSMutableArray<UIColor *> *colors = [NSMutableArray arrayWithCapacity:CGRectGetWidth(rect) * CGRectGetHeight(rect)];
-    for (size_t index = start; index < end; index += 4) {
+    for (size_t index = start; index < end; index += components) {
         if (index >= row * bytesPerRow + col * components) {
             // Index beyond the end of current row, go next row
             row++;
             index = row * bytesPerRow + CGRectGetMinX(rect) * components;
-            index -= 4;
+            index -= components;
             continue;
         }
-        Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], pixels[index+3]};
-        UIColor *color = SDGetColorFromPixel(pixel, bitmapInfo);
-        [colors addObject:color];
+        UIColor *color;
+        if (components == 2) {
+            Pixel_88 pixel = {pixels[index], pixel[index+1]};
+            color = SDGetColorFromGrayscale(pixel, bitmapInfo);
+        } else {
+            if (components == 3) {
+                Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], 0};
+                color = SDGetColorFromRGBA(pixel, bitmapInfo);
+            } else if (components == 4) {
+                Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], pixels[index+3]};
+                color = SDGetColorFromRGBA(pixel, bitmapInfo);
+            } else {
+                NSLog(@"Unsupported components: %zu", components);
+            }
+        }
+        if (color) {
+            [colors addObject:color];
+        }
     }
     CFRelease(data);
     CGImageRelease(imageRef);
@@ -588,15 +701,8 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
 #endif
     
     CGImageRef imageRef = self.CGImage;
-    
-    //convert to BGRA if it isn't
-    if (CGImageGetBitsPerPixel(imageRef) != 32 ||
-        CGImageGetBitsPerComponent(imageRef) != 8 ||
-        !((CGImageGetBitmapInfo(imageRef) & kCGBitmapAlphaInfoMask))) {
-        SDGraphicsBeginImageContextWithOptions(self.size, NO, self.scale);
-        [self drawInRect:CGRectMake(0, 0, self.size.width, self.size.height)];
-        imageRef = SDGraphicsGetImageFromCurrentImageContext().CGImage;
-        SDGraphicsEndImageContext();
+    if (!imageRef) {
+        return nil;
     }
     
     vImage_Buffer effect = {}, scratch = {};
@@ -609,11 +715,11 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
         .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host, //requests a BGRA buffer.
         .version = 0,
         .decode = NULL,
-        .renderingIntent = kCGRenderingIntentDefault
+        .renderingIntent = CGImageGetRenderingIntent(imageRef)
     };
     
     vImage_Error err;
-    err = vImageBuffer_InitWithCGImage(&effect, &format, NULL, imageRef, kvImageNoFlags);
+    err = vImageBuffer_InitWithCGImage(&effect, &format, NULL, imageRef, kvImageNoFlags); // vImage will convert to format we requests, no need `vImageConvert`
     if (err != kvImageNoError) {
         NSLog(@"UIImage+Transform error: vImageBuffer_InitWithCGImage returned error code %zi for inputImage: %@", err, self);
         return nil;
@@ -627,6 +733,7 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
     input = &effect;
     output = &scratch;
     
+    // See: https://developer.apple.com/library/archive/samplecode/UIImageEffects/Introduction/Intro.html
     if (hasBlur) {
         // A description of how to compute the box kernel width from the Gaussian
         // radius (aka standard deviation) appears in the SVG spec:
@@ -643,19 +750,16 @@ static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull
         if (inputRadius - 2.0 < __FLT_EPSILON__) inputRadius = 2.0;
         uint32_t radius = floor(inputRadius * 3.0 * sqrt(2 * M_PI) / 4 + 0.5);
         radius |= 1; // force radius to be odd so that the three box-blur methodology works.
-        int iterations;
-        if (blurRadius * scale < 0.5) iterations = 1;
-        else if (blurRadius * scale < 1.5) iterations = 2;
-        else iterations = 3;
         NSInteger tempSize = vImageBoxConvolve_ARGB8888(input, output, NULL, 0, 0, radius, radius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend);
         void *temp = malloc(tempSize);
-        for (int i = 0; i < iterations; i++) {
-            vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend);
-            vImage_Buffer *tmp = input;
-            input = output;
-            output = tmp;
-        }
+        vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend);
+        vImageBoxConvolve_ARGB8888(output, input, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend);
+        vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend);
         free(temp);
+        
+        vImage_Buffer *tmp = input;
+        input = output;
+        output = tmp;
     }
     
     CGImageRef effectCGImage = NULL;

+ 8 - 7
Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.h

@@ -71,14 +71,15 @@ typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable ima
  *   block is called a last time with the full image and the last parameter set to YES.
  *
  *   The last parameter is the original image URL
+ *  @return The returned operation for cancelling cache and download operation, typically type is `SDWebImageCombinedOperation`
  */
-- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
-                  placeholderImage:(nullable UIImage *)placeholder
-                           options:(SDWebImageOptions)options
-                           context:(nullable SDWebImageContext *)context
-                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
-                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
-                         completed:(nullable SDInternalCompletionBlock)completedBlock;
+- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)url
+                                              placeholderImage:(nullable UIImage *)placeholder
+                                                       options:(SDWebImageOptions)options
+                                                       context:(nullable SDWebImageContext *)context
+                                                 setImageBlock:(nullable SDSetImageBlock)setImageBlock
+                                                      progress:(nullable SDImageLoaderProgressBlock)progressBlock
+                                                     completed:(nullable SDInternalCompletionBlock)completedBlock;
 
 /**
  * Cancel the current image load

+ 42 - 25
Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.m

@@ -12,6 +12,7 @@
 #import "SDWebImageError.h"
 #import "SDInternalMacros.h"
 #import "SDWebImageTransitionInternal.h"
+#import "SDImageCache.h"
 
 const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
 
@@ -46,13 +47,13 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
     objc_setAssociatedObject(self, @selector(sd_imageProgress), sd_imageProgress, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
 }
 
-- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
-                  placeholderImage:(nullable UIImage *)placeholder
-                           options:(SDWebImageOptions)options
-                           context:(nullable SDWebImageContext *)context
-                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
-                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
-                         completed:(nullable SDInternalCompletionBlock)completedBlock {
+- (nullable id<SDWebImageOperation>)sd_internalSetImageWithURL:(nullable NSURL *)url
+                                              placeholderImage:(nullable UIImage *)placeholder
+                                                       options:(SDWebImageOptions)options
+                                                       context:(nullable SDWebImageContext *)context
+                                                 setImageBlock:(nullable SDSetImageBlock)setImageBlock
+                                                      progress:(nullable SDImageLoaderProgressBlock)progressBlock
+                                                     completed:(nullable SDInternalCompletionBlock)completedBlock {
     if (context) {
         // copy to avoid mutable object
         context = [context copy];
@@ -71,12 +72,35 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
     [self sd_cancelImageLoadOperationWithKey:validOperationKey];
     self.sd_imageURL = url;
     
+    SDWebImageManager *manager = context[SDWebImageContextCustomManager];
+    if (!manager) {
+        manager = [SDWebImageManager sharedManager];
+    } else {
+        // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
+        SDWebImageMutableContext *mutableContext = [context mutableCopy];
+        mutableContext[SDWebImageContextCustomManager] = nil;
+        context = [mutableContext copy];
+    }
+    
+    BOOL shouldUseWeakCache = NO;
+    if ([manager.imageCache isKindOfClass:SDImageCache.class]) {
+        shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache;
+    }
     if (!(options & SDWebImageDelayPlaceholder)) {
+        if (shouldUseWeakCache) {
+            NSString *key = [manager cacheKeyForURL:url context:context];
+            // call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query
+            // this unfortunately will cause twice memory cache query, but it's fast enough
+            // in the future the weak cache feature may be re-design or removed
+            [((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key];
+        }
         dispatch_main_async_safe(^{
             [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
         });
     }
     
+    id <SDWebImageOperation> operation = nil;
+    
     if (url) {
         // reset the progress
         NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
@@ -90,15 +114,6 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
         [self sd_startImageIndicator];
         id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
 #endif
-        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
-        if (!manager) {
-            manager = [SDWebImageManager sharedManager];
-        } else {
-            // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
-            SDWebImageMutableContext *mutableContext = [context mutableCopy];
-            mutableContext[SDWebImageContextCustomManager] = nil;
-            context = [mutableContext copy];
-        }
         
         SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
             if (imageProgress) {
@@ -122,7 +137,7 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
             }
         };
         @weakify(self);
-        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
+        operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
             @strongify(self);
             if (!self) { return; }
             // if the progress not been updated, mark it to complete state
@@ -141,7 +156,7 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
             BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
             BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                       (!image && !(options & SDWebImageDelayPlaceholder)));
-            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
+            SDWebImageNoParamsBlock callCompletedBlockClosure = ^{
                 if (!self) { return; }
                 if (!shouldNotSetImage) {
                     [self sd_setNeedsLayout];
@@ -155,7 +170,7 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
             // OR
             // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
             if (shouldNotSetImage) {
-                dispatch_main_async_safe(callCompletedBlockClojure);
+                dispatch_main_async_safe(callCompletedBlockClosure);
                 return;
             }
             
@@ -206,7 +221,7 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
 #else
                 [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
 #endif
-                callCompletedBlockClojure();
+                callCompletedBlockClosure();
             });
         }];
         [self sd_setImageLoadOperation:operation forKey:validOperationKey];
@@ -214,13 +229,15 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
 #if SD_UIKIT || SD_MAC
         [self sd_stopImageIndicator];
 #endif
-        dispatch_main_async_safe(^{
-            if (completedBlock) {
+        if (completedBlock) {
+            dispatch_main_async_safe(^{
                 NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                 completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url);
-            }
-        });
+            });
+        }
     }
+    
+    return operation;
 }
 
 - (void)sd_cancelCurrentImageLoad {
@@ -283,7 +300,7 @@ const int64_t SDWebImageProgressUnitCountUnknown = 1LL;
             if (transition.prepares) {
                 transition.prepares(view, image, imageData, cacheType, imageURL);
             }
-        } completion:^(BOOL finished) {
+        } completion:^(BOOL tempFinished) {
             [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{
                 if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) {
                     return;

+ 2 - 2
Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheOperation.h

@@ -32,14 +32,14 @@
 - (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key;
 
 /**
- *  Cancel all operations for the current UIView and key
+ *  Cancel the operation for the current UIView and key
  *
  *  @param key key for identifying the operations
  */
 - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key;
 
 /**
- *  Just remove the operations corresponding to the current UIView and key without cancelling them
+ *  Just remove the operation corresponding to the current UIView and key without cancelling them
  *
  *  @param key key for identifying the operations
  */

+ 3 - 5
Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheOperation.m

@@ -9,8 +9,6 @@
 #import "UIView+WebCacheOperation.h"
 #import "objc/runtime.h"
 
-static char loadOperationKey;
-
 // key is strong, value is weak because operation instance is retained by SDWebImageManager's runningOperations property
 // we should use lock to keep thread-safe because these method may not be accessed from main queue
 typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
@@ -19,12 +17,12 @@ typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
 
 - (SDOperationsDictionary *)sd_operationDictionary {
     @synchronized(self) {
-        SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
+        SDOperationsDictionary *operations = objc_getAssociatedObject(self, @selector(sd_operationDictionary));
         if (operations) {
             return operations;
         }
         operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
-        objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
+        objc_setAssociatedObject(self, @selector(sd_operationDictionary), operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
         return operations;
     }
 }
@@ -62,7 +60,7 @@ typedef NSMapTable<NSString *, id<SDWebImageOperation>> SDOperationsDictionary;
             operation = [operationDictionary objectForKey:key];
         }
         if (operation) {
-            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
+            if ([operation respondsToSelector:@selector(cancel)]) {
                 [operation cancel];
             }
             @synchronized (self) {

+ 2 - 0
Pods/SDWebImage/SDWebImage/Private/SDAssociatedObject.m

@@ -18,6 +18,8 @@ void SDImageCopyAssociatedObject(UIImage * _Nullable source, UIImage * _Nullable
     }
     // Image Metadata
     target.sd_isIncremental = source.sd_isIncremental;
+    target.sd_isTransformed = source.sd_isTransformed;
+    target.sd_decodeOptions = source.sd_decodeOptions;
     target.sd_imageLoopCount = source.sd_imageLoopCount;
     target.sd_imageFormat = source.sd_imageFormat;
     // Force Decode

+ 12 - 12
Pods/SDWebImage/SDWebImage/Private/SDAsyncBlockOperation.m

@@ -7,11 +7,10 @@
  */
 
 #import "SDAsyncBlockOperation.h"
+#import "SDInternalMacros.h"
 
 @interface SDAsyncBlockOperation ()
 
-@property (assign, nonatomic, getter = isExecuting) BOOL executing;
-@property (assign, nonatomic, getter = isFinished) BOOL finished;
 @property (nonatomic, copy, nonnull) SDAsyncBlock executionBlock;
 
 @end
@@ -40,16 +39,17 @@
             self.finished = YES;
             return;
         }
-        
         self.finished = NO;
         self.executing = YES;
-        
-        if (self.executionBlock) {
-            self.executionBlock(self);
-        } else {
-            self.executing = NO;
-            self.finished = YES;
-        }
+    }
+    SDAsyncBlock executionBlock = self.executionBlock;
+    if (executionBlock) {
+        @weakify(self);
+        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
+            @strongify(self);
+            if (!self) return;
+            executionBlock(self);
+        });
     }
 }
 
@@ -71,7 +71,7 @@
             self.executing = NO;
         }
     }
- }
+}
 
 - (void)setFinished:(BOOL)finished {
     [self willChangeValueForKey:@"isFinished"];
@@ -85,7 +85,7 @@
     [self didChangeValueForKey:@"isExecuting"];
 }
 
-- (BOOL)isConcurrent {
+- (BOOL)isAsynchronous {
     return YES;
 }
 

+ 1 - 1
Pods/SDWebImage/SDWebImage/Private/SDDisplayLink.h

@@ -15,7 +15,7 @@
 
 @property (readonly, nonatomic, weak, nullable) id target;
 @property (readonly, nonatomic, assign, nonnull) SEL selector;
-@property (readonly, nonatomic) CFTimeInterval duration;
+@property (readonly, nonatomic) NSTimeInterval duration; // elapsed time in seconds of previous callback. (or it's first callback, use the time between `start` and callback). Always zero when display link not running
 @property (readonly, nonatomic) BOOL isRunning;
 
 + (nonnull instancetype)displayLinkWithTarget:(nonnull id)target selector:(nonnull SEL)sel;

+ 56 - 22
Pods/SDWebImage/SDWebImage/Private/SDDisplayLink.m

@@ -13,6 +13,7 @@
 #elif SD_IOS || SD_TV
 #import <QuartzCore/QuartzCore.h>
 #endif
+#include <mach/mach_time.h>
 
 #if SD_MAC
 static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext);
@@ -22,6 +23,9 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
 
 @interface SDDisplayLink ()
 
+@property (nonatomic, assign) NSTimeInterval previousFireTime;
+@property (nonatomic, assign) NSTimeInterval nextFireTime;
+
 #if SD_MAC
 @property (nonatomic, assign) CVDisplayLinkRef displayLink;
 @property (nonatomic, assign) CVTimeStamp outputTime;
@@ -32,7 +36,6 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
 @property (nonatomic, strong) NSTimer *displayLink;
 @property (nonatomic, strong) NSRunLoop *runloop;
 @property (nonatomic, copy) NSRunLoopMode runloopMode;
-@property (nonatomic, assign) NSTimeInterval currentFireDate;
 #endif
 
 @end
@@ -78,28 +81,53 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
     return displayLink;
 }
 
-- (CFTimeInterval)duration {
+- (NSTimeInterval)duration {
+    NSTimeInterval duration = 0;
 #if SD_MAC
     CVTimeStamp outputTime = self.outputTime;
-    NSTimeInterval duration = 0;
     double periodPerSecond = (double)outputTime.videoTimeScale * outputTime.rateScalar;
     if (periodPerSecond > 0) {
         duration = (double)outputTime.videoRefreshPeriod / periodPerSecond;
     }
-#elif SD_IOS || SD_TV
-#pragma clang diagnostic push
-#pragma clang diagnostic ignored "-Wdeprecated-declarations"
-    NSTimeInterval duration = self.displayLink.duration * self.displayLink.frameInterval;
-#pragma clang diagnostic pop
+#elif SD_UIKIT
+    // iOS 10+/watchOS use `nextTime`
+    if (@available(iOS 10.0, tvOS 10.0, *)) {
+        duration = self.nextFireTime - CACurrentMediaTime();
+    } else {
+        // iOS 9 use `previousTime`
+        duration = CACurrentMediaTime() - self.previousFireTime;
+    }
 #else
-    NSTimeInterval duration = 0;
-    if (self.displayLink.isValid && self.currentFireDate != 0) {
-        NSTimeInterval nextFireDate = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink);
-        duration = nextFireDate - self.currentFireDate;
+    if (self.nextFireTime != 0) {
+        // `CFRunLoopTimerGetNextFireDate`: This time could be a date in the past if a run loop has not been able to process the timer since the firing time arrived.
+        // Don't rely on this, always calculate based on elapsed time
+        duration = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink) - self.nextFireTime;
     }
 #endif
-    if (duration == 0) {
+    // When system sleep, the targetTimestamp will mass up, fallback refresh rate
+    if (duration < 0) {
+#if SD_MAC
+        // Supports Pro display 120Hz
+        CGDirectDisplayID display = CVDisplayLinkGetCurrentCGDisplay(_displayLink);
+        CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display);
+        if (mode) {
+            double refreshRate = CGDisplayModeGetRefreshRate(mode);
+            if (refreshRate > 0) {
+                duration = 1.0 / refreshRate;
+            } else {
+                duration = kSDDisplayLinkInterval;
+            }
+            CGDisplayModeRelease(mode);
+        } else {
+            duration = kSDDisplayLinkInterval;
+        }
+#elif SD_IOS || SD_TV
+        // Fallback
+        duration = self.displayLink.duration;
+#else
+        // Watch always 60Hz
         duration = kSDDisplayLinkInterval;
+#endif
     }
     return duration;
 }
@@ -184,15 +212,16 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
 #else
     [self.displayLink invalidate];
 #endif
+    self.previousFireTime = 0;
+    self.nextFireTime = 0;
 }
 
 - (void)displayLinkDidRefresh:(id)displayLink {
-#if SD_MAC
-    // CVDisplayLink does not use runloop, but we can provide similar behavior for modes
-    // May use `default` runloop to avoid extra callback when in `eventTracking` (mouse drag, scroll) or `modalPanel` (modal panel)
-    NSString *runloopMode = self.runloopMode;
-    if (![runloopMode isEqualToString:NSRunLoopCommonModes] && ![runloopMode isEqualToString:NSRunLoop.mainRunLoop.currentMode]) {
-        return;
+#if SD_IOS || SD_TV
+    if (@available(iOS 10.0, tvOS 10.0, *)) {
+        self.nextFireTime = self.displayLink.targetTimestamp;
+    } else {
+        self.previousFireTime = self.displayLink.timestamp;
     }
 #endif
 #pragma clang diagnostic push
@@ -200,7 +229,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
     [_target performSelector:_selector withObject:self];
 #pragma clang diagnostic pop
 #if SD_WATCH
-    self.currentFireDate = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink);
+    self.nextFireTime = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink);
 #endif
 }
 
@@ -210,11 +239,16 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
 static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
     // CVDisplayLink callback is not on main queue
     SDDisplayLink *object = (__bridge SDDisplayLink *)displayLinkContext;
-    if (inOutputTime) {
-        object.outputTime = *inOutputTime;
+    // CVDisplayLink does not use runloop, but we can provide similar behavior for modes
+    // May use `default` runloop to avoid extra callback when in `eventTracking` (mouse drag, scroll) or `modalPanel` (modal panel)
+    NSString *runloopMode = object.runloopMode;
+    if (![runloopMode isEqualToString:NSRunLoopCommonModes] && ![runloopMode isEqualToString:NSRunLoop.mainRunLoop.currentMode]) {
+        return kCVReturnSuccess;
     }
+    CVTimeStamp outputTime = inOutputTime ? *inOutputTime : *inNow;
     __weak SDDisplayLink *weakObject = object;
     dispatch_async(dispatch_get_main_queue(), ^{
+        weakObject.outputTime = outputTime;
         [weakObject displayLinkDidRefresh:(__bridge id)(displayLink)];
     });
     return kCVReturnSuccess;

+ 10 - 4
Pods/SDWebImage/SDWebImage/Private/SDImageAssetManager.m

@@ -9,7 +9,7 @@
 #import "SDImageAssetManager.h"
 #import "SDInternalMacros.h"
 
-static NSArray *SDBundlePreferredScales() {
+static NSArray *SDBundlePreferredScales(void) {
     static NSArray *scales;
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
@@ -18,7 +18,13 @@ static NSArray *SDBundlePreferredScales() {
 #elif SD_UIKIT
         CGFloat screenScale = [UIScreen mainScreen].scale;
 #elif SD_MAC
-        CGFloat screenScale = [NSScreen mainScreen].backingScaleFactor;
+      NSScreen *mainScreen = nil;
+      if (@available(macOS 10.12, *)) {
+          mainScreen = [NSScreen mainScreen];
+      } else {
+          mainScreen = [NSScreen screens].firstObject;
+      }
+      CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f;
 #endif
         if (screenScale <= 1) {
             scales = @[@1,@2,@3];
@@ -32,7 +38,7 @@ static NSArray *SDBundlePreferredScales() {
 }
 
 @implementation SDImageAssetManager {
-    dispatch_semaphore_t _lock;
+    SD_LOCK_DECLARE(_lock);
 }
 
 + (instancetype)sharedAssetManager {
@@ -56,7 +62,7 @@ static NSArray *SDBundlePreferredScales() {
         valueOptions = NSPointerFunctionsStrongMemory;
 #endif
         _imageTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsCopyIn valueOptions:valueOptions];
-        _lock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_lock);
 #if SD_UIKIT
         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
 #endif

+ 3 - 4
Pods/SDWebImage/SDWebImage/Private/SDImageCachesManagerOperation.m

@@ -9,9 +9,8 @@
 #import "SDImageCachesManagerOperation.h"
 #import "SDInternalMacros.h"
 
-@implementation SDImageCachesManagerOperation
-{
-    dispatch_semaphore_t _pendingCountLock;
+@implementation SDImageCachesManagerOperation {
+    SD_LOCK_DECLARE(_pendingCountLock);
 }
 
 @synthesize executing = _executing;
@@ -21,7 +20,7 @@
 
 - (instancetype)init {
     if (self = [super init]) {
-        _pendingCountLock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_pendingCountLock);
         _pendingCount = 0;
     }
     return self;

+ 16 - 5
Pods/SDWebImage/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h

@@ -7,21 +7,32 @@
 */
 
 #import <Foundation/Foundation.h>
+#import <ImageIO/ImageIO.h>
 #import "SDImageIOAnimatedCoder.h"
 
 // AVFileTypeHEIC/AVFileTypeHEIF is defined in AVFoundation via iOS 11, we use this without import AVFoundation
-#define kSDUTTypeHEIC ((__bridge CFStringRef)@"public.heic")
-#define kSDUTTypeHEIF ((__bridge CFStringRef)@"public.heif")
+#define kSDUTTypeHEIC  ((__bridge CFStringRef)@"public.heic")
+#define kSDUTTypeHEIF  ((__bridge CFStringRef)@"public.heif")
 // HEIC Sequence (Animated Image)
 #define kSDUTTypeHEICS ((__bridge CFStringRef)@"public.heics")
-// kUTTypeWebP seems not defined in public UTI framework, Apple use the hardcode string, we define them :)
-#define kSDUTTypeWebP ((__bridge CFStringRef)@"org.webmproject.webp")
+// kSDUTTypeWebP seems not defined in public UTI framework, Apple use the hardcode string, we define them :)
+#define kSDUTTypeWebP  ((__bridge CFStringRef)@"org.webmproject.webp")
+
+#define kSDUTTypeImage ((__bridge CFStringRef)@"public.image")
+#define kSDUTTypeJPEG  ((__bridge CFStringRef)@"public.jpeg")
+#define kSDUTTypePNG   ((__bridge CFStringRef)@"public.png")
+#define kSDUTTypeTIFF  ((__bridge CFStringRef)@"public.tiff")
+#define kSDUTTypeSVG   ((__bridge CFStringRef)@"public.svg-image")
+#define kSDUTTypeGIF   ((__bridge CFStringRef)@"com.compuserve.gif")
+#define kSDUTTypePDF   ((__bridge CFStringRef)@"com.adobe.pdf")
+#define kSDUTTypeBMP   ((__bridge CFStringRef)@"com.microsoft.bmp")
+#define kSDUTTypeRAW   ((__bridge CFStringRef)@"public.camera-raw-image")
 
 @interface SDImageIOAnimatedCoder ()
 
 + (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 options:(nullable NSDictionary *)options;
++ (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage;
 + (BOOL)canEncodeToFormat:(SDImageFormat)format;
 + (BOOL)canDecodeFromFormat:(SDImageFormat)format;
 

+ 47 - 2
Pods/SDWebImage/SDWebImage/Private/SDInternalMacros.h

@@ -7,14 +7,59 @@
  */
 
 #import <Foundation/Foundation.h>
+#import <os/lock.h>
+#import <libkern/OSAtomic.h>
 #import "SDmetamacros.h"
 
+#define SD_USE_OS_UNFAIR_LOCK TARGET_OS_MACCATALYST ||\
+    (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0) ||\
+    (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_12) ||\
+    (__TV_OS_VERSION_MIN_REQUIRED >= __TVOS_10_0) ||\
+    (__WATCH_OS_VERSION_MIN_REQUIRED >= __WATCHOS_3_0)
+
+#ifndef SD_LOCK_DECLARE
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_LOCK_DECLARE(lock) os_unfair_lock lock
+#else
+#define SD_LOCK_DECLARE(lock) os_unfair_lock lock API_AVAILABLE(ios(10.0), tvos(10), watchos(3), macos(10.12)); \
+OSSpinLock lock##_deprecated;
+#endif
+#endif
+
+#ifndef SD_LOCK_DECLARE_STATIC
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_LOCK_DECLARE_STATIC(lock) static os_unfair_lock lock
+#else
+#define SD_LOCK_DECLARE_STATIC(lock) static os_unfair_lock lock API_AVAILABLE(ios(10.0), tvos(10), watchos(3), macos(10.12)); \
+static OSSpinLock lock##_deprecated;
+#endif
+#endif
+
+#ifndef SD_LOCK_INIT
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_LOCK_INIT(lock) lock = OS_UNFAIR_LOCK_INIT
+#else
+#define SD_LOCK_INIT(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) lock = OS_UNFAIR_LOCK_INIT; \
+else lock##_deprecated = OS_SPINLOCK_INIT;
+#endif
+#endif
+
 #ifndef SD_LOCK
-#define SD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_LOCK(lock) os_unfair_lock_lock(&lock)
+#else
+#define SD_LOCK(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) os_unfair_lock_lock(&lock); \
+else OSSpinLockLock(&lock##_deprecated);
+#endif
 #endif
 
 #ifndef SD_UNLOCK
-#define SD_UNLOCK(lock) dispatch_semaphore_signal(lock);
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_UNLOCK(lock) os_unfair_lock_unlock(&lock)
+#else
+#define SD_UNLOCK(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) os_unfair_lock_unlock(&lock); \
+else OSSpinLockUnlock(&lock##_deprecated);
+#endif
 #endif
 
 #ifndef SD_OPTIONS_CONTAINS

+ 1 - 0
Pods/SDWebImage/WebImage/SDWebImage.h

@@ -18,6 +18,7 @@ FOUNDATION_EXPORT const unsigned char SDWebImageVersionString[];
 // In this header, you should import all the public headers of your framework using statements like #import <SDWebImage/PublicHeader.h>
 
 #import <SDWebImage/SDWebImageManager.h>
+#import <SDWebImage/SDCallbackQueue.h>
 #import <SDWebImage/SDWebImageCacheKeyFilter.h>
 #import <SDWebImage/SDWebImageCacheSerializer.h>
 #import <SDWebImage/SDImageCacheConfig.h>

+ 12 - 5
Pods/SDWebImageSVGKitPlugin/README.md

@@ -20,9 +20,10 @@ You can modify the code or use some other SVG files to check the compatibility.
 
 ## Requirements
 
-+ iOS 8+
++ iOS 9+
 + tvOS 9+
-+ macOS 10.10+
++ macOS 10.11+
++ Xcode 11+
 
 ## Installation
 
@@ -35,10 +36,16 @@ it, simply add the following line to your Podfile:
 pod 'SDWebImageSVGKitPlugin'
 ```
 
-Note: The [SVGKit](https://github.com/SVGKit/SVGKit) dependency seems does not follow sem-version and didn't not release versions frequently. You can manually specify a branch or CID dependency for it. Like below:
+#### Swift Package Manager (Xcode 11+)
 
-```
-pod 'SVGKit', :git => 'https://github.com/SVGKit/SVGKit.git', :branch => '3.x'
+SDWebImagePhotosPlugin is available through [Swift Package Manager](https://swift.org/package-manager).
+
+```swift
+let package = Package(
+    dependencies: [
+        .package(url: "https://github.com/SDWebImage/SDWebImageSVGKitPlugin.git", from: "1.4")
+    ]
+)
 ```
 
 #### Carthage

+ 4 - 0
Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDImageSVGKCoder.h

@@ -5,7 +5,11 @@
 //  Created by DreamPiggy on 2018/9/27.
 //
 
+#if __has_include(<SDWebImage/SDWebImage.h>)
 #import <SDWebImage/SDWebImage.h>
+#else
+@import SDWebImage;
+#endif
 
 NS_ASSUME_NONNULL_BEGIN
 

+ 4 - 1
Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDImageSVGKCoder.m

@@ -8,8 +8,11 @@
 #import "SDImageSVGKCoder.h"
 #import "SDSVGKImage.h"
 #import "SDWebImageSVGKitDefine.h"
+#if __has_include(<SVGKit/SVGKit.h>)
 #import <SVGKit/SVGKit.h>
-
+#else
+@import SVGKit;
+#endif
 #define kSVGTagEnd @"</svg>"
 
 @implementation SDImageSVGKCoder

+ 8 - 0
Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDSVGKImage.h

@@ -5,8 +5,16 @@
 //  Created by DreamPiggy on 2018/10/10.
 //
 
+#if __has_include(<SDWebImage/SDWebImage.h>)
 #import <SDWebImage/SDWebImage.h>
+#else
+@import SDWebImage;
+#endif
+#if __has_include(<SVGKit/SVGKit.h>)
 #import <SVGKit/SVGKit.h>
+#else
+@import SVGKit;
+#endif
 
 NS_ASSUME_NONNULL_BEGIN
 

+ 4 - 0
Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDWebImageSVGKitDefine.h

@@ -5,7 +5,11 @@
 //  Created by DreamPiggy on 2018/10/11.
 //
 
+#if __has_include(<SDWebImage/SDWebImage.h>)
 #import <SDWebImage/SDWebImage.h>
+#else
+@import SDWebImage;
+#endif
 
 @class SVGKImage;
 

+ 4 - 1
Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SDWebImageSVGKitDefine.m

@@ -6,8 +6,11 @@
 //
 
 #import "SDWebImageSVGKitDefine.h"
+#if __has_include(<SVGKit/SVGKit.h>)
 #import <SVGKit/SVGKit.h>
-
+#else
+@import SVGKit;
+#endif
 #if SD_UIKIT
 void SDAdjustSVGContentMode(SVGKImage * svgImage, UIViewContentMode contentMode, CGSize viewSize) {
     NSCParameterAssert(svgImage);

+ 4 - 0
Pods/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin/Classes/SVGKImageView+WebCache.h

@@ -5,7 +5,11 @@
 //  Created by DreamPiggy on 2018/10/10.
 //
 
+#if __has_include(<SVGKit/SVGKit.h>)
 #import <SVGKit/SVGKit.h>
+#else
+@import SVGKit;
+#endif
 #import "SDWebImageSVGKitDefine.h"
 
 NS_ASSUME_NONNULL_BEGIN

+ 80 - 4
Pods/SDWebImageWebPCoder/README.md

@@ -14,10 +14,11 @@ SDWebImageWebPCoder supports both WebP decoding and encoding, for Static WebP or
 
 ## Requirements
 
-+ iOS 8
-+ macOS 10.10
++ iOS 9.0
++ macOS 10.11
 + tvOS 9.0
 + watchOS 2.0
++ Xcode 11.0
 
 ## Installation
 
@@ -155,7 +156,7 @@ let image = SDImageWebPCoder.shared.decodedImage(with: data, options: nil)
 // WebP thumbnail image decoding
 NSData *webpData;
 CGSize thumbnailSize = CGSizeMake(300, 300);
-UIImage *thumbnailImage = [[SDImageWebPCoder sharedCoder] decodedImageWithData:webpData options:@{SDImageCoderDecodeThumbnailPixelSize : @(thumbnailSize}];
+UIImage *thumbnailImage = [[SDImageWebPCoder sharedCoder] decodedImageWithData:webpData options:@{SDImageCoderDecodeThumbnailPixelSize : @(thumbnailSize)}];
 ```
 
 + Swift
@@ -175,10 +176,12 @@ let image = SDImageWebPCoder.shared.decodedImage(with: data, options: [.decodeTh
 // WebP image encoding
 UIImage *image;
 NSData *webpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage:image format:SDImageFormatWebP options:nil];
+// Animated encoding
+NSArray<SDImageFrames *> *frames;
+NSData *awebpData = [[SDImageWebPCoder sharedCoder] encodedDataWithFrames:frames loopCount:0 format:SDImageFormatWebP options:nil];
 // Encode Quality
 NSData *lossyWebpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage:image format:SDImageFormatWebP options:@{SDImageCoderEncodeCompressionQuality : @(0.1)}]; // [0, 1] compression quality
 NSData *limitedWebpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage:image format:SDImageFormatWebP options:@{SDImageCoderEncodeMaxFileSize : @(1024 * 10)}]; // v0.6.0 feature, limit output file size <= 10KB
-NSData *thumbnailWebpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage:image format:SDImageFormatWebP options:@{SDImageCoderEncodeMaxPixelSize : @(CGSizeMake(200, 200)}]; // v0.6.1 feature, encoding max pixel size
 ```
 
 + Swift
@@ -187,13 +190,86 @@ NSData *thumbnailWebpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage
 // WebP image encoding
 let image: UIImage
 let webpData = SDImageWebPCoder.shared.encodedData(with: image, format: .webP, options: nil)
+// Animated encoding
+let frames: [SDImageFrame]
+let awebpData = SDImageWebPCoder.shared.encodedData(with: frames, loopCount: 0, format: .webP, options: nil)
+// Encode Quality
 let lossyWebpData = SDImageWebPCoder.shared.encodedData(with: image, format: .webP, options: [.encodeCompressionQuality: 0.1]) // [0, 1] compression quality
 let limitedWebpData = SDImageWebPCoder.shared.encodedData(with: image, format: .webP, options: [.encodeMaxFileSize: 1024 * 10]) // v0.6.0 feature, limit output file size <= 10KB
+```
+
+### Thumbnail Encoding (0.6.1+)
+
++ Objective-C
+
+```objective-c
+// WebP image thumbnail encoding
+UIImage *image;
+NSData *thumbnailWebpData = [[SDImageWebPCoder sharedCoder] encodedDataWithImage:image format:SDImageFormatWebP options:@{SDImageCoderEncodeMaxPixelSize : @(CGSizeMake(200, 200))}]; // v0.6.1 feature, encoding max pixel size
+```
+
++ Swift
+
+```swift
+// WebP image thumbnail encoding
+let image: UIImage
 let thumbnailWebpData = SDImageWebPCoder.shared.encodedData(with: image, format: .webP, options: [.encodeMaxPixelSize: CGSize(width: 200, height: 200)]) // v0.6.1 feature, encoding max pixel size
 ```
 
 See more documentation in [SDWebImage Wiki - Coders](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#custom-coder-420)
 
+### Animated WebP Encoding (0.10+)
+
++ Objective-c
+
+```objective-c
+// Animated encoding
+NSMutableArray<SDImageFrames *> *frames = [NSMutableArray array];
+for (size_t i = 0; i < images.count; i++) {
+    SDImageFrame *frame = [SDImageFrame frameWithImage:images[i] duration:0.1];
+    [frames appendObject:frame];
+}
+NSData *awebpData = [[SDImageWebPCoder sharedCoder] encodedDataWithFrames:frames loopCount:0 format:SDImageFormatWebP options:nil];
+```
+
++ Swift
+
+```swift
+// Animated encoding
+var frames: [SDImageFrame] = []
+for i in 0..<images.count {
+    let frame = SDImageFrame(image: images[i], duration: 0.1)
+    frames.append(frame)
+}
+let awebpData = SDImageWebPCoder.shared.encodedData(with: frames, loopCount: 0, format: .webP, options: nil)
+```
+
+### Advanced WebP codec options (0.8+)
+
+The WebP codec [libwebp](https://developers.google.com/speed/webp/docs/api) we use, supports some advanced control options for encoding/decoding. You can pass them to libwebp by using the wrapper top level API:
+
++ Objective-C
+
+```objective-c
+UIImage *image;
+SDImageCoderOptions *options = @{SDImageCoderEncodeWebPMethod: @(0), SDImageCoderEncodeWebPAlphaCompression: @(100)};
+NSData *data = [SDImageWebPCoder.sharedCoder encodedDataWithImage:image format:SDImageFormatWebP options:options];
+// Will translate into:
+// config->method = 0;
+// config->alpha_quality = 100;
+```
+
++ Swift
+
+```swift
+let image: UIImage
+let options = [.encodeWebPMethod: 0, .encodeWebPAlphaCompression: 100]
+let data = SDImageWebPCoder.shared.encodedData(with: image, format: .webP, options: options)
+// Will translate into:
+// config->method = 0;
+// config->alpha_quality = 100;
+```
+
 ## Example
 
 To run the example project, clone the repo, and run `pod install` from the root directory first. Then open `SDWebImageWebPCoder.xcworkspace`.

+ 233 - 175
Pods/SDWebImageWebPCoder/SDWebImageWebPCoder/Classes/SDImageWebPCoder.m

@@ -7,6 +7,10 @@
  */
 
 #import "SDImageWebPCoder.h"
+#import "SDWebImageWebPCoderDefine.h"
+#import <Accelerate/Accelerate.h>
+#import <os/lock.h>
+#import <libkern/OSAtomic.h>
 
 #if __has_include("webp/decode.h") && __has_include("webp/encode.h") && __has_include("webp/demux.h") && __has_include("webp/mux.h")
 #import "webp/decode.h"
@@ -22,48 +26,68 @@
 @import libwebp;
 #endif
 
-#import <Accelerate/Accelerate.h>
+#define SD_USE_OS_UNFAIR_LOCK TARGET_OS_MACCATALYST ||\
+    (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0) ||\
+    (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_12) ||\
+    (__TV_OS_VERSION_MIN_REQUIRED >= __TVOS_10_0) ||\
+    (__WATCH_OS_VERSION_MIN_REQUIRED >= __WATCHOS_3_0)
 
-/// Calculate the actual thumnail pixel size
-static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio, CGSize thumbnailSize) {
-    CGFloat width = fullSize.width;
-    CGFloat height = fullSize.height;
-    CGFloat resultWidth;
-    CGFloat resultHeight;
-    
-    if (width == 0 || height == 0 || thumbnailSize.width == 0 || thumbnailSize.height == 0 || (width <= thumbnailSize.width && height <= thumbnailSize.height)) {
-        // Full Pixel
-        resultWidth = width;
-        resultHeight = height;
-    } else {
-        // Thumbnail
-        if (preserveAspectRatio) {
-            CGFloat pixelRatio = width / height;
-            CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height;
-            if (pixelRatio > thumbnailRatio) {
-                resultWidth = thumbnailSize.width;
-                resultHeight = ceil(thumbnailSize.width / pixelRatio);
-            } else {
-                resultHeight = thumbnailSize.height;
-                resultWidth = ceil(thumbnailSize.height * pixelRatio);
-            }
-        } else {
-            resultWidth = thumbnailSize.width;
-            resultHeight = thumbnailSize.height;
-        }
-    }
-    
-    return CGSizeMake(resultWidth, resultHeight);
-}
+#ifndef SD_LOCK_DECLARE
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_LOCK_DECLARE(lock) os_unfair_lock lock
+#else
+#define SD_LOCK_DECLARE(lock) os_unfair_lock lock API_AVAILABLE(ios(10.0), tvos(10), watchos(3), macos(10.12)); \
+OSSpinLock lock##_deprecated;
+#endif
+#endif
+
+#ifndef SD_LOCK_INIT
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_LOCK_INIT(lock) lock = OS_UNFAIR_LOCK_INIT
+#else
+#define SD_LOCK_INIT(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) lock = OS_UNFAIR_LOCK_INIT; \
+else lock##_deprecated = OS_SPINLOCK_INIT;
+#endif
+#endif
 
 #ifndef SD_LOCK
-#define SD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_LOCK(lock) os_unfair_lock_lock(&lock)
+#else
+#define SD_LOCK(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) os_unfair_lock_lock(&lock); \
+else OSSpinLockLock(&lock##_deprecated);
+#endif
 #endif
 
 #ifndef SD_UNLOCK
-#define SD_UNLOCK(lock) dispatch_semaphore_signal(lock);
+#if SD_USE_OS_UNFAIR_LOCK
+#define SD_UNLOCK(lock) os_unfair_lock_unlock(&lock)
+#else
+#define SD_UNLOCK(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) os_unfair_lock_unlock(&lock); \
+else OSSpinLockUnlock(&lock##_deprecated);
+#endif
 #endif
 
+/// Used for animated WebP, which need a canvas for decoding (rendering), possible apply a scale transform for thumbnail decoding (avoiding post-rescale using vImage)
+/// See more in #73
+static inline CGContextRef _Nullable CreateWebPCanvas(BOOL hasAlpha, CGSize canvasSize, CGSize thumbnailSize, BOOL preserveAspectRatio) {
+    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
+    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
+    // Check whether we need to use thumbnail
+    CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(canvasSize.width, canvasSize.height) scaleSize:thumbnailSize preserveAspectRatio:preserveAspectRatio shouldScaleUp:NO];
+    CGContextRef canvas = CGBitmapContextCreate(NULL, scaledSize.width, scaledSize.height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
+    if (!canvas) {
+        return nil;
+    }
+    // Check whether we need to use thumbnail
+    if (!CGSizeEqualToSize(canvasSize, scaledSize)) {
+        CGFloat sx = scaledSize.width / canvasSize.width;
+        CGFloat sy = scaledSize.height / canvasSize.height;
+        CGContextScaleCTM(canvas, sx, sy);
+    }
+    return canvas;
+}
+
 @interface SDWebPCoderFrame : NSObject
 
 @property (nonatomic, assign) NSUInteger index; // Frame index (zero based)
@@ -98,7 +122,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     BOOL _finished;
     CGFloat _canvasWidth;
     CGFloat _canvasHeight;
-    dispatch_semaphore_t _lock;
+    SD_LOCK_DECLARE(_lock);
     NSUInteger _currentBlendIndex;
     BOOL _preserveAspectRatio;
     CGSize _thumbnailSize;
@@ -195,7 +219,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     int canvasWidth = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_WIDTH);
     int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
     // Check whether we need to use thumbnail
-    CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(canvasWidth, canvasHeight), preserveAspectRatio, thumbnailSize);
+    CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(canvasWidth, canvasHeight) scaleSize:thumbnailSize preserveAspectRatio:preserveAspectRatio shouldScaleUp:NO];
     
     if (!hasAnimation || decodeFirstFrame) {
         // first frame for animated webp image
@@ -214,9 +238,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     }
     
     BOOL hasAlpha = flags & ALPHA_FLAG;
-    CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
-    bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
-    CGContextRef canvas = CGBitmapContextCreate(NULL, canvasWidth, canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
+    CGContextRef canvas = CreateWebPCanvas(hasAlpha, CGSizeMake(canvasWidth, canvasHeight), thumbnailSize, preserveAspectRatio);
     if (!canvas) {
         WebPDemuxDelete(demuxer);
         CGColorSpaceRelease(colorSpace);
@@ -228,7 +250,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     
     do {
         @autoreleasepool {
-            CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas iterator:iter colorSpace:colorSpace scaledSize:scaledSize];
+            CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:canvas demuxer:demuxer iterator:iter colorSpace:colorSpace];
             if (!imageRef) {
                 continue;
             }
@@ -291,7 +313,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
         }
         _preserveAspectRatio = preserveAspectRatio;
         _currentBlendIndex = NSNotFound;
-        _lock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_lock);
     }
     return self;
 }
@@ -303,8 +325,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     _finished = finished;
     // check whether we can detect Animated WebP or Static WebP, they need different codec (Demuxer or IDecoder)
     if (!_hasAnimation) {
-        _imageData = [data copy];
-        VP8StatusCode status = WebPIUpdate(_idec, _imageData.bytes, _imageData.length);
+        VP8StatusCode status = WebPIUpdate(_idec, data.bytes, data.length);
         // For Static WebP, all things done.
         // For Animated WebP (currently use `VP8_STATUS_UNSUPPORTED_FEATURE` to check), continue to create demuxer
         if (status != VP8_STATUS_UNSUPPORTED_FEATURE) {
@@ -320,18 +341,17 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
         WebPDemuxDelete(_demux);
         _demux = NULL;
     }
-    _imageData = [data copy];
+    _imageData = data;
     WebPData webpData;
     WebPDataInit(&webpData);
     webpData.bytes = _imageData.bytes;
     webpData.size = _imageData.length;
     WebPDemuxState state;
     _demux = WebPDemuxPartial(&webpData, &state);
-    SD_UNLOCK(_lock);
-    
     if (_demux && state != WEBP_DEMUX_PARSE_ERROR) {
         [self scanAndCheckFramesValidWithDemuxer:_demux];
     }
+    SD_UNLOCK(_lock);
 }
 
 - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options {
@@ -379,7 +399,7 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
             return nil;
         }
         
-        CGContextRef canvas = CGBitmapContextCreate(NULL, width, height, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
+        CGContextRef canvas = CreateWebPCanvas(YES, CGSizeMake(width, height), _thumbnailSize, _preserveAspectRatio);
         if (!canvas) {
             CGImageRelease(imageRef);
             return nil;
@@ -401,13 +421,6 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
                 scale = 1;
             }
         }
-        CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(width, height), _preserveAspectRatio, _thumbnailSize);
-        // Check whether we need to use thumbnail
-        if (!CGSizeEqualToSize(CGSizeMake(width, height), scaledSize)) {
-            CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:newImageRef size:scaledSize];
-            CGImageRelease(newImageRef);
-            newImageRef = scaledImageRef;
-        }
         
 #if SD_UIKIT || SD_WATCH
         image = [[UIImage alloc] initWithCGImage:newImageRef scale:scale orientation:UIImageOrientationUp];
@@ -423,8 +436,8 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     return image;
 }
 
-- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef {
-    size_t canvasHeight = CGBitmapContextGetHeight(canvas);
+- (void)sd_blendWebpImageWithCanvas:(CGContextRef)canvas demuxer:(nonnull WebPDemuxer *)demuxer iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef {
+    int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
     CGFloat tmpX = iter.x_offset;
     CGFloat tmpY = canvasHeight - iter.height - iter.y_offset;
     CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
@@ -446,14 +459,13 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     }
 }
 
-- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef scaledSize:(CGSize)scaledSize CF_RETURNS_RETAINED {
+- (nullable CGImageRef)sd_drawnWebpImageWithCanvas:(CGContextRef)canvas demuxer:(nonnull WebPDemuxer *)demuxer iterator:(WebPIterator)iter colorSpace:(nonnull CGColorSpaceRef)colorSpaceRef CF_RETURNS_RETAINED {
     CGImageRef imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:colorSpaceRef scaledSize:CGSizeZero];
     if (!imageRef) {
         return nil;
     }
     
-    size_t canvasWidth = CGBitmapContextGetWidth(canvas);
-    size_t canvasHeight = CGBitmapContextGetHeight(canvas);
+    int canvasHeight = WebPDemuxGetI(demuxer, WEBP_FF_CANVAS_HEIGHT);
     CGFloat tmpX = iter.x_offset;
     CGFloat tmpY = canvasHeight - iter.height - iter.y_offset;
     CGRect imageRect = CGRectMake(tmpX, tmpY, iter.width, iter.height);
@@ -464,26 +476,15 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     if (!shouldBlend) {
         CGContextClearRect(canvas, imageRect);
     }
+    
     CGContextDrawImage(canvas, imageRect, imageRef);
     CGImageRef newImageRef = CGBitmapContextCreateImage(canvas);
-    
     CGImageRelease(imageRef);
 
     if (iter.dispose_method == WEBP_MUX_DISPOSE_BACKGROUND) {
         CGContextClearRect(canvas, imageRect);
     }
     
-    // Check whether we need to use thumbnail
-    if (!CGSizeEqualToSize(CGSizeMake(canvasWidth, canvasHeight), scaledSize)) {
-        // Important: For Animated WebP thumbnail generation, we can not just use a scaled small canvas and draw each thumbnail frame
-        // This works **On Theory**. However, image scale down loss details. Animated WebP use the partial pixels with blend mode / dispose method with offset, to cover previous canvas status
-        // Because of this reason, even each frame contains small zigzag, the final animation contains visible glitch, this is not we want.
-        // So, always create the full pixels canvas (even though this consume more RAM), after drawn on the canvas, re-scale again with the final size
-        CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:newImageRef size:scaledSize];
-        CGImageRelease(newImageRef);
-        newImageRef = scaledImageRef;
-    }
-    
     return newImageRef;
 }
 
@@ -592,6 +593,24 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     if (!image) {
         return nil;
     }
+    NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
+    if (!frames || frames.count == 0) {
+        SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0];
+        frames = @[frame];
+    }
+    return [self encodedDataWithFrames:frames loopCount:image.sd_imageLoopCount format:format options:options];
+}
+
+- (NSData *)encodedDataWithFrames:(NSArray<SDImageFrame *> *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options {
+    UIImage *image = frames.firstObject.image; // Primary image
+    if (!image) {
+        return nil;
+    }
+    CGImageRef imageRef = image.CGImage;
+    if (!imageRef) {
+        // Earily return, supports CGImage only
+        return nil;
+    }
     
     NSData *data;
     
@@ -612,12 +631,15 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     if (options[SDImageCoderEncodeMaxFileSize]) {
         maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue];
     }
-    NSArray<SDImageFrame *> *frames = [SDImageCoderHelper framesFromAnimatedImage:image];
     
     BOOL encodeFirstFrame = [options[SDImageCoderEncodeFirstFrameOnly] boolValue];
-    if (encodeFirstFrame || frames.count == 0) {
+    if (encodeFirstFrame || frames.count <= 1) {
         // for static single webp image
-        data = [self sd_encodedWebpDataWithImage:image.CGImage quality:compressionQuality maxPixelSize:maxPixelSize maxFileSize:maxFileSize];
+        data = [self sd_encodedWebpDataWithImage:imageRef
+                                         quality:compressionQuality
+                                    maxPixelSize:maxPixelSize
+                                     maxFileSize:maxFileSize
+                                         options:options];
     } else {
         // for animated webp image
         WebPMux *mux = WebPMuxNew();
@@ -626,7 +648,11 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
         }
         for (size_t i = 0; i < frames.count; i++) {
             SDImageFrame *currentFrame = frames[i];
-            NSData *webpData = [self sd_encodedWebpDataWithImage:currentFrame.image.CGImage quality:compressionQuality maxPixelSize:maxPixelSize maxFileSize:maxFileSize];
+            NSData *webpData = [self sd_encodedWebpDataWithImage:currentFrame.image.CGImage
+                                                         quality:compressionQuality
+                                                    maxPixelSize:maxPixelSize
+                                                     maxFileSize:maxFileSize
+                                                         options:options];
             int duration = currentFrame.duration * 1000;
             WebPMuxFrameInfo frame = { .bitstream.bytes = webpData.bytes,
                 .bitstream.size = webpData.length,
@@ -641,9 +667,8 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
             }
         }
         
-        int loopCount = (int)image.sd_imageLoopCount;
         WebPMuxAnimParams params = { .bgcolor = 0,
-            .loop_count = loopCount
+            .loop_count = (int)loopCount
         };
         if (WebPMuxSetAnimationParams(mux, &params) != WEBP_MUX_OK) {
             WebPMuxDelete(mux);
@@ -663,7 +688,12 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     return data;
 }
 
-- (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef quality:(double)quality maxPixelSize:(CGSize)maxPixelSize maxFileSize:(NSUInteger)maxFileSize {
+- (nullable NSData *)sd_encodedWebpDataWithImage:(nullable CGImageRef)imageRef
+                                         quality:(double)quality
+                                    maxPixelSize:(CGSize)maxPixelSize
+                                     maxFileSize:(NSUInteger)maxFileSize
+                                         options:(nullable SDImageCoderOptions *)options
+{
     NSData *webpData;
     if (!imageRef) {
         return nil;
@@ -679,6 +709,9 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     }
     
     size_t bytesPerRow = CGImageGetBytesPerRow(imageRef);
+    size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef);
+    size_t bitsPerPixel = CGImageGetBitsPerPixel(imageRef);
+    size_t components = bitsPerPixel / bitsPerComponent;
     CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef);
     CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask;
     CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask;
@@ -702,68 +735,22 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     if (!dataProvider) {
         return nil;
     }
-    CFDataRef dataRef = CGDataProviderCopyData(dataProvider);
-    if (!dataRef) {
-        return nil;
-    }
     
     uint8_t *rgba = NULL; // RGBA Buffer managed by CFData, don't call `free` on it, instead call `CFRelease` on `dataRef`
     // We could not assume that input CGImage's color mode is always RGB888/RGBA8888. Convert all other cases to target color mode using vImage
-    if (byteOrderNormal && ((alphaInfo == kCGImageAlphaNone) || (alphaInfo == kCGImageAlphaLast))) {
-        // If the input CGImage is already RGB888/RGBA8888
-        rgba = (uint8_t *)CFDataGetBytePtr(dataRef);
-    } else {
-        // Convert all other cases to target color mode using vImage
-        vImageConverterRef convertor = NULL;
-        vImage_Error error = kvImageNoError;
-        
-        vImage_CGImageFormat srcFormat = {
-            .bitsPerComponent = (uint32_t)CGImageGetBitsPerComponent(imageRef),
-            .bitsPerPixel = (uint32_t)CGImageGetBitsPerPixel(imageRef),
-            .colorSpace = CGImageGetColorSpace(imageRef),
-            .bitmapInfo = bitmapInfo
-        };
-        vImage_CGImageFormat destFormat = {
-            .bitsPerComponent = 8,
-            .bitsPerPixel = hasAlpha ? 32 : 24,
-            .colorSpace = [SDImageCoderHelper colorSpaceGetDeviceRGB],
-            .bitmapInfo = hasAlpha ? kCGImageAlphaLast | kCGBitmapByteOrderDefault : kCGImageAlphaNone | kCGBitmapByteOrderDefault // RGB888/RGBA8888 (Non-premultiplied to works for libwebp)
-        };
-        
-        convertor = vImageConverter_CreateWithCGImageFormat(&srcFormat, &destFormat, NULL, kvImageNoFlags, &error);
-        if (error != kvImageNoError) {
-            CFRelease(dataRef);
-            return nil;
-        }
-        
-        vImage_Buffer src = {
-            .data = (uint8_t *)CFDataGetBytePtr(dataRef),
-            .width = width,
-            .height = height,
-            .rowBytes = bytesPerRow
-        };
-        vImage_Buffer dest;
-        
-        error = vImageBuffer_Init(&dest, height, width, destFormat.bitsPerPixel, kvImageNoFlags);
-        if (error != kvImageNoError) {
-            vImageConverter_Release(convertor);
-            CFRelease(dataRef);
-            return nil;
-        }
-        
-        // Convert input color mode to RGB888/RGBA8888
-        error = vImageConvert_AnyToAny(convertor, &src, &dest, NULL, kvImageNoFlags);
-        vImageConverter_Release(convertor);
-        if (error != kvImageNoError) {
-            CFRelease(dataRef);
-            return nil;
-        }
-        
-        rgba = dest.data; // Converted buffer
-        bytesPerRow = dest.rowBytes; // Converted bytePerRow
-        CFRelease(dataRef); // Use CFData to manage bytes for free, the same code path for error handling
-        dataRef = CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, rgba, bytesPerRow * height, kCFAllocatorDefault);
+    vImage_CGImageFormat destFormat = {
+        .bitsPerComponent = 8,
+        .bitsPerPixel = hasAlpha ? 32 : 24,
+        .colorSpace = [SDImageCoderHelper colorSpaceGetDeviceRGB],
+        .bitmapInfo = hasAlpha ? kCGImageAlphaLast | kCGBitmapByteOrderDefault : kCGImageAlphaNone | kCGBitmapByteOrderDefault // RGB888/RGBA8888 (Non-premultiplied to works for libwebp)
+    };
+    vImage_Buffer dest;
+    vImage_Error error = vImageBuffer_InitWithCGImage(&dest, &destFormat, NULL, imageRef, kvImageNoFlags);
+    if (error != kvImageNoError) {
+        return nil;
     }
+    rgba = dest.data;
+    bytesPerRow = dest.rowBytes;
     
     float qualityFactor = quality * 100; // WebP quality is 0-100
     // Encode RGB888/RGBA8888 buffer to WebP data
@@ -775,14 +762,11 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     if (!WebPConfigPreset(&config, WEBP_PRESET_DEFAULT, qualityFactor) ||
         !WebPPictureInit(&picture)) {
         // shouldn't happen, except if system installation is broken
-        CFRelease(dataRef);
+        free(dest.data);
         return nil;
     }
 
-    config.target_size = (int)maxFileSize; // Max filesize for output, 0 means use quality instead
-    config.pass = maxFileSize > 0 ? 6 : 1; // Use 6 passes for file size limited encoding, which is the default value of `cwebp` command line
-    config.thread_level = 1; // Thread encoding for fast
-    config.lossless = 0; // Disable lossless encoding (If we need, can add new Encoding Options in future version)
+    [self updateWebPOptionsToConfig:&config maxFileSize:maxFileSize options:options];
     picture.use_argb = 0; // Lossy encoding use YUV for internel bitstream
     picture.width = (int)width;
     picture.height = (int)height;
@@ -798,25 +782,25 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     }
     if (!result) {
         WebPMemoryWriterClear(&writer);
-        CFRelease(dataRef);
+        free(dest.data);
         return nil;
     }
     
     // Check if need to scale pixel size
-    if (maxPixelSize.width > 0 && maxPixelSize.height > 0 && width > maxPixelSize.width && height > maxPixelSize.height) {
-        CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(width, height), YES, maxPixelSize);
+    CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(width, height) scaleSize:maxPixelSize preserveAspectRatio:YES shouldScaleUp:NO];
+    if (!CGSizeEqualToSize(scaledSize, CGSizeMake(width, height))) {
         result = WebPPictureRescale(&picture, scaledSize.width, scaledSize.height);
         if (!result) {
             WebPMemoryWriterClear(&writer);
             WebPPictureFree(&picture);
-            CFRelease(dataRef);
+            free(dest.data);
             return nil;
         }
     }
     
     result = WebPEncode(&config, &picture);
     WebPPictureFree(&picture);
-    CFRelease(dataRef); // Free bitmap buffer
+    free(dest.data);
     
     if (result) {
         // success
@@ -830,10 +814,60 @@ static CGSize SDCalculateThumbnailSize(CGSize fullSize, BOOL preserveAspectRatio
     return webpData;
 }
 
+- (void) updateWebPOptionsToConfig:(WebPConfig * _Nonnull)config
+                       maxFileSize:(NSUInteger)maxFileSize
+                           options:(nullable SDImageCoderOptions *)options {
+
+    config->target_size = (int)maxFileSize; // Max filesize for output, 0 means use quality instead
+    config->pass = maxFileSize > 0 ? 6 : 1; // Use 6 passes for file size limited encoding, which is the default value of `cwebp` command line
+    config->lossless = 0; // Disable lossless encoding (If we need, can add new Encoding Options in future version)
+    
+    config->method = GetIntValueForKey(options, SDImageCoderEncodeWebPMethod, config->method);
+    config->pass = GetIntValueForKey(options, SDImageCoderEncodeWebPPass, config->pass);
+    config->preprocessing = GetIntValueForKey(options, SDImageCoderEncodeWebPPreprocessing, config->preprocessing);
+    config->thread_level = GetIntValueForKey(options, SDImageCoderEncodeWebPThreadLevel, 1);
+    config->low_memory = GetIntValueForKey(options, SDImageCoderEncodeWebPLowMemory, config->low_memory);
+    config->target_PSNR = GetFloatValueForKey(options, SDImageCoderEncodeWebPTargetPSNR, config->target_PSNR);
+    config->segments = GetIntValueForKey(options, SDImageCoderEncodeWebPSegments, config->segments);
+    config->sns_strength = GetIntValueForKey(options, SDImageCoderEncodeWebPSnsStrength, config->sns_strength);
+    config->filter_strength = GetIntValueForKey(options, SDImageCoderEncodeWebPFilterStrength, config->filter_strength);
+    config->filter_sharpness = GetIntValueForKey(options, SDImageCoderEncodeWebPFilterSharpness, config->filter_sharpness);
+    config->filter_type = GetIntValueForKey(options, SDImageCoderEncodeWebPFilterType, config->filter_type);
+    config->autofilter = GetIntValueForKey(options, SDImageCoderEncodeWebPAutofilter, config->autofilter);
+    config->alpha_compression = GetIntValueForKey(options, SDImageCoderEncodeWebPAlphaCompression, config->alpha_compression);
+    config->alpha_filtering = GetIntValueForKey(options, SDImageCoderEncodeWebPAlphaFiltering, config->alpha_filtering);
+    config->alpha_quality = GetIntValueForKey(options, SDImageCoderEncodeWebPAlphaQuality, config->alpha_quality);
+    config->show_compressed = GetIntValueForKey(options, SDImageCoderEncodeWebPShowCompressed, config->show_compressed);
+    config->partitions = GetIntValueForKey(options, SDImageCoderEncodeWebPPartitions, config->partitions);
+    config->partition_limit = GetIntValueForKey(options, SDImageCoderEncodeWebPPartitionLimit, config->partition_limit);
+    config->use_sharp_yuv = GetIntValueForKey(options, SDImageCoderEncodeWebPUseSharpYuv, config->use_sharp_yuv);
+}
+
 static void FreeImageData(void *info, const void *data, size_t size) {
     free((void *)data);
 }
 
+static int GetIntValueForKey(NSDictionary * _Nonnull dictionary, NSString * _Nonnull key, int defaultValue) {
+    id value = [dictionary objectForKey:key];
+    if (value != nil) {
+        if ([value isKindOfClass: [NSNumber class]]) {
+            return [value intValue];
+        }
+    }
+    return defaultValue;
+}
+
+static float GetFloatValueForKey(NSDictionary * _Nonnull dictionary, NSString * _Nonnull key, float defaultValue) {
+    id value = [dictionary objectForKey:key];
+    if (value != nil) {
+        if ([value isKindOfClass: [NSNumber class]]) {
+            return [value floatValue];
+        }
+    }
+    return defaultValue;
+}
+
+
 #pragma mark - SDAnimatedImageCoder
 - (instancetype)initWithAnimatedImageData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
     if (!data) {
@@ -881,7 +915,7 @@ static void FreeImageData(void *info, const void *data, size_t size) {
         _demux = demuxer;
         _imageData = data;
         _currentBlendIndex = NSNotFound;
-        _lock = dispatch_semaphore_create(1);
+        SD_LOCK_INIT(_lock);
     }
     return self;
 }
@@ -911,7 +945,6 @@ static void FreeImageData(void *info, const void *data, size_t size) {
     _hasAlpha = hasAlpha;
     _canvasWidth = canvasWidth;
     _canvasHeight = canvasHeight;
-    _frameCount = frameCount;
     _loopCount = loopCount;
     
     // If static WebP, does not need to parse the frame blend index
@@ -957,8 +990,10 @@ static void FreeImageData(void *info, const void *data, size_t size) {
     WebPDemuxReleaseIterator(&iter);
     
     if (frames.count != frameCount) {
+        // frames not match, do not override current value
         return NO;
     }
+    _frameCount = frameCount;
     _frames = [frames copy];
     
     return YES;
@@ -977,27 +1012,57 @@ static void FreeImageData(void *info, const void *data, size_t size) {
 }
 
 - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index {
-    if (index >= _frameCount) {
-        return 0;
-    }
-    if (_frameCount <= 1) {
-        return 0;
+    NSTimeInterval duration;
+    // Incremental Animation decoding may update frames when new bytes available
+    // Which should use lock to ensure frame count and frames match, ensure atomic logic
+    if (_idec != NULL) {
+        SD_LOCK(_lock);
+        if (index >= _frames.count) {
+            SD_UNLOCK(_lock);
+            return 0;
+        }
+        duration = _frames[index].duration;
+        SD_UNLOCK(_lock);
+    } else {
+        if (index >= _frames.count) {
+            return 0;
+        }
+        duration = _frames[index].duration;
     }
-    return _frames[index].duration;
+    return duration;
 }
 
 - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index {
     UIImage *image;
-    if (index >= _frameCount) {
-        return nil;
-    }
-    SD_LOCK(_lock);
-    if (_frameCount <= 1) {
-        image = [self safeStaticImageFrame];
+    // Incremental Animation decoding may update frames when new bytes available
+    // Which should use lock to ensure frame count and frames match, ensure atomic logic
+    if (_idec != NULL) {
+        SD_LOCK(_lock);
+        if (index >= _frames.count) {
+            SD_UNLOCK(_lock);
+            return nil;
+        }
+        if (_frames.count <= 1) {
+            image = [self safeStaticImageFrame];
+        } else {
+            image = [self safeAnimatedImageFrameAtIndex:index];
+        }
+        SD_UNLOCK(_lock);
     } else {
-        image = [self safeAnimatedImageFrameAtIndex:index];
+        // Animation Decoding need a lock on the canvas (which is shared), but the _frames is immutable and no lock needed
+        if (index >= _frames.count) {
+            return nil;
+        }
+        if (_frames.count <= 1) {
+            SD_LOCK(_lock);
+            image = [self safeStaticImageFrame];
+            SD_UNLOCK(_lock);
+        } else {
+            SD_LOCK(_lock);
+            image = [self safeAnimatedImageFrameAtIndex:index];
+            SD_UNLOCK(_lock);
+        }
     }
-    SD_UNLOCK(_lock);
     return image;
 }
 
@@ -1017,18 +1082,15 @@ static void FreeImageData(void *info, const void *data, size_t size) {
     if (_hasAnimation) {
         // If have animation, we still need to allocate a CGContext, because the poster frame may be smaller than canvas
         if (!_canvas) {
-            CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
-            bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
-            CGContextRef canvas = CGBitmapContextCreate(NULL, _canvasWidth, _canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
+            CGContextRef canvas = CreateWebPCanvas(_hasAlpha, CGSizeMake(_canvasWidth, _canvasHeight), _thumbnailSize, _preserveAspectRatio);
             if (!canvas) {
                 return nil;
             }
             _canvas = canvas;
         }
-        CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(_canvasWidth, _canvasHeight), _preserveAspectRatio, _thumbnailSize);
-        imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace scaledSize:scaledSize];
+        imageRef = [self sd_drawnWebpImageWithCanvas:_canvas demuxer:_demux iterator:iter colorSpace:_colorSpace];
     } else {
-        CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(iter.width, iter.height), _preserveAspectRatio, _thumbnailSize);
+        CGSize scaledSize = [SDImageCoderHelper scaledSizeWithImageSize:CGSizeMake(iter.width, iter.height) scaleSize:_thumbnailSize preserveAspectRatio:_preserveAspectRatio shouldScaleUp:NO];
         imageRef = [self sd_createWebpImageWithData:iter.fragment colorSpace:_colorSpace scaledSize:scaledSize];
     }
     if (!imageRef) {
@@ -1046,9 +1108,7 @@ static void FreeImageData(void *info, const void *data, size_t size) {
 
 - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index {
     if (!_canvas) {
-        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
-        bitmapInfo |= _hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
-        CGContextRef canvas = CGBitmapContextCreate(NULL, _canvasWidth, _canvasHeight, 8, 0, [SDImageCoderHelper colorSpaceGetDeviceRGB], bitmapInfo);
+        CGContextRef canvas = CreateWebPCanvas(_hasAlpha, CGSizeMake(_canvasWidth, _canvasHeight), _thumbnailSize, _preserveAspectRatio);
         if (!canvas) {
             return nil;
         }
@@ -1092,7 +1152,7 @@ static void FreeImageData(void *info, const void *data, size_t size) {
         if (endIndex > startIndex) {
             do {
                 @autoreleasepool {
-                    [self sd_blendWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace];
+                    [self sd_blendWebpImageWithCanvas:_canvas demuxer:_demux iterator:iter colorSpace:_colorSpace];
                 }
             } while ((size_t)iter.frame_num < endIndex && WebPDemuxNextFrame(&iter));
         }
@@ -1105,9 +1165,7 @@ static void FreeImageData(void *info, const void *data, size_t size) {
     _currentBlendIndex = index;
     
     // Now the canvas is ready, which respects of dispose method behavior. Just do normal decoding and produce image.
-    // Check whether we need to use thumbnail
-    CGSize scaledSize = SDCalculateThumbnailSize(CGSizeMake(_canvasWidth, _canvasHeight), _preserveAspectRatio, _thumbnailSize);
-    CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas iterator:iter colorSpace:_colorSpace scaledSize:scaledSize];
+    CGImageRef imageRef = [self sd_drawnWebpImageWithCanvas:_canvas demuxer:_demux iterator:iter colorSpace:_colorSpace];
     if (!imageRef) {
         return nil;
     }

+ 135 - 0
Pods/SDWebImageWebPCoder/SDWebImageWebPCoder/Classes/SDWebImageWebPCoderDefine.h

@@ -0,0 +1,135 @@
+/*
+ * This file is part of the SDWebImage package.
+ * (c) Olivier Poitrey <rs@dailymotion.com>
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+#if __has_include(<SDWebImage/SDWebImage.h>)
+#import <SDWebImage/SDWebImage.h>
+#else
+@import SDWebImage;
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+/**
+Integer value
+Quality/speed trade-off (0=fast, 6=slower-better)
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPMethod;
+
+/**
+Integer value
+Number of entropy-analysis passes (in [1..10])
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPPass;
+
+/**
+ Integer value
+ Preprocessing filter (0=none, 1=segment-smooth)
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPPreprocessing;
+
+/**
+ Float value
+ if non-zero, specifies the minimal distortion to try to achieve. Takes precedence over target_size.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPTargetPSNR;
+
+/**
+ Integer value
+ If non-zero, try and use multi-threaded encoding.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPThreadLevel;
+
+/**
+ Integer value
+ If set, reduce memory usage (but increase CPU use).
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPLowMemory;
+
+/**
+ Integer value
+ if non-zero, specifies the minimal distortion to try to achieve. Takes precedence over target_size.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPSegments;
+
+/**
+ Integer value
+ Spatial Noise Shaping. 0=off, 100=maximum.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPSnsStrength;
+
+/**
+ Integer value
+ Range: [0 = off .. 100 = strongest]
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPFilterStrength;
+
+/**
+ Integer value
+ range: [0 = off .. 7 = least sharp]
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPFilterSharpness;
+
+/**
+ Integer value
+ Filtering type: 0 = simple, 1 = strong (only used If filter_strength > 0 or autofilter > 0)
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPFilterType;
+
+/**
+ Integer value
+ Auto adjust filter's strength [0 = off, 1 = on]
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPAutofilter;
+
+/**
+ Integer value
+ Algorithm for encoding the alpha plane (0 = none, 1 = compressed with WebP lossless). Default is 1.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPAlphaCompression;
+
+/**
+ Integer value
+ Predictive filtering method for alpha plane. 0: none, 1: fast, 2: best. Default if 1.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPAlphaFiltering;
+
+/**
+ Integer value
+ Between 0 (smallest size) and 100 (lossless).
+ Default is 100.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPAlphaQuality;
+
+/**
+ Integer value
+ If true, export the compressed picture back.
+ In-loop filtering is not applied.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPShowCompressed;
+
+/**
+ Integer
+ Log2(number of token partitions) in [0..3]
+ Default is set to 0 for easier progressive decoding.
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPPartitions;
+
+/**
+ Integer value
+ Quality degradation allowed to fit the 512k limit on
+ Prediction modes coding (0: no degradation, 100: maximum possible degradation).
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPPartitionLimit;
+
+/**
+ Integer value
+ if needed, use sharp (and slow) RGB->YUV conversion
+ */
+FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPUseSharpYuv;
+
+NS_ASSUME_NONNULL_END

+ 28 - 0
Pods/SDWebImageWebPCoder/SDWebImageWebPCoder/Classes/SDWebImageWebPCoderDefine.m

@@ -0,0 +1,28 @@
+//
+//  SDWebImageWebPCoderDefine.m
+//  SDWebImageWebPCoder
+//
+//  Created by Antti Kortetmaa on 2020/12/06.
+//
+
+#import "SDWebImageWebPCoderDefine.h"
+
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPMethod = @"webPMethod";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPPass = @"webPPass";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPPreprocessing = @"webPPreprocessing";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPThreadLevel = @"webPThreadLevel";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPLowMemory = @"webPLowMemory";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPTargetPSNR = @"webPTargetPSNR";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPSegments = @"webPSegments";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPSnsStrength = @"webPSnsStrength";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPFilterStrength = @"webPFilterStrength";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPFilterSharpness = @"webPFilterSharpness";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPFilterType = @"webPFilterType";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPAutofilter = @"webPAutofilter";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPAlphaCompression = @"webPAlphaCompression";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPAlphaFiltering = @"webPAlphaFiltering";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPAlphaQuality = @"webPAlphaQuality";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPShowCompressed = @"webPShowCompressed";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPPartitions = @"webPPartitions";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPPartitionLimit = @"webPPartitionLimit";
+SDImageCoderOption _Nonnull const SDImageCoderEncodeWebPUseSharpYuv = @"webPUseSharpYuv";

+ 1 - 0
Pods/SDWebImageWebPCoder/SDWebImageWebPCoder/Module/SDWebImageWebPCoder.h

@@ -12,4 +12,5 @@ FOUNDATION_EXPORT double SDWebImageWebPCoderVersionNumber;
 FOUNDATION_EXPORT const unsigned char SDWebImageWebPCoderVersionString[];
 
 #import <SDWebImageWebPCoder/SDImageWebPCoder.h>
+#import <SDWebImageWebPCoder/SDWebImageWebPCoderDefine.h>
 #import <SDWebImageWebPCoder/UIImage+WebP.h>

+ 1 - 1
Pods/Target Support Files/SDWebImage/SDWebImage-Info.plist

@@ -15,7 +15,7 @@
   <key>CFBundlePackageType</key>
   <string>FMWK</string>
   <key>CFBundleShortVersionString</key>
-  <string>5.9.5</string>
+  <string>5.15.6</string>
   <key>CFBundleSignature</key>
   <string>????</string>
   <key>CFBundleVersion</key>

+ 1 - 0
Pods/Target Support Files/SDWebImage/SDWebImage-umbrella.h

@@ -18,6 +18,7 @@
 #import "SDAnimatedImageRep.h"
 #import "SDAnimatedImageView+WebCache.h"
 #import "SDAnimatedImageView.h"
+#import "SDCallbackQueue.h"
 #import "SDDiskCache.h"
 #import "SDGraphicsImageRenderer.h"
 #import "SDImageAPNGCoder.h"

+ 1 - 1
Pods/Target Support Files/SDWebImageSVGKitPlugin/SDWebImageSVGKitPlugin-Info.plist

@@ -15,7 +15,7 @@
   <key>CFBundlePackageType</key>
   <string>FMWK</string>
   <key>CFBundleShortVersionString</key>
-  <string>1.2.0</string>
+  <string>1.4.0</string>
   <key>CFBundleSignature</key>
   <string>????</string>
   <key>CFBundleVersion</key>

+ 1 - 1
Pods/Target Support Files/SDWebImageWebPCoder/SDWebImageWebPCoder-Info.plist

@@ -15,7 +15,7 @@
   <key>CFBundlePackageType</key>
   <string>FMWK</string>
   <key>CFBundleShortVersionString</key>
-  <string>0.6.1</string>
+  <string>0.11.0</string>
   <key>CFBundleSignature</key>
   <string>????</string>
   <key>CFBundleVersion</key>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä