react-native-nitro-image-pipeline 1.1.0 → 1.2.1

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 (56) hide show
  1. package/README.md +84 -8
  2. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +36 -15
  3. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/ResizeTransformation.kt +58 -0
  4. package/ios/HybridNitroImagePipeline.swift +54 -27
  5. package/ios/RoundedCornersProcessor.swift +6 -3
  6. package/lib/commonjs/NitroImagePipeline.js +9 -0
  7. package/lib/commonjs/NitroImagePipeline.js.map +1 -0
  8. package/lib/commonjs/PipelineImage.js +111 -0
  9. package/lib/commonjs/PipelineImage.js.map +1 -0
  10. package/lib/commonjs/index.js +40 -81
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/resizeForStyle.js +68 -0
  13. package/lib/commonjs/resizeForStyle.js.map +1 -0
  14. package/lib/commonjs/useImage.js +95 -0
  15. package/lib/commonjs/useImage.js.map +1 -0
  16. package/lib/module/NitroImagePipeline.js +5 -0
  17. package/lib/module/NitroImagePipeline.js.map +1 -0
  18. package/lib/module/PipelineImage.js +107 -0
  19. package/lib/module/PipelineImage.js.map +1 -0
  20. package/lib/module/index.js +4 -79
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/resizeForStyle.js +62 -0
  23. package/lib/module/resizeForStyle.js.map +1 -0
  24. package/lib/module/useImage.js +91 -0
  25. package/lib/module/useImage.js.map +1 -0
  26. package/lib/typescript/src/NitroImagePipeline.d.ts +3 -0
  27. package/lib/typescript/src/NitroImagePipeline.d.ts.map +1 -0
  28. package/lib/typescript/src/PipelineImage.d.ts +57 -0
  29. package/lib/typescript/src/PipelineImage.d.ts.map +1 -0
  30. package/lib/typescript/src/index.d.ts +5 -38
  31. package/lib/typescript/src/index.d.ts.map +1 -1
  32. package/lib/typescript/src/resizeForStyle.d.ts +30 -0
  33. package/lib/typescript/src/resizeForStyle.d.ts.map +1 -0
  34. package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts +33 -5
  35. package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
  36. package/lib/typescript/src/useImage.d.ts +53 -0
  37. package/lib/typescript/src/useImage.d.ts.map +1 -0
  38. package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +4 -0
  39. package/nitrogen/generated/android/c++/JOptions.hpp +9 -3
  40. package/nitrogen/generated/android/c++/JResizeOptions.hpp +61 -0
  41. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/Options.kt +9 -4
  42. package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ResizeOptions.kt +56 -0
  43. package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +18 -0
  44. package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Umbrella.hpp +3 -0
  45. package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +3 -0
  46. package/nitrogen/generated/ios/swift/Options.swift +12 -1
  47. package/nitrogen/generated/ios/swift/ResizeOptions.swift +34 -0
  48. package/nitrogen/generated/shared/c++/Options.hpp +9 -2
  49. package/nitrogen/generated/shared/c++/ResizeOptions.hpp +87 -0
  50. package/package.json +1 -1
  51. package/src/NitroImagePipeline.ts +6 -0
  52. package/src/PipelineImage.tsx +156 -0
  53. package/src/index.ts +10 -123
  54. package/src/resizeForStyle.ts +96 -0
  55. package/src/specs/nitro-image-toolkit.nitro.ts +34 -5
  56. package/src/useImage.ts +149 -0
package/README.md CHANGED
@@ -10,7 +10,7 @@ A high-performance image loading, caching, and processing library for React Nati
10
10
 
11
11
  - Load remote images with built-in memory and disk caching
12
12
  - Prefetch single or multiple images in the background
13
- - Apply Gaussian blur and rounded corners (uniform or per-corner) at load time
13
+ - Resize (aspect-fill, center-crop) and apply Gaussian blur and rounded corners (uniform or per-corner) at load time
14
14
  - Apply Gaussian blur to already-loaded images
15
15
  - Clear the image cache on demand
16
16
  - `useImage` hook for declarative image loading in components
@@ -38,36 +38,80 @@ bun add react-native-nitro-image-pipeline react-native-nitro-modules react-nativ
38
38
 
39
39
  ## Usage
40
40
 
