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.
Files changed (50) hide show
  1. package/LICENSE +20 -0
  2. package/LibyuvResizer.podspec +20 -0
  3. package/README.md +188 -0
  4. package/android/CMakeLists.txt +30 -0
  5. package/android/build.gradle +100 -0
  6. package/android/src/androidTest/java/com/libyuvresizer/ExifCopierTest.kt +131 -0
  7. package/android/src/androidTest/java/com/libyuvresizer/FakePromise.kt +71 -0
  8. package/android/src/androidTest/java/com/libyuvresizer/FakeReactContext.kt +55 -0
  9. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleErrorTest.kt +135 -0
  10. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleExifTest.kt +140 -0
  11. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleFilterModeTest.kt +85 -0
  12. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleFormatTest.kt +146 -0
  13. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleIntegrationTest.kt +157 -0
  14. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleOutputPathTest.kt +96 -0
  15. package/android/src/androidTest/java/com/libyuvresizer/LibyuvResizerModuleRotationTest.kt +120 -0
  16. package/android/src/androidTest/java/com/libyuvresizer/TestFixtures.kt +48 -0
  17. package/android/src/main/AndroidManifest.xml +2 -0
  18. package/android/src/main/cpp/LibyuvResizerModule.cpp +137 -0
  19. package/android/src/main/java/com/libyuvresizer/DimensionCalculator.kt +52 -0
  20. package/android/src/main/java/com/libyuvresizer/ExifCopier.kt +133 -0
  21. package/android/src/main/java/com/libyuvresizer/LibyuvResizerModule.kt +179 -0
  22. package/android/src/main/java/com/libyuvresizer/LibyuvResizerPackage.kt +30 -0
  23. package/android/src/main/java/com/libyuvresizer/ResizeValidator.kt +71 -0
  24. package/android/src/test/java/com/libyuvresizer/DimensionCalculatorTest.kt +181 -0
  25. package/android/src/test/java/com/libyuvresizer/ResizeValidatorTest.kt +203 -0
  26. package/ios/LibyuvResizer.h +6 -0
  27. package/ios/LibyuvResizer.mm +31 -0
  28. package/lib/module/NativeLibyuvResizer.js +28 -0
  29. package/lib/module/NativeLibyuvResizer.js.map +1 -0
  30. package/lib/module/index.js +20 -0
  31. package/lib/module/index.js.map +1 -0
  32. package/lib/module/package.json +1 -0
  33. package/lib/module/resizer.js +15 -0
  34. package/lib/module/resizer.js.map +1 -0
  35. package/lib/module/resizer.native.js +110 -0
  36. package/lib/module/resizer.native.js.map +1 -0
  37. package/lib/typescript/package.json +1 -0
  38. package/lib/typescript/src/NativeLibyuvResizer.d.ts +52 -0
  39. package/lib/typescript/src/NativeLibyuvResizer.d.ts.map +1 -0
  40. package/lib/typescript/src/index.d.ts +19 -0
  41. package/lib/typescript/src/index.d.ts.map +1 -0
  42. package/lib/typescript/src/resizer.d.ts +13 -0
  43. package/lib/typescript/src/resizer.d.ts.map +1 -0
  44. package/lib/typescript/src/resizer.native.d.ts +119 -0
  45. package/lib/typescript/src/resizer.native.d.ts.map +1 -0
  46. package/package.json +184 -0
  47. package/src/NativeLibyuvResizer.ts +81 -0
  48. package/src/index.tsx +23 -0
  49. package/src/resizer.native.tsx +175 -0
  50. 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
+ }
@@ -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
+ }