react-native-nitro-image-pipeline 1.2.0 → 1.3.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 (41) hide show
  1. package/README.md +147 -5
  2. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +17 -15
  3. package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/ResizeTransformation.kt +1 -2
  4. package/ios/HybridNitroImagePipeline.swift +54 -38
  5. package/ios/RoundedCornersProcessor.swift +1 -1
  6. package/lib/commonjs/NitroImagePipeline.js +9 -0
  7. package/lib/commonjs/NitroImagePipeline.js.map +1 -0
  8. package/lib/commonjs/PipelineImage.js +126 -0
  9. package/lib/commonjs/PipelineImage.js.map +1 -0
  10. package/lib/commonjs/index.js +40 -88
  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 +122 -0
  19. package/lib/module/PipelineImage.js.map +1 -0
  20. package/lib/module/index.js +4 -86
  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 +73 -0
  29. package/lib/typescript/src/PipelineImage.d.ts.map +1 -0
  30. package/lib/typescript/src/index.d.ts +5 -45
  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/useImage.d.ts +53 -0
  35. package/lib/typescript/src/useImage.d.ts.map +1 -0
  36. package/package.json +3 -2
  37. package/src/NitroImagePipeline.ts +6 -0
  38. package/src/PipelineImage.tsx +183 -0
  39. package/src/index.ts +13 -139
  40. package/src/resizeForStyle.ts +96 -0
  41. package/src/useImage.ts +149 -0
package/README.md CHANGED
@@ -38,32 +38,143 @@ 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
+
74
+ ### Animating with `react-native-reanimated`
75
+
76
+ `<PipelineImage>` forwards its `ref` to the underlying `NativeNitroImage` host view, so it can be
77
+ passed straight to `Animated.createAnimatedComponent` — from
78
+ [react-native-reanimated](https://docs.swmansion.com/react-native-reanimated/) or React Native's
79
+ built-in `Animated`:
80
+
81
+ ```tsx
82
+ import Animated, { FadeIn, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
83
+ import { PipelineImage } from 'react-native-nitro-image-pipeline';
84
+
85
+ const AnimatedPipelineImage = Animated.createAnimatedComponent(PipelineImage);
86
+
87
+ function Photo({ url }: { url: string }) {
88
+ const pressed = useSharedValue(false);
89
+ const animatedStyle = useAnimatedStyle(() => ({
90
+ transform: [{ scale: withSpring(pressed.value ? 1.1 : 1) }],
91
+ }));
92
+
93
+ return (
94
+ <AnimatedPipelineImage
95
+ url={url}
96
+ entering={FadeIn}
97
+ style={[styles.photo, animatedStyle]}
98
+ onTouchStart={() => (pressed.value = true)}
99
+ onTouchEnd={() => (pressed.value = false)}
100
+ />
101
+ );
102
+ }
103
+
104
+ const styles = StyleSheet.create({ photo: { width: 300, height: 200, borderRadius: 12 } });
105
+ ```
106
+
107
+ Layout animations (`entering`/`exiting`) work as on any animated component, and wrapping a plain
108
+ `<PipelineImage>` in an `Animated.View` is always an option if you'd rather not create one.
109
+
110
+ Because the pipeline bakes its processing into the bitmap at load time, the props fall into two
111
+ groups — view-layer properties that animate freely, and bitmap properties that don't:
112
+
113
+ - **`transform` and `opacity`** — the ideal case: they run entirely on the UI thread and never
114
+ touch the bitmap. Prefer a `scale` transform over animating `width`/`height`.
115
+ - **`width`/`height`** — the animation itself works (Reanimated drives the native view directly),
116
+ but the bitmap doesn't follow it. With numeric dimensions in `style` (including an animated
117
+ style's initial values) the bitmap is decoded once at that size and stretched by the view while
118
+ it animates; with flex/percent sizing the size comes from `onLayout`, which fires repeatedly
119
+ during the animation and requests a new variant each time. Animate a `scale` transform instead
120
+ and let the layout settle where it will.
121
+ - **`borderRadius`** — by default the component bakes `style`'s `borderRadius` into the bitmap;
122
+ an animated radius updates only the view layer, so the baked rounding wins and stays stale. To
123
+ animate rounding, opt out of baking with `cornerRadius={0}` and round at the view layer instead:
124
+ `overflow: 'hidden'` plus the animated `borderRadius`.
125
+ - **`blur`** — not animatable. It's a load-time bitmap operation behind an async native call, not
126
+ a view property, so a changing `blur` re-runs the pipeline per value — far too slow to drive
127
+ per-frame. To animate blurriness, render the sharp and blurred variants as two stacked
128
+ `<PipelineImage>`s and cross-fade the blurred one's `opacity` (both share the URL cache, so the
129
+ second variant loads from the same fetched source):
130
+
131
+ ```tsx
132
+ function BlurFade({ url, blurred }: { url: string; blurred: boolean }) {
133
+ const blurOpacity = useAnimatedStyle(() => ({
134
+ opacity: withTiming(blurred ? 1 : 0),
135
+ }));
136
+
137
+ return (
138
+ <View style={styles.photo}>
139
+ <PipelineImage url={url} style={StyleSheet.absoluteFill} />
140
+ <Animated.View style={[StyleSheet.absoluteFill, blurOpacity]}>
141
+ <PipelineImage url={url} blur={12} style={StyleSheet.absoluteFill} />
142
+ </Animated.View>
143
+ </View>
144
+ );
145
+ }
146
+ ```
147
+
41
148
  ### `useImage` hook
