@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.
- package/.eslintrc.js +2 -0
- package/CHANGELOG.md +285 -0
- package/CMakeLists.txt +91 -0
- package/LICENSE +7 -0
- package/README.kepler.md +123 -0
- package/README.md +54 -0
- package/THIRD_PARTY_NOTICES.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/proguard-rules.pro +28 -0
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/expo/modules/image/Exceptions.kt +9 -0
- package/android/src/main/java/expo/modules/image/ExpoImageAppGlideModule.kt +19 -0
- package/android/src/main/java/expo/modules/image/ExpoImageModule.kt +192 -0
- package/android/src/main/java/expo/modules/image/ExpoImageView.kt +220 -0
- package/android/src/main/java/expo/modules/image/ExpoImageViewWrapper.kt +652 -0
- package/android/src/main/java/expo/modules/image/GlideModel.kt +73 -0
- package/android/src/main/java/expo/modules/image/ImageUtils.kt +50 -0
- package/android/src/main/java/expo/modules/image/ImageViewWrapperTarget.kt +313 -0
- package/android/src/main/java/expo/modules/image/ResourceIdHelper.kt +47 -0
- package/android/src/main/java/expo/modules/image/ThumbnailRequestCoordinatorExtension.kt +21 -0
- package/android/src/main/java/expo/modules/image/YogaUtils.kt +20 -0
- package/android/src/main/java/expo/modules/image/blurhash/BlurHashFetcher.kt +32 -0
- package/android/src/main/java/expo/modules/image/blurhash/BlurhashDecoder.kt +189 -0
- package/android/src/main/java/expo/modules/image/blurhash/BlurhashModelLoader.kt +30 -0
- package/android/src/main/java/expo/modules/image/blurhash/BlurhashModelLoaderFactory.kt +14 -0
- package/android/src/main/java/expo/modules/image/blurhash/BlurhashModule.kt +17 -0
- package/android/src/main/java/expo/modules/image/dataurls/Base64DataFetcher.kt +27 -0
- package/android/src/main/java/expo/modules/image/dataurls/Base64ModelLoader.kt +24 -0
- package/android/src/main/java/expo/modules/image/dataurls/Base64ModelLoaderFactory.kt +12 -0
- package/android/src/main/java/expo/modules/image/dataurls/Base64Module.kt +16 -0
- package/android/src/main/java/expo/modules/image/drawing/OutlineProvider.kt +201 -0
- package/android/src/main/java/expo/modules/image/enums/ContentFit.kt +89 -0
- package/android/src/main/java/expo/modules/image/enums/ImageCacheType.kt +14 -0
- package/android/src/main/java/expo/modules/image/enums/Priority.kt +15 -0
- package/android/src/main/java/expo/modules/image/events/GlideRequestListener.kt +64 -0
- package/android/src/main/java/expo/modules/image/events/OkHttpProgressListener.kt +26 -0
- package/android/src/main/java/expo/modules/image/okhttp/ExpoImageOkHttpClientGlideModule.kt +80 -0
- package/android/src/main/java/expo/modules/image/okhttp/GlideUrlWrapperLoader.kt +55 -0
- package/android/src/main/java/expo/modules/image/records/CachePolicy.kt +10 -0
- package/android/src/main/java/expo/modules/image/records/ContentPosition.kt +89 -0
- package/android/src/main/java/expo/modules/image/records/ImageTransition.kt +8 -0
- package/android/src/main/java/expo/modules/image/records/SourceMap.kt +160 -0
- package/android/src/main/java/expo/modules/image/records/events.kt +25 -0
- package/android/src/main/java/expo/modules/image/svg/SVGDecoder.kt +33 -0
- package/android/src/main/java/expo/modules/image/svg/SVGDrawableTranscoder.kt +37 -0
- package/android/src/main/java/expo/modules/image/svg/SVGModule.kt +26 -0
- package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashDecoder.kt +365 -0
- package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashFetcher.kt +31 -0
- package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModelLoader.kt +26 -0
- package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModelLoaderFactory.kt +14 -0
- package/android/src/main/java/expo/modules/image/thumbhash/ThumbhashModule.kt +17 -0
- package/build/ExpoImage.d.ts +15 -0
- package/build/ExpoImage.d.ts.map +1 -0
- package/build/ExpoImage.js +75 -0
- package/build/ExpoImage.js.map +1 -0
- package/build/ExpoImage.kepler.d.ts +23 -0
- package/build/ExpoImage.kepler.d.ts.map +1 -0
- package/build/ExpoImage.kepler.js +71 -0
- package/build/ExpoImage.kepler.js.map +1 -0
- package/build/ExpoImage.web.d.ts +9 -0
- package/build/ExpoImage.web.d.ts.map +1 -0
- package/build/ExpoImage.web.js +90 -0
- package/build/ExpoImage.web.js.map +1 -0
- package/build/ExpoImageModules.d.ts +3 -0
- package/build/ExpoImageModules.d.ts.map +1 -0
- package/build/ExpoImageModules.js +4 -0
- package/build/ExpoImageModules.js.map +1 -0
- package/build/ExpoImageModules.kepler.d.ts +6 -0
- package/build/ExpoImageModules.kepler.d.ts.map +1 -0
- package/build/ExpoImageModules.kepler.js +5 -0
- package/build/ExpoImageModules.kepler.js.map +1 -0
- package/build/Image.d.ts +30 -0
- package/build/Image.d.ts.map +1 -0
- package/build/Image.js +49 -0
- package/build/Image.js.map +1 -0
- package/build/Image.types.d.ts +374 -0
- package/build/Image.types.d.ts.map +1 -0
- package/build/Image.types.js +2 -0
- package/build/Image.types.js.map +1 -0
- package/build/ImageBackground.d.ts +6 -0
- package/build/ImageBackground.d.ts.map +1 -0
- package/build/ImageBackground.js +13 -0
- package/build/ImageBackground.js.map +1 -0
- package/build/ImageViewNativeComponent.d.ts +20 -0
- package/build/ImageViewNativeComponent.d.ts.map +1 -0
- package/build/ImageViewNativeComponent.js +42 -0
- package/build/ImageViewNativeComponent.js.map +1 -0
- package/build/NativeImageLoaderKepler.d.ts +18 -0
- package/build/NativeImageLoaderKepler.d.ts.map +1 -0
- package/build/NativeImageLoaderKepler.js +3 -0
- package/build/NativeImageLoaderKepler.js.map +1 -0
- package/build/fabric/registerNativeComponent.d.ts +15 -0
- package/build/fabric/registerNativeComponent.d.ts.map +1 -0
- package/build/fabric/registerNativeComponent.js +47 -0
- package/build/fabric/registerNativeComponent.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +4 -0
- package/build/index.js.map +1 -0
- package/build/utils/AssetSourceResolver.web.d.ts +24 -0
- package/build/utils/AssetSourceResolver.web.d.ts.map +1 -0
- package/build/utils/AssetSourceResolver.web.js +67 -0
- package/build/utils/AssetSourceResolver.web.js.map +1 -0
- package/build/utils/blurhash/base83.d.ts +3 -0
- package/build/utils/blurhash/base83.d.ts.map +1 -0
- package/build/utils/blurhash/base83.js +103 -0
- package/build/utils/blurhash/base83.js.map +1 -0
- package/build/utils/blurhash/decode.d.ts +7 -0
- package/build/utils/blurhash/decode.d.ts.map +1 -0
- package/build/utils/blurhash/decode.js +92 -0
- package/build/utils/blurhash/decode.js.map +1 -0
- package/build/utils/blurhash/error.d.ts +4 -0
- package/build/utils/blurhash/error.d.ts.map +1 -0
- package/build/utils/blurhash/error.js +8 -0
- package/build/utils/blurhash/error.js.map +1 -0
- package/build/utils/blurhash/useBlurhash.d.ts +8 -0
- package/build/utils/blurhash/useBlurhash.d.ts.map +1 -0
- package/build/utils/blurhash/useBlurhash.js +64 -0
- package/build/utils/blurhash/useBlurhash.js.map +1 -0
- package/build/utils/blurhash/utils.d.ts +5 -0
- package/build/utils/blurhash/utils.d.ts.map +1 -0
- package/build/utils/blurhash/utils.js +21 -0
- package/build/utils/blurhash/utils.js.map +1 -0
- package/build/utils/resolveAssetSource.d.ts +3 -0
- package/build/utils/resolveAssetSource.d.ts.map +1 -0
- package/build/utils/resolveAssetSource.js +3 -0
- package/build/utils/resolveAssetSource.js.map +1 -0
- package/build/utils/resolveAssetSource.web.d.ts +9 -0
- package/build/utils/resolveAssetSource.web.d.ts.map +1 -0
- package/build/utils/resolveAssetSource.web.js +31 -0
- package/build/utils/resolveAssetSource.web.js.map +1 -0
- package/build/utils/resolveHashString.d.ts +14 -0
- package/build/utils/resolveHashString.d.ts.map +1 -0
- package/build/utils/resolveHashString.js +31 -0
- package/build/utils/resolveHashString.js.map +1 -0
- package/build/utils/resolveHashString.web.d.ts +20 -0
- package/build/utils/resolveHashString.web.d.ts.map +1 -0
- package/build/utils/resolveHashString.web.js +31 -0
- package/build/utils/resolveHashString.web.js.map +1 -0
- package/build/utils/resolveSources.d.ts +8 -0
- package/build/utils/resolveSources.d.ts.map +1 -0
- package/build/utils/resolveSources.js +45 -0
- package/build/utils/resolveSources.js.map +1 -0
- package/build/utils/thumbhash/thumbhash.d.ts +66 -0
- package/build/utils/thumbhash/thumbhash.d.ts.map +1 -0
- package/build/utils/thumbhash/thumbhash.js +338 -0
- package/build/utils/thumbhash/thumbhash.js.map +1 -0
- package/build/utils.d.ts +17 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +85 -0
- package/build/utils.js.map +1 -0
- package/build/web/AnimationManager.d.ts +37 -0
- package/build/web/AnimationManager.d.ts.map +1 -0
- package/build/web/AnimationManager.js +155 -0
- package/build/web/AnimationManager.js.map +1 -0
- package/build/web/ColorTintFilter.d.ts +12 -0
- package/build/web/ColorTintFilter.d.ts.map +1 -0
- package/build/web/ColorTintFilter.js +30 -0
- package/build/web/ColorTintFilter.js.map +1 -0
- package/build/web/ImageWrapper.d.ts +5 -0
- package/build/web/ImageWrapper.d.ts.map +1 -0
- package/build/web/ImageWrapper.js +44 -0
- package/build/web/ImageWrapper.js.map +1 -0
- package/build/web/ImageWrapper.types.d.ts +29 -0
- package/build/web/ImageWrapper.types.d.ts.map +1 -0
- package/build/web/ImageWrapper.types.js +2 -0
- package/build/web/ImageWrapper.types.js.map +1 -0
- package/build/web/getImageWrapperEventHandler.d.ts +8 -0
- package/build/web/getImageWrapperEventHandler.d.ts.map +1 -0
- package/build/web/getImageWrapperEventHandler.js +25 -0
- package/build/web/getImageWrapperEventHandler.js.map +1 -0
- package/build/web/hooks.d.ts +13 -0
- package/build/web/hooks.d.ts.map +1 -0
- package/build/web/hooks.js +58 -0
- package/build/web/hooks.js.map +1 -0
- package/build/web/imageStyles.d.ts +2 -0
- package/build/web/imageStyles.d.ts.map +1 -0
- package/build/web/imageStyles.js +137 -0
- package/build/web/imageStyles.js.map +1 -0
- package/build/web/positioning.d.ts +11 -0
- package/build/web/positioning.d.ts.map +1 -0
- package/build/web/positioning.js +35 -0
- package/build/web/positioning.js.map +1 -0
- package/build/web/useSourceSelection.d.ts +14 -0
- package/build/web/useSourceSelection.d.ts.map +1 -0
- package/build/web/useSourceSelection.js +98 -0
- package/build/web/useSourceSelection.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Blurhash.swift +326 -0
- package/ios/BlurhashLoader.swift +52 -0
- package/ios/ContentFit.swift +55 -0
- package/ios/ContentPosition.swift +108 -0
- package/ios/ExpoImage.podspec +30 -0
- package/ios/ImageCachePolicy.swift +24 -0
- package/ios/ImageCacheType.swift +21 -0
- package/ios/ImageModule.swift +127 -0
- package/ios/ImagePriority.swift +22 -0
- package/ios/ImageSource.swift +44 -0
- package/ios/ImageTransition.swift +67 -0
- package/ios/ImageUtils.swift +227 -0
- package/ios/ImageView.swift +469 -0
- package/ios/Loaders/PhotoLibraryAssetLoader.swift +146 -0
- package/ios/Thumbhash.swift +550 -0
- package/ios/ThumbhashLoader.swift +53 -0
- package/jest.config.js +3 -0
- package/kepler/ExpoImage.cpp +63 -0
- package/kepler/ExpoImage.h +29 -0
- package/kepler/ExpoImageCacheHandler.cpp +15 -0
- package/kepler/ExpoImageCacheHandler.h +28 -0
- package/kepler/ExpoImageComponent.cpp +579 -0
- package/kepler/ExpoImageComponent.h +199 -0
- package/kepler/ImageWrapper/BlurhashImageWrapper.cpp +37 -0
- package/kepler/ImageWrapper/BlurhashImageWrapper.h +23 -0
- package/kepler/ImageWrapper/CacheImageWrapper.cpp +80 -0
- package/kepler/ImageWrapper/CacheImageWrapper.h +41 -0
- package/kepler/ImageWrapper/HashImageWrapper.cpp +21 -0
- package/kepler/ImageWrapper/HashImageWrapper.h +22 -0
- package/kepler/ImageWrapper/ImageWrapper.cpp +24 -0
- package/kepler/ImageWrapper/ImageWrapper.h +33 -0
- package/kepler/ImageWrapper/LocalImageWrapper.cpp +61 -0
- package/kepler/ImageWrapper/LocalImageWrapper.h +30 -0
- package/kepler/ImageWrapper/ThumbhashImageWrapper.cpp +33 -0
- package/kepler/ImageWrapper/ThumbhashImageWrapper.h +20 -0
- package/kepler/KeplerImageViewComponent.h +64 -0
- package/kepler/KeplerInit.cpp +35 -0
- package/kepler/blurhash/common.h +32 -0
- package/kepler/blurhash/decode.cpp +187 -0
- package/kepler/blurhash/decode.h +58 -0
- package/kepler/contentposition/ImageContentPosition.cpp +86 -0
- package/kepler/contentposition/ImageContentPosition.h +48 -0
- package/kepler/enums/ImageContentFit.h +17 -0
- package/kepler/enums/ImagePriority.h +17 -0
- package/kepler/managers/ExpoImageManager.cpp +38 -0
- package/kepler/managers/ExpoImageManager.h +23 -0
- package/kepler/test/ExpoImage.cpp +73 -0
- package/kepler/test/ExpoImageComponent.cpp +290 -0
- package/kepler/test/blurhash/decode.cpp +39 -0
- package/kepler/test/contentposition/ImageContentPosition.cpp +100 -0
- package/kepler/test/managers/ExpoImageManager.cpp +39 -0
- package/kepler/test/thumbhash/decode.cpp +18 -0
- package/kepler/test/utils/LoaderDelegate.cpp +130 -0
- package/kepler/thumbhash/decode.cpp +166 -0
- package/kepler/thumbhash/decode.h +22 -0
- package/kepler/utils/PlaceholderImageLoaderDelegate.cpp +26 -0
- package/kepler/utils/PlaceholderImageLoaderDelegate.h +26 -0
- package/kepler/utils/PrefetchImageLoaderDelegate.cpp +55 -0
- package/kepler/utils/PrefetchImageLoaderDelegate.h +35 -0
- package/kepler/utils/SourceImageLoaderDelegate.cpp +47 -0
- package/kepler/utils/SourceImageLoaderDelegate.h +42 -0
- package/kepler/utils/SourceImageLoaderDelegateEmitter.cpp +15 -0
- package/kepler/utils/SourceImageLoaderDelegateEmitter.h +28 -0
- package/kepler-compatibility.json +8 -0
- package/package.json +1 -0
- package/react-native.config.js +22 -0
- package/src/ExpoImage.kepler.tsx +119 -0
- package/src/ExpoImage.tsx +119 -0
- package/src/ExpoImage.web.tsx +155 -0
- package/src/ExpoImageModules.kepler.ts +7 -0
- package/src/ExpoImageModules.ts +4 -0
- package/src/Image.tsx +82 -0
- package/src/Image.types.ts +450 -0
- package/src/ImageBackground.tsx +18 -0
- package/src/ImageViewNativeComponent.ts +59 -0
- package/src/NativeImageLoaderKepler.ts +22 -0
- package/src/fabric/registerNativeComponent.ts +70 -0
- package/src/index.ts +3 -0
- package/src/utils/AssetSourceResolver.web.ts +88 -0
- package/src/utils/blurhash/base83.ts +104 -0
- package/src/utils/blurhash/decode.ts +114 -0
- package/src/utils/blurhash/error.ts +7 -0
- package/src/utils/blurhash/useBlurhash.tsx +81 -0
- package/src/utils/blurhash/utils.ts +21 -0
- package/src/utils/resolveAssetSource.tsx +3 -0
- package/src/utils/resolveAssetSource.web.ts +40 -0
- package/src/utils/resolveHashString.tsx +36 -0
- package/src/utils/resolveHashString.web.tsx +33 -0
- package/src/utils/resolveSources.tsx +48 -0
- package/src/utils/thumbhash/thumbhash.ts +387 -0
- package/src/utils.ts +109 -0
- package/src/web/AnimationManager.tsx +224 -0
- package/src/web/ColorTintFilter.tsx +36 -0
- package/src/web/ImageWrapper.tsx +85 -0
- package/src/web/ImageWrapper.types.ts +36 -0
- package/src/web/getImageWrapperEventHandler.ts +29 -0
- package/src/web/hooks.ts +72 -0
- package/src/web/imageStyles.tsx +136 -0
- package/src/web/positioning.ts +48 -0
- package/src/web/useSourceSelection.ts +151 -0
- 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
|
+
}
|