@amazon-devices/expo-image 2.0.1758683737

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/.eslintrc.js +2 -0
  2. package/CHANGELOG.md +285 -0
  3. package/CMakeLists.txt +91 -0
  4. package/LICENSE +7 -0
  5. package/README.kepler.md +123 -0
  6. package/README.md +54 -0
  7. package/THIRD_PARTY_NOTICES.txt +24 -0
  8. package/android/build.gradle +124 -0
  9. package/android/proguard-rules.pro +28 -0
  10. package/android/src/main/AndroidManifest.xml +15 -0
  11. package/android/src/main/java/expo/modules/image/Exceptions.kt +9 -0
  12. package/android/src/main/java/expo/modules/image/ExpoImageAppGlideModule.kt +19 -0
  13. package/android/src/main/java/expo/modules/image/ExpoImageModule.kt +192 -0
  14. package/android/src/main/java/expo/modules/image/ExpoImageView.kt +220 -0
  15. package/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt +652 -0
  16. package/android/src/main/java/expo/modules/image/GlideModel.kt +73 -0
  17. package/android/src/main/java/expo/modules/image/ImageUtils.kt +50 -0
  18. package/android/src/main/java/expo/modules/image/ImageViewWrapperTarget.kt +313 -0
  19. package/android/src/main/java/expo/modules/image/ResourceIdHelper.kt +47 -0
  20. package/android/src/main/java/expo/modules/image/ThumbnailRequestCoordinatorExtension.kt +21 -0
  21. package/android/src/main/java/expo/modules/image/YogaUtils.kt +20 -0
  22. package/android/src/main/java/expo/modules/image/blurhash/BlurHashFetcher.kt +32 -0
  23. package/android/src/main/java/expo/modules/image/blurhash/BlurhashDecoder.kt +189 -0
  24. package/android/src/main/java/expo/modules/image/blurhash/BlurhashModelLoader.kt +30 -0
  25. package/android/src/main/java/expo/modules/image/blurhash/BlurhashModelLoaderFactory.kt +14 -0
  26. package/android/src/main/java/expo/modules/image/blurhash/BlurhashModule.kt +17 -0
  27. package/android/src/main/java/expo/modules/image/dataurls/Base64DataFetcher.kt +27 -0
  28. package/android/src/main/java/expo/modules/image/dataurls/Base64ModelLoader.kt +24 -0
  29. package/android/src/main/java/expo/modules/image/dataurls/Base64ModelLoaderFactory.kt +12 -0
  30. package/android/src/main/java/expo/modules/image/dataurls/Base64Module.kt +16 -0
  31. package/android/src/main/java/expo/modules/image/drawing/OutlineProvider.kt +201 -0
  32. package/android/src/main/java/expo/modules/image/enums/ContentFit.kt +89 -0
  33. package/android/src/main/java/expo/modules/image/enums/ImageCacheType.kt +14 -0
  34. package/android/src/main/java/expo/modules/image/enums/Priority.kt +15 -0
  35. package/android/src/main/java/expo/modules/image/events/GlideRequestListener.kt +64 -0
  36. package/android/src/main/java/expo/modules/image/events/OkHttpProgressListener.kt +26 -0
  37. package/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt +80 -0
  38. package/android/src/main/java/expo/modules/image/okhttp/GlideUrlWrapperLoader.kt +55 -0
  39. package/android/src/main/java/expo/modules/image/records/CachePolicy.kt +10 -0
  40. package/android/src/main/java/expo/modules/image/records/ContentPosition.kt +89 -0
  41. package/android/src/main/java/expo/modules/image/records/ImageTransition.kt +8 -0
  42. package/android/src/main/java/expo/modules/image/records/SourceMap.kt +160 -0
  43. package/android/src/main/java/expo/modules/image/records/events.kt +25 -0
  44. package/android/src/main/java/expo/modules/image/svg/SVGDecoder.kt +33 -0
  45. package/android/src/main/java/expo/modules/image/svg/SVGDrawableTranscoder.kt +37 -0
  46. package/android/src/main/java/expo/modules/image/svg/SVGModule.kt +26 -0
  47. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashDecoder.kt +365 -0
  48. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashFetcher.kt +31 -0
  49. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModelLoader.kt +26 -0
  50. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModelLoaderFactory.kt +14 -0
  51. package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModule.kt +17 -0
  52. package/build/ExpoImage.d.ts +15 -0
  53. package/build/ExpoImage.d.ts.map +1 -0
  54. package/build/ExpoImage.js +75 -0
  55. package/build/ExpoImage.js.map +1 -0
  56. package/build/ExpoImage.kepler.d.ts +23 -0
  57. package/build/ExpoImage.kepler.d.ts.map +1 -0
  58. package/build/ExpoImage.kepler.js +71 -0
  59. package/build/ExpoImage.kepler.js.map +1 -0
  60. package/build/ExpoImage.web.d.ts +9 -0
  61. package/build/ExpoImage.web.d.ts.map +1 -0
  62. package/build/ExpoImage.web.js +90 -0
  63. package/build/ExpoImage.web.js.map +1 -0
  64. package/build/ExpoImageModules.d.ts +3 -0
  65. package/build/ExpoImageModules.d.ts.map +1 -0
  66. package/build/ExpoImageModules.js +4 -0
  67. package/build/ExpoImageModules.js.map +1 -0
  68. package/build/ExpoImageModules.kepler.d.ts +6 -0
  69. package/build/ExpoImageModules.kepler.d.ts.map +1 -0
  70. package/build/ExpoImageModules.kepler.js +5 -0
  71. package/build/ExpoImageModules.kepler.js.map +1 -0
  72. package/build/Image.d.ts +30 -0
  73. package/build/Image.d.ts.map +1 -0
  74. package/build/Image.js +49 -0
  75. package/build/Image.js.map +1 -0
  76. package/build/Image.types.d.ts +374 -0
  77. package/build/Image.types.d.ts.map +1 -0
  78. package/build/Image.types.js +2 -0
  79. package/build/Image.types.js.map +1 -0
  80. package/build/ImageBackground.d.ts +6 -0
  81. package/build/ImageBackground.d.ts.map +1 -0
  82. package/build/ImageBackground.js +13 -0
  83. package/build/ImageBackground.js.map +1 -0
  84. package/build/ImageViewNativeComponent.d.ts +20 -0
  85. package/build/ImageViewNativeComponent.d.ts.map +1 -0
  86. package/build/ImageViewNativeComponent.js +42 -0
  87. package/build/ImageViewNativeComponent.js.map +1 -0
  88. package/build/NativeImageLoaderKepler.d.ts +18 -0
  89. package/build/NativeImageLoaderKepler.d.ts.map +1 -0
  90. package/build/NativeImageLoaderKepler.js +3 -0
  91. package/build/NativeImageLoaderKepler.js.map +1 -0
  92. package/build/fabric/registerNativeComponent.d.ts +15 -0
  93. package/build/fabric/registerNativeComponent.d.ts.map +1 -0
  94. package/build/fabric/registerNativeComponent.js +47 -0
  95. package/build/fabric/registerNativeComponent.js.map +1 -0
  96. package/build/index.d.ts +4 -0
  97. package/build/index.d.ts.map +1 -0
  98. package/build/index.js +4 -0
  99. package/build/index.js.map +1 -0
  100. package/build/utils/AssetSourceResolver.web.d.ts +24 -0
  101. package/build/utils/AssetSourceResolver.web.d.ts.map +1 -0
  102. package/build/utils/AssetSourceResolver.web.js +67 -0
  103. package/build/utils/AssetSourceResolver.web.js.map +1 -0
  104. package/build/utils/blurhash/base83.d.ts +3 -0
  105. package/build/utils/blurhash/base83.d.ts.map +1 -0
  106. package/build/utils/blurhash/base83.js +103 -0
  107. package/build/utils/blurhash/base83.js.map +1 -0
  108. package/build/utils/blurhash/decode.d.ts +7 -0
  109. package/build/utils/blurhash/decode.d.ts.map +1 -0
  110. package/build/utils/blurhash/decode.js +92 -0
  111. package/build/utils/blurhash/decode.js.map +1 -0
  112. package/build/utils/blurhash/error.d.ts +4 -0
  113. package/build/utils/blurhash/error.d.ts.map +1 -0
  114. package/build/utils/blurhash/error.js +8 -0
  115. package/build/utils/blurhash/error.js.map +1 -0
  116. package/build/utils/blurhash/useBlurhash.d.ts +8 -0
  117. package/build/utils/blurhash/useBlurhash.d.ts.map +1 -0
  118. package/build/utils/blurhash/useBlurhash.js +64 -0
  119. package/build/utils/blurhash/useBlurhash.js.map +1 -0
  120. package/build/utils/blurhash/utils.d.ts +5 -0
  121. package/build/utils/blurhash/utils.d.ts.map +1 -0
  122. package/build/utils/blurhash/utils.js +21 -0
  123. package/build/utils/blurhash/utils.js.map +1 -0
  124. package/build/utils/resolveAssetSource.d.ts +3 -0
  125. package/build/utils/resolveAssetSource.d.ts.map +1 -0
  126. package/build/utils/resolveAssetSource.js +3 -0
  127. package/build/utils/resolveAssetSource.js.map +1 -0
  128. package/build/utils/resolveAssetSource.web.d.ts +9 -0
  129. package/build/utils/resolveAssetSource.web.d.ts.map +1 -0
  130. package/build/utils/resolveAssetSource.web.js +31 -0
  131. package/build/utils/resolveAssetSource.web.js.map +1 -0
  132. package/build/utils/resolveHashString.d.ts +14 -0
  133. package/build/utils/resolveHashString.d.ts.map +1 -0
  134. package/build/utils/resolveHashString.js +31 -0
  135. package/build/utils/resolveHashString.js.map +1 -0
  136. package/build/utils/resolveHashString.web.d.ts +20 -0
  137. package/build/utils/resolveHashString.web.d.ts.map +1 -0
  138. package/build/utils/resolveHashString.web.js +31 -0
  139. package/build/utils/resolveHashString.web.js.map +1 -0
  140. package/build/utils/resolveSources.d.ts +8 -0
  141. package/build/utils/resolveSources.d.ts.map +1 -0
  142. package/build/utils/resolveSources.js +45 -0
  143. package/build/utils/resolveSources.js.map +1 -0
  144. package/build/utils/thumbhash/thumbhash.d.ts +66 -0
  145. package/build/utils/thumbhash/thumbhash.d.ts.map +1 -0
  146. package/build/utils/thumbhash/thumbhash.js +338 -0
  147. package/build/utils/thumbhash/thumbhash.js.map +1 -0
  148. package/build/utils.d.ts +17 -0
  149. package/build/utils.d.ts.map +1 -0
  150. package/build/utils.js +85 -0
  151. package/build/utils.js.map +1 -0
  152. package/build/web/AnimationManager.d.ts +37 -0
  153. package/build/web/AnimationManager.d.ts.map +1 -0
  154. package/build/web/AnimationManager.js +155 -0
  155. package/build/web/AnimationManager.js.map +1 -0
  156. package/build/web/ColorTintFilter.d.ts +12 -0
  157. package/build/web/ColorTintFilter.d.ts.map +1 -0
  158. package/build/web/ColorTintFilter.js +30 -0
  159. package/build/web/ColorTintFilter.js.map +1 -0
  160. package/build/web/ImageWrapper.d.ts +5 -0
  161. package/build/web/ImageWrapper.d.ts.map +1 -0
  162. package/build/web/ImageWrapper.js +44 -0
  163. package/build/web/ImageWrapper.js.map +1 -0
  164. package/build/web/ImageWrapper.types.d.ts +29 -0
  165. package/build/web/ImageWrapper.types.d.ts.map +1 -0
  166. package/build/web/ImageWrapper.types.js +2 -0
  167. package/build/web/ImageWrapper.types.js.map +1 -0
  168. package/build/web/getImageWrapperEventHandler.d.ts +8 -0
  169. package/build/web/getImageWrapperEventHandler.d.ts.map +1 -0
  170. package/build/web/getImageWrapperEventHandler.js +25 -0
  171. package/build/web/getImageWrapperEventHandler.js.map +1 -0
  172. package/build/web/hooks.d.ts +13 -0
  173. package/build/web/hooks.d.ts.map +1 -0
  174. package/build/web/hooks.js +58 -0
  175. package/build/web/hooks.js.map +1 -0
  176. package/build/web/imageStyles.d.ts +2 -0
  177. package/build/web/imageStyles.d.ts.map +1 -0
  178. package/build/web/imageStyles.js +137 -0
  179. package/build/web/imageStyles.js.map +1 -0
  180. package/build/web/positioning.d.ts +11 -0
  181. package/build/web/positioning.d.ts.map +1 -0
  182. package/build/web/positioning.js +35 -0
  183. package/build/web/positioning.js.map +1 -0
  184. package/build/web/useSourceSelection.d.ts +14 -0
  185. package/build/web/useSourceSelection.d.ts.map +1 -0
  186. package/build/web/useSourceSelection.js +98 -0
  187. package/build/web/useSourceSelection.js.map +1 -0
  188. package/expo-module.config.json +9 -0
  189. package/ios/Blurhash.swift +326 -0
  190. package/ios/BlurhashLoader.swift +52 -0
  191. package/ios/ContentFit.swift +55 -0
  192. package/ios/ContentPosition.swift +108 -0
  193. package/ios/ExpoImage.podspec +30 -0
  194. package/ios/ImageCachePolicy.swift +24 -0
  195. package/ios/ImageCacheType.swift +21 -0
  196. package/ios/ImageModule.swift +127 -0
  197. package/ios/ImagePriority.swift +22 -0
  198. package/ios/ImageSource.swift +44 -0
  199. package/ios/ImageTransition.swift +67 -0
  200. package/ios/ImageUtils.swift +227 -0
  201. package/ios/ImageView.swift +469 -0
  202. package/ios/Loaders/PhotoLibraryAssetLoader.swift +146 -0
  203. package/ios/Thumbhash.swift +550 -0
  204. package/ios/ThumbhashLoader.swift +53 -0
  205. package/jest.config.js +3 -0
  206. package/kepler/ExpoImage.cpp +63 -0
  207. package/kepler/ExpoImage.h +29 -0
  208. package/kepler/ExpoImageCacheHandler.cpp +15 -0
  209. package/kepler/ExpoImageCacheHandler.h +28 -0
  210. package/kepler/ExpoImageComponent.cpp +579 -0
  211. package/kepler/ExpoImageComponent.h +199 -0
  212. package/kepler/ImageWrapper/BlurhashImageWrapper.cpp +37 -0
  213. package/kepler/ImageWrapper/BlurhashImageWrapper.h +23 -0
  214. package/kepler/ImageWrapper/CacheImageWrapper.cpp +80 -0
  215. package/kepler/ImageWrapper/CacheImageWrapper.h +41 -0
  216. package/kepler/ImageWrapper/HashImageWrapper.cpp +21 -0
  217. package/kepler/ImageWrapper/HashImageWrapper.h +22 -0
  218. package/kepler/ImageWrapper/ImageWrapper.cpp +24 -0
  219. package/kepler/ImageWrapper/ImageWrapper.h +33 -0
  220. package/kepler/ImageWrapper/LocalImageWrapper.cpp +61 -0
  221. package/kepler/ImageWrapper/LocalImageWrapper.h +30 -0
  222. package/kepler/ImageWrapper/ThumbhashImageWrapper.cpp +33 -0
  223. package/kepler/ImageWrapper/ThumbhashImageWrapper.h +20 -0
  224. package/kepler/KeplerImageViewComponent.h +64 -0
  225. package/kepler/KeplerInit.cpp +35 -0
  226. package/kepler/blurhash/common.h +32 -0
  227. package/kepler/blurhash/decode.cpp +187 -0
  228. package/kepler/blurhash/decode.h +58 -0
  229. package/kepler/contentposition/ImageContentPosition.cpp +86 -0
  230. package/kepler/contentposition/ImageContentPosition.h +48 -0
  231. package/kepler/enums/ImageContentFit.h +17 -0
  232. package/kepler/enums/ImagePriority.h +17 -0
  233. package/kepler/managers/ExpoImageManager.cpp +38 -0
  234. package/kepler/managers/ExpoImageManager.h +23 -0
  235. package/kepler/test/ExpoImage.cpp +73 -0
  236. package/kepler/test/ExpoImageComponent.cpp +290 -0
  237. package/kepler/test/blurhash/decode.cpp +39 -0
  238. package/kepler/test/contentposition/ImageContentPosition.cpp +100 -0
  239. package/kepler/test/managers/ExpoImageManager.cpp +39 -0
  240. package/kepler/test/thumbhash/decode.cpp +18 -0
  241. package/kepler/test/utils/LoaderDelegate.cpp +130 -0
  242. package/kepler/thumbhash/decode.cpp +166 -0
  243. package/kepler/thumbhash/decode.h +22 -0
  244. package/kepler/utils/PlaceholderImageLoaderDelegate.cpp +26 -0
  245. package/kepler/utils/PlaceholderImageLoaderDelegate.h +26 -0
  246. package/kepler/utils/PrefetchImageLoaderDelegate.cpp +55 -0
  247. package/kepler/utils/PrefetchImageLoaderDelegate.h +35 -0
  248. package/kepler/utils/SourceImageLoaderDelegate.cpp +47 -0
  249. package/kepler/utils/SourceImageLoaderDelegate.h +42 -0
  250. package/kepler/utils/SourceImageLoaderDelegateEmitter.cpp +15 -0
  251. package/kepler/utils/SourceImageLoaderDelegateEmitter.h +28 -0
  252. package/kepler-compatibility.json +8 -0
  253. package/package.json +1 -0
  254. package/react-native.config.js +22 -0
  255. package/src/ExpoImage.kepler.tsx +119 -0
  256. package/src/ExpoImage.tsx +119 -0
  257. package/src/ExpoImage.web.tsx +155 -0
  258. package/src/ExpoImageModules.kepler.ts +7 -0
  259. package/src/ExpoImageModules.ts +4 -0
  260. package/src/Image.tsx +82 -0
  261. package/src/Image.types.ts +450 -0
  262. package/src/ImageBackground.tsx +18 -0
  263. package/src/ImageViewNativeComponent.ts +59 -0
  264. package/src/NativeImageLoaderKepler.ts +22 -0
  265. package/src/fabric/registerNativeComponent.ts +70 -0
  266. package/src/index.ts +3 -0
  267. package/src/utils/AssetSourceResolver.web.ts +88 -0
  268. package/src/utils/blurhash/base83.ts +104 -0
  269. package/src/utils/blurhash/decode.ts +114 -0
  270. package/src/utils/blurhash/error.ts +7 -0
  271. package/src/utils/blurhash/useBlurhash.tsx +81 -0
  272. package/src/utils/blurhash/utils.ts +21 -0
  273. package/src/utils/resolveAssetSource.tsx +3 -0
  274. package/src/utils/resolveAssetSource.web.ts +40 -0
  275. package/src/utils/resolveHashString.tsx +36 -0
  276. package/src/utils/resolveHashString.web.tsx +33 -0
  277. package/src/utils/resolveSources.tsx +48 -0
  278. package/src/utils/thumbhash/thumbhash.ts +387 -0
  279. package/src/utils.ts +109 -0
  280. package/src/web/AnimationManager.tsx +224 -0
  281. package/src/web/ColorTintFilter.tsx +36 -0
  282. package/src/web/ImageWrapper.tsx +85 -0
  283. package/src/web/ImageWrapper.types.ts +36 -0
  284. package/src/web/getImageWrapperEventHandler.ts +29 -0
  285. package/src/web/hooks.ts +72 -0
  286. package/src/web/imageStyles.tsx +136 -0
  287. package/src/web/positioning.ts +48 -0
  288. package/src/web/useSourceSelection.ts +151 -0
  289. package/tsconfig.json +9 -0