41
+ ### `<PipelineImage>` component
42
+
43
+ The zero-math way to load an image in a component — no manual `PixelRatio` conversions:
44
+
45
+ ```tsx
46
+ import { PipelineImage } from 'react-native-nitro-image-pipeline';
47
+
48
+ function MyComponent() {
49
+ return (
50
+ <PipelineImage
51
+ url="https://example.com/photo.jpg"
52
+ style={styles.photo} // bitmap is sized to this layout × PixelRatio.get()
53
+ blur={2} // points, like style
54
+ />
55
+ );
56
+ }
57
+
58
+ const styles = StyleSheet.create({
59
+ photo: { width: 300, height: 200, borderRadius: 12 }, // baked into the bitmap
60
+ });
61
+ ```
62
+
63
+ Numeric `width`/`height` in `style` load immediately; percentage or flex-based sizes wait for the
64
+ first `onLayout` before fetching, so the full-size image is never requested just to be squeezed
65
+ into a small view. `blur` is in points **on this component only** — it's converted to bitmap
66
+ pixels internally, unlike the pixel-based values used everywhere else in this library.
67
+ `cornerRadius` works the same way, but if you don't pass it, it's derived instead from `style`'s
68
+ `borderRadius` (or the per-corner `borderTopLeftRadius`/etc. properties, e.g. a "ticket" shape) —
69
+ so a style that already rounds the view rounds the bitmap too, with no separate prop. Pass
70
+ `cornerRadius` explicitly to override that. `onLoad`/`onError` callbacks are supported, and every
71
+ other prop (`resizeMode`, `recyclingKey`, `testID`, …) is passed straight through to
72
+ `NativeNitroImage`.
73
+
41
74
  ### `useImage` hook
42
75
 
43
76
  The simplest way to load an image in a component:
44
77
 
45
78
  ```tsx
46
- import { useImage } from 'react-native-nitro-image-pipeline';
79
+ import { PixelRatio, useImage, resizeForStyle } from 'react-native-nitro-image-pipeline';
47
80
 
48
81
  function MyComponent() {
49
82
  const { image, error } = useImage({
50
83
  url: 'https://example.com/photo.jpg',
51
- blur: 4, // Gaussian sigma in source pixels — same result on iOS and Android
52
- cornerRadius: 12,
84
+ blur: 4, // Gaussian sigma in bitmap pixels — same result on iOS and Android
85
+ // Resize to the size you display (points × screen scale) so the corner
86
+ // radii apply 1:1 to what you see instead of the full-resolution source.
87
+ resize: resizeForStyle(styles.image), // display size × PixelRatio.get()
88
+ cornerRadius: 12 * PixelRatio.get(), // bitmap pixels
53
89
  });
54
90
 
55
91
  if (error) return <Text>Failed to load image</Text>;
56
92
  if (!image) return <ActivityIndicator />;
57
93
 
58
94
  // use `image` with react-native-nitro-image
59
- return <NitroImage source={image} />;
95
+ return <NitroImage image={image} style={styles.image} />;
60
96
  }
97
+
98
+ const styles = StyleSheet.create({ image: { width: 300, height: 200 } });
61
99
  ```
62
100
 
101
+ Pass `enabled: false` to defer the request — used internally by `<PipelineImage>` to wait for
102
+ layout before it has a size to resize to.
103
+
63
104
  ### Direct API
64
105
 
