react-native-libyuv-resizer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/LibyuvResizer.podspec +20 -0
- package/README.md +188 -0
- package/android/CMakeLists.txt +30 -0
- package/android/build.gradle +100 -0
- package/android/src/androidTest/java/com/libyuvresizer/ExifCopierTest.kt +131 -0
- package/android/src/androidTest/java/com/libyuvresizer/FakePromise.kt +71 -0
- package/android/src/androidTest/java/com/libyuvresizer/FakeReactContext.kt +55 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleErrorTest.kt +135 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleExifTest.kt +140 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleFilterModeTest.kt +85 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleFormatTest.kt +146 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleIntegrationTest.kt +157 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleOutputPathTest.kt +96 -0
- package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleRotationTest.kt +120 -0
- package/android/src/androidTest/java/com/libyuvresizer/TestFixtures.kt +48 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/LibyuvResizerModule.cpp +137 -0
- package/android/src/main/java/com/libyuvresizer/DimensionCalculator.kt +52 -0
- package/android/src/main/java/com/libyuvresizer/ExifCopier.kt +133 -0
- package/android/src/main/java/com/libyuvresizer/LibyuvResizerModule.kt +179 -0
- package/android/src/main/java/com/libyuvresizer/LibyuvResizerPackage.kt +30 -0
- package/android/src/main/java/com/libyuvresizer/ResizeValidator.kt +71 -0
- package/android/src/test/java/com/libyuvresizer/DimensionCalculatorTest.kt +181 -0
- package/android/src/test/java/com/libyuvresizer/ResizeValidatorTest.kt +203 -0
- package/ios/LibyuvResizer.h +6 -0
- package/ios/LibyuvResizer.mm +31 -0
- package/lib/module/NativeLibyuvResizer.js +28 -0
- package/lib/module/NativeLibyuvResizer.js.map +1 -0
- package/lib/module/index.js +20 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/resizer.js +15 -0
- package/lib/module/resizer.js.map +1 -0
- package/lib/module/resizer.native.js +110 -0
- package/lib/module/resizer.native.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeLibyuvResizer.d.ts +52 -0
- package/lib/typescript/src/NativeLibyuvResizer.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +19 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/resizer.d.ts +13 -0
- package/lib/typescript/src/resizer.d.ts.map +1 -0
- package/lib/typescript/src/resizer.native.d.ts +119 -0
- package/lib/typescript/src/resizer.native.d.ts.map +1 -0
- package/package.json +184 -0
- package/src/NativeLibyuvResizer.ts +81 -0
- package/src/index.tsx +23 -0
- package/src/resizer.native.tsx +175 -0
- package/src/resizer.tsx +31 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {
|
|
2
|
+
NativeModules,
|
|
3
|
+
TurboModuleRegistry,
|
|
4
|
+
type TurboModule,
|
|
5
|
+
} from 'react-native';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Metadata returned by the native `resize()` bridge call.
|
|
9
|
+
*
|
|
10
|
+
* All numeric fields reflect the **output** file / bitmap — not the source.
|
|
11
|
+
*/
|
|
12
|
+
export type ResizeResult = {
|
|
13
|
+
/** Absolute path of the resized image file. */
|
|
14
|
+
path: string;
|
|
15
|
+
/** `file://` URI of the resized image file. */
|
|
16
|
+
uri: string;
|
|
17
|
+
/** File size in bytes. */
|
|
18
|
+
size: number;
|
|
19
|
+
/** File name (e.g. `"a3f2…1c.jpg"`). */
|
|
20
|
+
name: string;
|
|
21
|
+
/** Output bitmap width in pixels. */
|
|
22
|
+
width: number;
|
|
23
|
+
/** Output bitmap height in pixels. */
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Turbo Module contract for the native `LibyuvResizer` implementation.
|
|
29
|
+
*
|
|
30
|
+
* **Do not call this interface directly.** Use the public {@link resize}
|
|
31
|
+
* function from `./resizer` (or the package root) instead — it validates
|
|
32
|
+
* arguments and applies sensible defaults before forwarding to this bridge.
|
|
33
|
+
*
|
|
34
|
+
* This interface is consumed by React Native codegen to auto-generate the
|
|
35
|
+
* JSI glue code (`LibyuvResizerSpec`).
|
|
36
|
+
*/
|
|
37
|
+
export interface Spec extends TurboModule {
|
|
38
|
+
/**
|
|
39
|
+
* Low-level bridge method. Parameter order mirrors the public `resize()`
|
|
40
|
+
* signature with defaults already resolved.
|
|
41
|
+
*
|
|
42
|
+
* @param filePath - Absolute path to the source image.
|
|
43
|
+
* @param targetWidth - Output width in pixels.
|
|
44
|
+
* @param targetHeight - Output height in pixels.
|
|
45
|
+
* @param quality - JPEG/WebP quality `0–100`.
|
|
46
|
+
* @param rotation - Canonical rotation in degrees (`0 | 90 | 180 | 270`).
|
|
47
|
+
* @param mode - Resize mode string (`'contain' | 'cover' | 'stretch'`).
|
|
48
|
+
* @param outputPath - Absolute output path, or empty string for auto.
|
|
49
|
+
* @param filterMode - Scaling filter (`'none' | 'linear' | 'bilinear' | 'box'`).
|
|
50
|
+
* @param keepMeta - Copy EXIF tags from source to output (JPEG only, Android only).
|
|
51
|
+
* @param format - Output format (`'jpeg' | 'png' | 'webp'`).
|
|
52
|
+
* @returns Metadata about the resized image.
|
|
53
|
+
*/
|
|
54
|
+
resize(
|
|
55
|
+
filePath: string,
|
|
56
|
+
targetWidth: number,
|
|
57
|
+
targetHeight: number,
|
|
58
|
+
quality: number,
|
|
59
|
+
rotation: number,
|
|
60
|
+
mode: string,
|
|
61
|
+
outputPath: string,
|
|
62
|
+
filterMode: string,
|
|
63
|
+
keepMeta: boolean,
|
|
64
|
+
format: string
|
|
65
|
+
): Promise<ResizeResult>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isTurboModuleEnabled =
|
|
69
|
+
(globalThis as Record<string, unknown>).__turboModuleProxy != null;
|
|
70
|
+
|
|
71
|
+
const LibyuvResizerModule: Spec = isTurboModuleEnabled
|
|
72
|
+
? TurboModuleRegistry.getEnforcing<Spec>('LibyuvResizer')
|
|
73
|
+
: (NativeModules.LibyuvResizer as Spec);
|
|
74
|
+
|
|
75
|
+
if (!LibyuvResizerModule) {
|
|
76
|
+
throw new Error(
|
|
77
|
+
'react-native-libyuv-resizer: native module not found. Did you forget to link the library?'
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default LibyuvResizerModule;
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `react-native-libyuv-resizer` — high-performance image resizer for React
|
|
3
|
+
* Native backed by libyuv on Android.
|
|
4
|
+
*
|
|
5
|
+
* **Entry point.** Import {@link resize} and the supporting types from here.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { resize } from 'react-native-libyuv-resizer';
|
|
10
|
+
*
|
|
11
|
+
* const result = await resize('/path/to/photo.jpg', 1280, 720, 85);
|
|
12
|
+
* console.log(result.path, result.width, result.height);
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* @packageDocumentation
|
|
16
|
+
*/
|
|
17
|
+
export { resize } from './resizer';
|
|
18
|
+
export type {
|
|
19
|
+
RotationAngle,
|
|
20
|
+
ResizeMode,
|
|
21
|
+
ResizeOptions,
|
|
22
|
+
ResizeResult,
|
|
23
|
+
} from './resizer';
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import LibyuvResizer, { type ResizeResult } from './NativeLibyuvResizer';
|
|
2
|
+
|
|
3
|
+
export type { ResizeResult };
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Valid rotation values in degrees.
|
|
7
|
+
* Negative angles are normalised to their positive equivalents before being
|
|
8
|
+
* sent to the native layer (`-90` → `270`, etc.).
|
|
9
|
+
*/
|
|
10
|
+
export type RotationAngle = 0 | 90 | 180 | 270 | -90 | -180 | -270;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Controls how the source image is fitted into the target bounding box.
|
|
14
|
+
*
|
|
15
|
+
* - `'contain'` — scales uniformly so the entire image fits within the box,
|
|
16
|
+
* preserving aspect ratio. Empty space is left uncropped.
|
|
17
|
+
* - `'cover'` — scales uniformly so the image fills the box, preserving
|
|
18
|
+
* aspect ratio. Excess pixels are cropped.
|
|
19
|
+
* - `'stretch'` — scales to exactly `targetWidth × targetHeight`, ignoring
|
|
20
|
+
* aspect ratio.
|
|
21
|
+
*/
|
|
22
|
+
export type ResizeMode = 'contain' | 'cover' | 'stretch';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Scaling filter applied during the resize operation.
|
|
26
|
+
*
|
|
27
|
+
* Higher quality filters are slower. Choose based on your latency vs quality
|
|
28
|
+
* requirements.
|
|
29
|
+
*
|
|
30
|
+
* - `'none'` — nearest-neighbour; fastest, lowest quality.
|
|
31
|
+
* - `'linear'` — linear interpolation.
|
|
32
|
+
* - `'bilinear'` — bilinear interpolation.
|
|
33
|
+
* - `'box'` — box filter; best quality for downscaling *(default)*.
|
|
34
|
+
*/
|
|
35
|
+
export type FilterMode = 'none' | 'linear' | 'bilinear' | 'box';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Output image format.
|
|
39
|
+
*
|
|
40
|
+
* When omitted, format is derived from `quality`: `quality === 100` → `'png'`, else `'jpeg'`.
|
|
41
|
+
* When specified, this value takes precedence over the quality-based heuristic.
|
|
42
|
+
*
|
|
43
|
+
* - `'jpeg'` — lossy JPEG.
|
|
44
|
+
* - `'png'` — lossless PNG. `quality` is ignored.
|
|
45
|
+
* - `'webp'` — lossy WebP. `quality` controls compression level *(Android only; iOS produces JPEG)*.
|
|
46
|
+
*/
|
|
47
|
+
export type OutputFormat = 'jpeg' | 'png' | 'webp';
|
|
48
|
+
|
|
49
|
+
/** Options accepted by {@link resize}. */
|
|
50
|
+
export interface ResizeOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Clockwise rotation applied to the image **before** resizing.
|
|
53
|
+
* @default 0
|
|
54
|
+
*/
|
|
55
|
+
rotation?: RotationAngle;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* How the image is fitted into the target bounding box.
|
|
59
|
+
* @default 'contain'
|
|
60
|
+
*/
|
|
61
|
+
mode?: ResizeMode;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Scaling filter used during the resize operation.
|
|
65
|
+
* @default 'box'
|
|
66
|
+
*/
|
|
67
|
+
filterMode?: FilterMode;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Absolute path for the output file.
|
|
71
|
+
* When omitted the native layer generates a path in the app's cache
|
|
72
|
+
* directory automatically.
|
|
73
|
+
*/
|
|
74
|
+
outputPath?: string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* When `true`, copies all EXIF tags from the source image to the output
|
|
78
|
+
* JPEG. Has no effect on PNG or WebP output, or on iOS (no-op).
|
|
79
|
+
* @default false
|
|
80
|
+
* @platform android
|
|
81
|
+
*/
|
|
82
|
+
keepMeta?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Output image format. When omitted, `quality === 100` produces PNG; otherwise JPEG.
|
|
86
|
+
* Specifying `format` takes precedence over the quality-based heuristic.
|
|
87
|
+
* @default derived from quality
|
|
88
|
+
*/
|
|
89
|
+
format?: OutputFormat;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const VALID_MODES: ResizeMode[] = ['contain', 'cover', 'stretch'];
|
|
93
|
+
const VALID_FILTER_MODES: FilterMode[] = ['none', 'linear', 'bilinear', 'box'];
|
|
94
|
+
const VALID_FORMATS: OutputFormat[] = ['jpeg', 'png', 'webp'];
|
|
95
|
+
|
|
96
|
+
/** Normalises negative or out-of-range angles to 0 | 90 | 180 | 270. */
|
|
97
|
+
function toCanonicalAngle(angle: RotationAngle): 0 | 90 | 180 | 270 {
|
|
98
|
+
return (((angle % 360) + 360) % 360) as 0 | 90 | 180 | 270;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Resizes an image using the libyuv native backend (Android).
|
|
103
|
+
*
|
|
104
|
+
* The source image is read from `filePath`, resized to the requested
|
|
105
|
+
* dimensions, and saved as a JPEG. The absolute path of the output file is
|
|
106
|
+
* returned on success.
|
|
107
|
+
*
|
|
108
|
+
* @param filePath - Absolute path to the source image.
|
|
109
|
+
* @param targetWidth - Output width in pixels (must be > 0).
|
|
110
|
+
* @param targetHeight - Output height in pixels (must be > 0).
|
|
111
|
+
* @param quality - JPEG encoding quality from `1` (lowest) to `100` (highest).
|
|
112
|
+
* Use `100` to produce PNG output instead of JPEG.
|
|
113
|
+
* @param options - Optional resize behaviour overrides.
|
|
114
|
+
* @returns A `Promise` that resolves to a {@link ResizeResult} with the output
|
|
115
|
+
* file path, URI, size, name, and dimensions.
|
|
116
|
+
* @throws {TypeError} When `options.mode` or `options.filterMode` is not one
|
|
117
|
+
* of the accepted string literals.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* // Basic resize
|
|
122
|
+
* const result = await resize('/path/to/photo.jpg', 1280, 720, 85);
|
|
123
|
+
* console.log(result.path, result.width, result.height);
|
|
124
|
+
*
|
|
125
|
+
* // With options
|
|
126
|
+
* const result = await resize('/path/to/photo.jpg', 800, 600, 80, {
|
|
127
|
+
* rotation: 90,
|
|
128
|
+
* mode: 'cover',
|
|
129
|
+
* filterMode: 'bilinear',
|
|
130
|
+
* outputPath: '/path/to/output-dir',
|
|
131
|
+
* });
|
|
132
|
+
*
|
|
133
|
+
* // Preserve EXIF (GPS, camera, date) — Android only
|
|
134
|
+
* const result = await resize('/path/to/photo.jpg', 800, 600, 80, {
|
|
135
|
+
* keepMeta: true,
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export function resize(
|
|
140
|
+
filePath: string,
|
|
141
|
+
targetWidth: number,
|
|
142
|
+
targetHeight: number,
|
|
143
|
+
quality: number,
|
|
144
|
+
options?: ResizeOptions
|
|
145
|
+
): Promise<ResizeResult> {
|
|
146
|
+
const rotation =
|
|
147
|
+
options?.rotation != null ? toCanonicalAngle(options.rotation) : 0;
|
|
148
|
+
const mode: ResizeMode = options?.mode ?? 'contain';
|
|
149
|
+
if (!VALID_MODES.includes(mode)) {
|
|
150
|
+
return Promise.reject(new TypeError(`Invalid resize mode: '${mode}'`));
|
|
151
|
+
}
|
|
152
|
+
const filterMode: FilterMode = options?.filterMode ?? 'box';
|
|
153
|
+
if (!VALID_FILTER_MODES.includes(filterMode)) {
|
|
154
|
+
return Promise.reject(
|
|
155
|
+
new TypeError(`Invalid filter mode: '${filterMode}'`)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const format: OutputFormat =
|
|
159
|
+
options?.format ?? (quality === 100 ? 'png' : 'jpeg');
|
|
160
|
+
if (!VALID_FORMATS.includes(format)) {
|
|
161
|
+
return Promise.reject(new TypeError(`Invalid format: '${format}'`));
|
|
162
|
+
}
|
|
163
|
+
return LibyuvResizer.resize(
|
|
164
|
+
filePath,
|
|
165
|
+
targetWidth,
|
|
166
|
+
targetHeight,
|
|
167
|
+
quality,
|
|
168
|
+
rotation,
|
|
169
|
+
mode,
|
|
170
|
+
options?.outputPath ?? '',
|
|
171
|
+
filterMode,
|
|
172
|
+
options?.keepMeta ?? false,
|
|
173
|
+
format
|
|
174
|
+
);
|
|
175
|
+
}
|
package/src/resizer.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ResizeOptions, ResizeResult } from './resizer.native';
|
|
2
|
+
|
|
3
|
+
export type {
|
|
4
|
+
RotationAngle,
|
|
5
|
+
ResizeMode,
|
|
6
|
+
ResizeOptions,
|
|
7
|
+
ResizeResult,
|
|
8
|
+
} from './resizer.native';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Web / non-native fallback for {@link resize}.
|
|
12
|
+
*
|
|
13
|
+
* This module is selected by Metro on platforms other than Android/iOS.
|
|
14
|
+
* It always rejects because `react-native-libyuv-resizer` requires a native
|
|
15
|
+
* runtime. Import the real implementation via the `.native` platform extension.
|
|
16
|
+
*
|
|
17
|
+
* @throws {Error} Always — native platform required.
|
|
18
|
+
*/
|
|
19
|
+
export function resize(
|
|
20
|
+
_filePath: string,
|
|
21
|
+
_targetWidth: number,
|
|
22
|
+
_targetHeight: number,
|
|
23
|
+
_quality: number,
|
|
24
|
+
_options?: ResizeOptions
|
|
25
|
+
): Promise<ResizeResult> {
|
|
26
|
+
return Promise.reject(
|
|
27
|
+
new Error(
|
|
28
|
+
"'react-native-libyuv-resizer' is only supported on native platforms."
|
|
29
|
+
)
|
|
30
|
+
);
|
|
31
|
+
}
|