@@ -0,0 +1,469 @@
1
+ // Copyright 2022-present 650 Industries. All rights reserved.
2
+
3
+ import SDWebImage
4
+ import ExpoModulesCore
5
+ import VisionKit
6
+
7
+ typealias SDWebImageContext = [SDWebImageContextOption: Any]
8
+
9
+ // swiftlint:disable:next type_body_length
10
+ public final class ImageView: ExpoView {
11
+ static let contextSourceKey = SDWebImageContextOption(rawValue: "source")
12
+ static let screenScaleKey = SDWebImageContextOption(rawValue: "screenScale")
13
+
14
+ let sdImageView = SDAnimatedImageView(frame: .zero)
15
+
16
+ // Custom image manager doesn't use shared loaders managers by default,
17
+ // so make sure it is provided here.
18
+ let imageManager = SDWebImageManager(
19
+ cache: SDImageCache.shared,
20
+ loader: SDImageLoadersManager.shared
21
+ )
22
+
23
+ var loadingOptions: SDWebImageOptions = [
24
+ .retryFailed, // Don't blacklist URLs that failed downloading
25
+ .handleCookies // Handle cookies stored in the shared `HTTPCookieStore`
26
+ ]
27
+
28
+ var sources: [ImageSource]?
29
+
30
+ var pendingOperation: SDWebImageCombinedOperation?
31
+
32
+ var contentFit: ContentFit = .cover
33
+
34
+ var contentPosition: ContentPosition = .center
35
+
36
+ var transition: ImageTransition?
37
+
38
+ var blurRadius: CGFloat = 0.0
39
+
40
+ var imageTintColor: UIColor?
41
+
42
+ var cachePolicy: ImageCachePolicy = .disk
43
+
44
+ var recyclingKey: String? {
45
+ didSet {
46
+ if oldValue != nil && recyclingKey != oldValue {
47
+ sdImageView.image = nil
48
+ }
49
+ }
50
+ }
51
+
52
+ // MARK: - Events
53
+
54
+ let onLoadStart = EventDispatcher()
55
+
56
+ let onProgress = EventDispatcher()
57
+
58
+ let onError = EventDispatcher()
59
+
60
+ let onLoad = EventDispatcher()
61
+
62
+ // MARK: - View
63
+
64
+ public override var bounds: CGRect {
65
+ didSet {
66
+ // Reload the image when the bounds size has changed and the view is mounted.
67
+ if oldValue.size != bounds.size && window != nil {
68
+ reload()
69
+ }
70
+ }
71
+ }
72
+
73
+ public required init(appContext: AppContext? = nil) {
74
+ super.init(appContext: appContext)
75
+
76
+ clipsToBounds = true
77
+ sdImageView.contentMode = contentFit.toContentMode()
78
+ sdImageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
79
+ sdImageView.layer.masksToBounds = false
80
+
81
+ // Apply trilinear filtering to smooth out mis-sized images.
82
+ sdImageView.layer.magnificationFilter = .trilinear
83
+ sdImageView.layer.minificationFilter = .trilinear
84
+
85
+ addSubview(sdImageView)
86
+ }
87
+
88
+ public override func didMoveToWindow() {
89
+ if window == nil {
90
+ // Cancel pending requests when the view is unmounted.
91
+ cancelPendingOperation()
92
+ } else if !bounds.isEmpty {
93
+ // Reload the image after mounting the view with non-empty bounds.
94
+ reload()
95
+ } else {
96
+ loadPlaceholderIfNecessary()
97
+ }
98
+ }
99
+
100
+ // MARK: - Implementation
101
+
102
+ func reload() {
103
+ if isViewEmpty {
104
+ displayPlaceholderIfNecessary()
105
+ }
106
+ guard let source = bestSource else {
107
+ displayPlaceholderIfNecessary()
108
+ return
109
+ }
110
+ if sdImageView.image == nil {
111
+ sdImageView.contentMode = contentFit.toContentMode()
112
+ }
113
+ var context = SDWebImageContext()
114
+
115
+ // Cancel currently running load requests.
116
+ cancelPendingOperation()
117
+
118
+ // Modify URL request to add headers.
119
+ if let headers = source.headers {
120
+ context[SDWebImageContextOption.downloadRequestModifier] = SDWebImageDownloaderRequestModifier(headers: headers)
121
+ }
122
+
123
+ context[.cacheKeyFilter] = createCacheKeyFilter(source.cacheKey)
124
+ context[.imageTransformer] = createTransformPipeline()
125
+
126
+ // Assets from the bundler have `scale` prop which needs to be passed to the context,
127
+ // otherwise they would be saved in cache with scale = 1.0 which may result in
128
+ // incorrectly rendered images for resize modes that don't scale (`center` and `repeat`).
129
+ context[.imageScaleFactor] = source.scale
130
+
131
+ // It seems that `UIImageView` can't tint some vector graphics. If the `tintColor` prop is specified,
132
+ // we tell the SVG coder to decode to a bitmap instead. This will become useless when we switch to SVGNative coder.
133
+ if imageTintColor != nil {
134
+ context[.imageDecodeOptions] = [
135
+ SDImageCoderOption.webImageContext: [
136
+ "svgPrefersBitmap": true,
137
+ "svgImageSize": sdImageView.bounds.size,
138
+ "svgImagePreserveAspectRatio": true
139
+ ]
140
+ ]
141
+ }
142
+
143
+ if source.isCachingAllowed {
144
+ let sdCacheType = cachePolicy.toSdCacheType().rawValue
145
+ context[.originalQueryCacheType] = sdCacheType
146
+ context[.originalStoreCacheType] = sdCacheType
147
+ } else {
148
+ context[.originalQueryCacheType] = SDImageCacheType.none.rawValue
149
+ context[.originalStoreCacheType] = SDImageCacheType.none.rawValue
150
+ }
151
+ // Set which cache can be used to query and store the downloaded image.
152
+ // We want to store only original images (without transformations).
153
+ context[.queryCacheType] = SDImageCacheType.none.rawValue
154
+ context[.storeCacheType] = SDImageCacheType.none.rawValue
155
+
156
+ // Some loaders (e.g. blurhash) need access to the source and the screen scale.
157
+ context[ImageView.contextSourceKey] = source
158
+ context[ImageView.screenScaleKey] = screenScale
159
+
160
+ onLoadStart([:])
161
+
162
+ pendingOperation = imageManager.loadImage(
163
+ with: source.uri,
164
+ options: loadingOptions,
165
+ context: context,
166
+ progress: imageLoadProgress(_:_:_:),
167
+ completed: imageLoadCompleted(_:_:_:_:_:_:)
168
+ )
169
+ }
170
+
171
+ // MARK: - Loading
172
+
173
+ private func imageLoadProgress(_ receivedSize: Int, _ expectedSize: Int, _ imageUrl: URL?) {
174
+ // Don't send the event when the expected size is unknown (it's usually -1 or 0 when called for the first time).
175
+ if expectedSize <= 0 {
176
+ return
177
+ }
178
+
179
+ // Photos library requester emits the progress as a double `0...1` that we map to `0...100` int in `PhotosLoader`.
180
+ // When that loader is used, we don't have any information about the sizes in bytes, so we only send the `progress` param.
181
+ let isPhotoLibraryAsset = isPhotoLibraryAssetUrl(imageUrl)
182
+
183
+ onProgress([
184
+ "loaded": isPhotoLibraryAsset ? nil : receivedSize,
185
+ "total": isPhotoLibraryAsset ? nil : expectedSize,
186
+ "progress": Double(receivedSize) / Double(expectedSize)
187
+ ])
188
+ }
189
+
190
+ // swiftlint:disable:next function_parameter_count
191
+ private func imageLoadCompleted(
192
+ _ image: UIImage?,
193
+ _ data: Data?,
194
+ _ error: Error?,
195
+ _ cacheType: SDImageCacheType,
196
+ _ finished: Bool,
197
+ _ imageUrl: URL?
198
+ ) {
199
+ if let error = error {
200
+ let code = (error as NSError).code
201
+
202
+ // SDWebImage throws an error when loading operation is canceled (interrupted) by another load request.
203
+ // We do want to ignore that one and wait for the new request to load.
204
+ if code != SDWebImageError.cancelled.rawValue {
205
+ onError(["error": error.localizedDescription])
206
+ }
207
+ return
208
+ }
209
+ guard finished else {
210
+ log.debug("Loading the image has been canceled")
211
+ return
212
+ }
213
+ if let image = image {
214
+ onLoad([
215
+ "cacheType": cacheTypeToString(cacheType),
216
+ "source": [
217
+ "url": imageUrl?.absoluteString,
218
+ "width": image.size.width,
219
+ "height": image.size.height,
220
+ "mediaType": imageFormatToMediaType(image.sd_imageFormat)
221
+ ]
222
+ ])
223
+
224
+ let scale = window?.screen.scale ?? UIScreen.main.scale
225
+ let idealSize = idealSize(
226
+ contentPixelSize: image.size * image.scale,
227
+ containerSize: frame.size,
228
+ scale: scale,
229
+ contentFit: contentFit
230
+ ).rounded(.up)
231
+
232
+ Task {
233
+ let image = await processImage(image, idealSize: idealSize, scale: scale)
234
+
235
+ applyContentPosition(contentSize: idealSize, containerSize: frame.size)
236
+ renderImage(image)
237
+ }
238
+ } else {
239
+ displayPlaceholderIfNecessary()
240
+ }
241
+ }
242
+
243
+ // MARK: - Placeholder
244
+
245
+ /**
246
+ A list of sources that the placeholder can be loaded from.
247
+ */
248
+ var placeholderSources: [ImageSource] = [] {
249
+ didSet {
250
+ loadPlaceholderIfNecessary()
251
+ }
252
+ }
253
+
254
+ /**
255
+ A placeholder image to use when the proper image is unset.
256
+ */
257
+ var placeholderImage: UIImage?
258
+
259
+ /**
260
+ Content fit for the placeholder. `scale-down` seems to be the best choice for spinners
261
+ and that the placeholders are usually smaller than the proper image, but it doesn't
262
+ apply to blurhash that by default could use the same fitting as the proper image.
263
+ */
264
+ var placeholderContentFit: ContentFit = .scaleDown
265
+
266
+ /**
267
+ Same as `bestSource`, but for placeholders.
268
+ */
269
+ var bestPlaceholder: ImageSource? {
270
+ return getBestSource(from: placeholderSources, forSize: bounds.size, scale: screenScale) ?? placeholderSources.first
271
+ }
272
+
273
+ /**
274
+ Loads a placeholder from the best source provided in `placeholder` prop.
275
+ A placeholder should be a local asset to have more time to show before the proper image is loaded,
276
+ but remote assets are also supported – for the bundler and to cache them on the disk to load faster next time.
277
+ - Note: Placeholders are not being resized nor transformed, so try to keep them small.
278
+ */
279
+ func loadPlaceholderIfNecessary() {
280
+ // Exit early if placeholder is not set or there is already an image attached to the view.
281
+ // The placeholder is only used until the first image is loaded.
282
+ guard let placeholder = bestPlaceholder, isViewEmpty || !hasAnySource else {
283
+ return
284
+ }
285
+ var context = SDWebImageContext()
286
+ let isPlaceholderHash = placeholder.isBlurhash || placeholder.isThumbhash
287
+
288
+ context[.imageScaleFactor] = placeholder.scale
289
+ context[.cacheKeyFilter] = createCacheKeyFilter(placeholder.cacheKey)
290
+
291
+ // Cache placeholders on the disk. Should we let the user choose whether
292
+ // to cache them or apply the same policy as with the proper image?
293
+ // Basically they are also cached in memory as the `placeholderImage` property,
294
+ // so just `disk` policy sounds like a good idea.
295
+ context[.queryCacheType] = SDImageCacheType.disk.rawValue
296
+ context[.storeCacheType] = SDImageCacheType.disk.rawValue
297
+
298
+ // Some loaders (e.g. blurhash) need access to the source.
299
+ context[ImageView.contextSourceKey] = placeholder
300
+
301
+ imageManager.loadImage(with: placeholder.uri, context: context, progress: nil) { [weak self] placeholder, _, _, _, finished, _ in
302
+ guard let self = self, let placeholder = placeholder, finished else {
303
+ return
304
+ }
305
+ self.placeholderImage = placeholder
306
+ self.placeholderContentFit = isPlaceholderHash ? self.contentFit : self.placeholderContentFit
307
+ self.displayPlaceholderIfNecessary()
308
+ }
309
+ }
310
+
311
+ /**
312
+ Displays a placeholder if necessary – the placeholder can only be displayed when no image has been displayed yet or the sources are unset.
313
+ */
314
+ private func displayPlaceholderIfNecessary() {
315
+ guard isViewEmpty || !hasAnySource, let placeholder = placeholderImage else {
316
+ return
317
+ }
318
+ setImage(placeholder, contentFit: placeholderContentFit, isPlaceholder: true)
319
+ }
320
+
321
+ // MARK: - Processing
322
+
323
+ private func createTransformPipeline() -> SDImagePipelineTransformer {
324
+ let transformers: [SDImageTransformer] = [
325
+ SDImageBlurTransformer(radius: blurRadius)
326
+ ]
327
+ return SDImagePipelineTransformer(transformers: transformers)
328
+ }
329
+
330
+ private func processImage(_ image: UIImage?, idealSize: CGSize, scale: Double) async -> UIImage? {
331
+ guard let image = image, !bounds.isEmpty else {
332
+ return nil
333
+ }
334
+ // Downscale the image only when necessary
335
+ if shouldDownscale(image: image, toSize: idealSize, scale: scale) {
336
+ return await resize(animatedImage: image, toSize: idealSize, scale: scale)
337
+ }
338
+ return image
339
+ }
340
+
341
+ // MARK: - Rendering
342
+
343
+ /**
344
+ Moves the layer on which the image is rendered to respect the `contentPosition` prop.
345
+ */
346
+ private func applyContentPosition(contentSize: CGSize, containerSize: CGSize) {
347
+ let offset = contentPosition.offset(contentSize: contentSize, containerSize: containerSize)
348
+ sdImageView.layer.frame.origin = offset
349
+ }
350
+
351
+ private func renderImage(_ image: UIImage?) {
352
+ if let transition = transition, transition.duration > 0 {
353
+ let options = transition.toAnimationOptions()
354
+ let seconds = transition.duration / 1000
355
+
356
+ UIView.transition(with: sdImageView, duration: seconds, options: options) { [weak self] in
357
+ if let self = self {
358
+ self.setImage(image, contentFit: self.contentFit, isPlaceholder: false)
359
+ }
360
+ }
361
+ } else {
362
+ setImage(image, contentFit: contentFit, isPlaceholder: false)
363
+ }
364
+ }
365
+
366
+ private func setImage(_ image: UIImage?, contentFit: ContentFit, isPlaceholder: Bool) {
367
+ sdImageView.contentMode = contentFit.toContentMode()
368
+
369
+ if let imageTintColor, !isPlaceholder {
370
+ sdImageView.tintColor = imageTintColor
371
+ sdImageView.image = image?.withRenderingMode(.alwaysTemplate)
372
+ } else {
373
+ sdImageView.tintColor = nil
374
+ sdImageView.image = image
375
+ }
376
+
377
+ if enableLiveTextInteraction {
378
+ analyzeImage()
379
+ }
380
+ }
381
+
382
+ // MARK: - Helpers
383
+
384
+ func cancelPendingOperation() {
385
+ pendingOperation?.cancel()
386
+ pendingOperation = nil
387
+ }
388
+
389
+ /**
390
+ A scale of the screen where the view is presented,
391
+ or the main scale if the view is not mounted yet.
392
+ */
393
+ var screenScale: Double {
394
+ return window?.screen.scale as? Double ?? UIScreen.main.scale
395
+ }
396
+
397
+ /**
398
+ The image source that fits best into the view bounds.
399
+ */
400
+ var bestSource: ImageSource? {
401
+ return getBestSource(from: sources, forSize: bounds.size, scale: screenScale)
402
+ }
403
+
404
+ /**
405
+ A bool value whether the image view doesn't render any image.
406
+ */
407
+ var isViewEmpty: Bool {
408
+ return sdImageView.image == nil
409
+ }
410
+
411
+ /**
412
+ A bool value whether there is any source to load from.
413
+ */
414
+ var hasAnySource: Bool {
415
+ return sources?.isEmpty == false
416
+ }
417
+
418
+ // MARK: - Live Text Interaction
419
+
420
+ @available(iOS 16.0, macCatalyst 17.0, *)
421
+ static let imageAnalyzer = ImageAnalyzer.isSupported ? ImageAnalyzer() : nil
422
+
423
+ var enableLiveTextInteraction: Bool = false {
424
+ didSet {
425
+ guard #available(iOS 16.0, macCatalyst 17.0, *), oldValue != enableLiveTextInteraction, ImageAnalyzer.isSupported else {
426
+ return
427
+ }
428
+ if enableLiveTextInteraction {
429
+ let imageAnalysisInteraction = ImageAnalysisInteraction()
430
+ sdImageView.addInteraction(imageAnalysisInteraction)
431
+ } else if let interaction = findImageAnalysisInteraction() {
432
+ sdImageView.removeInteraction(interaction)
433
+ }
434
+ }
435
+ }
436
+
437
+ private func analyzeImage() {
438
+ guard #available(iOS 16.0, macCatalyst 17.0, *), ImageAnalyzer.isSupported, let image = sdImageView.image else {
439
+ return
440
+ }
441
+
442
+ Task {
443
+ guard let imageAnalyzer = Self.imageAnalyzer, let imageAnalysisInteraction = findImageAnalysisInteraction() else {
444
+ return
445
+ }
446
+ let configuration = ImageAnalyzer.Configuration([.text, .machineReadableCode])
447
+
448
+ do {
449
+ let imageAnalysis = try await imageAnalyzer.analyze(image, configuration: configuration)
450
+
451
+ // Make sure the image haven't changed in the meantime.
452
+ if image == sdImageView.image {
453
+ imageAnalysisInteraction.analysis = imageAnalysis
454
+ imageAnalysisInteraction.preferredInteractionTypes = .automatic
455
+ }
456
+ } catch {
457
+ log.error(error)
458
+ }
459
+ }
460
+ }
461
+
462
+ @available(iOS 16.0, macCatalyst 17.0, *)
463
+ private func findImageAnalysisInteraction() -> ImageAnalysisInteraction? {
464
+ let interaction = sdImageView.interactions.first {
465
+ return $0 is ImageAnalysisInteraction
466
+ }
467
+ return interaction as? ImageAnalysisInteraction
468
+ }
469
+ }
@@ -0,0 +1,146 @@
1
+ import Photos
2
+ import Dispatch
3
+ import SDWebImage
4
+ import ExpoModulesCore
5
+
6
+ /**
7
+ A custom loader for assets from the Photo Library. It handles all urls with the `ph` scheme.
8
+ */
9
+ final class PhotoLibraryAssetLoader: NSObject, SDImageLoader {
10
+ // MARK: - SDImageLoader
11
+
12
+ func canRequestImage(for url: URL?) -> Bool {
13
+ return isPhotoLibraryAssetUrl(url)
14
+ }
15
+
16
+ func requestImage(
17
+ with url: URL?,
18
+ options: SDWebImageOptions = [],
19
+ context: SDWebImageContext?,
20
+ progress progressBlock: SDImageLoaderProgressBlock?,
21
+ completed completedBlock: SDImageLoaderCompletedBlock? = nil
22
+ ) -> SDWebImageOperation? {
23
+ guard isPhotoLibraryStatusAuthorized() else {
24
+ let error = makeNSError(description: "Unauthorized access to the Photo Library")
25
+ completedBlock?(nil, nil, error, false)
26
+ return nil
27
+ }
28
+ let operation = PhotoLibraryAssetLoaderOperation()
29
+
30
+ DispatchQueue.global(qos: .userInitiated).async {
31
+ guard let url = url, let assetLocalIdentifier = assetLocalIdentifier(fromUrl: url) else {
32
+ let error = makeNSError(description: "Unable to obtain the asset identifier from the url: '\(url?.absoluteString)'")
33
+ completedBlock?(nil, nil, error, false)
34
+ return
35
+ }
36
+ guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [assetLocalIdentifier], options: .none).firstObject else {
37
+ let error = makeNSError(description: "Asset with identifier '\(assetLocalIdentifier)' not found in the Photo Library")
38
+ completedBlock?(nil, nil, error, false)
39
+ return
40
+ }
41
+ operation.requestId = requestAsset(
42
+ asset,
43
+ url: url,
44
+ context: context,
45
+ progressBlock: progressBlock,
46
+ completedBlock: completedBlock
47
+ )
48
+ }
49
+ return operation
50
+ }
51
+
52
+ func shouldBlockFailedURL(with url: URL, error: Error) -> Bool {
53
+ // The lack of permission is one of the reasons of failed request,
54
+ // but in that single case we don't want to blacklist the url as
55
+ // the permission might be granted later and then the retry should be possible.
56
+ return isPhotoLibraryStatusAuthorized()
57
+ }
58
+ }
59
+
60
+ /**
61
+ Returns a bool value whether the given url references the Photo Library asset.
62
+ */
63
+ internal func isPhotoLibraryAssetUrl(_ url: URL?) -> Bool {
64
+ return url?.scheme == "ph"
65
+ }
66
+
67
+ /**
68
+ Returns the local identifier of the asset from the given `ph://` url.
69
+ These urls have the form of "ph://26687849-33F9-4402-8EC0-A622CD011D70",
70
+ where the asset local identifier is used as the host part.
71
+ */
72
+ private func assetLocalIdentifier(fromUrl url: URL) -> String? {
73
+ return url.host
74
+ }
75
+
76
+ /**
77
+ Checks whether the app is authorized to read the Photo Library.
78
+ */
79
+ private func isPhotoLibraryStatusAuthorized() -> Bool {
80
+ if #available(iOS 14, *) {
81
+ let status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
82
+ return status == .authorized || status == .limited
83
+ } else {
84
+ return PHPhotoLibrary.authorizationStatus() == .authorized
85
+ }
86
+ }
87
+
88
+ /**
89
+ Requests the image of the given asset object and returns the request identifier.
90
+ */
91
+ private func requestAsset(
92
+ _ asset: PHAsset,
93
+ url: URL,
94
+ context: SDWebImageContext?,
95
+ progressBlock: SDImageLoaderProgressBlock?,
96
+ completedBlock: SDImageLoaderCompletedBlock?
97
+ ) -> PHImageRequestID {
98
+ let options = PHImageRequestOptions()
99
+ options.isSynchronous = false
100
+ options.version = .current
101
+ options.deliveryMode = .highQualityFormat
102
+ options.resizeMode = .fast
103
+ options.normalizedCropRect = .zero
104
+ options.isNetworkAccessAllowed = true
105
+
106
+ if let progressBlock = progressBlock {
107
+ options.progressHandler = { progress, _, _, _ in
108
+ // The `progress` is a double from 0.0 to 1.0, but the loader needs integers so we map it to 0...100 range
109
+ let progressPercentage = Int((progress * 100.0).rounded())
110
+ progressBlock(progressPercentage, 100, url)
111
+ }
112
+ }
113
+
114
+ let screenScale = context?[ImageView.screenScaleKey] as? Double ?? UIScreen.main.scale
115
+ let targetSize = CGSize(width: Double(asset.pixelWidth) / screenScale, height: Double(asset.pixelHeight) / screenScale)
116
+
117
+ return PHImageManager.default().requestImage(
118
+ for: asset,
119
+ targetSize: targetSize,
120
+ contentMode: .aspectFit,
121
+ options: options,
122
+ resultHandler: { image, info in
123
+ // This value can be `true` only when network access is allowed and the photo is stored in the iCloud.
124
+ let isDegraded: Bool = info?[PHImageResultIsDegradedKey] as? Bool ?? false
125
+
126
+ completedBlock?(image, nil, nil, !isDegraded)
127
+ }
128
+ )
129
+ }
130
+
131
+ /**
132
+ Loader operation specialized for the Photo Library by keeping the request identifier.
133
+ */
134
+ private class PhotoLibraryAssetLoaderOperation: NSObject, SDWebImageOperation {
135
+ var canceled: Bool = false
136
+ var requestId: PHImageRequestID?
137
+
138
+ // MARK: - SDWebImageOperation
139
+
140
+ func cancel() {
141
+ if let requestId = requestId {
142
+ PHImageManager.default().cancelImageRequest(requestId)
143
+ }
144
+ canceled = true
145
+ }
146
+ }