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.
Files changed (72) hide show
  1. package/README.md +124 -8
  2. package/android/CMakeLists.txt +4 -0
  3. package/android/src/main/cpp/GaussianBlur.cpp +165 -0
  4. package/android/src/main/cpp/GaussianBlur.hpp +52 -0
  5. package/android/src/main/cpp/GaussianBlurJni.cpp +50 -0
  6. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +167 -75
  7. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/PipelineImageLoader.kt +177 -0
  8. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/BlurTransformation.kt +36 -81
  9. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/HardwareBitmapTransformation.kt +45 -0
  10. package/ios/GaussianBlur.swift +48 -9
  11. package/ios/HybridNitroImagePipeline.swift +109 -41
  12. package/ios/PipelineImageLoader.swift +195 -0
  13. package/ios/RoundedCornersProcessor.swift +5 -0
  14. package/lib/commonjs/NativePipelineImage.js +72 -0
  15. package/lib/commonjs/NativePipelineImage.js.map +1 -0
  16. package/lib/commonjs/PipelineImage.js.map +1 -1
  17. package/lib/commonjs/index.js +27 -0
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/resolveImageSource.js +89 -0
  20. package/lib/commonjs/resolveImageSource.js.map +1 -0
  21. package/lib/commonjs/useImage.js +8 -4
  22. package/lib/commonjs/useImage.js.map +1 -1
  23. package/lib/commonjs/usePipelineImageLoader.js +74 -0
  24. package/lib/commonjs/usePipelineImageLoader.js.map +1 -0
  25. package/lib/module/NativePipelineImage.js +68 -0
  26. package/lib/module/NativePipelineImage.js.map +1 -0
  27. package/lib/module/PipelineImage.js.map +1 -1
  28. package/lib/module/index.js +3 -0
  29. package/lib/module/index.js.map +1 -1
  30. package/lib/module/resolveImageSource.js +83 -0
  31. package/lib/module/resolveImageSource.js.map +1 -0
  32. package/lib/module/useImage.js +8 -4
  33. package/lib/module/useImage.js.map +1 -1
  34. package/lib/module/usePipelineImageLoader.js +70 -0
  35. package/lib/module/usePipelineImageLoader.js.map +1 -0
  36. package/lib/typescript/src/NativePipelineImage.d.ts +71 -0
  37. package/lib/typescript/src/NativePipelineImage.d.ts.map +1 -0
  38. package/lib/typescript/src/PipelineImage.d.ts +7 -2
  39. package/lib/typescript/src/PipelineImage.d.ts.map +1 -1
  40. package/lib/typescript/src/index.d.ts +4 -1
  41. package/lib/typescript/src/index.d.ts.map +1 -1
  42. package/lib/typescript/src/resolveImageSource.d.ts +56 -0
  43. package/lib/typescript/src/resolveImageSource.d.ts.map +1 -0
  44. package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts +56 -1
  45. package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
  46. package/lib/typescript/src/useImage.d.ts +10 -4
  47. package/lib/typescript/src/useImage.d.ts.map +1 -1
  48. package/lib/typescript/src/usePipelineImageLoader.d.ts +17 -0
  49. package/lib/typescript/src/usePipelineImageLoader.d.ts.map +1 -0
  50. package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +13 -0
  51. package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.hpp +1 -0
  52. package/nitrogen/generated/android/c++/JViewOptions.hpp +77 -0
  53. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipelineSpec.kt +5 -0
  54. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ViewOptions.kt +66 -0
  55. package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.cpp +10 -0
  56. package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +44 -0
  57. package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Umbrella.hpp +8 -0
  58. package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +14 -0
  59. package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec.swift +1 -0
  60. package/nitrogen/generated/ios/swift/HybridNitroImagePipelineSpec_cxx.swift +15 -0
  61. package/nitrogen/generated/ios/swift/ViewOptions.swift +101 -0
  62. package/nitrogen/generated/shared/c++/HybridNitroImagePipelineSpec.cpp +1 -0
  63. package/nitrogen/generated/shared/c++/HybridNitroImagePipelineSpec.hpp +7 -0
  64. package/nitrogen/generated/shared/c++/ViewOptions.hpp +104 -0
  65. package/package.json +4 -3
  66. package/src/NativePipelineImage.tsx +108 -0
  67. package/src/PipelineImage.tsx +7 -2
  68. package/src/index.ts +12 -0
  69. package/src/resolveImageSource.ts +83 -0
  70. package/src/specs/nitro-image-toolkit.nitro.ts +57 -1
  71. package/src/useImage.ts +26 -15
  72. package/src/usePipelineImageLoader.ts +90 -0
@@ -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: CGColorSpaceCreateDeviceRGB(),
36
- bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedFirst.rawValue),
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
- var error = vImage_Error(kvImageNoError)
79
- guard let blurred = vImageCreateCGImageFromBuffer(
80
- &input, &format, nil, nil, vImage_Flags(kvImageNoFlags), &error
81
- )?.takeRetainedValue(), error == kvImageNoError else { return nil }
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 blurred
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
- private class HybridImage: HybridImageSpec, NativeImage {
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
- if let memoryWarningObserver {
43
- NotificationCenter.default.removeObserver(memoryWarningObserver)
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
- private static let sharedPipeline: ImagePipeline = {
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
- private static func cacheOptions(for cache: CacheOption?) -> ImageRequest.Options {
125
- switch cache {
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 = URL(string: url) else {
258
+ guard let imageUrl = Self.url(from: url) else {
187
259
  throw RuntimeError.error(withMessage: "Invalid URL: \(url)")
188
260
  }
189
261
 
190
- var imgRequest = ImageRequest(
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 = URL(string: url) else {
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
- let imageUrls = urls.compactMap { URL(string: $0) }
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":"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","ignoreList":[]}
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":[]}