65
106
  ```ts
66
107
  import { NitroImagePipeline } from 'react-native-nitro-image-pipeline';
67
108
 
68
- // Load an image with options
109
+ // Load an image with options. resize and cornerRadius are in pixels of the
110
+ // produced bitmap: without resize, the radius applies to the full-resolution
111
+ // source and shrinks along with it when displayed small.
69
112
  const image = await NitroImagePipeline.loadImage('https://example.com/photo.jpg', {
70
- blur: 4, // Gaussian sigma in source pixels — see "Blur units"
113
+ blur: 4, // Gaussian sigma in bitmap pixels — see "Blur units"
114
+ resize: { width: 600, height: 400 }, // aspect-fill + center-crop, exact output size
71
115
  cornerRadius: 12,
72
116
  cache: 'disk',
73
117
  });
@@ -75,6 +119,7 @@ const image = await NitroImagePipeline.loadImage('https://example.com/photo.jpg'
75
119
  // Per-corner radii — e.g. a "ticket" shape with larger bottom corners.
76
120
  // The rounding is baked into the bitmap, so no view-layer masking is needed.
77
121
  const ticket = await NitroImagePipeline.loadImage('https://example.com/photo.jpg', {
122
+ resize: { width: 600, height: 400 },
78
123
  cornerRadius: { topLeft: 24, topRight: 24, bottomLeft: 48, bottomRight: 48 },
79
124
  });
80
125
 
@@ -103,8 +148,39 @@ Loads an image from a URL and returns a `Promise<Image>`.
103
148
  | Option | Type | Default | Description |
104
149
  |---|---|---|---|
105
150
  | `blur` | `number` | `0` | Gaussian blur strength applied at load time — see [Blur units](#blur-units) |
106
- | `cornerRadius` | `number \| CornerRadii` | `0` | Corner radius in points a single number for all four corners, or `{ topLeft?, topRight?, bottomLeft?, bottomRight? }` for independent per-corner radii (omitted corners stay square) |
151
+ | `resize` | `{ width, height }` | source size | Target bitmap size in pixels. Scales to fill and center-crops (CSS `object-fit: cover`, upscaling if needed) before `blur`/`cornerRadius` run, so their pixel units refer to this final size. Typically your display size in points × `PixelRatio.get()` |
152
+ | `cornerRadius` | `number \| CornerRadii` | `0` | Corner radius in pixels of the produced bitmap — a single number for all four corners, or `{ topLeft?, topRight?, bottomLeft?, bottomRight? }` for independent per-corner radii (omitted corners stay square). Pair with `resize` for radii that match your layout |
153
+ | `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
154
+
155
+ ### `<PipelineImage>`
156
+
157
+ | Prop | Type | Default | Description |
158
+ |---|---|---|---|
159
+ | `url` | `string` | — | Image URL to load |
160
+ | `style` | `StyleProp<ViewStyle>` | — | Layout style; also determines the resize target (see [`resizeForStyle`](#resizeforstyle-style--resizeforlayoutwidth-height)) and, if `cornerRadius` is omitted, the corner radius (see [`cornerRadiusForStyle`](#cornerradiusforstylestyle)) |
161
+ | `blur` | `number` | `0` | Gaussian blur strength, in **points** (converted to bitmap pixels internally) |
162
+ | `cornerRadius` | `number \| CornerRadii` | derived from `style` | Corner radius, in **points** (converted to bitmap pixels internally). When omitted, derived from `style`'s `borderRadius`/`borderTopLeftRadius`/etc.; square if neither is set |
107
163
  | `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
164
+ | `onLoad` | `(image: Image) => void` | — | Called when the image finishes loading |
165
+ | `onError` | `(error: Error) => void` | — | Called if loading fails |
166
+ | `onLayout` | `(event: LayoutChangeEvent) => void` | — | Standard `View` layout callback; also drives the deferred resize for non-numeric sizes |
167
+ | `…NativeNitroImage props` | — | — | Everything else (`resizeMode`, `recyclingKey`, `testID`, …) is passed through to `NativeNitroImage` |
168
+
169
+ ### `resizeForStyle(style)` / `resizeForLayout(width, height)`
170
+
171
+ Converts a layout size in points to a bitmap `resize` option in pixels. Returns
172
+ `{ width, height }` in whole pixels via `PixelRatio.getPixelSizeForLayoutSize`, or `undefined` for
173
+ non-numeric sizes (e.g. `'100%'`, `undefined`) — `resizeForStyle` reads `style.width`/`style.height`,
174
+ `resizeForLayout` takes explicit numbers.
175
+
176
+ ### `cornerRadiusForStyle(style)`
177
+
178
+ Converts a view style's `borderRadius`/`borderTopLeftRadius`/`borderTopRightRadius`/
179
+ `borderBottomLeftRadius`/`borderBottomRightRadius` (in points) into a `cornerRadius` option — a
180
+ plain number for uniform `borderRadius` alone, or a `CornerRadii` object once any per-corner
181
+ property is set (falling back to `borderRadius` for the corners left unset). Returns `undefined`
182
+ when none are set. This is what `<PipelineImage>` uses internally when its `cornerRadius` prop is
183
+ omitted.
108
184
 
109
185
  ### `preLoadImage(url)`
110
186
 
@@ -14,6 +14,7 @@ import coil3.request.ImageRequest
14
14
  import coil3.request.SuccessResult
15
15
  import coil3.request.allowHardware
16
16
  import coil3.request.transformations
17
+ import coil3.size.Scale
17
18
  import coil3.size.Size
18
19
  import coil3.transform.RoundedCornersTransformation
19
20
  import com.google.net.cronet.okhttptransport.CronetInterceptor
@@ -22,6 +23,8 @@ import com.margelo.nitro.core.Promise
22
23
  import com.margelo.nitro.image.HybridImage
23
24
  import com.margelo.nitro.image.HybridImageSpec
24
25
  import com.margelo.nitro.nitroimagepipeline.transform.BlurTransformation
26
+ import com.margelo.nitro.nitroimagepipeline.transform.ResizeTransformation
27
+ import kotlin.math.roundToInt
25
28
  import kotlinx.coroutines.Dispatchers
26
29
  import kotlinx.coroutines.async
27
30
  import kotlinx.coroutines.awaitAll
@@ -40,23 +43,34 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
40
43
 
41
44
  override fun loadImage(url: String, options: Options?): Promise<HybridImageSpec> = Promise.async {
42
45
  val blur = options?.blur?.toFloat() ?: 0f
46
+ val resize =
47
+ options?.resize?.let { r ->
48
+ val width = r.width.roundToInt()
49
+ val height = r.height.roundToInt()
50
+ if (width > 0 && height > 0) width to height else null
51
+ }
43
52
  val transformations = buildList {
53
+ // Resize first: blur sigma and corner radii are in pixels of the bitmap
54
+ // they run on, so they must see the final size.
55
+ resize?.let { (width, height) -> add(ResizeTransformation(width, height)) }
44
56
  if (blur > 0f) add(BlurTransformation(context, blur))
45
- options?.cornerRadius?.match(
46
- first = { radius ->
47
- if (radius > 0.0) add(RoundedCornersTransformation(radius.toFloat()))
48
- },
49
- second = { radii ->
50
- // RoundedCornersTransformation rejects negative radii; treat them as square.
51
- val topLeft = (radii.topLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
52
- val topRight = (radii.topRight?.toFloat() ?: 0f).coerceAtLeast(0f)
53
- val bottomLeft = (radii.bottomLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
54
- val bottomRight = (radii.bottomRight?.toFloat() ?: 0f).coerceAtLeast(0f)
55
- if (topLeft > 0f || topRight > 0f || bottomLeft > 0f || bottomRight > 0f) {
56
- add(RoundedCornersTransformation(topLeft, topRight, bottomLeft, bottomRight))
57
- }
58
- },
59
- )
57
+ options
58
+ ?.cornerRadius
59
+ ?.match(
60
+ first = { radius ->
61
+ if (radius > 0.0) add(RoundedCornersTransformation(radius.toFloat()))
62
+ },
63
+ second = { radii ->
64
+ // RoundedCornersTransformation rejects negative radii; treat them as square.
65
+ val topLeft = (radii.topLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
66
+ val topRight = (radii.topRight?.toFloat() ?: 0f).coerceAtLeast(0f)
67
+ val bottomLeft = (radii.bottomLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
68
+ val bottomRight = (radii.bottomRight?.toFloat() ?: 0f).coerceAtLeast(0f)
69
+ if (topLeft > 0f || topRight > 0f || bottomLeft > 0f || bottomRight > 0f) {
70
+ add(RoundedCornersTransformation(topLeft, topRight, bottomLeft, bottomRight))
71
+ }
72
+ },
73
+ )
60
74
  }
61
75
  val request =
62
76
  ImageRequest.Builder(context)
@@ -77,6 +91,13 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
77
91
  }
78
92
  null -> Unit // Coil defaults: both enabled
79
93
  }
94
+ // Ask the decoder for the target size so a large source is
95
+ // subsampled near it instead of decoded at full resolution;
96
+ // ResizeTransformation then makes the size exact.
97
+ resize?.let { (width, height) ->
98
+ size(width, height)
99
+ scale(Scale.FILL)
100
+ }
80
101
  }
81
102
  .allowHardware(true)
82
103
  .transformations(transformations)
@@ -0,0 +1,58 @@
1
+ package com.margelo.nitro.nitroimagepipeline.transform
2
+
3
+ import android.graphics.Bitmap
4
+ import android.graphics.Canvas
5
+ import android.graphics.Matrix
6
+ import android.graphics.Paint
7
+ import coil3.size.Size
8
+ import coil3.transform.Transformation
9
+ import kotlin.math.max
10
+
11
+ /**
12
+ * Scales the input to fill exactly [width] × [height] pixels and center-crops the overflow (CSS
13
+ * `object-fit: cover`), upscaling smaller sources. It runs before [BlurTransformation] and rounded
14
+ * corners so their pixel units refer to the final bitmap — matching Nuke's
15
+ * `ImageProcessors.Resize(contentMode: .aspectFill, crop: true)` on iOS.
16
+ */
17
+ class ResizeTransformation(
18
+ private val width: Int,
19
+ private val height: Int,
20
+ ) : Transformation() {
21
+
22
+ init {
23
+ require(width > 0 && height > 0) { "width and height must be > 0." }
24
+ }
25
+
26
+ override val cacheKey = "${ResizeTransformation::class.java.name}-$width-$height"
27
+
28
+ override suspend fun transform(input: Bitmap, size: Size): Bitmap {
29
+ val softwareInput =
30
+ if (input.config == Bitmap.Config.HARDWARE) input.copy(Bitmap.Config.ARGB_8888, false)
31
+ else input
32
+ if (softwareInput.width == width && softwareInput.height == height) return softwareInput
33
+
34
+ val scale = max(width.toFloat() / softwareInput.width, height.toFloat() / softwareInput.height)
35
+ val matrix =
36
+ Matrix().apply {
37
+ setScale(scale, scale)
38
+ postTranslate(
39
+ (width - softwareInput.width * scale) / 2f,
40
+ (height - softwareInput.height * scale) / 2f,
41
+ )
42
+ }
43
+ val config = softwareInput.config ?: Bitmap.Config.ARGB_8888
44
+ val output = Bitmap.createBitmap(width, height, config)
45
+ Canvas(output)
46
+ .drawBitmap(softwareInput, matrix, Paint(Paint.ANTI_ALIAS_FLAG or Paint.FILTER_BITMAP_FLAG))
47
+ return output
48
+ }
49
+
50
+ override fun equals(other: Any?): Boolean {
51
+ if (this === other) return true
52
+ return other is ResizeTransformation && width == other.width && height == other.height
53
+ }
54
+
55
+ override fun hashCode(): Int = 31 * width + height
56
+
57
+ override fun toString() = "ResizeTransformation(width=$width, height=$height)"
58
+ }
@@ -103,41 +103,68 @@ class HybridNitroImagePipeline: HybridNitroImagePipelineSpec {
103
103
  private var pipeline: ImagePipeline { Self.sharedPipeline }
104
104
  private var prefetcher: ImagePrefetcher { Self.sharedPrefetcher }
105
105
 
106
+ private static func cacheOptions(for cache: CacheOption?) -> ImageRequest.Options {
107
+ switch cache {
108
+ case .memory: [.disableDiskCache]
109
+ case .disk: [.disableMemoryCache]
110
+ case .none?: [.disableDiskCache, .disableMemoryCache]
111
+ default: []
112
+ }
113
+ }
114
+
115
+ /// The processor that bakes `cornerRadius` into the bitmap, or `nil` when
116
+ /// no corner is actually rounded.
117
+ private static func cornerRadiusProcessor(
118
+ for cornerRadius: Variant_Double_CornerRadii?
119
+ ) -> (any ImageProcessing)? {
120
+ switch cornerRadius {
121
+ case .first(let radius):
122
+ guard radius > 0 else { return nil }
123
+ // `unit: .pixels` is required: Nuke defaults to `.points`, which
124
+ // multiplies the radius by the screen scale. The radius is
125
+ // documented — and implemented on Android and in
126
+ // RoundedCornersProcessor — as bitmap pixels.
127
+ return ImageProcessors.RoundedCorners(radius: radius, unit: .pixels)
128
+ case .second(let radii):
129
+ let roundedCorners = RoundedCornersProcessor(radii: radii)
130
+ return roundedCorners.hasRounding ? roundedCorners : nil
131
+ case nil:
132
+ return nil
133
+ }
134
+ }
135
+
136
+ private static func processors(for options: Options?) -> [any ImageProcessing] {
137
+ var processors: [any ImageProcessing] = []
138
+ // Resize first: blur sigma and corner radii are defined in pixels
139
+ // of the bitmap they run on, so they must see the final size.
140
+ if let resize = options?.resize, resize.width > 0, resize.height > 0 {
141
+ processors.append(ImageProcessors.Resize(
142
+ size: CGSize(width: resize.width, height: resize.height),
143
+ unit: .pixels,
144
+ contentMode: .aspectFill,
145
+ crop: true,
146
+ upscale: true
147
+ ))
148
+ }
149
+ if let blur = options?.blur, blur > 0 {
150
+ processors.append(GaussianBlurProcessor(sigma: blur))
151
+ }
152
+ if let roundedCorners = cornerRadiusProcessor(for: options?.cornerRadius) {
153
+ processors.append(roundedCorners)
154
+ }
155
+ return processors
156
+ }
157
+
106
158
  func loadImage(url: String, options: Options?) throws -> Promise<any HybridImageSpec> {
107
159
  return Promise.async {
108
160
  guard let imageUrl = URL(string: url) else {
109
161
  throw RuntimeError.error(withMessage: "Invalid URL: \(url)")
110
162
  }
111
163
 
112
- let cacheOptions: ImageRequest.Options = switch options?.cache {
113
- case .memory: [.disableDiskCache]
114
- case .disk: [.disableMemoryCache]
115
- case .none?: [.disableDiskCache, .disableMemoryCache]
116
- default: []
117
- }
118
-
119
- var processors: [any ImageProcessing] = []
120
- if let blur = options?.blur, blur > 0 {
121
- processors.append(GaussianBlurProcessor(sigma: blur))
122
- }
123
- switch options?.cornerRadius {
124
- case .first(let radius):
125
- if radius > 0 {
126
- processors.append(.roundedCorners(radius: radius))
127
- }
128
- case .second(let radii):
129
- let roundedCorners = RoundedCornersProcessor(radii: radii)
130
- if roundedCorners.hasRounding {
131
- processors.append(roundedCorners)
132
- }
133
- case nil:
134
- break
135
- }
136
-
137
164
  let imgRequest = ImageRequest(
138
165
  url: imageUrl,
139
- processors: processors,
140
- options: cacheOptions
166
+ processors: Self.processors(for: options),
167
+ options: Self.cacheOptions(for: options?.cache)
141
168
  )
142
169
 
143
170
  let image = try await self.pipeline.image(for: imgRequest)
@@ -12,8 +12,11 @@ import Foundation
12
12
  import Nuke
13
13
  import UIKit
14
14
 
15
- /// Nuke processor that rounds each corner with its own radius (in points,
16
- /// like `ImageProcessors.RoundedCorners` with `unit: .points`).
15
+ /// Nuke processor that rounds each corner with its own radius, measured in
16
+ /// pixels of the bitmap it receives (like `ImageProcessors.RoundedCorners`
17
+ /// with `unit: .points` on the scale-1 images the pipeline decodes). Runs
18
+ /// after `ImageProcessors.Resize` when `Options.resize` is set, so the radii
19
+ /// refer to the final output size.
17
20
  struct RoundedCornersProcessor: ImageProcessing {
18
21
  let topLeft: CGFloat
19
22
  let topRight: CGFloat
@@ -75,7 +78,7 @@ struct RoundedCornersProcessor: ImageProcessing {
75
78
  (rect.width, topLeft + topRight),
76
79
  (rect.width, bottomLeft + bottomRight),
77
80
  (rect.height, topLeft + bottomLeft),
78
- (rect.height, topRight + bottomRight),
81
+ (rect.height, topRight + bottomRight)
79
82
  ] where pair > edge {
80
83
  scale = min(scale, edge / pair)
81
84
  }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.NitroImagePipeline = void 0;
7
+ var _reactNativeNitroModules = require("react-native-nitro-modules");
8
+ const NitroImagePipeline = exports.NitroImagePipeline = _reactNativeNitroModules.NitroModules.createHybridObject('NitroImagePipeline');
9
+ //# sourceMappingURL=NitroImagePipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["_reactNativeNitroModules","require","NitroImagePipeline","exports","NitroModules","createHybridObject"],"sourceRoot":"../../src","sources":["NitroImagePipeline.ts"],"mappings":";;;;;;AAAA,IAAAA,wBAAA,GAAAC,OAAA;AAIO,MAAMC,kBAAkB,GAAAC,OAAA,CAAAD,kBAAA,GAC7BE,qCAAY,CAACC,kBAAkB,CAAyB,oBAAoB,CAAC","ignoreList":[]}
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.PipelineImage = PipelineImage;
7
+ var _react = require("react");
8
+ var _reactNative = require("react-native");
9
+ var _reactNativeNitroImage = require("react-native-nitro-image");
10
+ var _resizeForStyle = require("./resizeForStyle");
11
+ var _useImage = require("./useImage");
12
+ var _jsxRuntime = require("react/jsx-runtime");
13
+ function sameSize(a, b) {
14
+ return a?.width === b?.width && a?.height === b?.height;
15
+ }
16
+ function scaleRadius(cornerRadius, scale) {
17
+ if (typeof cornerRadius === 'number') {
18
+ return cornerRadius * scale;
19
+ }
20
+ return {
21
+ topLeft: (cornerRadius.topLeft ?? 0) * scale,
22
+ topRight: (cornerRadius.topRight ?? 0) * scale,
23
+ bottomLeft: (cornerRadius.bottomLeft ?? 0) * scale,
24
+ bottomRight: (cornerRadius.bottomRight ?? 0) * scale
25
+ };
26
+ }
27
+
28
+ /**
29
+ * A `NativeNitroImage` that loads `url` through the pipeline at exactly the
30
+ * size it is displayed: the bitmap is resized to the view's size in points ×
31
+ * `PixelRatio.get()`, so `blur` and `cornerRadius` (both in points here) apply
32
+ * 1:1 to what is on screen and large sources are never decoded at full size.
33
+ *
34
+ * A numeric `width`/`height` in `style` starts loading immediately; otherwise
35
+ * (`'50%'`, `flex`, `aspectRatio`, …) loading waits for the first `onLayout`.
36
+ * If the layout size later changes, a new variant is loaded and swapped in
37
+ * without flashing. Without an explicit `cornerRadius` prop, `style`'s
38
+ * `borderRadius`-family properties are baked into the bitmap instead — no
39
+ * separate view-layer rounding needed.
40
+ * @example
41
+ * ```tsx
42
+ * <PipelineImage
43
+ * url="https://example.com/photo.jpg"
44
+ * style={{ width: 300, height: 200 }}
45
+ * cornerRadius={24}
46
+ * />
47
+ * ```
48
+ */
49
+ function PipelineImage({
50
+ url,
51
+ blur = 0,
52
+ cornerRadius,
53
+ cache,
54
+ onLoad,
55
+ onError,
56
+ style,
57
+ onLayout,
58
+ ...viewProps
59
+ }) {
60
+ const scale = _reactNative.PixelRatio.get();
61
+ const styleSize = (0, _resizeForStyle.resizeForStyle)(style);
62
+ const [layoutSize, setLayoutSize] = (0, _react.useState)(undefined);
63
+ // A numeric style is what the caller declared, so it wins and starts the
64
+ // request a frame earlier; the measured layout is the fallback.
65
+ const resize = styleSize ?? layoutSize;
66
+ // Same precedence: an explicit prop wins over what style implies.
67
+ const effectiveCornerRadius = cornerRadius ?? (0, _resizeForStyle.cornerRadiusForStyle)(style) ?? 0;
68
+ const {
69
+ image,
70
+ error
71
+ } = (0, _useImage.useImage)({
72
+ url,
73
+ blur: blur * scale,
74
+ cornerRadius: scaleRadius(effectiveCornerRadius, scale),
75
+ cache,
76
+ resize,
77
+ enabled: resize !== undefined
78
+ });
79
+
80
+ // Latest callbacks in refs so inline arrow props don't re-fire the effects.
81
+ const onLoadRef = (0, _react.useRef)(onLoad);
82
+ const onErrorRef = (0, _react.useRef)(onError);
83
+ (0, _react.useEffect)(() => {
84
+ onLoadRef.current = onLoad;
85
+ onErrorRef.current = onError;
86
+ });
87
+ (0, _react.useEffect)(() => {
88
+ if (image) onLoadRef.current?.(image);
89
+ }, [image]);
90
+ (0, _react.useEffect)(() => {
91
+ if (error) onErrorRef.current?.(error);
92
+ }, [error]);
93
+ const handleLayout = event => {
94
+ onLayout?.(event);
95
+ const {
96
+ width,
97
+ height
98
+ } = event.nativeEvent.layout;
99
+ const next = (0, _resizeForStyle.resizeForLayout)(width, height);
100
+ // Always record it (even when a numeric style is in charge) so a later
101
+ // switch to a non-numeric style has a size to fall back on.
102
+ setLayoutSize(prev => sameSize(prev, next) ? prev : next);
103
+ };
104
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeNitroImage.NativeNitroImage, {
105
+ ...viewProps,
106
+ style: style,
107
+ onLayout: handleLayout,
108
+ image: image
109
+ });
110
+ }
111
+ //# sourceMappingURL=PipelineImage.js.map
@@ -0,0 +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","url","blur","cache","onLoad","onError","style","onLayout","viewProps","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"],"sourceRoot":"../../src","sources":["PipelineImage.tsx"],"mappings":"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","ignoreList":[]}
@@ -3,85 +3,44 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.NitroImagePipeline = void 0;
7
- exports.useImage = useImage;
8
- var _react = require("react");
9
- var _reactNativeNitroModules = require("react-native-nitro-modules");
10
- const NitroImagePipeline = exports.NitroImagePipeline = _reactNativeNitroModules.NitroModules.createHybridObject('NitroImagePipeline');
11
- /**
12
- * A hook to asynchronously load an image from the
13
- * given {@linkcode AsyncImageSource} into memory.
14
- * @example
15
- * ```ts
16
- * const { image, error } = useImage({ filePath: '/tmp/image.jpg' })
17
- * ```
18
- */
19
- function useImage({
20
- url,
21
- blur = 0,
22
- cornerRadius = 0,
23
- cache
24
- }) {
25
- const [image, setImage] = (0, _react.useState)({
26
- image: undefined,
27
- error: undefined
28
- });
29
- const loadedUrlRef = (0, _react.useRef)(url);
30
-
31
- // Split the option into primitives so an inline `{ topLeft: 24, ... }`
32
- // literal (new identity every render) doesn't re-trigger the effect.
33
- const isUniformRadius = typeof cornerRadius === 'number';
34
- const uniformRadius = isUniformRadius ? cornerRadius : 0;
35
- const {
36
- topLeft = 0,
37
- topRight = 0,
38
- bottomLeft = 0,
39
- bottomRight = 0
40
- } = isUniformRadius ? {} : cornerRadius;
41
- (0, _react.useEffect)(() => {
42
- let cancelled = false;
43
- // Only reset to the loading state when the URL changes; for same-URL
44
- // param tweaks (blur/cornerRadius/cache) keep showing the current image
45
- // until the new variant resolves, to avoid flashing empty.
46
- if (loadedUrlRef.current !== url) {
47
- loadedUrlRef.current = url;
48
- setImage({
49
- image: undefined,
50
- error: undefined
51
- });
52
- }
53
- (async () => {
54
- try {
55
- const result = await NitroImagePipeline.loadImage(url, {
56
- blur,
57
- cornerRadius: isUniformRadius ? uniformRadius : {
58
- topLeft,
59
- topRight,
60
- bottomLeft,
61
- bottomRight
62
- },
63
- cache
64
- });
65
- if (!cancelled) {
66
- setImage({
67
- image: result,
68
- error: undefined
69
- });
70
- }
71
- } catch (e) {
72
- const error = e instanceof Error ? e : new Error(`${e}`);
73
- if (!cancelled) {
74
- setImage({
75
- image: undefined,
76
- error: error
77
- });
78
- }
79
- }
80
- })();
81
- return () => {
82
- cancelled = true;
83
- };
84
- }, [url, blur, isUniformRadius, uniformRadius, topLeft, topRight, bottomLeft, bottomRight, cache]);
85
- return image;
86
- }
6
+ Object.defineProperty(exports, "NitroImagePipeline", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _NitroImagePipeline.NitroImagePipeline;
10
+ }
11
+ });
12
+ Object.defineProperty(exports, "PipelineImage", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _PipelineImage.PipelineImage;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "cornerRadiusForStyle", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _resizeForStyle.cornerRadiusForStyle;
22
+ }
23
+ });
24
+ Object.defineProperty(exports, "resizeForLayout", {
25
+ enumerable: true,
26
+ get: function () {
27
+ return _resizeForStyle.resizeForLayout;
28
+ }
29
+ });
30
+ Object.defineProperty(exports, "resizeForStyle", {
31
+ enumerable: true,
32
+ get: function () {
33
+ return _resizeForStyle.resizeForStyle;
34
+ }
35
+ });
36
+ Object.defineProperty(exports, "useImage", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _useImage.useImage;
40
+ }
41
+ });
42
+ var _NitroImagePipeline = require("./NitroImagePipeline");
43
+ var _PipelineImage = require("./PipelineImage");
44
+ var _resizeForStyle = require("./resizeForStyle");
45
+ var _useImage = require("./useImage");
87
46
  //# sourceMappingURL=index.js.map