react-native-nitro-image-pipeline 1.4.0 → 1.6.0
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/README.md +124 -8
- package/android/CMakeLists.txt +4 -0
- package/android/src/main/cpp/GaussianBlur.cpp +165 -0
- package/android/src/main/cpp/GaussianBlur.hpp +52 -0
- package/android/src/main/cpp/GaussianBlurJni.cpp +50 -0
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +167 -75
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/PipelineImageLoader.kt +177 -0
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/BlurTransformation.kt +36 -81
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/HardwareBitmapTransformation.kt +45 -0
- package/ios/GaussianBlur.swift +48 -9
- package/ios/HybridNitroImagePipeline.swift +109 -41
- package/ios/PipelineImageLoader.swift +195 -0
- package/ios/RoundedCornersProcessor.swift +5 -0
- package/lib/commonjs/NativePipelineImage.js +72 -0
- package/lib/commonjs/NativePipelineImage.js.map +1 -0
- package/lib/commonjs/PipelineImage.js.map +1 -1
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/resolveImageSource.js +89 -0
- package/lib/commonjs/resolveImageSource.js.map +1 -0
- package/lib/commonjs/useImage.js +8 -4
- package/lib/commonjs/useImage.js.map +1 -1
- package/lib/commonjs/usePipelineImageLoader.js +74 -0
- package/lib/commonjs/usePipelineImageLoader.js.map +1 -0
- package/lib/module/NativePipelineImage.js +68 -0
- package/lib/module/NativePipelineImage.js.map +1 -0
- package/lib/module/PipelineImage.js.map +1 -1
- package/lib/module/index.js +3 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/resolveImageSource.js +83 -0
- package/lib/module/resolveImageSource.js.map +1 -0
- package/lib/module/useImage.js +8 -4
- package/lib/module/useImage.js.map +1 -1
- package/lib/module/usePipelineImageLoader.js +70 -0
- package/lib/module/usePipelineImageLoader.js.map +1 -0
- package/lib/typescript/src/NativePipelineImage.d.ts +71 -0
- package/lib/typescript/src/NativePipelineImage.d.ts.map +1 -0
- package/lib/typescript/src/PipelineImage.d.ts +7 -2
- package/lib/typescript/src/PipelineImage.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +4 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/resolveImageSource.d.ts +56 -0
- package/lib/typescript/src/resolveImageSource.d.ts.map +1 -0
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts +56 -1
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
- package/lib/typescript/src/useImage.d.ts +10 -4
- package/lib/typescript/src/useImage.d.ts.map +1 -1
- package/lib/typescript/src/usePipelineImageLoader.d.ts +17 -0
- package/lib/typescript/src/usePipelineImageLoader.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +13 -0
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.hpp +1 -0
- package/nitrogen/generated/android/c++/JViewOptions.hpp +77 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipelineSpec.kt +5 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ViewOptions.kt +66 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.cpp +10 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +44 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Umbrella.hpp +8 -0
- package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +14 -0
- package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec_cxx.swift +15 -0
- package/nitrogen/generated/ios/swift/ViewOptions.swift +101 -0
- package/nitrogen/generated/shared/c++/HybridNitroImagePipelineSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridNitroImagePipelineSpec.hpp +7 -0
- package/nitrogen/generated/shared/c++/ViewOptions.hpp +104 -0
- package/package.json +4 -3
- package/src/NativePipelineImage.tsx +108 -0
- package/src/PipelineImage.tsx +7 -2
- package/src/index.ts +12 -0
- package/src/resolveImageSource.ts +83 -0
- package/src/specs/nitro-image-toolkit.nitro.ts +57 -1
- package/src/useImage.ts +26 -15
- package/src/usePipelineImageLoader.ts +90 -0
package/ios/GaussianBlur.swift
CHANGED
|
@@ -29,25 +29,30 @@ enum GaussianBlur {
|
|
|
29
29
|
// Normalise to premultiplied ARGB8888 — vImageBoxConvolve_ARGB8888
|
|
30
30
|
// needs four 8-bit channels, and premultiplied alpha is what keeps
|
|
31
31
|
// transparent edges from bleeding dark halos into the blur.
|
|
32
|
+
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
33
|
+
let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue)
|
|
32
34
|
guard var format = vImage_CGImageFormat(
|
|
33
35
|
bitsPerComponent: 8,
|
|
34
36
|
bitsPerPixel: 32,
|
|
35
|
-
colorSpace:
|
|
36
|
-
bitmapInfo:
|
|
37
|
+
colorSpace: colorSpace,
|
|
38
|
+
bitmapInfo: bitmapInfo,
|
|
37
39
|
renderingIntent: .defaultIntent
|
|
38
40
|
) else { return nil }
|
|
39
41
|
|
|
42
|
+
// Both buffers are freed on exit, except the one handed over to the
|
|
43
|
+
// result CGImage below (`handedOver`), which is freed with the image.
|
|
44
|
+
var handedOver: UnsafeMutableRawPointer?
|
|
40
45
|
var source = vImage_Buffer()
|
|
41
46
|
guard vImageBuffer_InitWithCGImage(
|
|
42
47
|
&source, &format, nil, cgImage, vImage_Flags(kvImageNoFlags)
|
|
43
48
|
) == kvImageNoError else { return nil }
|
|
44
|
-
defer { free(source.data) }
|
|
49
|
+
defer { if source.data != handedOver { free(source.data) } }
|
|
45
50
|
|
|
46
51
|
var scratch = vImage_Buffer()
|
|
47
52
|
guard vImageBuffer_Init(
|
|
48
53
|
&scratch, source.height, source.width, 32, vImage_Flags(kvImageNoFlags)
|
|
49
54
|
) == kvImageNoError else { return nil }
|
|
50
|
-
defer { free(scratch.data) }
|
|
55
|
+
defer { if scratch.data != handedOver { free(scratch.data) } }
|
|
51
56
|
|
|
52
57
|
// kvImageEdgeExtend clamps at the borders instead of sampling
|
|
53
58
|
// transparent black, so the image keeps its edges instead of fading
|
|
@@ -75,12 +80,46 @@ enum GaussianBlur {
|
|
|
75
80
|
swap(&input, &output)
|
|
76
81
|
}
|
|
77
82
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
// Ownership of the result buffer moves to the CGImage (no copy);
|
|
84
|
+
// `handedOver` keeps the exits above from freeing it a second time.
|
|
85
|
+
return makeImage(from: input, colorSpace: colorSpace, bitmapInfo: bitmapInfo, handedOver: &handedOver)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Wraps `buffer` in a CGImage instead of copying it (one full-bitmap
|
|
89
|
+
/// memcpy less per blur). Ownership is explicit: once the data provider
|
|
90
|
+
/// exists, it frees the buffer — together with the image, or right away
|
|
91
|
+
/// if creating the image fails and the provider is released — so
|
|
92
|
+
/// `handedOver` is set to the buffer's data as soon as that is the case.
|
|
93
|
+
/// (vImageCreateCGImageFromBuffer with kvImageNoAllocate would do the
|
|
94
|
+
/// same, but leaves unspecified whether its free callback runs when the
|
|
95
|
+
/// call fails.)
|
|
96
|
+
private static func makeImage(
|
|
97
|
+
from buffer: vImage_Buffer,
|
|
98
|
+
colorSpace: CGColorSpace,
|
|
99
|
+
bitmapInfo: CGBitmapInfo,
|
|
100
|
+
handedOver: inout UnsafeMutableRawPointer?
|
|
101
|
+
) -> CGImage? {
|
|
102
|
+
guard let provider = CGDataProvider(
|
|
103
|
+
dataInfo: nil,
|
|
104
|
+
data: buffer.data,
|
|
105
|
+
size: buffer.rowBytes * Int(buffer.height),
|
|
106
|
+
releaseData: { _, data, _ in free(UnsafeMutableRawPointer(mutating: data)) }
|
|
107
|
+
) else { return nil }
|
|
108
|
+
handedOver = buffer.data
|
|
82
109
|
|
|
83
|
-
return
|
|
110
|
+
return CGImage(
|
|
111
|
+
width: Int(buffer.width),
|
|
112
|
+
height: Int(buffer.height),
|
|
113
|
+
bitsPerComponent: 8,
|
|
114
|
+
bitsPerPixel: 32,
|
|
115
|
+
bytesPerRow: buffer.rowBytes,
|
|
116
|
+
space: colorSpace,
|
|
117
|
+
bitmapInfo: bitmapInfo,
|
|
118
|
+
provider: provider,
|
|
119
|
+
decode: nil,
|
|
120
|
+
shouldInterpolate: true,
|
|
121
|
+
intent: .defaultIntent
|
|
122
|
+
)
|
|
84
123
|
}
|
|
85
124
|
|
|
86
125
|
/// Widths for the three box-blur passes that approximate a Gaussian of
|
|
@@ -12,7 +12,7 @@ import Nuke
|
|
|
12
12
|
|
|
13
13
|
import UIKit
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
class HybridImage: HybridImageSpec, NativeImage {
|
|
16
16
|
let uiImage: UIImage
|
|
17
17
|
|
|
18
18
|
// PNG encoding is expensive; encode once and reuse for both
|
|
@@ -21,26 +21,22 @@ private class HybridImage: HybridImageSpec, NativeImage {
|
|
|
21
21
|
// doesn't pin its encoding forever.
|
|
22
22
|
private let pngLock = NSLock()
|
|
23
23
|
private var cachedPngData: Data?
|
|
24
|
+
// Registered only once there is encoded data to drop: most images are
|
|
25
|
+
// only ever displayed, and an observer per image is wasted work in a
|
|
26
|
+
// list of hundreds. Guarded by `pngLock` like the data it protects.
|
|
24
27
|
private var memoryWarningObserver: (any NSObjectProtocol)?
|
|
25
28
|
|
|
26
29
|
init(uiImage: UIImage) {
|
|
27
30
|
self.uiImage = uiImage
|
|
28
31
|
super.init()
|
|
29
|
-
memoryWarningObserver = NotificationCenter.default.addObserver(
|
|
30
|
-
forName: UIApplication.didReceiveMemoryWarningNotification,
|
|
31
|
-
object: nil,
|
|
32
|
-
queue: nil
|
|
33
|
-
) { [weak self] _ in
|
|
34
|
-
guard let self else { return }
|
|
35
|
-
self.pngLock.lock()
|
|
36
|
-
self.cachedPngData = nil
|
|
37
|
-
self.pngLock.unlock()
|
|
38
|
-
}
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
deinit {
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
pngLock.lock()
|
|
36
|
+
let observer = memoryWarningObserver
|
|
37
|
+
pngLock.unlock()
|
|
38
|
+
if let observer {
|
|
39
|
+
NotificationCenter.default.removeObserver(observer)
|
|
44
40
|
}
|
|
45
41
|
}
|
|
46
42
|
|
|
@@ -52,6 +48,18 @@ private class HybridImage: HybridImageSpec, NativeImage {
|
|
|
52
48
|
}
|
|
53
49
|
let data = uiImage.pngData()
|
|
54
50
|
cachedPngData = data
|
|
51
|
+
if data != nil, memoryWarningObserver == nil {
|
|
52
|
+
memoryWarningObserver = NotificationCenter.default.addObserver(
|
|
53
|
+
forName: UIApplication.didReceiveMemoryWarningNotification,
|
|
54
|
+
object: nil,
|
|
55
|
+
queue: nil
|
|
56
|
+
) { [weak self] _ in
|
|
57
|
+
guard let self else { return }
|
|
58
|
+
self.pngLock.lock()
|
|
59
|
+
self.cachedPngData = nil
|
|
60
|
+
self.pngLock.unlock()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
55
63
|
return data
|
|
56
64
|
}
|
|
57
65
|
|
|
@@ -89,7 +97,7 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
|
|
|
89
97
|
// users in the host app are left untouched, and repeated instantiation
|
|
90
98
|
// (e.g. Metro reloads) never puts two DataCache instances on the same
|
|
91
99
|
// directory.
|
|
92
|
-
|
|
100
|
+
static let sharedPipeline: ImagePipeline = {
|
|
93
101
|
var configuration = ImagePipeline.Configuration.withDataCache
|
|
94
102
|
// Store processed (blurred/rounded) variants on disk in addition to
|
|
95
103
|
// the original download, so they survive memory eviction and
|
|
@@ -105,6 +113,15 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
|
|
|
105
113
|
configuration.imageCache = ImageCache(
|
|
106
114
|
costLimit: min(ImageCache.defaultCostLimit, 128 * 1024 * 1024)
|
|
107
115
|
)
|
|
116
|
+
// Nuke decodes on a serial queue by default, so a screenful of new
|
|
117
|
+
// list cells is decoded one image at a time however many cores the
|
|
118
|
+
// device has. Decode in parallel instead — capped so a burst of
|
|
119
|
+
// full-resolution decodes (no `resize`) can't multiply peak memory
|
|
120
|
+
// by the core count. Processing stays at Nuke's default of 2: the
|
|
121
|
+
// blur allocates several full-size buffers per image.
|
|
122
|
+
configuration.imageDecodingQueue.maxConcurrentOperationCount = min(
|
|
123
|
+
4, max(1, ProcessInfo.processInfo.activeProcessorCount)
|
|
124
|
+
)
|
|
108
125
|
return ImagePipeline(configuration: configuration)
|
|
109
126
|
}()
|
|
110
127
|
|
|
@@ -121,13 +138,43 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
|
|
|
121
138
|
private var pipeline: ImagePipeline { Self.sharedPipeline }
|
|
122
139
|
private var prefetcher: ImagePrefetcher { Self.sharedPrefetcher }
|
|
123
140
|
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
/// The `URL` for a `url` string as the JS API accepts it: a URL with a
|
|
142
|
+
/// scheme (`https://`, `file://`, …), or a plain absolute file-system
|
|
143
|
+
/// path — the form react-native-nitro-image's `saveToTemporaryFileAsync`
|
|
144
|
+
/// returns — which becomes a `file://` URL.
|
|
145
|
+
static func url(from string: String) -> URL? {
|
|
146
|
+
if string.hasPrefix("/") {
|
|
147
|
+
return URL(fileURLWithPath: string)
|
|
148
|
+
}
|
|
149
|
+
return URL(string: string)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/// Whether `url` is fetched over the network, as opposed to read from the
|
|
153
|
+
/// file system (`file://`, incl. `require()`d assets in release builds)
|
|
154
|
+
/// or decoded inline (`data:`).
|
|
155
|
+
static func isNetworkURL(_ url: URL) -> Bool {
|
|
156
|
+
switch url.scheme?.lowercased() {
|
|
157
|
+
case "http", "https": true
|
|
158
|
+
default: false
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private static func cacheOptions(for cache: CacheOption?, url: URL) -> ImageRequest.Options {
|
|
163
|
+
var options: ImageRequest.Options = switch cache {
|
|
126
164
|
case .memory: [.disableDiskCache]
|
|
127
165
|
case .disk: [.disableMemoryCache]
|
|
128
166
|
case .none?: [.disableDiskCache, .disableMemoryCache]
|
|
129
167
|
default: []
|
|
130
168
|
}
|
|
169
|
+
// A local source has nothing to gain from the disk cache: the
|
|
170
|
+
// pipeline's `.storeAll` policy would copy the file's bytes into the
|
|
171
|
+
// data cache next to the original. Keep local images in memory only —
|
|
172
|
+
// Coil does the same on Android, where only network fetches are
|
|
173
|
+
// written to disk — so `cache: 'disk'` on a local URL means no cache.
|
|
174
|
+
if !isNetworkURL(url) {
|
|
175
|
+
options.insert(.disableDiskCache)
|
|
176
|
+
}
|
|
177
|
+
return options
|
|
131
178
|
}
|
|
132
179
|
|
|
133
180
|
/// The processor that bakes `cornerRadius` into the bitmap, or `nil` when
|
|
@@ -181,53 +228,74 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
|
|
|
181
228
|
return processors
|
|
182
229
|
}
|
|
183
230
|
|
|
231
|
+
/// The `ImageRequest` for `url` with `options` applied — shared by
|
|
232
|
+
/// `loadImage` and `PipelineImageLoader` so both hit the same caches with
|
|
233
|
+
/// identical cache keys.
|
|
234
|
+
static func makeRequest(url: URL, options: Options?) -> ImageRequest {
|
|
235
|
+
var imgRequest = ImageRequest(
|
|
236
|
+
url: url,
|
|
237
|
+
processors: processors(for: options),
|
|
238
|
+
options: cacheOptions(for: options?.cache, url: url)
|
|
239
|
+
)
|
|
240
|
+
// With a target size known, decode near it (aspect-fill, so the
|
|
241
|
+
// decoded image always covers the target) instead of at full
|
|
242
|
+
// resolution — a 48 MP photo displayed as a 300 pt card would
|
|
243
|
+
// otherwise decompress to ~190 MB before Resize shrinks it.
|
|
244
|
+
// Matches Android, where the request's size() drives subsampling;
|
|
245
|
+
// the exact size and crop still come from the Resize processor.
|
|
246
|
+
if let size = resizeSize(for: options) {
|
|
247
|
+
imgRequest.thumbnail = ImageRequest.ThumbnailOptions(
|
|
248
|
+
size: size,
|
|
249
|
+
unit: .pixels,
|
|
250
|
+
contentMode: .aspectFill
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
return imgRequest
|
|
254
|
+
}
|
|
255
|
+
|
|
184
256
|
func loadImage(url: String, options: Options?) throws -> Promise<any HybridImageSpec> {
|
|
185
257
|
return Promise.async {
|
|
186
|
-
guard let imageUrl =
|
|
258
|
+
guard let imageUrl = Self.url(from: url) else {
|
|
187
259
|
throw RuntimeError.error(withMessage: "Invalid URL: \(url)")
|
|
188
260
|
}
|
|
189
261
|
|
|
190
|
-
|
|
191
|
-
url: imageUrl,
|
|
192
|
-
processors: Self.processors(for: options),
|
|
193
|
-
options: Self.cacheOptions(for: options?.cache)
|
|
194
|
-
)
|
|
195
|
-
// With a target size known, decode near it (aspect-fill, so the
|
|
196
|
-
// decoded image always covers the target) instead of at full
|
|
197
|
-
// resolution — a 48 MP photo displayed as a 300 pt card would
|
|
198
|
-
// otherwise decompress to ~190 MB before Resize shrinks it.
|
|
199
|
-
// Matches Android, where the request's size() drives subsampling;
|
|
200
|
-
// the exact size and crop still come from the Resize processor.
|
|
201
|
-
if let size = Self.resizeSize(for: options) {
|
|
202
|
-
imgRequest.thumbnail = ImageRequest.ThumbnailOptions(
|
|
203
|
-
size: size,
|
|
204
|
-
unit: .pixels,
|
|
205
|
-
contentMode: .aspectFill
|
|
206
|
-
)
|
|
207
|
-
}
|
|
208
|
-
|
|
262
|
+
let imgRequest = Self.makeRequest(url: imageUrl, options: options)
|
|
209
263
|
let image = try await self.pipeline.image(for: imgRequest)
|
|
210
264
|
return HybridImage(uiImage: image)
|
|
211
265
|
}
|
|
212
266
|
}
|
|
213
267
|
|
|
268
|
+
func createImageLoader(url: String, options: ViewOptions?) throws -> any HybridImageLoaderSpec {
|
|
269
|
+
// An unparseable URL fails at load time, not here — matching Android,
|
|
270
|
+
// where Coil validates the URL only when the request runs.
|
|
271
|
+
return PipelineImageLoader(url: url, options: options)
|
|
272
|
+
}
|
|
273
|
+
|
|
214
274
|
func preLoadImage(url: String) throws -> Promise<Void> {
|
|
215
275
|
return Promise.async {
|
|
216
|
-
guard let imageUrl =
|
|
276
|
+
guard let imageUrl = Self.url(from: url) else {
|
|
217
277
|
throw RuntimeError.error(withMessage: "Invalid URL: \(url)")
|
|
218
278
|
}
|
|
219
|
-
|
|
220
|
-
self.prefetcher.startPrefetching(with: [imageUrl])
|
|
279
|
+
self.prefetch([imageUrl])
|
|
221
280
|
}
|
|
222
281
|
}
|
|
223
282
|
|
|
224
283
|
func preLoadImages(urls: [String]) throws -> Promise<Void> {
|
|
225
284
|
return Promise.async {
|
|
226
|
-
|
|
227
|
-
self.prefetcher.startPrefetching(with: imageUrls)
|
|
285
|
+
self.prefetch(urls.compactMap { Self.url(from: $0) })
|
|
228
286
|
}
|
|
229
287
|
}
|
|
230
288
|
|
|
289
|
+
/// Prefetching warms the disk cache with a download; a local source has no
|
|
290
|
+
/// download to warm it with, so those are a no-op (as on Android, where
|
|
291
|
+
/// Coil never writes non-network sources to disk) rather than a copy of
|
|
292
|
+
/// the file into the data cache.
|
|
293
|
+
private func prefetch(_ urls: [URL]) {
|
|
294
|
+
let networkUrls = urls.filter(Self.isNetworkURL)
|
|
295
|
+
guard !networkUrls.isEmpty else { return }
|
|
296
|
+
prefetcher.startPrefetching(with: networkUrls)
|
|
297
|
+
}
|
|
298
|
+
|
|
231
299
|
func setMemoryCacheLimit(bytes: Double) throws {
|
|
232
300
|
guard bytes >= 0, bytes.isFinite else {
|
|
233
301
|
throw RuntimeError.error(
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
//
|
|
2
|
+
// PipelineImageLoader.swift
|
|
3
|
+
// NitroImagePipeline
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
import Foundation
|
|
7
|
+
import NitroImage
|
|
8
|
+
import NitroModules
|
|
9
|
+
import Nuke
|
|
10
|
+
import UIKit
|
|
11
|
+
|
|
12
|
+
/// An `ImageLoader` (react-native-nitro-image) backed by the shared pipeline.
|
|
13
|
+
///
|
|
14
|
+
/// `<NativeNitroImage image={loader} />` drives it entirely natively: the view
|
|
15
|
+
/// calls `requestImage` when it attaches to a window and `dropImage` when it
|
|
16
|
+
/// detaches. The load runs at the view's laid-out size (× screen scale) with
|
|
17
|
+
/// no JS round trips, and detaching cancels the request and releases the
|
|
18
|
+
/// bitmap — the decoded image stays in the shared memory/disk caches, so
|
|
19
|
+
/// re-attaching (list recycling) is instant.
|
|
20
|
+
///
|
|
21
|
+
/// `ViewOptions` values are in points; this class converts them to the
|
|
22
|
+
/// pixel-based `Options` of the shared request builder using the view's
|
|
23
|
+
/// display scale, so cache keys match an equivalent `loadImage` call.
|
|
24
|
+
class PipelineImageLoader: HybridImageLoaderSpec {
|
|
25
|
+
private let url: String
|
|
26
|
+
private let options: ViewOptions?
|
|
27
|
+
|
|
28
|
+
// Per-view in-flight work, keyed by the view's identity, so a loader
|
|
29
|
+
// shared between several views cancels only the right request. Confined
|
|
30
|
+
// to the main thread — every access happens inside a main-queue block.
|
|
31
|
+
private var tasks: [ObjectIdentifier: Task<Void, Never>] = [:]
|
|
32
|
+
private var pendingLayouts: [ObjectIdentifier: NSKeyValueObservation] = [:]
|
|
33
|
+
|
|
34
|
+
init(url: String, options: ViewOptions?) {
|
|
35
|
+
self.url = url
|
|
36
|
+
self.options = options
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private var pipeline: ImagePipeline { HybridNitroImagePipeline.sharedPipeline }
|
|
40
|
+
|
|
41
|
+
/// visionOS has no `UIScreen.main`; there UIKit reports 2.0 as the
|
|
42
|
+
/// display scale of trait environments.
|
|
43
|
+
private static var fallbackScale: CGFloat {
|
|
44
|
+
#if os(visionOS)
|
|
45
|
+
return 2.0
|
|
46
|
+
#else
|
|
47
|
+
return UIScreen.main.scale
|
|
48
|
+
#endif
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private static func displayScale(of view: UIView) -> CGFloat {
|
|
52
|
+
let scale = view.traitCollection.displayScale
|
|
53
|
+
// 0 means "unspecified" (view not in a hierarchy yet).
|
|
54
|
+
return scale > 0 ? scale : fallbackScale
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// The point-based `ViewOptions` as pixel-based `Options`, resolved
|
|
58
|
+
/// against `scale` and the target size in pixels.
|
|
59
|
+
private func pixelOptions(scale: CGFloat, sizePx: CGSize?) -> Options {
|
|
60
|
+
let cornerRadius = options?.cornerRadius.map { radius -> Variant_Double_CornerRadii in
|
|
61
|
+
switch radius {
|
|
62
|
+
case .first(let uniform):
|
|
63
|
+
return .first(uniform * scale)
|
|
64
|
+
case .second(let radii):
|
|
65
|
+
return .second(CornerRadii(
|
|
66
|
+
topLeft: radii.topLeft.map { $0 * scale },
|
|
67
|
+
topRight: radii.topRight.map { $0 * scale },
|
|
68
|
+
bottomLeft: radii.bottomLeft.map { $0 * scale },
|
|
69
|
+
bottomRight: radii.bottomRight.map { $0 * scale }
|
|
70
|
+
))
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return Options(
|
|
74
|
+
blur: options?.blur.map { $0 * scale },
|
|
75
|
+
cache: options?.cache,
|
|
76
|
+
cornerRadius: cornerRadius,
|
|
77
|
+
resize: sizePx.map { ResizeOptions(width: Double($0.width), height: Double($0.height)) }
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// An explicit `resize` override (pixels), when set and valid.
|
|
82
|
+
private var explicitResize: CGSize? {
|
|
83
|
+
guard let resize = options?.resize, resize.width > 0, resize.height > 0 else {
|
|
84
|
+
return nil
|
|
85
|
+
}
|
|
86
|
+
return CGSize(width: resize.width, height: resize.height)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// MARK: - ImageLoader
|
|
90
|
+
|
|
91
|
+
func loadImage() throws -> Promise<any HybridImageSpec> {
|
|
92
|
+
return Promise.async {
|
|
93
|
+
guard let imageUrl = HybridNitroImagePipeline.url(from: self.url) else {
|
|
94
|
+
throw RuntimeError.error(withMessage: "Invalid URL: \(self.url)")
|
|
95
|
+
}
|
|
96
|
+
// No view to measure here: use the explicit resize if given, and
|
|
97
|
+
// the main screen's scale for the point-based options.
|
|
98
|
+
let scale = await MainActor.run { Self.fallbackScale }
|
|
99
|
+
let request = HybridNitroImagePipeline.makeRequest(
|
|
100
|
+
url: imageUrl,
|
|
101
|
+
options: self.pixelOptions(scale: scale, sizePx: self.explicitResize)
|
|
102
|
+
)
|
|
103
|
+
let image = try await self.pipeline.image(for: request)
|
|
104
|
+
return HybridImage(uiImage: image)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
func requestImage(forView view: any HybridNitroImageViewSpec) throws {
|
|
109
|
+
guard let nativeView = view as? NativeImageView else { return }
|
|
110
|
+
let key = ObjectIdentifier(view)
|
|
111
|
+
// Always hop (asynchronously) to main: `requestImage` fires while the
|
|
112
|
+
// view is being mounted, and only after the current mounting
|
|
113
|
+
// transaction finishes is its final frame guaranteed to be set.
|
|
114
|
+
DispatchQueue.main.async {
|
|
115
|
+
self.load(into: nativeView.imageView, key: key)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func dropImage(forView view: any HybridNitroImageViewSpec) throws {
|
|
120
|
+
guard let nativeView = view as? NativeImageView else { return }
|
|
121
|
+
let key = ObjectIdentifier(view)
|
|
122
|
+
// Same queue as `requestImage`, so rapid attach/detach sequences
|
|
123
|
+
// (list recycling) replay in call order.
|
|
124
|
+
DispatchQueue.main.async {
|
|
125
|
+
self.cancel(key: key)
|
|
126
|
+
nativeView.imageView.image = nil
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MARK: - Main-thread loading
|
|
131
|
+
|
|
132
|
+
private func cancel(key: ObjectIdentifier) {
|
|
133
|
+
tasks[key]?.cancel()
|
|
134
|
+
tasks[key] = nil
|
|
135
|
+
// Releasing the observation invalidates it.
|
|
136
|
+
pendingLayouts[key] = nil
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private func load(into imageView: UIImageView, key: ObjectIdentifier) {
|
|
140
|
+
cancel(key: key)
|
|
141
|
+
|
|
142
|
+
if let sizePx = explicitResize {
|
|
143
|
+
start(into: imageView, key: key, sizePx: sizePx)
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let bounds = imageView.bounds.size
|
|
148
|
+
if bounds.width > 0, bounds.height > 0 {
|
|
149
|
+
let scale = Self.displayScale(of: imageView)
|
|
150
|
+
start(into: imageView, key: key, sizePx: CGSize(
|
|
151
|
+
width: bounds.width * scale,
|
|
152
|
+
height: bounds.height * scale
|
|
153
|
+
))
|
|
154
|
+
} else {
|
|
155
|
+
// Mounted at zero size (e.g. a flex child before its container
|
|
156
|
+
// grows): wait for real bounds. Observed on the layer — CALayer
|
|
157
|
+
// properties are KVO-compliant, UIView's are not.
|
|
158
|
+
pendingLayouts[key] = imageView.layer.observe(\.bounds) { [weak self, weak imageView] layer, _ in
|
|
159
|
+
guard layer.bounds.width > 0, layer.bounds.height > 0 else { return }
|
|
160
|
+
DispatchQueue.main.async {
|
|
161
|
+
guard let self, let imageView else { return }
|
|
162
|
+
guard self.pendingLayouts[key] != nil else { return }
|
|
163
|
+
self.load(into: imageView, key: key)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private func start(into imageView: UIImageView, key: ObjectIdentifier, sizePx: CGSize) {
|
|
170
|
+
guard let imageUrl = HybridNitroImagePipeline.url(from: url) else { return }
|
|
171
|
+
let scale = Self.displayScale(of: imageView)
|
|
172
|
+
let request = HybridNitroImagePipeline.makeRequest(
|
|
173
|
+
url: imageUrl,
|
|
174
|
+
options: pixelOptions(scale: scale, sizePx: sizePx)
|
|
175
|
+
)
|
|
176
|
+
let pipeline = self.pipeline
|
|
177
|
+
// A memory-cache hit is served synchronously. Going through
|
|
178
|
+
// `pipeline.image(for:)` would resume on Nuke's queue and then hop
|
|
179
|
+
// back to the main actor, so a recycled cell (whose image `dropImage`
|
|
180
|
+
// just cleared) would show empty for a frame or two even though the
|
|
181
|
+
// bitmap is already in memory. The subscript honours the request's
|
|
182
|
+
// `.disableMemoryCacheReads`, so `cache: 'disk'`/`'none'` still miss.
|
|
183
|
+
if !request.options.contains(.disableMemoryCacheReads), let cached = pipeline.cache[request] {
|
|
184
|
+
imageView.image = cached.image
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
// Cancelling the Task cancels Nuke's request; a finished task stays in
|
|
188
|
+
// the map (cancelling it is a no-op) until `cancel` replaces it.
|
|
189
|
+
tasks[key] = Task { @MainActor [weak imageView] in
|
|
190
|
+
guard let image = try? await pipeline.image(for: request) else { return }
|
|
191
|
+
guard !Task.isCancelled else { return }
|
|
192
|
+
imageView?.image = image
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -48,6 +48,11 @@ struct RoundedCornersProcessor: ImageProcessing {
|
|
|
48
48
|
let format = UIGraphicsImageRendererFormat()
|
|
49
49
|
format.scale = image.scale
|
|
50
50
|
format.opaque = false
|
|
51
|
+
// `.automatic` picks the extended (16-bit-per-channel) range on
|
|
52
|
+
// wide-gamut displays, which doubles the bytes of this bitmap — the
|
|
53
|
+
// one that ends up in the memory cache and on screen — for an 8-bit
|
|
54
|
+
// sRGB source that gains nothing from it.
|
|
55
|
+
format.preferredRange = .standard
|
|
51
56
|
|
|
52
57
|
let rect = CGRect(origin: .zero, size: size)
|
|
53
58
|
return UIGraphicsImageRenderer(size: size, format: format).image { context in
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.NativePipelineImage = void 0;
|
|
7
|
+
var _react = require("react");
|
|
8
|
+
var _reactNativeNitroImage = require("react-native-nitro-image");
|
|
9
|
+
var _resizeForStyle = require("./resizeForStyle");
|
|
10
|
+
var _resolveImageSource = require("./resolveImageSource");
|
|
11
|
+
var _usePipelineImageLoader = require("./usePipelineImageLoader");
|
|
12
|
+
var _jsxRuntime = require("react/jsx-runtime");
|
|
13
|
+
/**
|
|
14
|
+
* The instance `<NativePipelineImage>` exposes through its `ref` — the
|
|
15
|
+
* underlying `NativeNitroImage` host view, with the usual native-view
|
|
16
|
+
* methods (`measure`, …).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The fully native-driven variant of `PipelineImage`: after the first render
|
|
21
|
+
* there is **zero JS work per image**. The native view starts the request
|
|
22
|
+
* when it attaches to the window — at its own laid-out size, so nothing
|
|
23
|
+
* waits for an `onLayout` round trip — and cancels it (releasing the bitmap)
|
|
24
|
+
* when it detaches, which makes off-screen list cells free. Loads go through
|
|
25
|
+
* the same pipeline and caches as `useImage`/`preLoadImage`.
|
|
26
|
+
*
|
|
27
|
+
* Compared to `PipelineImage`:
|
|
28
|
+
* - No `onLoad`/`onError` callbacks — the loaded `Image` never crosses into
|
|
29
|
+
* JS. Use `PipelineImage` (or `useImage`) when you need them.
|
|
30
|
+
* - The bitmap is loaded once at the size the view first has; if the view is
|
|
31
|
+
* resized later, the bitmap scales with it instead of reloading.
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* <NativePipelineImage
|
|
35
|
+
* url="https://example.com/photo.jpg"
|
|
36
|
+
* style={{ width: 300, height: 200, borderRadius: 24 }}
|
|
37
|
+
* blur={4}
|
|
38
|
+
* />
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
const NativePipelineImage = exports.NativePipelineImage = /*#__PURE__*/(0, _react.forwardRef)(function NativePipelineImage({
|
|
42
|
+
url,
|
|
43
|
+
blur,
|
|
44
|
+
cornerRadius,
|
|
45
|
+
cache,
|
|
46
|
+
resize,
|
|
47
|
+
style,
|
|
48
|
+
...viewProps
|
|
49
|
+
}, ref) {
|
|
50
|
+
// Same precedence as PipelineImage: an explicit prop wins over style.
|
|
51
|
+
const effectiveCornerRadius = cornerRadius ?? (0, _resizeForStyle.cornerRadiusForStyle)(style);
|
|
52
|
+
const loader = (0, _usePipelineImageLoader.usePipelineImageLoader)(url, {
|
|
53
|
+
blur,
|
|
54
|
+
cornerRadius: effectiveCornerRadius,
|
|
55
|
+
cache,
|
|
56
|
+
resize
|
|
57
|
+
});
|
|
58
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeNitroImage.NativeNitroImage
|
|
59
|
+
// Before the spread so a caller-provided recyclingKey wins.
|
|
60
|
+
, {
|
|
61
|
+
recyclingKey: (0, _resolveImageSource.recyclingKeyFor)(url),
|
|
62
|
+
...viewProps,
|
|
63
|
+
ref: ref,
|
|
64
|
+
style: style,
|
|
65
|
+
image: loader
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Reanimated and DevTools read the display name; the forwardRef wrapper
|
|
70
|
+
// would otherwise report as anonymous.
|
|
71
|
+
NativePipelineImage.displayName = 'NativePipelineImage';
|
|
72
|
+
//# sourceMappingURL=NativePipelineImage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["_react","require","_reactNativeNitroImage","_resizeForStyle","_resolveImageSource","_usePipelineImageLoader","_jsxRuntime","NativePipelineImage","exports","forwardRef","url","blur","cornerRadius","cache","resize","style","viewProps","ref","effectiveCornerRadius","cornerRadiusForStyle","loader","usePipelineImageLoader","jsx","NativeNitroImage","recyclingKey","recyclingKeyFor","image","displayName"],"sourceRoot":"../../src","sources":["NativePipelineImage.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,sBAAA,GAAAD,OAAA;AAEA,IAAAE,eAAA,GAAAF,OAAA;AACA,IAAAG,mBAAA,GAAAH,OAAA;AAMA,IAAAI,uBAAA,GAAAJ,OAAA;AAAkE,IAAAK,WAAA,GAAAL,OAAA;AAKlE;AACA;AACA;AACA;AACA;;AAmCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAMM,mBAAmB,GAAAC,OAAA,CAAAD,mBAAA,gBAAG,IAAAE,iBAAU,EAG3C,SAASF,mBAAmBA,CAC5B;EAAEG,GAAG;EAAEC,IAAI;EAAEC,YAAY;EAAEC,KAAK;EAAEC,MAAM;EAAEC,KAAK;EAAE,GAAGC;AAAU,CAAC,EAC/DC,GAAG,EACH;EACA;EACA,MAAMC,qBAAqB,GAAGN,YAAY,IAAI,IAAAO,oCAAoB,EAACJ,KAAK,CAAC;EACzE,MAAMK,MAAM,GAAG,IAAAC,8CAAsB,EAACX,GAAG,EAAE;IACzCC,IAAI;IACJC,YAAY,EAAEM,qBAAqB;IACnCL,KAAK;IACLC;EACF,CAAC,CAAC;EAEF,oBACE,IAAAR,WAAA,CAAAgB,GAAA,EAACpB,sBAAA,CAAAqB;EACC;EAAA;IACAC,YAAY,EAAE,IAAAC,mCAAe,EAACf,GAAG,CAAE;IAAA,GAC/BM,SAAS;IACbC,GAAG,EAAEA,GAAI;IACTF,KAAK,EAAEA,KAAM;IACbW,KAAK,EAAEN;EAAO,CACf,CAAC;AAEN,CAAC,CAAC;;AAEF;AACA;AACAb,mBAAmB,CAACoB,WAAW,GAAG,qBAAqB","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_react","require","_reactNative","_reactNativeNitroImage","_resizeForStyle","_useImage","_jsxRuntime","sameSize","a","b","width","height","scaleRadius","cornerRadius","scale","topLeft","topRight","bottomLeft","bottomRight","PipelineImage","exports","forwardRef","url","blur","cache","onLoad","onError","style","onLayout","viewProps","ref","PixelRatio","get","styleSize","resizeForStyle","layoutSize","setLayoutSize","useState","undefined","resize","effectiveCornerRadius","cornerRadiusForStyle","image","error","useImage","enabled","onLoadRef","useRef","onErrorRef","useEffect","current","handleLayout","event","nativeEvent","layout","next","resizeForLayout","prev","jsx","NativeNitroImage","displayName"],"sourceRoot":"../../src","sources":["PipelineImage.tsx"],"mappings":";;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAOA,IAAAC,YAAA,GAAAD,OAAA;AAKA,IAAAE,sBAAA,GAAAF,OAAA;AAEA,IAAAG,eAAA,GAAAH,OAAA;
|
|
1
|
+
{"version":3,"names":["_react","require","_reactNative","_reactNativeNitroImage","_resizeForStyle","_useImage","_jsxRuntime","sameSize","a","b","width","height","scaleRadius","cornerRadius","scale","topLeft","topRight","bottomLeft","bottomRight","PipelineImage","exports","forwardRef","url","blur","cache","onLoad","onError","style","onLayout","viewProps","ref","PixelRatio","get","styleSize","resizeForStyle","layoutSize","setLayoutSize","useState","undefined","resize","effectiveCornerRadius","cornerRadiusForStyle","image","error","useImage","enabled","onLoadRef","useRef","onErrorRef","useEffect","current","handleLayout","event","nativeEvent","layout","next","resizeForLayout","prev","jsx","NativeNitroImage","displayName"],"sourceRoot":"../../src","sources":["PipelineImage.tsx"],"mappings":"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","ignoreList":[]}
|