rn-remove-image-bg 0.0.10

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 (74) hide show
  1. package/NitroRnRemoveImageBg.podspec +33 -0
  2. package/README.md +386 -0
  3. package/android/CMakeLists.txt +28 -0
  4. package/android/build.gradle +142 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt +189 -0
  10. package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgPackage.kt +31 -0
  11. package/app.plugin.js +12 -0
  12. package/ios/Bridge.h +8 -0
  13. package/ios/HybridImageBackgroundRemover.swift +224 -0
  14. package/ios/NitroRnRemoveImageBgOnLoad.mm +22 -0
  15. package/lib/ImageProcessing.d.ts +167 -0
  16. package/lib/ImageProcessing.js +323 -0
  17. package/lib/ImageProcessing.web.d.ts +80 -0
  18. package/lib/ImageProcessing.web.js +248 -0
  19. package/lib/__tests__/cache.test.d.ts +1 -0
  20. package/lib/__tests__/cache.test.js +87 -0
  21. package/lib/__tests__/errors.test.d.ts +1 -0
  22. package/lib/__tests__/errors.test.js +82 -0
  23. package/lib/cache.d.ts +72 -0
  24. package/lib/cache.js +228 -0
  25. package/lib/errors.d.ts +20 -0
  26. package/lib/errors.js +64 -0
  27. package/lib/index.d.ts +6 -0
  28. package/lib/index.js +9 -0
  29. package/lib/specs/Example.nitro.d.ts +0 -0
  30. package/lib/specs/Example.nitro.js +2 -0
  31. package/lib/specs/ImageBackgroundRemover.nitro.d.ts +41 -0
  32. package/lib/specs/ImageBackgroundRemover.nitro.js +1 -0
  33. package/nitro.json +17 -0
  34. package/nitrogen/generated/.gitattributes +1 -0
  35. package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.cmake +81 -0
  36. package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.gradle +27 -0
  37. package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.cpp +44 -0
  38. package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.hpp +25 -0
  39. package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.cpp +72 -0
  40. package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.hpp +65 -0
  41. package/nitrogen/generated/android/c++/JNativeRemoveBackgroundOptions.hpp +66 -0
  42. package/nitrogen/generated/android/c++/JOutputFormat.hpp +59 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemoverSpec.kt +58 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NativeRemoveBackgroundOptions.kt +44 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgOnLoad.kt +35 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/OutputFormat.kt +21 -0
  47. package/nitrogen/generated/ios/NitroRnRemoveImageBg+autolinking.rb +60 -0
  48. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.cpp +49 -0
  49. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.hpp +111 -0
  50. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Umbrella.hpp +51 -0
  51. package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.cpp +11 -0
  52. package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.hpp +82 -0
  53. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  54. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  55. package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec.swift +56 -0
  56. package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec_cxx.swift +138 -0
  57. package/nitrogen/generated/ios/swift/NativeRemoveBackgroundOptions.swift +58 -0
  58. package/nitrogen/generated/ios/swift/OutputFormat.swift +40 -0
  59. package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.cpp +21 -0
  60. package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.hpp +65 -0
  61. package/nitrogen/generated/shared/c++/NativeRemoveBackgroundOptions.hpp +84 -0
  62. package/nitrogen/generated/shared/c++/OutputFormat.hpp +76 -0
  63. package/package.json +104 -0
  64. package/react-native.config.js +16 -0
  65. package/src/ImageProcessing.ts +532 -0
  66. package/src/ImageProcessing.web.ts +342 -0
  67. package/src/__tests__/ImageProcessing.test.ts +278 -0
  68. package/src/__tests__/cache.test.ts +110 -0
  69. package/src/__tests__/errors.test.ts +117 -0
  70. package/src/cache.ts +305 -0
  71. package/src/errors.ts +93 -0
  72. package/src/index.ts +49 -0
  73. package/src/specs/Example.nitro.ts +1 -0
  74. package/src/specs/ImageBackgroundRemover.nitro.ts +49 -0
