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
@@ -0,0 +1,73 @@
1
+ import { type ComponentRef } from 'react';
2
+ import { type HostComponent } from 'react-native';
3
+ import { type Image, NativeNitroImage } from 'react-native-nitro-image';
4
+ import type { CacheOption, CornerRadii } from './specs/nitro-image-toolkit.nitro';
5
+ type ReactProps<T> = T extends HostComponent<infer P> ? P : never;
6
+ type NativeImageProps = ReactProps<typeof NativeNitroImage>;
7
+ /**
8
+ * The instance `<PipelineImage>` exposes through its `ref` — the underlying
9
+ * `NativeNitroImage` host view, with the usual native-view methods
10
+ * (`measure`, …).
11
+ */
12
+ export type PipelineImageRef = ComponentRef<typeof NativeNitroImage>;
13
+ export interface PipelineImageProps extends Omit<NativeImageProps, 'image'> {
14
+ /** URL of the image to load through the pipeline. */
15
+ url: string;
16
+ /**
17
+ * Gaussian blur sigma in **points** (density-independent). Unlike
18
+ * `useImage`/`loadImage`, where it is bitmap pixels, the component
19
+ * multiplies it by `PixelRatio.get()` so the same value looks the same on
20
+ * every device.
21
+ * @default 0
22
+ */
23
+ blur?: number;
24
+ /**
25
+ * Corner radius in **points** — a single number or per-corner radii.
26
+ * Converted to bitmap pixels with `PixelRatio.get()` and baked into the
27
+ * bitmap at the display size.
28
+ *
29
+ * When omitted, it's derived from `style`'s `borderRadius` /
30
+ * `borderTopLeftRadius` / `borderTopRightRadius` / `borderBottomLeftRadius`
31
+ * / `borderBottomRightRadius` instead — set this prop to override that.
32
+ * @default undefined (derived from `style`, or square corners if unset there)
33
+ */
34
+ cornerRadius?: number | CornerRadii;
35
+ cache?: CacheOption;
36
+ /** Called with the processed `Image` each time a new variant resolves. */
37
+ onLoad?: (image: Image) => void;
38
+ /** Called when loading fails. */
39
+ onError?: (error: Error) => void;
40
+ }
41
+ /**
42
+ * A `NativeNitroImage` that loads `url` through the pipeline at exactly the
43
+ * size it is displayed: the bitmap is resized to the view's size in points ×
44
+ * `PixelRatio.get()`, so `blur` and `cornerRadius` (both in points here) apply
45
+ * 1:1 to what is on screen and large sources are never decoded at full size.
46
+ *
47
+ * A numeric `width`/`height` in `style` starts loading immediately; otherwise
48
+ * (`'50%'`, `flex`, `aspectRatio`, …) loading waits for the first `onLayout`.
49
+ * If the layout size later changes, a new variant is loaded and swapped in
50
+ * without flashing. Without an explicit `cornerRadius` prop, `style`'s
51
+ * `borderRadius`-family properties are baked into the bitmap instead — no
52
+ * separate view-layer rounding needed.
53
+ *
54
+ * The `ref` is forwarded to the underlying `NativeNitroImage` host view, so
55
+ * the component works with `Animated.createAnimatedComponent` (Reanimated or
56
+ * React Native's built-in `Animated`).
57
+ * @example
58
+ * ```tsx
59
+ * <PipelineImage
60
+ * url="https://example.com/photo.jpg"
61
+ * style={{ width: 300, height: 200 }}
62
+ * cornerRadius={24}
63
+ * />
64
+ * ```
65
+ */
66
+ export declare const PipelineImage: import("react").ForwardRefExoticComponent<PipelineImageProps & import("react").RefAttributes<import("react").Component<{
67
+ hybridRef?: import("react-native-nitro-modules").NitroViewWrappedCallback<((ref: import("react-native-nitro-modules").HybridView<import("react-native-nitro-image/lib/typescript/specs/ImageView.nitro").NativeNitroImageViewProps, import("react-native-nitro-image/lib/typescript/specs/ImageView.nitro").NativeNitroImageViewMethods>) => void) | undefined> | undefined;
68
+ image?: Image | import("react-native-nitro-image").ImageLoader;
69
+ resizeMode?: import("react-native-nitro-image/lib/typescript/specs/ImageView.nitro").ResizeMode;
70
+ recyclingKey?: string;
71
+ } & import("react-native").ViewProps, {}, any> & import("react-native").ReactNativeElement>>;
72
+ export {};
73
+ //# sourceMappingURL=PipelineImage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PipelineImage.d.ts","sourceRoot":"","sources":["../../../src/PipelineImage.tsx"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EAKlB,MAAM,OAAO,CAAC;AACf,OAAO,EACL,KAAK,aAAa,EAGnB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,KAAK,KAAK,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAOxE,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EAEZ,MAAM,mCAAmC,CAAC;AAG3C,KAAK,UAAU,CAAC,CAAC,IAAI,CAAC,SAAS,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;AAClE,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE5D;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAErE,MAAM,WAAW,kBAAmB,SAAQ,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACzE,qDAAqD;IACrD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAChC,iCAAiC;IACjC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;CAClC;AAqBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,aAAa;;;;;4FAqEzB,CAAC"}
@@ -1,46 +1,6 @@
1
- import type { Image } from 'react-native-nitro-image';
2
- import type { CacheOption, CornerRadii, NitroImagePipeline as NitroImagePipelineSpec, Options, ResizeOptions } from './specs/nitro-image-toolkit.nitro';
3
- export type { CacheOption, CornerRadii, Options, ResizeOptions };
4
- export declare const NitroImagePipeline: NitroImagePipelineSpec;
5
- type Result = {
6
- image: undefined;
7
- error: undefined;
8
- } | {
9
- image: Image;
10
- error: undefined;
11
- } | {
12
- image: undefined;
13
- error: Error;
14
- };
15
- /**
16
- * A hook to asynchronously load an image from the
17
- * given {@linkcode AsyncImageSource} into memory.
18
- * @example
19
- * ```ts
20
- * const { image, error } = useImage({ filePath: '/tmp/image.jpg' })
21
- * ```
22
- */
23
- export declare function useImage({ url, blur, cornerRadius, resize, cache, }: {
24
- url: string;
25
- /**
26
- * Gaussian blur strength, as the standard deviation (sigma) of the blur in
27
- * source-image pixels (of the resized bitmap when `resize` is set). Matches
28
- * across iOS and Android; roughly half of React Native's `blurRadius`.
29
- */
30
- blur?: number;
31
- /**
32
- * Corner radius in pixels of the loaded bitmap. Pass a single number for
33
- * uniform rounding, or per-corner radii (inline object literals are fine —
34
- * the hook compares the radii by value, not identity). Pair with `resize`
35
- * so the radii apply at the size you display instead of the source size.
36
- */
37
- cornerRadius?: number | CornerRadii;
38
- /**
39
- * Resize the bitmap to exactly this size in pixels (aspect-fill,
40
- * center-crop) before blur/rounding. Typically your display size in points
41
- * multiplied by `PixelRatio.get()`. Inline object literals are fine.
42
- */
43
- resize?: ResizeOptions;
44
- cache?: CacheOption;
45
- }): Result;
1
+ export { NitroImagePipeline } from './NitroImagePipeline';
2
+ export { PipelineImage, type PipelineImageProps, type PipelineImageRef, } from './PipelineImage';
3
+ export { cornerRadiusForStyle, resizeForLayout, resizeForStyle, } from './resizeForStyle';
4
+ export type { CacheOption, CornerRadii, Options, ResizeOptions, } from './specs/nitro-image-toolkit.nitro';
5
+ export { useImage } from './useImage';
46
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAGtD,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,kBAAkB,IAAI,sBAAsB,EAC5C,OAAO,EACP,aAAa,EACd,MAAM,mCAAmC,CAAC;AAE3C,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC;AAEjE,eAAO,MAAM,kBAAkB,wBACgD,CAAC;AAEhF,KAAK,MAAM,GAEP;IACE,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;CAClB,GAED;IACE,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;CAClB,GAED;IACE,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEN;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,IAAQ,EACR,YAAgB,EAChB,MAAM,EACN,KAAK,GACN,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC;;;;OAIG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB,GAAG,MAAM,CAyET"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EACL,aAAa,EACb,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,GACtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,oBAAoB,EACpB,eAAe,EACf,cAAc,GACf,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EACV,WAAW,EACX,WAAW,EACX,OAAO,EACP,aAAa,GACd,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { type StyleProp, type ViewStyle } from 'react-native';
2
+ import type { CornerRadii, ResizeOptions } from './specs/nitro-image-toolkit.nitro';
3
+ /**
4
+ * Converts a layout size in points (dp) to the pipeline's `resize` option in
5
+ * whole bitmap pixels using `PixelRatio.getPixelSizeForLayoutSize`. Returns
6
+ * `undefined` unless both values are positive numbers.
7
+ */
8
+ export declare function resizeForLayout(width: unknown, height: unknown): ResizeOptions | undefined;
9
+ /**
10
+ * Derives the pipeline's `resize` option from a view style whose `width` and
11
+ * `height` are numeric points, so the bitmap matches the display size on
12
+ * every screen density:
13
+ * ```ts
14
+ * useImage({ url, resize: resizeForStyle(styles.image) });
15
+ * ```
16
+ * Arrays and registered styles are flattened. Returns `undefined` when either
17
+ * dimension is missing or not a number (`'50%'`, `'auto'`, flex-driven) —
18
+ * use `<PipelineImage>` for those, which measures the view instead.
19
+ */
20
+ export declare function resizeForStyle(style: StyleProp<ViewStyle>): ResizeOptions | undefined;
21
+ /**
22
+ * Derives a `cornerRadius` option from a view style's `borderRadius` /
23
+ * `borderTopLeftRadius` / `borderTopRightRadius` / `borderBottomLeftRadius` /
24
+ * `borderBottomRightRadius`, in points. Per-corner properties override
25
+ * `borderRadius` for that corner; a corner with neither set stays square.
26
+ * Non-numeric values (percentages, animated values) are ignored, like
27
+ * {@linkcode resizeForStyle}. Returns `undefined` when none are set.
28
+ */
29
+ export declare function cornerRadiusForStyle(style: StyleProp<ViewStyle>): number | CornerRadii | undefined;
30
+ //# sourceMappingURL=resizeForStyle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resizeForStyle.d.ts","sourceRoot":"","sources":["../../../src/resizeForStyle.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,SAAS,EAEd,KAAK,SAAS,EACf,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EACV,WAAW,EACX,aAAa,EACd,MAAM,mCAAmC,CAAC;AAE3C;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,OAAO,GACd,aAAa,GAAG,SAAS,CAS3B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC,GAC1B,aAAa,GAAG,SAAS,CAG3B;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,SAAS,CAAC,SAAS,CAAC,GAC1B,MAAM,GAAG,WAAW,GAAG,SAAS,CAoClC"}
@@ -0,0 +1,53 @@
1
+ import type { Image } from 'react-native-nitro-image';
2
+ import type { CacheOption, CornerRadii, ResizeOptions } from './specs/nitro-image-toolkit.nitro';
3
+ type Result = {
4
+ image: undefined;
5
+ error: undefined;
6
+ } | {
7
+ image: Image;
8
+ error: undefined;
9
+ } | {
10
+ image: undefined;
11
+ error: Error;
12
+ };
13
+ /**
14
+ * A hook to asynchronously load an image from the
15
+ * given {@linkcode AsyncImageSource} into memory.
16
+ * @example
17
+ * ```ts
18
+ * const { image, error } = useImage({ filePath: '/tmp/image.jpg' })
19
+ * ```
20
+ */
21
+ export declare function useImage({ url, blur, cornerRadius, resize, cache, enabled, }: {
22
+ url: string;
23
+ /**
24
+ * Gaussian blur strength, as the standard deviation (sigma) of the blur in
25
+ * source-image pixels (of the resized bitmap when `resize` is set). Matches
26
+ * across iOS and Android; roughly half of React Native's `blurRadius`.
27
+ */
28
+ blur?: number;
29
+ /**
30
+ * Corner radius in pixels of the loaded bitmap. Pass a single number for
31
+ * uniform rounding, or per-corner radii (inline object literals are fine —
32
+ * the hook compares the radii by value, not identity). Pair with `resize`
33
+ * so the radii apply at the size you display instead of the source size.
34
+ */
35
+ cornerRadius?: number | CornerRadii;
36
+ /**
37
+ * Resize the bitmap to exactly this size in pixels (aspect-fill,
38
+ * center-crop) before blur/rounding. Typically your display size in points
39
+ * multiplied by `PixelRatio.get()`. Inline object literals are fine.
40
+ */
41
+ resize?: ResizeOptions;
42
+ cache?: CacheOption;
43
+ /**
44
+ * When `false`, no request is made and the result stays in the loading
45
+ * state (or keeps the current image if `url` is unchanged) until it becomes
46
+ * `true`. Use it to defer loading until inputs such as a layout size are
47
+ * known.
48
+ * @default true
49
+ */
50
+ enabled?: boolean;
51
+ }): Result;
52
+ export {};
53
+ //# sourceMappingURL=useImage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useImage.d.ts","sourceRoot":"","sources":["../../../src/useImage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAC;AAGtD,OAAO,KAAK,EACV,WAAW,EACX,WAAW,EACX,aAAa,EACd,MAAM,mCAAmC,CAAC;AAE3C,KAAK,MAAM,GAEP;IACE,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,SAAS,CAAC;CAClB,GAED;IACE,KAAK,EAAE,KAAK,CAAC;IACb,KAAK,EAAE,SAAS,CAAC;CAClB,GAED;IACE,KAAK,EAAE,SAAS,CAAC;IACjB,KAAK,EAAE,KAAK,CAAC;CACd,CAAC;AAEN;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,EACvB,GAAG,EACH,IAAQ,EACR,YAAgB,EAChB,MAAM,EACN,KAAK,EACL,OAAc,GACf,EAAE;IACD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,GAAG,WAAW,CAAC;IACpC;;;;OAIG;IACH,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,GAAG,MAAM,CA4ET"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-image-pipeline",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "High-performance image loading, caching, and processing for React Native, built with Nitro Modules",
5
5
  "main": "./lib/commonjs/index.js",
