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.
- package/README.md +84 -8
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/HybridNitroImagePipeline.kt +36 -15
- package/android/src/main/java/com/margelo/nitro/nitroimagepipeline/transform/ResizeTransformation.kt +58 -0
- package/ios/HybridNitroImagePipeline.swift +54 -27
- package/ios/RoundedCornersProcessor.swift +6 -3
- package/lib/commonjs/NitroImagePipeline.js +9 -0
- package/lib/commonjs/NitroImagePipeline.js.map +1 -0
- package/lib/commonjs/PipelineImage.js +111 -0
- package/lib/commonjs/PipelineImage.js.map +1 -0
- package/lib/commonjs/index.js +40 -81
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/resizeForStyle.js +68 -0
- package/lib/commonjs/resizeForStyle.js.map +1 -0
- package/lib/commonjs/useImage.js +95 -0
- package/lib/commonjs/useImage.js.map +1 -0
- package/lib/module/NitroImagePipeline.js +5 -0
- package/lib/module/NitroImagePipeline.js.map +1 -0
- package/lib/module/PipelineImage.js +107 -0
- package/lib/module/PipelineImage.js.map +1 -0
- package/lib/module/index.js +4 -79
- package/lib/module/index.js.map +1 -1
- package/lib/module/resizeForStyle.js +62 -0
- package/lib/module/resizeForStyle.js.map +1 -0
- package/lib/module/useImage.js +91 -0
- package/lib/module/useImage.js.map +1 -0
- package/lib/typescript/src/NitroImagePipeline.d.ts +3 -0
- package/lib/typescript/src/NitroImagePipeline.d.ts.map +1 -0
- package/lib/typescript/src/PipelineImage.d.ts +57 -0
- package/lib/typescript/src/PipelineImage.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -38
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/resizeForStyle.d.ts +30 -0
- package/lib/typescript/src/resizeForStyle.d.ts.map +1 -0
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts +33 -5
- package/lib/typescript/src/specs/nitro-image-toolkit.nitro.d.ts.map +1 -1
- package/lib/typescript/src/useImage.d.ts +53 -0
- package/lib/typescript/src/useImage.d.ts.map +1 -0
- package/nitrogen/generated/android/c++/JHybridNitroImagePipelineSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JOptions.hpp +9 -3
- package/nitrogen/generated/android/c++/JResizeOptions.hpp +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/Options.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroimagepipeline/ResizeOptions.kt +56 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Bridge.hpp +18 -0
- package/nitrogen/generated/ios/NitroImagePipeline-Swift-Cxx-Umbrella.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridNitroImagePipelineSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/swift/Options.swift +12 -1
- package/nitrogen/generated/ios/swift/ResizeOptions.swift +34 -0
- package/nitrogen/generated/shared/c++/Options.hpp +9 -2
- package/nitrogen/generated/shared/c++/ResizeOptions.hpp +87 -0
- package/package.json +1 -1
- package/src/NitroImagePipeline.ts +6 -0
- package/src/PipelineImage.tsx +156 -0
- package/src/index.ts +10 -123
- package/src/resizeForStyle.ts +96 -0
- package/src/specs/nitro-image-toolkit.nitro.ts +34 -5
- package/src/useImage.ts +149 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// ResizeOptions.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#if __has_include(<NitroModules/JSIConverter.hpp>)
|
|
11
|
+
#include <NitroModules/JSIConverter.hpp>
|
|
12
|
+
#else
|
|
13
|
+
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
|
14
|
+
#endif
|
|
15
|
+
#if __has_include(<NitroModules/NitroDefines.hpp>)
|
|
16
|
+
#include <NitroModules/NitroDefines.hpp>
|
|
17
|
+
#else
|
|
18
|
+
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
|
19
|
+
#endif
|
|
20
|
+
#if __has_include(<NitroModules/JSIHelpers.hpp>)
|
|
21
|
+
#include <NitroModules/JSIHelpers.hpp>
|
|
22
|
+
#else
|
|
23
|
+
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
|
24
|
+
#endif
|
|
25
|
+
#if __has_include(<NitroModules/PropNameIDCache.hpp>)
|
|
26
|
+
#include <NitroModules/PropNameIDCache.hpp>
|
|
27
|
+
#else
|
|
28
|
+
#error NitroModules cannot be found! Are you sure you installed NitroModules properly?
|
|
29
|
+
#endif
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
namespace margelo::nitro::nitroimagepipeline {
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A struct which can be represented as a JavaScript object (ResizeOptions).
|
|
39
|
+
*/
|
|
40
|
+
struct ResizeOptions final {
|
|
41
|
+
public:
|
|
42
|
+
double width SWIFT_PRIVATE;
|
|
43
|
+
double height SWIFT_PRIVATE;
|
|
44
|
+
|
|
45
|
+
public:
|
|
46
|
+
ResizeOptions() = default;
|
|
47
|
+
explicit ResizeOptions(double width, double height): width(width), height(height) {}
|
|
48
|
+
|
|
49
|
+
public:
|
|
50
|
+
friend bool operator==(const ResizeOptions& lhs, const ResizeOptions& rhs) = default;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
} // namespace margelo::nitro::nitroimagepipeline
|
|
54
|
+
|
|
55
|
+
namespace margelo::nitro {
|
|
56
|
+
|
|
57
|
+
// C++ ResizeOptions <> JS ResizeOptions (object)
|
|
58
|
+
template <>
|
|
59
|
+
struct JSIConverter<margelo::nitro::nitroimagepipeline::ResizeOptions> final {
|
|
60
|
+
static inline margelo::nitro::nitroimagepipeline::ResizeOptions fromJSI(jsi::Runtime& runtime, const jsi::Value& arg) {
|
|
61
|
+
jsi::Object obj = arg.asObject(runtime);
|
|
62
|
+
return margelo::nitro::nitroimagepipeline::ResizeOptions(
|
|
63
|
+
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "width"))),
|
|
64
|
+
JSIConverter<double>::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "height")))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
static inline jsi::Value toJSI(jsi::Runtime& runtime, const margelo::nitro::nitroimagepipeline::ResizeOptions& arg) {
|
|
68
|
+
jsi::Object obj(runtime);
|
|
69
|
+
obj.setProperty(runtime, PropNameIDCache::get(runtime, "width"), JSIConverter<double>::toJSI(runtime, arg.width));
|
|
70
|
+
obj.setProperty(runtime, PropNameIDCache::get(runtime, "height"), JSIConverter<double>::toJSI(runtime, arg.height));
|
|
71
|
+
return obj;
|
|
72
|
+
}
|
|
73
|
+
static inline bool canConvert(jsi::Runtime& runtime, const jsi::Value& value) {
|
|
74
|
+
if (!value.isObject()) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
jsi::Object obj = value.getObject(runtime);
|
|
78
|
+
if (!nitro::isPlainObject(runtime, obj)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "width")))) return false;
|
|
82
|
+
if (!JSIConverter<double>::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "height")))) return false;
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
} // namespace margelo::nitro
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-nitro-image-pipeline",
|
|
3
|
-
"version": "1.1
|
|
3
|
+
"version": "1.2.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",
|
|
@@ -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,156 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
type HostComponent,
|
|
4
|
+
type LayoutChangeEvent,
|
|
5
|
+
PixelRatio,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
import { type Image, NativeNitroImage } from 'react-native-nitro-image';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
cornerRadiusForStyle,
|
|
11
|
+
resizeForLayout,
|
|
12
|
+
resizeForStyle,
|
|
13
|
+
} from './resizeForStyle';
|
|
14
|
+
import type {
|
|
15
|
+
CacheOption,
|
|
16
|
+
CornerRadii,
|
|
17
|
+
ResizeOptions,
|
|
18
|
+
} from './specs/nitro-image-toolkit.nitro';
|
|
19
|
+
import { useImage } from './useImage';
|
|
20
|
+
|
|
21
|
+
type ReactProps<T> = T extends HostComponent<infer P> ? P : never;
|
|
22
|
+
type NativeImageProps = ReactProps<typeof NativeNitroImage>;
|
|
23
|
+
|
|
24
|
+
export interface PipelineImageProps extends Omit<NativeImageProps, 'image'> {
|
|
25
|
+
/** URL of the image to load through the pipeline. */
|
|
26
|
+
url: string;
|
|
27
|
+
/**
|
|
28
|
+
* Gaussian blur sigma in **points** (density-independent). Unlike
|
|
29
|
+
* `useImage`/`loadImage`, where it is bitmap pixels, the component
|
|
30
|
+
* multiplies it by `PixelRatio.get()` so the same value looks the same on
|
|
31
|
+
* every device.
|
|
32
|
+
* @default 0
|
|
33
|
+
*/
|
|
34
|
+
blur?: number;
|
|
35
|
+
/**
|
|
36
|
+
* Corner radius in **points** — a single number or per-corner radii.
|
|
37
|
+
* Converted to bitmap pixels with `PixelRatio.get()` and baked into the
|
|
38
|
+
* bitmap at the display size.
|
|
39
|
+
*
|
|
40
|
+
* When omitted, it's derived from `style`'s `borderRadius` /
|
|
41
|
+
* `borderTopLeftRadius` / `borderTopRightRadius` / `borderBottomLeftRadius`
|
|
42
|
+
* / `borderBottomRightRadius` instead — set this prop to override that.
|
|
43
|
+
* @default undefined (derived from `style`, or square corners if unset there)
|
|
44
|
+
*/
|
|
45
|
+
cornerRadius?: number | CornerRadii;
|
|
46
|
+
cache?: CacheOption;
|
|
47
|
+
/** Called with the processed `Image` each time a new variant resolves. */
|
|
48
|
+
onLoad?: (image: Image) => void;
|
|
49
|
+
/** Called when loading fails. */
|
|
50
|
+
onError?: (error: Error) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sameSize(a?: ResizeOptions, b?: ResizeOptions): boolean {
|
|
54
|
+
return a?.width === b?.width && a?.height === b?.height;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function scaleRadius(
|
|
58
|
+
cornerRadius: number | CornerRadii,
|
|
59
|
+
scale: number,
|
|
60
|
+
): number | CornerRadii {
|
|
61
|
+
if (typeof cornerRadius === 'number') {
|
|
62
|
+
return cornerRadius * scale;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
topLeft: (cornerRadius.topLeft ?? 0) * scale,
|
|
66
|
+
topRight: (cornerRadius.topRight ?? 0) * scale,
|
|
67
|
+
bottomLeft: (cornerRadius.bottomLeft ?? 0) * scale,
|
|
68
|
+
bottomRight: (cornerRadius.bottomRight ?? 0) * scale,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A `NativeNitroImage` that loads `url` through the pipeline at exactly the
|
|
74
|
+
* size it is displayed: the bitmap is resized to the view's size in points ×
|
|
75
|
+
* `PixelRatio.get()`, so `blur` and `cornerRadius` (both in points here) apply
|
|
76
|
+
* 1:1 to what is on screen and large sources are never decoded at full size.
|
|
77
|
+
*
|
|
78
|
+
* A numeric `width`/`height` in `style` starts loading immediately; otherwise
|
|
79
|
+
* (`'50%'`, `flex`, `aspectRatio`, …) loading waits for the first `onLayout`.
|
|
80
|
+
* If the layout size later changes, a new variant is loaded and swapped in
|
|
81
|
+
* without flashing. Without an explicit `cornerRadius` prop, `style`'s
|
|
82
|
+
* `borderRadius`-family properties are baked into the bitmap instead — no
|
|
83
|
+
* separate view-layer rounding needed.
|
|
84
|
+
* @example
|
|
85
|
+
* ```tsx
|
|
86
|
+
* <PipelineImage
|
|
87
|
+
* url="https://example.com/photo.jpg"
|
|
88
|
+
* style={{ width: 300, height: 200 }}
|
|
89
|
+
* cornerRadius={24}
|
|
90
|
+
* />
|
|
91
|
+
* ```
|
|
92
|
+
*/
|
|
93
|
+
export function PipelineImage({
|
|
94
|
+
url,
|
|
95
|
+
blur = 0,
|
|
96
|
+
cornerRadius,
|
|
97
|
+
cache,
|
|
98
|
+
onLoad,
|
|
99
|
+
onError,
|
|
100
|
+
style,
|
|
101
|
+
onLayout,
|
|
102
|
+
...viewProps
|
|
103
|
+
}: PipelineImageProps) {
|
|
104
|
+
const scale = PixelRatio.get();
|
|
105
|
+
const styleSize = resizeForStyle(style);
|
|
106
|
+
const [layoutSize, setLayoutSize] = useState<ResizeOptions | undefined>(
|
|
107
|
+
undefined,
|
|
108
|
+
);
|
|
109
|
+
// A numeric style is what the caller declared, so it wins and starts the
|
|
110
|
+
// request a frame earlier; the measured layout is the fallback.
|
|
111
|
+
const resize = styleSize ?? layoutSize;
|
|
112
|
+
// Same precedence: an explicit prop wins over what style implies.
|
|
113
|
+
const effectiveCornerRadius =
|
|
114
|
+
cornerRadius ?? cornerRadiusForStyle(style) ?? 0;
|
|
115
|
+
|
|
116
|
+
const { image, error } = useImage({
|
|
117
|
+
url,
|
|
118
|
+
blur: blur * scale,
|
|
119
|
+
cornerRadius: scaleRadius(effectiveCornerRadius, scale),
|
|
120
|
+
cache,
|
|
121
|
+
resize,
|
|
122
|
+
enabled: resize !== undefined,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Latest callbacks in refs so inline arrow props don't re-fire the effects.
|
|
126
|
+
const onLoadRef = useRef(onLoad);
|
|
127
|
+
const onErrorRef = useRef(onError);
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
onLoadRef.current = onLoad;
|
|
130
|
+
onErrorRef.current = onError;
|
|
131
|
+
});
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (image) onLoadRef.current?.(image);
|
|
134
|
+
}, [image]);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (error) onErrorRef.current?.(error);
|
|
137
|
+
}, [error]);
|
|
138
|
+
|
|
139
|
+
const handleLayout = (event: LayoutChangeEvent) => {
|
|
140
|
+
onLayout?.(event);
|
|
141
|
+
const { width, height } = event.nativeEvent.layout;
|
|
142
|
+
const next = resizeForLayout(width, height);
|
|
143
|
+
// Always record it (even when a numeric style is in charge) so a later
|
|
144
|
+
// switch to a non-numeric style has a size to fall back on.
|
|
145
|
+
setLayoutSize((prev) => (sameSize(prev, next) ? prev : next));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<NativeNitroImage
|
|
150
|
+
{...viewProps}
|
|
151
|
+
style={style}
|
|
152
|
+
onLayout={handleLayout}
|
|
153
|
+
image={image}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,127 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
export { NitroImagePipeline } from './NitroImagePipeline';
|
|
2
|
+
export { PipelineImage, type PipelineImageProps } from './PipelineImage';
|
|
3
|
+
export {
|
|
4
|
+
cornerRadiusForStyle,
|
|
5
|
+
resizeForLayout,
|
|
6
|
+
resizeForStyle,
|
|
7
|
+
} from './resizeForStyle';
|
|
8
|
+
export type {
|
|
6
9
|
CacheOption,
|
|
7
10
|
CornerRadii,
|
|
8
|
-
NitroImagePipeline as NitroImagePipelineSpec,
|
|
9
11
|
Options,
|
|
12
|
+
ResizeOptions,
|
|
10
13
|
} from './specs/nitro-image-toolkit.nitro';
|
|
11
|
-
|
|
12
|
-
export type { CacheOption, CornerRadii, Options };
|
|
13
|
-
|
|
14
|
-
export const NitroImagePipeline =
|
|
15
|
-
NitroModules.createHybridObject<NitroImagePipelineSpec>('NitroImagePipeline');
|
|
16
|
-
|
|
17
|
-
type Result =
|
|
18
|
-
// Loading State
|
|
19
|
-
| {
|
|
20
|
-
image: undefined;
|
|
21
|
-
error: undefined;
|
|
22
|
-
}
|
|
23
|
-
// Loaded state
|
|
24
|
-
| {
|
|
25
|
-
image: Image;
|
|
26
|
-
error: undefined;
|
|
27
|
-
}
|
|
28
|
-
// Error state
|
|
29
|
-
| {
|
|
30
|
-
image: undefined;
|
|
31
|
-
error: Error;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* A hook to asynchronously load an image from the
|
|
36
|
-
* given {@linkcode AsyncImageSource} into memory.
|
|
37
|
-
* @example
|
|
38
|
-
* ```ts
|
|
39
|
-
* const { image, error } = useImage({ filePath: '/tmp/image.jpg' })
|
|
40
|
-
* ```
|
|
41
|
-
*/
|
|
42
|
-
export function useImage({
|
|
43
|
-
url,
|
|
44
|
-
blur = 0,
|
|
45
|
-
cornerRadius = 0,
|
|
46
|
-
cache,
|
|
47
|
-
}: {
|
|
48
|
-
url: string;
|
|
49
|
-
/**
|
|
50
|
-
* Gaussian blur strength, as the standard deviation (sigma) of the blur in
|
|
51
|
-
* source-image pixels. Matches across iOS and Android; roughly half of
|
|
52
|
-
* React Native's `blurRadius`.
|
|
53
|
-
*/
|
|
54
|
-
blur?: number;
|
|
55
|
-
/**
|
|
56
|
-
* Corner radius in points, baked into the loaded bitmap. Pass a single
|
|
57
|
-
* number for uniform rounding, or per-corner radii (inline object literals
|
|
58
|
-
* are fine — the hook compares the radii by value, not identity).
|
|
59
|
-
*/
|
|
60
|
-
cornerRadius?: number | CornerRadii;
|
|
61
|
-
cache?: CacheOption;
|
|
62
|
-
}): Result {
|
|
63
|
-
const [image, setImage] = useState<Result>({
|
|
64
|
-
image: undefined,
|
|
65
|
-
error: undefined,
|
|
66
|
-
});
|
|
67
|
-
const loadedUrlRef = useRef(url);
|
|
68
|
-
|
|
69
|
-
// Split the option into primitives so an inline `{ topLeft: 24, ... }`
|
|
70
|
-
// literal (new identity every render) doesn't re-trigger the effect.
|
|
71
|
-
const isUniformRadius = typeof cornerRadius === 'number';
|
|
72
|
-
const uniformRadius = isUniformRadius ? cornerRadius : 0;
|
|
73
|
-
const {
|
|
74
|
-
topLeft = 0,
|
|
75
|
-
topRight = 0,
|
|
76
|
-
bottomLeft = 0,
|
|
77
|
-
bottomRight = 0,
|
|
78
|
-
} = isUniformRadius ? {} : cornerRadius;
|
|
79
|
-
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
let cancelled = false;
|
|
82
|
-
// Only reset to the loading state when the URL changes; for same-URL
|
|
83
|
-
// param tweaks (blur/cornerRadius/cache) keep showing the current image
|
|
84
|
-
// until the new variant resolves, to avoid flashing empty.
|
|
85
|
-
if (loadedUrlRef.current !== url) {
|
|
86
|
-
loadedUrlRef.current = url;
|
|
87
|
-
setImage({ image: undefined, error: undefined });
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
(async () => {
|
|
91
|
-
try {
|
|
92
|
-
const result = await NitroImagePipeline.loadImage(url, {
|
|
93
|
-
blur,
|
|
94
|
-
cornerRadius: isUniformRadius
|
|
95
|
-
? uniformRadius
|
|
96
|
-
: { topLeft, topRight, bottomLeft, bottomRight },
|
|
97
|
-
cache,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
if (!cancelled) {
|
|
101
|
-
setImage({ image: result, error: undefined });
|
|
102
|
-
}
|
|
103
|
-
} catch (e) {
|
|
104
|
-
const error = e instanceof Error ? e : new Error(`${e}`);
|
|
105
|
-
if (!cancelled) {
|
|
106
|
-
setImage({ image: undefined, error: error });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
})();
|
|
110
|
-
|
|
111
|
-
return () => {
|
|
112
|
-
cancelled = true;
|
|
113
|
-
};
|
|
114
|
-
}, [
|
|
115
|
-
url,
|
|
116
|
-
blur,
|
|
117
|
-
isUniformRadius,
|
|
118
|
-
uniformRadius,
|
|
119
|
-
topLeft,
|
|
120
|
-
topRight,
|
|
121
|
-
bottomLeft,
|
|
122
|
-
bottomRight,
|
|
123
|
-
cache,
|
|
124
|
-
]);
|
|
125
|
-
|
|
126
|
-
return image;
|
|
127
|
-
}
|
|
14
|
+
export { useImage } from './useImage';
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PixelRatio,
|
|
3
|
+
type StyleProp,
|
|
4
|
+
StyleSheet,
|
|
5
|
+
type ViewStyle,
|
|
6
|
+
} from 'react-native';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
CornerRadii,
|
|
10
|
+
ResizeOptions,
|
|
11
|
+
} from './specs/nitro-image-toolkit.nitro';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Converts a layout size in points (dp) to the pipeline's `resize` option in
|
|
15
|
+
* whole bitmap pixels using `PixelRatio.getPixelSizeForLayoutSize`. Returns
|
|
16
|
+
* `undefined` unless both values are positive numbers.
|
|
17
|
+
*/
|
|
18
|
+
export function resizeForLayout(
|
|
19
|
+
width: unknown,
|
|
20
|
+
height: unknown,
|
|
21
|
+
): ResizeOptions | undefined {
|
|
22
|
+
if (typeof width !== 'number' || typeof height !== 'number') {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const pixelWidth = PixelRatio.getPixelSizeForLayoutSize(width);
|
|
26
|
+
const pixelHeight = PixelRatio.getPixelSizeForLayoutSize(height);
|
|
27
|
+
return pixelWidth > 0 && pixelHeight > 0
|
|
28
|
+
? { width: pixelWidth, height: pixelHeight }
|
|
29
|
+
: undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Derives the pipeline's `resize` option from a view style whose `width` and
|
|
34
|
+
* `height` are numeric points, so the bitmap matches the display size on
|
|
35
|
+
* every screen density:
|
|
36
|
+
* ```ts
|
|
37
|
+
* useImage({ url, resize: resizeForStyle(styles.image) });
|
|
38
|
+
* ```
|
|
39
|
+
* Arrays and registered styles are flattened. Returns `undefined` when either
|
|
40
|
+
* dimension is missing or not a number (`'50%'`, `'auto'`, flex-driven) —
|
|
41
|
+
* use `<PipelineImage>` for those, which measures the view instead.
|
|
42
|
+
*/
|
|
43
|
+
export function resizeForStyle(
|
|
44
|
+
style: StyleProp<ViewStyle>,
|
|
45
|
+
): ResizeOptions | undefined {
|
|
46
|
+
const flat = StyleSheet.flatten(style);
|
|
47
|
+
return resizeForLayout(flat?.width, flat?.height);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derives a `cornerRadius` option from a view style's `borderRadius` /
|
|
52
|
+
* `borderTopLeftRadius` / `borderTopRightRadius` / `borderBottomLeftRadius` /
|
|
53
|
+
* `borderBottomRightRadius`, in points. Per-corner properties override
|
|
54
|
+
* `borderRadius` for that corner; a corner with neither set stays square.
|
|
55
|
+
* Non-numeric values (percentages, animated values) are ignored, like
|
|
56
|
+
* {@linkcode resizeForStyle}. Returns `undefined` when none are set.
|
|
57
|
+
*/
|
|
58
|
+
export function cornerRadiusForStyle(
|
|
59
|
+
style: StyleProp<ViewStyle>,
|
|
60
|
+
): number | CornerRadii | undefined {
|
|
61
|
+
const flat = StyleSheet.flatten(style);
|
|
62
|
+
const base =
|
|
63
|
+
typeof flat?.borderRadius === 'number' ? flat.borderRadius : undefined;
|
|
64
|
+
const topLeft =
|
|
65
|
+
typeof flat?.borderTopLeftRadius === 'number'
|
|
66
|
+
? flat.borderTopLeftRadius
|
|
67
|
+
: undefined;
|
|
68
|
+
const topRight =
|
|
69
|
+
typeof flat?.borderTopRightRadius === 'number'
|
|
70
|
+
? flat.borderTopRightRadius
|
|
71
|
+
: undefined;
|
|
72
|
+
const bottomLeft =
|
|
73
|
+
typeof flat?.borderBottomLeftRadius === 'number'
|
|
74
|
+
? flat.borderBottomLeftRadius
|
|
75
|
+
: undefined;
|
|
76
|
+
const bottomRight =
|
|
77
|
+
typeof flat?.borderBottomRightRadius === 'number'
|
|
78
|
+
? flat.borderBottomRightRadius
|
|
79
|
+
: undefined;
|
|
80
|
+
|
|
81
|
+
if (
|
|
82
|
+
topLeft === undefined &&
|
|
83
|
+
topRight === undefined &&
|
|
84
|
+
bottomLeft === undefined &&
|
|
85
|
+
bottomRight === undefined
|
|
86
|
+
) {
|
|
87
|
+
return base;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
topLeft: topLeft ?? base,
|
|
92
|
+
topRight: topRight ?? base,
|
|
93
|
+
bottomLeft: bottomLeft ?? base,
|
|
94
|
+
bottomRight: bottomRight ?? base,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -4,7 +4,8 @@ import type { HybridObject } from 'react-native-nitro-modules';
|
|
|
4
4
|
export type CacheOption = 'memory' | 'disk' | 'none';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Independent corner radii in
|
|
7
|
+
* Independent corner radii, in pixels of the loaded bitmap (see
|
|
8
|
+
* {@linkcode Options.cornerRadius}). Omitted corners stay square.
|
|
8
9
|
*/
|
|
9
10
|
export interface CornerRadii {
|
|
10
11
|
topLeft?: number;
|
|
@@ -13,12 +14,25 @@ export interface CornerRadii {
|
|
|
13
14
|
bottomRight?: number;
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Target bitmap size in pixels. The image is scaled to fill this size and
|
|
19
|
+
* center-cropped (like CSS `object-fit: cover`), upscaling if needed, so the
|
|
20
|
+
* result is exactly `width` × `height` pixels.
|
|
21
|
+
*/
|
|
22
|
+
export interface ResizeOptions {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
16
27
|
export type Options = {
|
|
17
28
|
/**
|
|
18
29
|
* Gaussian blur strength, given as the standard deviation (sigma) of the
|
|
19
30
|
* blur in **source-image pixels**. The same value on the same source image
|
|
20
31
|
* produces the same result on iOS and Android.
|
|
21
32
|
*
|
|
33
|
+
* When {@linkcode resize} is set, the blur runs after resizing, so sigma is
|
|
34
|
+
* in pixels of the resized bitmap instead.
|
|
35
|
+
*
|
|
22
36
|
* React Native's `<Image blurRadius={n} />` is roughly `blur: n / 2`.
|
|
23
37
|
*
|
|
24
38
|
* @default 0 (no blur)
|
|
@@ -26,14 +40,29 @@ export type Options = {
|
|
|
26
40
|
blur?: number;
|
|
27
41
|
cache?: CacheOption;
|
|
28
42
|
/**
|
|
29
|
-
* Corner radius
|
|
30
|
-
* number to round all four corners uniformly, or a
|
|
31
|
-
* object to round each corner independently (e.g. a
|
|
32
|
-
* larger bottom corners).
|
|
43
|
+
* Corner radius baked into the loaded bitmap, in **pixels of that bitmap**.
|
|
44
|
+
* Pass a single number to round all four corners uniformly, or a
|
|
45
|
+
* {@linkcode CornerRadii} object to round each corner independently (e.g. a
|
|
46
|
+
* "ticket" shape with larger bottom corners).
|
|
47
|
+
*
|
|
48
|
+
* Without {@linkcode resize} the radius applies to the full-resolution
|
|
49
|
+
* source image, so on a large photo displayed small the rounding shrinks
|
|
50
|
+
* along with it. To get corners sized for your layout, pass `resize` with
|
|
51
|
+
* your display size in pixels (points × screen scale) — the radii then
|
|
52
|
+
* apply to that final bitmap, 1:1 with what you see.
|
|
33
53
|
*
|
|
34
54
|
* @default 0 (square corners)
|
|
35
55
|
*/
|
|
36
56
|
cornerRadius?: number | CornerRadii;
|
|
57
|
+
/**
|
|
58
|
+
* Resize the image to exactly this size in pixels (aspect-fill,
|
|
59
|
+
* center-crop) before `blur` and `cornerRadius` are applied. Besides making
|
|
60
|
+
* `cornerRadius` predictable, this avoids decoding and processing
|
|
61
|
+
* full-resolution bitmaps you only display small.
|
|
62
|
+
*
|
|
63
|
+
* @default undefined (keep the source size)
|
|
64
|
+
*/
|
|
65
|
+
resize?: ResizeOptions;
|
|
37
66
|
};
|
|
38
67
|
|
|
39
68
|
export interface NitroImagePipeline extends HybridObject<{
|