@@ -0,0 +1,532 @@
1
+ import { Image } from 'react-native';
2
+ import * as ImageManipulator from 'expo-image-manipulator';
3
+ import * as FileSystem from 'expo-file-system/legacy';
4
+ import { Buffer } from 'buffer';
5
+ import { rgbaToThumbHash } from 'thumbhash';
6
+ import { NitroModules } from 'react-native-nitro-modules';
7
+ import type {
8
+ ImageBackgroundRemover,
9
+ OutputFormat,
10
+ NativeRemoveBackgroundOptions,
11
+ } from './specs/ImageBackgroundRemover.nitro';
12
+ import { BackgroundRemovalError, wrapNativeError } from './errors';
13
+ import { bgRemovalCache } from './cache';
14
+
15
+ // Re-export types
16
+ export type { OutputFormat, NativeRemoveBackgroundOptions };
17
+
18
+ let nativeRemover: ImageBackgroundRemover | undefined;
19
+
20
+ function getNativeRemover(): ImageBackgroundRemover {
21
+ if (!nativeRemover) {
22
+ nativeRemover = NitroModules.createHybridObject<ImageBackgroundRemover>(
23
+ 'ImageBackgroundRemover'
24
+ );
25
+ }
26
+ return nativeRemover;
27
+ }
28
+
29
+ /**
30
+ * Validate image path format
31
+ */
32
+ function validateImagePath(uri: string): void {
33
+ if (!uri || uri.trim().length === 0) {
34
+ throw new BackgroundRemovalError(
35
+ 'Image path cannot be empty',
36
+ 'INVALID_PATH'
37
+ );
38
+ }
39
+
40
+ // Must be a file path or file:// URI
41
+ const isValidPath =
42
+ uri.startsWith('file://') ||
43
+ uri.startsWith('/') ||
44
+ uri.match(/^[a-zA-Z]:\\/);
45
+ if (!isValidPath) {
46
+ throw new BackgroundRemovalError(
47
+ `Invalid file path format: ${uri}. Expected file:// URI or absolute path.`,
48
+ 'INVALID_PATH'
49
+ );
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Validate options
55
+ */
56
+ function validateOptions(options: Partial<RemoveBgImageOptions>): void {
57
+ if (options.maxDimension !== undefined) {
58
+ if (options.maxDimension < 100 || options.maxDimension > 8192) {
59
+ throw new BackgroundRemovalError(
60
+ 'maxDimension must be between 100 and 8192',
61
+ 'INVALID_OPTIONS'
62
+ );
63
+ }
64
+ }
65
+
66
+ if (options.quality !== undefined) {
67
+ if (options.quality < 0 || options.quality > 100) {
68
+ throw new BackgroundRemovalError(
69
+ 'quality must be between 0 and 100',
70
+ 'INVALID_OPTIONS'
71
+ );
72
+ }
73
+ }
74
+
75
+ if (
76
+ options.format !== undefined &&
77
+ !['PNG', 'WEBP'].includes(options.format)
78
+ ) {
79
+ throw new BackgroundRemovalError(
80
+ 'format must be either "PNG" or "WEBP"',
81
+ 'INVALID_OPTIONS'
82
+ );
83
+ }
84
+ }
85
+
86
+ export interface CompressImageOptions {
87
+ /**
88
+ * Maximum file size in KB (default: 250)
89
+ */
90
+ maxSizeKB?: number;
91
+ /**
92
+ * Initial image width (default: 1024)
93
+ */
94
+ width?: number;
95
+ /**
96
+ * Initial image height (default: 1024)
97
+ */
98
+ height?: number;
99
+ /**
100
+ * Initial compression quality (0-1, default: 0.85)
101
+ */
102
+ quality?: number;
103
+ /**
104
+ * Image format (default: WEBP)
105
+ */
106
+ format?: ImageManipulator.SaveFormat;
107
+ }
108
+
109
+ export interface GenerateThumbhashOptions {
110
+ /**
111
+ * Thumbhash size (default: 32)
112
+ */
113
+ size?: number;
114
+ }
115
+
116
+ /**
117
+ * Options for background removal
118
+ */
119
+ export interface RemoveBgImageOptions {
120
+ /**
121
+ * Maximum dimension (width or height) for processing
122
+ * Larger images will be downsampled for better performance
123
+ * @default 2048
124
+ */
125
+ maxDimension?: number;
126
+
127
+ /**
128
+ * Output image format
129
+ * - PNG: Lossless, larger file size, best for transparency
130
+ * - WEBP: Smaller file size, good quality
131
+ * @default 'PNG'
132
+ */
133
+ format?: OutputFormat;
134
+
135
+ /**
136
+ * Quality for WEBP format (0-100)
137
+ * Ignored when format is PNG
138
+ * @default 100
139
+ */
140
+ quality?: number;
141
+
142
+ /**
143
+ * Progress callback (0-100)
144
+ * Note: Progress is approximate and may not be linear
145
+ */
146
+ onProgress?: (progress: number) => void;
147
+
148
+ /**
149
+ * Use cached result if available
150
+ * @default true
151
+ */
152
+ useCache?: boolean;
153
+
154
+ /**
155
+ * Enable debug logging
156
+ * @default false
157
+ */
158
+ debug?: boolean;
159
+ }
160
+
161
+ /** Default options for background removal */
162
+ const DEFAULT_OPTIONS: Required<Omit<RemoveBgImageOptions, 'onProgress'>> = {
163
+ maxDimension: 2048,
164
+ format: 'PNG',
165
+ quality: 100,
166
+ useCache: true,
167
+ debug: false,
168
+ };
169
+
170
+ /**
171
+ * Compress image to WebP format with configurable options
172
+ */
173
+ export async function compressImage(
174
+ uri: string,
175
+ options: CompressImageOptions = {}
176
+ ) {
177
+ const {
178
+ maxSizeKB = 250,
179
+ width = 1024,
180
+ height = 1024,
181
+ quality = 0.85,
182
+ format = ImageManipulator.SaveFormat.WEBP,
183
+ } = options;
184
+
185
+ const startTime = Date.now();
186
+ const maxSize = maxSizeKB * 1024;
187
+
188
+ // Get original file size and dimensions
189
+ const originalInfo = await FileSystem.getInfoAsync(uri);
190
+ const originalSize = 'size' in originalInfo ? originalInfo.size : 0;
191
+
192
+ const { width: originalWidth, height: originalHeight } = await new Promise<{
193
+ width: number;
194
+ height: number;
195
+ }>((resolve, reject) => {
196
+ Image.getSize(uri, (w, h) => resolve({ width: w, height: h }), reject);
197
+ });
198
+
199
+ // Calculate target dimensions maintaining aspect ratio
200
+ // We want the image to fit WITHIN the bounding box defined by width x height
201
+ const scale = Math.min(width / originalWidth, height / originalHeight);
202
+
203
+ // If image is smaller than target box, we can keep original size (scale = 1) if we don't want to upscale.
204
+ // Generally "compress" implies making smaller or equal.
205
+ // If the image is larger, scale < 1. If smaller, scale >= 1.
206
+ // Let's cap scale at 1 to prevent upscaling unless explicitly desired (usually not for compression).
207
+ const finalScale = Math.min(scale, 1);
208
+
209
+ const resizeWidth = Math.round(originalWidth * finalScale);
210
+ const resizeHeight = Math.round(originalHeight * finalScale);
211
+
212
+ // Start with calculated dimensions and quality
213
+ let result = await ImageManipulator.manipulateAsync(
214
+ uri,
215
+ [{ resize: { width: resizeWidth, height: resizeHeight } }],
216
+ {
217
+ compress: quality,
218
+ format,
219
+ }
220
+ );
221
+
222
+ let fileInfo = await FileSystem.getInfoAsync(result.uri);
223
+
224
+ // If still too large, reduce quality
225
+ if ('size' in fileInfo && fileInfo.size > maxSize) {
226
+ let currentQuality = quality * 0.9;
227
+
228
+ while (
229
+ currentQuality > 0.5 &&
230
+ 'size' in fileInfo &&
231
+ fileInfo.size > maxSize
232
+ ) {
233
+ result = await ImageManipulator.manipulateAsync(
234
+ uri,
235
+ [{ resize: { width: resizeWidth, height: resizeHeight } }],
236
+ {
237
+ compress: currentQuality,
238
+ format,
239
+ }
240
+ );
241
+
242
+ fileInfo = await FileSystem.getInfoAsync(result.uri);
243
+ if ('size' in fileInfo && fileInfo.size <= maxSize) break;
244
+
245
+ currentQuality -= 0.05;
246
+ }
247
+
248
+ // If still too large, reduce dimensions
249
+ if ('size' in fileInfo && fileInfo.size > maxSize) {
250
+ const smallerWidth = Math.floor(resizeWidth * 0.75);
251
+ const smallerHeight = Math.floor(resizeHeight * 0.75);
252
+
253
+ result = await ImageManipulator.manipulateAsync(
254
+ uri,
255
+ [{ resize: { width: smallerWidth, height: smallerHeight } }],
256
+ {
257
+ compress: 0.75,
258
+ format,
259
+ }
260
+ );
261
+ fileInfo = await FileSystem.getInfoAsync(result.uri);
262
+
263
+ // Final quality reduction if needed
264
+ if ('size' in fileInfo && fileInfo.size > maxSize) {
265
+ let finalQuality = 0.7;
266
+ while (
267
+ finalQuality > 0.5 &&
268
+ 'size' in fileInfo &&
269
+ fileInfo.size > maxSize
270
+ ) {
271
+ result = await ImageManipulator.manipulateAsync(
272
+ uri,
273
+ [{ resize: { width: smallerWidth, height: smallerHeight } }],
274
+ {
275
+ compress: finalQuality,
276
+ format,
277
+ }
278
+ );
279
+ fileInfo = await FileSystem.getInfoAsync(result.uri);
280
+ if ('size' in fileInfo && fileInfo.size <= maxSize) break;
281
+ finalQuality -= 0.05;
282
+ }
283
+ }
284
+ }
285
+ }
286
+
287
+ const finalSize = 'size' in fileInfo ? fileInfo.size : 0;
288
+ const duration = Date.now() - startTime;
289
+
290
+ console.log(`[Native] Image Compression:`, {
291
+ originalSize: `${(originalSize / 1024).toFixed(2)} KB`,
292
+ compressedSize: `${(finalSize / 1024).toFixed(2)} KB`,
293
+ reduction: `${(((originalSize - finalSize) / originalSize) * 100).toFixed(1)}%`,
294
+ duration: `${duration}ms`,
295
+ dimensions: `${resizeWidth}x${resizeHeight}`,
296
+ });
297
+
298
+ return result.uri;
299
+ }
300
+
301
+ /**
302
+ * Generate thumbhash from image URI (Native/Mobile)
303
+ */
304
+ export async function generateThumbhash(
305
+ imageUri: string,
306
+ options: GenerateThumbhashOptions = {}
307
+ ) {
308
+ const { size = 32 } = options;
309
+
310
+ // 1. Create tiny PNG
311
+ const tiny = await ImageManipulator.manipulateAsync(
312
+ imageUri,
313
+ [{ resize: { width: size, height: size } }],
314
+ { format: ImageManipulator.SaveFormat.PNG }
315
+ );
316
+
317
+ // 2. Read as base64
318
+ const base64 = await FileSystem.readAsStringAsync(tiny.uri, {
319
+ encoding: 'base64',
320
+ });
321
+
322
+ // 3. Decode PNG and generate thumbhash
323
+ const UPNG = require('upng-js');
324
+ const buffer = Buffer.from(base64, 'base64');
325
+ const img = UPNG.decode(buffer);
326
+ const rgba = UPNG.toRGBA8(img)[0];
327
+ const hash = rgbaToThumbHash(size, size, new Uint8Array(rgba));
328
+
329
+ // 4. Convert to base64
330
+ return Buffer.from(hash).toString('base64');
331
+ }
332
+
333
+ /**
334
+ * Remove background from image using native ML models
335
+ *
336
+ * @param uri - File path or file:// URI to the source image
337
+ * @param options - Processing options
338
+ * @returns Promise resolving to a URI suitable for use with `<Image>` component.
339
+ * - **iOS/Android**: File path (`file:///path/to/cache/bg_removed_xxx.png`)
340
+ * - **Web**: Data URL (`data:image/png;base64,...`)
341
+ *
342
+ * @throws {BackgroundRemovalError} When image cannot be processed
343
+ *
344
+ * @example
345
+ * ```typescript
346
+ * const result = await removeBgImage('file:///path/to/photo.jpg')
347
+ * // Use directly in Image component
348
+ * <Image source={{ uri: result }} />
349
+ * ```
350
+ *
351
+ * @example
352
+ * ```typescript
353
+ * // With options
354
+ * const result = await removeBgImage('file:///path/to/photo.jpg', {
355
+ * maxDimension: 1024,
356
+ * format: 'WEBP',
357
+ * quality: 90,
358
+ * onProgress: (p) => console.log(`Progress: ${p}%`)
359
+ * })
360
+ * ```
361
+ */
362
+ export async function removeBgImage(
363
+ uri: string,
364
+ options: RemoveBgImageOptions = {}
365
+ ): Promise<string> {
366
+ const startTime = Date.now();
367
+ const opts = { ...DEFAULT_OPTIONS, ...options };
368
+ const { onProgress, debug } = opts;
369
+
370
+ // Validate inputs
371
+ validateImagePath(uri);
372
+ validateOptions(options);
373
+
374
+ if (debug) {
375
+ console.log('[rn-remove-image-bg] Starting background removal:', uri);
376
+ console.log('[rn-remove-image-bg] Options:', opts);
377
+ }
378
+
379
+ // Report initial progress
380
+ onProgress?.(5);
381
+
382
+ // Check cache if enabled
383
+ if (opts.useCache) {
384
+ const optionsHash = bgRemovalCache.hashOptions({
385
+ maxDimension: opts.maxDimension,
386
+ format: opts.format,
387
+ quality: opts.quality,
388
+ });
389
+
390
+ const cached = await bgRemovalCache.get(uri, optionsHash);
391
+ if (cached) {
392
+ if (debug) {
393
+ console.log('[rn-remove-image-bg] Cache hit:', cached);
394
+ }
395
+ onProgress?.(100);
396
+ return cached.startsWith('file://') ? cached : `file://${cached}`;
397
+ }
398
+ }
399
+
400
+ onProgress?.(10);
401
+
402
+ try {
403
+ // Prepare native options
404
+ const nativeOptions: NativeRemoveBackgroundOptions = {
405
+ maxDimension: opts.maxDimension,
406
+ format: opts.format,
407
+ quality: opts.quality,
408
+ };
409
+
410
+ onProgress?.(20);
411
+
412
+ // Call native implementation
413
+ const result = await getNativeRemover().removeBackground(
414
+ uri,
415
+ nativeOptions
416
+ );
417
+
418
+ onProgress?.(90);
419
+
420
+ // Normalize result path
421
+ const resultPath = result.startsWith('file://')
422
+ ? result
423
+ : `file://${result}`;
424
+
425
+ // Cache the result
426
+ if (opts.useCache) {
427
+ const optionsHash = bgRemovalCache.hashOptions({
428
+ maxDimension: opts.maxDimension,
429
+ format: opts.format,
430
+ quality: opts.quality,
431
+ });
432
+ bgRemovalCache.set(uri, optionsHash, resultPath);
433
+ }
434
+
435
+ if (debug) {
436
+ console.log(
437
+ '[rn-remove-image-bg] Completed in',
438
+ Date.now() - startTime,
439
+ 'ms'
440
+ );
441
+ console.log('[rn-remove-image-bg] Result:', resultPath);
442
+ }
443
+
444
+ onProgress?.(100);
445
+ return resultPath;
446
+ } catch (error) {
447
+ if (debug) {
448
+ console.error('[rn-remove-image-bg] Failed:', error);
449
+ }
450
+ throw wrapNativeError(error);
451
+ }
452
+ }
453
+
454
+ /**
455
+ * Backward compatibility alias for removeBgImage
456
+ * @deprecated Use removeBgImage instead
457
+ */
458
+ export const removeBackground = removeBgImage;
459
+
460
+ /**
461
+ * Clear the background removal cache
462
+ * @param deleteFiles - Also delete cached files from disk (default: false)
463
+ */
464
+ export async function clearCache(deleteFiles = false): Promise<void> {
465
+ await bgRemovalCache.clear(deleteFiles);
466
+ }
467
+
468
+ /**
469
+ * Get the current cache size
470
+ */
471
+ export function getCacheSize(): number {
472
+ return bgRemovalCache.size;
473
+ }
474
+
475
+ /**
476
+ * Handle low memory conditions by clearing the cache
477
+ * Call this when your app receives memory warnings
478
+ *
479
+ * @param deleteFiles - Also delete cached files from disk (default: true)
480
+ * @returns Number of entries that were cleared
481
+ *
482
+ * @example
483
+ * ```typescript
484
+ * import { AppState } from 'react-native'
485
+ * import { onLowMemory } from 'rn-remove-image-bg'
486
+ *
487
+ * // In your app initialization
488
+ * AppState.addEventListener('memoryWarning', () => {
489
+ * onLowMemory()
490
+ * })
491
+ * ```
492
+ */
493
+ export async function onLowMemory(deleteFiles = true): Promise<number> {
494
+ const size = bgRemovalCache.size;
495
+ await bgRemovalCache.clear(deleteFiles);
496
+ console.log(
497
+ `[rn-remove-image-bg] Cleared ${size} cache entries due to memory pressure`
498
+ );
499
+ return size;
500
+ }
501
+
502
+ /**
503
+ * Configure the background removal cache
504
+ * Call this early in your app lifecycle to customize cache behavior
505
+ *
506
+ * @example
507
+ * ```typescript
508
+ * import { configureCache } from 'rn-remove-image-bg'
509
+ *
510
+ * configureCache({
511
+ * maxEntries: 100,
512
+ * maxAgeMinutes: 60,
513
+ * persistToDisk: true
514
+ * })
515
+ * ```
516
+ */
517
+ export function configureCache(config: {
518
+ maxEntries?: number;
519
+ maxAgeMinutes?: number;
520
+ persistToDisk?: boolean;
521
+ cacheDirectory?: string;
522
+ }): void {
523
+ bgRemovalCache.configure(config);
524
+ }
525
+
526
+ /**
527
+ * Get the cache directory path
528
+ * Useful for debugging or manual cache management
529
+ */
530
+ export function getCacheDirectory(): string {
531
+ return bgRemovalCache.getCacheDirectory();
532
+ }