42
149
 
43
150
  The simplest way to load an image in a component:
44
151
 
45
152
  ```tsx
46
- import { useImage } from 'react-native-nitro-image-pipeline';
153
+ import { PixelRatio, useImage, resizeForStyle } from 'react-native-nitro-image-pipeline';
47
154
 
48
155
  function MyComponent() {
49
- const px = PixelRatio.get();
50
156
  const { image, error } = useImage({
51
157
  url: 'https://example.com/photo.jpg',
52
158
  blur: 4, // Gaussian sigma in bitmap pixels — same result on iOS and Android
53
159
  // Resize to the size you display (points × screen scale) so the corner
54
160
  // radii apply 1:1 to what you see instead of the full-resolution source.
55
- resize: { width: 300 * px, height: 200 * px },
56
- cornerRadius: 12 * px,
161
+ resize: resizeForStyle(styles.image), // display size × PixelRatio.get()
162
+ cornerRadius: 12 * PixelRatio.get(), // bitmap pixels
57
163
  });
58
164
 
59
165
  if (error) return <Text>Failed to load image</Text>;
60
166
  if (!image) return <ActivityIndicator />;
61
167
 
62
168
  // use `image` with react-native-nitro-image
63
- return <NitroImage source={image} />;
169
+ return <NitroImage image={image} style={styles.image} />;
64
170
  }
171
+
172
+ const styles = StyleSheet.create({ image: { width: 300, height: 200 } });
65
173
  ```
66
174
 
175
+ Pass `enabled: false` to defer the request — used internally by `<PipelineImage>` to wait for
176
+ layout before it has a size to resize to.
177
+
67
178
  ### Direct API
68
179
 
69
180
  ```ts
@@ -115,6 +226,37 @@ Loads an image from a URL and returns a `Promise<Image>`.
115
226
  | `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 |