6
6
  "module": "./lib/module/index.js",
@@ -86,7 +86,8 @@
86
86
  "react-native-nitro-image": "0.15.1"
87
87
  },
88
88
  "overrides": {
89
- "lodash-es": "4.17.21"
89
+ "lodash-es": "4.17.21",
90
+ "typescript": "$typescript"
90
91
  },
91
92
  "react-native-builder-bob": {
92
93
  "source": "src",
@@ -0,0 +1,6 @@
1
+ import { NitroModules } from 'react-native-nitro-modules';
2
+
3
+ import type { NitroImagePipeline as NitroImagePipelineSpec } from './specs/nitro-image-toolkit.nitro';
4
+
5
+ export const NitroImagePipeline =
6
+ NitroModules.createHybridObject<NitroImagePipelineSpec>('NitroImagePipeline');
@@ -0,0 +1,183 @@
1
+ import {
2
+ type ComponentRef,
3
+ forwardRef,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from 'react';
8
+ import {
9
+ type HostComponent,
10
+ type LayoutChangeEvent,
11
+ PixelRatio,
12
+ } from 'react-native';
13
+ import { type Image, NativeNitroImage } from 'react-native-nitro-image';
14
+
15
+ import {
16
+ cornerRadiusForStyle,
17
+ resizeForLayout,
18
+ resizeForStyle,
19
+ } from './resizeForStyle';
20
+ import type {
21
+ CacheOption,
22
+ CornerRadii,
23
+ ResizeOptions,
24
+ } from './specs/nitro-image-toolkit.nitro';
25
+ import { useImage } from './useImage';
26
+
27
+ type ReactProps<T> = T extends HostComponent<infer P> ? P : never;
28
+ type NativeImageProps = ReactProps<typeof NativeNitroImage>;
29
+
30
+ /**
31
+ * The instance `<PipelineImage>` exposes through its `ref` — the underlying
32
+ * `NativeNitroImage` host view, with the usual native-view methods
33
+ * (`measure`, …).
34
+ */
35
+ export type PipelineImageRef = ComponentRef<typeof NativeNitroImage>;
36
+
37
+ export interface PipelineImageProps extends Omit<NativeImageProps, 'image'> {
38
+ /** URL of the image to load through the pipeline. */
39
+ url: string;
40
+ /**
41
+ * Gaussian blur sigma in **points** (density-independent). Unlike
42
+ * `useImage`/`loadImage`, where it is bitmap pixels, the component
43
+ * multiplies it by `PixelRatio.get()` so the same value looks the same on
44
+ * every device.
45
+ * @default 0
46
+ */
47
+ blur?: number;
48
+ /**
49
+ * Corner radius in **points** — a single number or per-corner radii.
50
+ * Converted to bitmap pixels with `PixelRatio.get()` and baked into the
51
+ * bitmap at the display size.
52
+ *
53
+ * When omitted, it's derived from `style`'s `borderRadius` /
54
+ * `borderTopLeftRadius` / `borderTopRightRadius` / `borderBottomLeftRadius`
55
+ * / `borderBottomRightRadius` instead — set this prop to override that.
56
+ * @default undefined (derived from `style`, or square corners if unset there)
57
+ */
58
+ cornerRadius?: number | CornerRadii;
59
+ cache?: CacheOption;
60
+ /** Called with the processed `Image` each time a new variant resolves. */
61
+ onLoad?: (image: Image) => void;
62
+ /** Called when loading fails. */
63
+ onError?: (error: Error) => void;
64
+ }
65
+
66
+ function sameSize(a?: ResizeOptions, b?: ResizeOptions): boolean {
67
+ return a?.width === b?.width && a?.height === b?.height;
68
+ }
69
+
70
+ function scaleRadius(
71
+ cornerRadius: number | CornerRadii,
72
+ scale: number,
73
+ ): number | CornerRadii {
74
+ if (typeof cornerRadius === 'number') {
75
+ return cornerRadius * scale;
76
+ }
77
+ return {
78
+ topLeft: (cornerRadius.topLeft ?? 0) * scale,
79
+ topRight: (cornerRadius.topRight ?? 0) * scale,
80
+ bottomLeft: (cornerRadius.bottomLeft ?? 0) * scale,
81
+ bottomRight: (cornerRadius.bottomRight ?? 0) * scale,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * A `NativeNitroImage` that loads `url` through the pipeline at exactly the
87
+ * size it is displayed: the bitmap is resized to the view's size in points ×
88
+ * `PixelRatio.get()`, so `blur` and `cornerRadius` (both in points here) apply
89
+ * 1:1 to what is on screen and large sources are never decoded at full size.
90
+ *
91
+ * A numeric `width`/`height` in `style` starts loading immediately; otherwise
92
+ * (`'50%'`, `flex`, `aspectRatio`, …) loading waits for the first `onLayout`.
93
+ * If the layout size later changes, a new variant is loaded and swapped in
94
+ * without flashing. Without an explicit `cornerRadius` prop, `style`'s
95
+ * `borderRadius`-family properties are baked into the bitmap instead — no
96
+ * separate view-layer rounding needed.
97
+ *
98
+ * The `ref` is forwarded to the underlying `NativeNitroImage` host view, so
99
+ * the component works with `Animated.createAnimatedComponent` (Reanimated or
100
+ * React Native's built-in `Animated`).
101
+ * @example
102
+ * ```tsx
103
+ * <PipelineImage
104
+ * url="https://example.com/photo.jpg"
105
+ * style={{ width: 300, height: 200 }}
106
+ * cornerRadius={24}
107
+ * />
108
+ * ```
109
+ */
110
+ export const PipelineImage = forwardRef<PipelineImageRef, PipelineImageProps>(
111
+ function PipelineImage(
112
+ {
113
+ url,
114
+ blur = 0,
115
+ cornerRadius,
116
+ cache,
117
+ onLoad,
118
+ onError,
119
+ style,
120
+ onLayout,
121
+ ...viewProps
122
+ },
123
+ ref,
124
+ ) {
125
+ const scale = PixelRatio.get();
126
+ const styleSize = resizeForStyle(style);
127
+ const [layoutSize, setLayoutSize] = useState<ResizeOptions | undefined>(
128
+ undefined,
129
+ );
130
+ // A numeric style is what the caller declared, so it wins and starts the
131
+ // request a frame earlier; the measured layout is the fallback.
132
+ const resize = styleSize ?? layoutSize;
133
+ // Same precedence: an explicit prop wins over what style implies.
134
+ const effectiveCornerRadius =
135
+ cornerRadius ?? cornerRadiusForStyle(style) ?? 0;
136
+
137
+ const { image, error } = useImage({
138
+ url,
139
+ blur: blur * scale,
140
+ cornerRadius: scaleRadius(effectiveCornerRadius, scale),
141
+ cache,
142
+ resize,
143
+ enabled: resize !== undefined,
144
+ });
145
+
146
+ // Latest callbacks in refs so inline arrow props don't re-fire the effects.
147
+ const onLoadRef = useRef(onLoad);
148
+ const onErrorRef = useRef(onError);
149
+ useEffect(() => {
150
+ onLoadRef.current = onLoad;
151
+ onErrorRef.current = onError;
152
+ });
153
+ useEffect(() => {
154
+ if (image) onLoadRef.current?.(image);
155
+ }, [image]);
156
+ useEffect(() => {
157
+ if (error) onErrorRef.current?.(error);
158
+ }, [error]);
159
+
160
+ const handleLayout = (event: LayoutChangeEvent) => {
161
+ onLayout?.(event);
162
+ const { width, height } = event.nativeEvent.layout;
163
+ const next = resizeForLayout(width, height);
164
+ // Always record it (even when a numeric style is in charge) so a later
165
+ // switch to a non-numeric style has a size to fall back on.
166
+ setLayoutSize((prev) => (sameSize(prev, next) ? prev : next));
167
+ };
168
+
169
+ return (
170
+ <NativeNitroImage
171
+ {...viewProps}
172
+ ref={ref}
173
+ style={style}
174
+ onLayout={handleLayout}
175
+ image={image}
176
+ />
177
+ );
178
+ },
179
+ );
180
+
181
+ // Reanimated and DevTools read the display name; the forwardRef wrapper
182
+ // would otherwise report as anonymous.
183
+ PipelineImage.displayName = 'PipelineImage';
package/src/index.ts CHANGED
@@ -1,144 +1,18 @@
1
- import { useEffect, useRef, useState } from 'react';
2
- import type { Image } from 'react-native-nitro-image';
3
- import { NitroModules } from 'react-native-nitro-modules';
4
-
5
- import type {
1
+ export { NitroImagePipeline } from './NitroImagePipeline';
2
+ export {
3
+ PipelineImage,
4
+ type PipelineImageProps,
5
+ type PipelineImageRef,
6
+ } from './PipelineImage';
7
+ export {
8
+ cornerRadiusForStyle,
9
+ resizeForLayout,
10
+ resizeForStyle,
11
+ } from './resizeForStyle';
12
+ export type {
6
13
  CacheOption,
7
14
  CornerRadii,
8
- NitroImagePipeline as NitroImagePipelineSpec,
9
15
  Options,
10
16
  ResizeOptions,
11
17
  } from './specs/nitro-image-toolkit.nitro';
12
-
13
- export type { CacheOption, CornerRadii, Options, ResizeOptions };
14
-
15
- export const NitroImagePipeline =
16
- NitroModules.createHybridObject<NitroImagePipelineSpec>('NitroImagePipeline');
17
-
18
- type Result =
19
- // Loading State
20
- | {
21
- image: undefined;
22
- error: undefined;
23
- }
24
- // Loaded state
25
- | {
26
- image: Image;
27
- error: undefined;
28
- }
29
- // Error state
30
- | {
31
- image: undefined;
32
- error: Error;
33
- };
34
-
35
- /**
36
- * A hook to asynchronously load an image from the
37
- * given {@linkcode AsyncImageSource} into memory.
38
- * @example
39
- * ```ts
40
- * const { image, error } = useImage({ filePath: '/tmp/image.jpg' })
41
- * ```
42
- */
43
- export function useImage({
44
- url,
45
- blur = 0,
46
- cornerRadius = 0,
47
- resize,
48
- cache,
49
- }: {
50
- url: string;
51
- /**
52
- * Gaussian blur strength, as the standard deviation (sigma) of the blur in
53
- * source-image pixels (of the resized bitmap when `resize` is set). Matches
54
- * across iOS and Android; roughly half of React Native's `blurRadius`.
55
- */
56
- blur?: number;
57
- /**
58
- * Corner radius in pixels of the loaded bitmap. Pass a single number for
59
- * uniform rounding, or per-corner radii (inline object literals are fine —
60
- * the hook compares the radii by value, not identity). Pair with `resize`
61
- * so the radii apply at the size you display instead of the source size.
62
- */
63
- cornerRadius?: number | CornerRadii;
64
- /**
65
- * Resize the bitmap to exactly this size in pixels (aspect-fill,
66
- * center-crop) before blur/rounding. Typically your display size in points
67
- * multiplied by `PixelRatio.get()`. Inline object literals are fine.
68
- */
69
- resize?: ResizeOptions;
70
- cache?: CacheOption;
71
- }): Result {
72
- const [image, setImage] = useState<Result>({
73
- image: undefined,
74
- error: undefined,
75
- });
76
- const loadedUrlRef = useRef(url);
77
-
78
- // Split the option into primitives so an inline `{ topLeft: 24, ... }`
79
- // literal (new identity every render) doesn't re-trigger the effect.
80
- const isUniformRadius = typeof cornerRadius === 'number';
81
- const uniformRadius = isUniformRadius ? cornerRadius : 0;
82
- const {
83
- topLeft = 0,
84
- topRight = 0,
85
- bottomLeft = 0,
86
- bottomRight = 0,
87
- } = isUniformRadius ? {} : cornerRadius;
88
- const resizeWidth = resize?.width ?? 0;
89
- const resizeHeight = resize?.height ?? 0;
90
-
91
- useEffect(() => {
92
- let cancelled = false;
93
- // Only reset to the loading state when the URL changes; for same-URL
94
- // param tweaks (blur/cornerRadius/cache) keep showing the current image
95
- // until the new variant resolves, to avoid flashing empty.
96
- if (loadedUrlRef.current !== url) {
97
- loadedUrlRef.current = url;
98
- setImage({ image: undefined, error: undefined });
99
- }
100
-
101
- (async () => {
102
- try {
103
- const result = await NitroImagePipeline.loadImage(url, {
104
- blur,
105
- cornerRadius: isUniformRadius
106
- ? uniformRadius
107
- : { topLeft, topRight, bottomLeft, bottomRight },
108
- resize:
109
- resizeWidth > 0 && resizeHeight > 0
110
- ? { width: resizeWidth, height: resizeHeight }
111
- : undefined,
112
- cache,
113
- });
114
-
115
- if (!cancelled) {
116
- setImage({ image: result, error: undefined });
117
- }
118
- } catch (e) {
119
- const error = e instanceof Error ? e : new Error(`${e}`);
120
- if (!cancelled) {
121
- setImage({ image: undefined, error: error });
122
- }
123
- }
124
- })();
125
-
126
- return () => {
127
- cancelled = true;
128
- };
129
- }, [
130
- url,
131
- blur,
132
- isUniformRadius,
133
- uniformRadius,
134
- topLeft,
135
- topRight,
136
- bottomLeft,
137
- bottomRight,
138
- resizeWidth,
139
- resizeHeight,
140
- cache,
141
- ]);
142
-
143
- return image;
144
- }
18
+ export { useImage } from './useImage';