116
227
  | `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
117
228
 
229
+ ### `<PipelineImage>`
230
+
231
+ | Prop | Type | Default | Description |
232
+ |---|---|---|---|
233
+ | `url` | `string` | — | Image URL to load |
234
+ | `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)) |
235
+ | `blur` | `number` | `0` | Gaussian blur strength, in **points** (converted to bitmap pixels internally) |
236
+ | `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 |
237
+ | `cache` | `'memory' \| 'disk' \| 'none'` | platform default | Caching strategy |
238
+ | `onLoad` | `(image: Image) => void` | — | Called when the image finishes loading |
239
+ | `onError` | `(error: Error) => void` | — | Called if loading fails |
240
+ | `onLayout` | `(event: LayoutChangeEvent) => void` | — | Standard `View` layout callback; also drives the deferred resize for non-numeric sizes |
241
+ | `ref` | `Ref<PipelineImageRef>` | — | Forwarded to the underlying `NativeNitroImage` host view — gives access to native-view methods (`measure`, …) and makes the component work with `Animated.createAnimatedComponent` (see [Animating](#animating-with-react-native-reanimated)) |
242
+ | `…NativeNitroImage props` | — | — | Everything else (`resizeMode`, `recyclingKey`, `testID`, …) is passed through to `NativeNitroImage` |
243
+
244
+ ### `resizeForStyle(style)` / `resizeForLayout(width, height)`
245
+
246
+ Converts a layout size in points to a bitmap `resize` option in pixels. Returns
247
+ `{ width, height }` in whole pixels via `PixelRatio.getPixelSizeForLayoutSize`, or `undefined` for
248
+ non-numeric sizes (e.g. `'100%'`, `undefined`) — `resizeForStyle` reads `style.width`/`style.height`,
249
+ `resizeForLayout` takes explicit numbers.
250
+
251
+ ### `cornerRadiusForStyle(style)`
252
+
253
+ Converts a view style's `borderRadius`/`borderTopLeftRadius`/`borderTopRightRadius`/
254
+ `borderBottomLeftRadius`/`borderBottomRightRadius` (in points) into a `cornerRadius` option — a
255
+ plain number for uniform `borderRadius` alone, or a `CornerRadii` object once any per-corner
256
+ property is set (falling back to `borderRadius` for the corners left unset). Returns `undefined`
257
+ when none are set. This is what `<PipelineImage>` uses internally when its `cornerRadius` prop is
258
+ omitted.
259
+
118
260
  ### `preLoadImage(url)`
119
261
 
120
262
  Prefetches a single image into the cache. Returns `Promise<void>`.
@@ -54,21 +54,23 @@ class HybridNitroImagePipeline : HybridNitroImagePipelineSpec() {
54
54
  // they run on, so they must see the final size.
55
55
  resize?.let { (width, height) -> add(ResizeTransformation(width, height)) }
56
56
  if (blur > 0f) add(BlurTransformation(context, blur))
57
- options?.cornerRadius?.match(
58
- first = { radius ->
59
- if (radius > 0.0) add(RoundedCornersTransformation(radius.toFloat()))
60
- },
61
- second = { radii ->
62
- // RoundedCornersTransformation rejects negative radii; treat them as square.
63
- val topLeft = (radii.topLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
64
- val topRight = (radii.topRight?.toFloat() ?: 0f).coerceAtLeast(0f)
65
- val bottomLeft = (radii.bottomLeft?.toFloat() ?: 0f).coerceAtLeast(0f)
66
- val bottomRight = (radii.bottomRight?.toFloat() ?: 0f).coerceAtLeast(0f)
67
- if (topLeft > 0f || topRight > 0f || bottomLeft > 0f || bottomRight > 0f) {
68
- add(RoundedCornersTransformation(topLeft, topRight, bottomLeft, bottomRight))
69
- }
70
- },
71
- )
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
+ )
72
74
  }
73
75
  val request =
74
76
  ImageRequest.Builder(context)
@@ -31,8 +31,7 @@ class ResizeTransformation(
31
31
  else input
32
32
  if (softwareInput.width == width && softwareInput.height == height) return softwareInput
33
33
 
34
- val scale =
35
- max(width.toFloat() / softwareInput.width, height.toFloat() / softwareInput.height)
34
+ val scale = max(width.toFloat() / softwareInput.width, height.toFloat() / softwareInput.height)
36
35
  val matrix =
37
36
  Matrix().apply {
38
37
  setScale(scale, scale)
@@ -103,52 +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
- // Resize first: blur sigma and corner radii are defined in pixels
121
- // of the bitmap they run on, so they must see the final size.
122
- if let resize = options?.resize, resize.width > 0, resize.height > 0 {
123
- processors.append(ImageProcessors.Resize(
124
- size: CGSize(width: resize.width, height: resize.height),
125
- unit: .pixels,
126
- contentMode: .aspectFill,
127
- crop: true,
128
- upscale: true
129
- ))
130
- }
131
- if let blur = options?.blur, blur > 0 {
132
- processors.append(GaussianBlurProcessor(sigma: blur))
133
- }
134
- switch options?.cornerRadius {
135
- case .first(let radius):
136
- if radius > 0 {
137
- processors.append(.roundedCorners(radius: radius))
138
- }
139
- case .second(let radii):
140
- let roundedCorners = RoundedCornersProcessor(radii: radii)
141
- if roundedCorners.hasRounding {
142
- processors.append(roundedCorners)
143
- }
144
- case nil:
145
- break
146
- }
147
-
148
164
  let imgRequest = ImageRequest(
149
165
  url: imageUrl,
150
- processors: processors,
151
- options: cacheOptions
166
+ processors: Self.processors(for: options),
167
+ options: Self.cacheOptions(for: options?.cache)
152
168
  )
153
169
 
154
170
  let image = try await self.pipeline.image(for: imgRequest)
@@ -78,7 +78,7 @@ struct RoundedCornersProcessor: ImageProcessing {
78
78
  (rect.width, topLeft + topRight),
79
79
  (rect.width, bottomLeft + bottomRight),
80
80
  (rect.height, topLeft + bottomLeft),
81
- (rect.height, topRight + bottomRight),
81
+ (rect.height, topRight + bottomRight)
82
82
  ] where pair > edge {
83
83
  scale = min(scale, edge / pair)
84
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,126 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.PipelineImage = void 0;
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
+ /**
14
+ * The instance `<PipelineImage>` exposes through its `ref` — the underlying
15
+ * `NativeNitroImage` host view, with the usual native-view methods
16
+ * (`measure`, …).
17
+ */
18
+
19
+ function sameSize(a, b) {
20
+ return a?.width === b?.width && a?.height === b?.height;
21
+ }
22
+ function scaleRadius(cornerRadius, scale) {
23
+ if (typeof cornerRadius === 'number') {
24
+ return cornerRadius * scale;
25
+ }
26
+ return {
27
+ topLeft: (cornerRadius.topLeft ?? 0) * scale,
28
+ topRight: (cornerRadius.topRight ?? 0) * scale,
29
+ bottomLeft: (cornerRadius.bottomLeft ?? 0) * scale,
30
+ bottomRight: (cornerRadius.bottomRight ?? 0) * scale
31
+ };
32
+ }
33
+
34
+ /**
35
+ * A `NativeNitroImage` that loads `url` through the pipeline at exactly the
36
+ * size it is displayed: the bitmap is resized to the view's size in points ×
37
+ * `PixelRatio.get()`, so `blur` and `cornerRadius` (both in points here) apply
38
+ * 1:1 to what is on screen and large sources are never decoded at full size.
39
+ *
40
+ * A numeric `width`/`height` in `style` starts loading immediately; otherwise
41
+ * (`'50%'`, `flex`, `aspectRatio`, …) loading waits for the first `onLayout`.
42
+ * If the layout size later changes, a new variant is loaded and swapped in
43
+ * without flashing. Without an explicit `cornerRadius` prop, `style`'s
44
+ * `borderRadius`-family properties are baked into the bitmap instead — no
45
+ * separate view-layer rounding needed.
46
+ *
47
+ * The `ref` is forwarded to the underlying `NativeNitroImage` host view, so
48
+ * the component works with `Animated.createAnimatedComponent` (Reanimated or
49
+ * React Native's built-in `Animated`).
50
+ * @example
51
+ * ```tsx
52
+ * <PipelineImage
53
+ * url="https://example.com/photo.jpg"
54
+ * style={{ width: 300, height: 200 }}
55
+ * cornerRadius={24}
56
+ * />
57
+ * ```
58
+ */
59
+ const PipelineImage = exports.PipelineImage = /*#__PURE__*/(0, _react.forwardRef)(function PipelineImage({
60
+ url,
61
+ blur = 0,
62
+ cornerRadius,
63
+ cache,
64
+ onLoad,
65
+ onError,
66
+ style,
67
+ onLayout,
68
+ ...viewProps
69
+ }, ref) {
70
+ const scale = _reactNative.PixelRatio.get();
71
+ const styleSize = (0, _resizeForStyle.resizeForStyle)(style);
72
+ const [layoutSize, setLayoutSize] = (0, _react.useState)(undefined);
73
+ // A numeric style is what the caller declared, so it wins and starts the
74
+ // request a frame earlier; the measured layout is the fallback.
75
+ const resize = styleSize ?? layoutSize;
76
+ // Same precedence: an explicit prop wins over what style implies.
77
+ const effectiveCornerRadius = cornerRadius ?? (0, _resizeForStyle.cornerRadiusForStyle)(style) ?? 0;
78
+ const {
79
+ image,
80
+ error
81
+ } = (0, _useImage.useImage)({
82
+ url,
83
+ blur: blur * scale,
84
+ cornerRadius: scaleRadius(effectiveCornerRadius, scale),
85
+ cache,
86
+ resize,
87
+ enabled: resize !== undefined
88
+ });
89
+
90
+ // Latest callbacks in refs so inline arrow props don't re-fire the effects.
91
+ const onLoadRef = (0, _react.useRef)(onLoad);
92
+ const onErrorRef = (0, _react.useRef)(onError);
93
+ (0, _react.useEffect)(() => {
94
+ onLoadRef.current = onLoad;
95
+ onErrorRef.current = onError;
96
+ });
97
+ (0, _react.useEffect)(() => {
98
+ if (image) onLoadRef.current?.(image);
99
+ }, [image]);
100
+ (0, _react.useEffect)(() => {
101
+ if (error) onErrorRef.current?.(error);
102
+ }, [error]);
103
+ const handleLayout = event => {
104
+ onLayout?.(event);
105
+ const {
106
+ width,
107
+ height
108
+ } = event.nativeEvent.layout;
109
+ const next = (0, _resizeForStyle.resizeForLayout)(width, height);
110
+ // Always record it (even when a numeric style is in charge) so a later
111
+ // switch to a non-numeric style has a size to fall back on.
112
+ setLayoutSize(prev => sameSize(prev, next) ? prev : next);
113
+ };
114
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeNitroImage.NativeNitroImage, {
115
+ ...viewProps,
116
+ ref: ref,
117
+ style: style,
118
+ onLayout: handleLayout,
119
+ image: image
120
+ });
121
+ });
122
+
123
+ // Reanimated and DevTools read the display name; the forwardRef wrapper
124
+ // would otherwise report as anonymous.
125
+ PipelineImage.displayName = 'PipelineImage';
126
+ //# 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","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;AAUA,IAAAI,SAAA,GAAAJ,OAAA;AAAsC,IAAAK,WAAA,GAAAL,OAAA;AAKtC;AACA;AACA;AACA;AACA;;AAgCA,SAASM,QAAQA,CAACC,CAAiB,EAAEC,CAAiB,EAAW;EAC/D,OAAOD,CAAC,EAAEE,KAAK,KAAKD,CAAC,EAAEC,KAAK,IAAIF,CAAC,EAAEG,MAAM,KAAKF,CAAC,EAAEE,MAAM;AACzD;AAEA,SAASC,WAAWA,CAClBC,YAAkC,EAClCC,KAAa,EACS;EACtB,IAAI,OAAOD,YAAY,KAAK,QAAQ,EAAE;IACpC,OAAOA,YAAY,GAAGC,KAAK;EAC7B;EACA,OAAO;IACLC,OAAO,EAAE,CAACF,YAAY,CAACE,OAAO,IAAI,CAAC,IAAID,KAAK;IAC5CE,QAAQ,EAAE,CAACH,YAAY,CAACG,QAAQ,IAAI,CAAC,IAAIF,KAAK;IAC9CG,UAAU,EAAE,CAACJ,YAAY,CAACI,UAAU,IAAI,CAAC,IAAIH,KAAK;IAClDI,WAAW,EAAE,CAACL,YAAY,CAACK,WAAW,IAAI,CAAC,IAAIJ;EACjD,CAAC;AACH;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAMK,aAAa,GAAAC,OAAA,CAAAD,aAAA,gBAAG,IAAAE,iBAAU,EACrC,SAASF,aAAaA,CACpB;EACEG,GAAG;EACHC,IAAI,GAAG,CAAC;EACRV,YAAY;EACZW,KAAK;EACLC,MAAM;EACNC,OAAO;EACPC,KAAK;EACLC,QAAQ;EACR,GAAGC;AACL,CAAC,EACDC,GAAG,EACH;EACA,MAAMhB,KAAK,GAAGiB,uBAAU,CAACC,GAAG,CAAC,CAAC;EAC9B,MAAMC,SAAS,GAAG,IAAAC,8BAAc,EAACP,KAAK,CAAC;EACvC,MAAM,CAACQ,UAAU,EAAEC,aAAa,CAAC,GAAG,IAAAC,eAAQ,EAC1CC,SACF,CAAC;EACD;EACA;EACA,MAAMC,MAAM,GAAGN,SAAS,IAAIE,UAAU;EACtC;EACA,MAAMK,qBAAqB,GACzB3B,YAAY,IAAI,IAAA4B,oCAAoB,EAACd,KAAK,CAAC,IAAI,CAAC;EAElD,MAAM;IAAEe,KAAK;IAAEC;EAAM,CAAC,GAAG,IAAAC,kBAAQ,EAAC;IAChCtB,GAAG;IACHC,IAAI,EAAEA,IAAI,GAAGT,KAAK;IAClBD,YAAY,EAAED,WAAW,CAAC4B,qBAAqB,EAAE1B,KAAK,CAAC;IACvDU,KAAK;IACLe,MAAM;IACNM,OAAO,EAAEN,MAAM,KAAKD;EACtB,CAAC,CAAC;;EAEF;EACA,MAAMQ,SAAS,GAAG,IAAAC,aAAM,EAACtB,MAAM,CAAC;EAChC,MAAMuB,UAAU,GAAG,IAAAD,aAAM,EAACrB,OAAO,CAAC;EAClC,IAAAuB,gBAAS,EAAC,MAAM;IACdH,SAAS,CAACI,OAAO,GAAGzB,MAAM;IAC1BuB,UAAU,CAACE,OAAO,GAAGxB,OAAO;EAC9B,CAAC,CAAC;EACF,IAAAuB,gBAAS,EAAC,MAAM;IACd,IAAIP,KAAK,EAAEI,SAAS,CAACI,OAAO,GAAGR,KAAK,CAAC;EACvC,CAAC,EAAE,CAACA,KAAK,CAAC,CAAC;EACX,IAAAO,gBAAS,EAAC,MAAM;IACd,IAAIN,KAAK,EAAEK,UAAU,CAACE,OAAO,GAAGP,KAAK,CAAC;EACxC,CAAC,EAAE,CAACA,KAAK,CAAC,CAAC;EAEX,MAAMQ,YAAY,GAAIC,KAAwB,IAAK;IACjDxB,QAAQ,GAAGwB,KAAK,CAAC;IACjB,MAAM;MAAE1C,KAAK;MAAEC;IAAO,CAAC,GAAGyC,KAAK,CAACC,WAAW,CAACC,MAAM;IAClD,MAAMC,IAAI,GAAG,IAAAC,+BAAe,EAAC9C,KAAK,EAAEC,MAAM,CAAC;IAC3C;IACA;IACAyB,aAAa,CAAEqB,IAAI,IAAMlD,QAAQ,CAACkD,IAAI,EAAEF,IAAI,CAAC,GAAGE,IAAI,GAAGF,IAAK,CAAC;EAC/D,CAAC;EAED,oBACE,IAAAjD,WAAA,CAAAoD,GAAA,EAACvD,sBAAA,CAAAwD,gBAAgB;IAAA,GACX9B,SAAS;IACbC,GAAG,EAAEA,GAAI;IACTH,KAAK,EAAEA,KAAM;IACbC,QAAQ,EAAEuB,YAAa;IACvBT,KAAK,EAAEA;EAAM,CACd,CAAC;AAEN,CACF,CAAC;;AAED;AACA;AACAvB,aAAa,CAACyC,WAAW,GAAG,eAAe","ignoreList":[]}
@@ -3,92 +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
- resize,
24
- cache
25
- }) {
26
- const [image, setImage] = (0, _react.useState)({
27
- image: undefined,
28
- error: undefined
29
- });
30
- const loadedUrlRef = (0, _react.useRef)(url);
31
-
32
- // Split the option into primitives so an inline `{ topLeft: 24, ... }`
33
- // literal (new identity every render) doesn't re-trigger the effect.
34
- const isUniformRadius = typeof cornerRadius === 'number';
35
- const uniformRadius = isUniformRadius ? cornerRadius : 0;
36
- const {
37
- topLeft = 0,
38
- topRight = 0,
39
- bottomLeft = 0,
40
- bottomRight = 0
41
- } = isUniformRadius ? {} : cornerRadius;
42
- const resizeWidth = resize?.width ?? 0;
43
- const resizeHeight = resize?.height ?? 0;
44
- (0, _react.useEffect)(() => {
45
- let cancelled = false;
46
- // Only reset to the loading state when the URL changes; for same-URL
47
- // param tweaks (blur/cornerRadius/cache) keep showing the current image
48
- // until the new variant resolves, to avoid flashing empty.
49
- if (loadedUrlRef.current !== url) {
50
- loadedUrlRef.current = url;
51
- setImage({
52
- image: undefined,
53
- error: undefined
54
- });
55
- }
56
- (async () => {
57
- try {
58
- const result = await NitroImagePipeline.loadImage(url, {
59
- blur,
60
- cornerRadius: isUniformRadius ? uniformRadius : {
61
- topLeft,
62
- topRight,
63
- bottomLeft,
64
- bottomRight
65
- },
66
- resize: resizeWidth > 0 && resizeHeight > 0 ? {
67
- width: resizeWidth,
68
- height: resizeHeight
69
- } : undefined,
70
- cache
71
- });
72
- if (!cancelled) {
73
- setImage({
74
- image: result,
75
- error: undefined
76
- });
77
- }
78
- } catch (e) {
79
- const error = e instanceof Error ? e : new Error(`${e}`);
80
- if (!cancelled) {
81
- setImage({
82
- image: undefined,
83
- error: error
84
- });
85
- }
86
- }
87
- })();
88
- return () => {
89
- cancelled = true;
90
- };
91
- }, [url, blur, isUniformRadius, uniformRadius, topLeft, topRight, bottomLeft, bottomRight, resizeWidth, resizeHeight, cache]);
92
- return image;
93
- }
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");
94
46
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["_react","require","_reactNativeNitroModules","NitroImagePipeline","exports","NitroModules","createHybridObject","useImage","url","blur","cornerRadius","resize","cache","image","setImage","useState","undefined","error","loadedUrlRef","useRef","isUniformRadius","uniformRadius","topLeft","topRight","bottomLeft","bottomRight","resizeWidth","width","resizeHeight","height","useEffect","cancelled","current","result","loadImage","e","Error"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;AAAA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,wBAAA,GAAAD,OAAA;AAYO,MAAME,kBAAkB,GAAAC,OAAA,CAAAD,kBAAA,GAC7BE,qCAAY,CAACC,kBAAkB,CAAyB,oBAAoB,CAAC;AAmB/E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,QAAQA,CAAC;EACvBC,GAAG;EACHC,IAAI,GAAG,CAAC;EACRC,YAAY,GAAG,CAAC;EAChBC,MAAM;EACNC;AAuBF,CAAC,EAAU;EACT,MAAM,CAACC,KAAK,EAAEC,QAAQ,CAAC,GAAG,IAAAC,eAAQ,EAAS;IACzCF,KAAK,EAAEG,SAAS;IAChBC,KAAK,EAAED;EACT,CAAC,CAAC;EACF,MAAME,YAAY,GAAG,IAAAC,aAAM,EAACX,GAAG,CAAC;;EAEhC;EACA;EACA,MAAMY,eAAe,GAAG,OAAOV,YAAY,KAAK,QAAQ;EACxD,MAAMW,aAAa,GAAGD,eAAe,GAAGV,YAAY,GAAG,CAAC;EACxD,MAAM;IACJY,OAAO,GAAG,CAAC;IACXC,QAAQ,GAAG,CAAC;IACZC,UAAU,GAAG,CAAC;IACdC,WAAW,GAAG;EAChB,CAAC,GAAGL,eAAe,GAAG,CAAC,CAAC,GAAGV,YAAY;EACvC,MAAMgB,WAAW,GAAGf,MAAM,EAAEgB,KAAK,IAAI,CAAC;EACtC,MAAMC,YAAY,GAAGjB,MAAM,EAAEkB,MAAM,IAAI,CAAC;EAExC,IAAAC,gBAAS,EAAC,MAAM;IACd,IAAIC,SAAS,GAAG,KAAK;IACrB;IACA;IACA;IACA,IAAIb,YAAY,CAACc,OAAO,KAAKxB,GAAG,EAAE;MAChCU,YAAY,CAACc,OAAO,GAAGxB,GAAG;MAC1BM,QAAQ,CAAC;QAAED,KAAK,EAAEG,SAAS;QAAEC,KAAK,EAAED;MAAU,CAAC,CAAC;IAClD;IAEA,CAAC,YAAY;MACX,IAAI;QACF,MAAMiB,MAAM,GAAG,MAAM9B,kBAAkB,CAAC+B,SAAS,CAAC1B,GAAG,EAAE;UACrDC,IAAI;UACJC,YAAY,EAAEU,eAAe,GACzBC,aAAa,GACb;YAAEC,OAAO;YAAEC,QAAQ;YAAEC,UAAU;YAAEC;UAAY,CAAC;UAClDd,MAAM,EACJe,WAAW,GAAG,CAAC,IAAIE,YAAY,GAAG,CAAC,GAC/B;YAAED,KAAK,EAAED,WAAW;YAAEG,MAAM,EAAED;UAAa,CAAC,GAC5CZ,SAAS;UACfJ;QACF,CAAC,CAAC;QAEF,IAAI,CAACmB,SAAS,EAAE;UACdjB,QAAQ,CAAC;YAAED,KAAK,EAAEoB,MAAM;YAAEhB,KAAK,EAAED;UAAU,CAAC,CAAC;QAC/C;MACF,CAAC,CAAC,OAAOmB,CAAC,EAAE;QACV,MAAMlB,KAAK,GAAGkB,CAAC,YAAYC,KAAK,GAAGD,CAAC,GAAG,IAAIC,KAAK,CAAC,GAAGD,CAAC,EAAE,CAAC;QACxD,IAAI,CAACJ,SAAS,EAAE;UACdjB,QAAQ,CAAC;YAAED,KAAK,EAAEG,SAAS;YAAEC,KAAK,EAAEA;UAAM,CAAC,CAAC;QAC9C;MACF;IACF,CAAC,EAAE,CAAC;IAEJ,OAAO,MAAM;MACXc,SAAS,GAAG,IAAI;IAClB,CAAC;EACH,CAAC,EAAE,CACDvB,GAAG,EACHC,IAAI,EACJW,eAAe,EACfC,aAAa,EACbC,OAAO,EACPC,QAAQ,EACRC,UAAU,EACVC,WAAW,EACXC,WAAW,EACXE,YAAY,EACZhB,KAAK,CACN,CAAC;EAEF,OAAOC,KAAK;AACd","ignoreList":[]}
1
+ {"version":3,"names":["_NitroImagePipeline","require","_PipelineImage","_resizeForStyle","_useImage"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,IAAAA,mBAAA,GAAAC,OAAA;AACA,IAAAC,cAAA,GAAAD,OAAA;AAKA,IAAAE,eAAA,GAAAF,OAAA;AAWA,IAAAG,SAAA,GAAAH,OAAA","ignoreList":[]}