rn-remove-image-bg 0.0.12 → 0.0.13

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 CHANGED
@@ -15,28 +15,8 @@
15
15
 
16
16
  ## Installation
17
17
 
18
- Install from GitHub:
19
-
20
18
  ```bash
21
- # Using npm
22
- npm install github:a-eid/rn-remove-image-bg react-native-nitro-modules
23
-
24
- # Using yarn
25
- yarn add github:a-eid/rn-remove-image-bg react-native-nitro-modules
26
-
27
- # Using pnpm
28
- pnpm add github:a-eid/rn-remove-image-bg react-native-nitro-modules
29
- ```
30
-
31
- Or add to your `package.json`:
32
-
33
- ```json
34
- {
35
- "dependencies": {
36
- "rn-remove-image-bg": "github:a-eid/rn-remove-image-bg",
37
- "react-native-nitro-modules": "0.31.10"
38
- }
39
- }
19
+ npm install rn-remove-image-bg react-native-nitro-modules
40
20
  ```
41
21
 
42
22
  ### Peer Dependencies
@@ -59,18 +39,86 @@ cd ios && pod install
59
39
 
60
40
  No additional setup required. The ML Kit model (~10MB) downloads automatically on first use.
61
41
 
42
+ > **Important:** Android requires Google Play Services. The first call may take 10-15 seconds while the model downloads.
43
+
62
44
  ---
63
45
 
64
46
  ## Quick Start
65
47
 
48
+ ### Basic Usage
49
+
66
50
  ```typescript
67
51
  import { removeBgImage } from 'rn-remove-image-bg'
68
52
 
69
- // Remove background from an image
70
53
  const resultUri = await removeBgImage('file:///path/to/photo.jpg')
71
- console.log(resultUri) // file:///path/to/cache/bg_removed_xxx.png
54
+ // Returns: file:///path/to/cache/bg_removed_xxx.png (native)
55
+ // Returns: data:image/png;base64,... (web)
56
+ ```
57
+
58
+ ### With React Query (Recommended)
59
+
60
+ ```typescript
61
+ import { useMutation } from '@tanstack/react-query'
62
+ import { removeBgImage } from 'rn-remove-image-bg'
63
+ import { Alert } from 'react-native'
64
+
65
+ function useRemoveBackground() {
66
+ return useMutation({
67
+ mutationFn: async (imageUri: string) => {
68
+ return await removeBgImage(imageUri, {
69
+ maxDimension: 1024, // Faster processing
70
+ format: 'PNG', // Best for transparency
71
+ useCache: true, // Cache results
72
+ })
73
+ },
74
+ onError: (error) => {
75
+ Alert.alert('Error', error.message)
76
+ },
77
+ })
78
+ }
79
+
80
+ // In your component:
81
+ function ImageEditor() {
82
+ const removeBackground = useRemoveBackground()
83
+
84
+ const handleRemoveBackground = () => {
85
+ removeBackground.mutate(selectedImageUri, {
86
+ onSuccess: (resultUri) => {
87
+ setProcessedImage(resultUri)
88
+ },
89
+ })
90
+ }
91
+
92
+ return (
93
+ <Button
94
+ onPress={handleRemoveBackground}
95
+ disabled={removeBackground.isPending}
96
+ title={removeBackground.isPending ? 'Processing...' : 'Remove Background'}
97
+ />
98
+ )
99
+ }
72
100
  ```
73
101
 
102
+ ### With Expo Image Picker
103
+
104
+ ```typescript
105
+ import * as ImagePicker from 'expo-image-picker'
106
+ import { removeBgImage } from 'rn-remove-image-bg'
107
+
108
+ async function pickAndProcessImage() {
109
+ // Pick image
110
+ const result = await ImagePicker.launchImageLibraryAsync({
111
+ mediaTypes: ImagePicker.MediaTypeOptions.Images,
112
+ quality: 1,
113
+ })
114
+
115
+ if (result.canceled) return null
116
+
117
+ // Remove background
118
+ const processedUri = await removeBgImage(result.assets[0].uri)
119
+ return processedUri
120
+ }
121
+
74
122
  ---
75
123
 
76
124
  ## API Reference
@@ -361,9 +409,39 @@ If you don't need web support, you can tree-shake the `@imgly/background-removal
361
409
 
362
410
  ### Android Model Not Loading?
363
411
 
364
- 1. Ensure device has Google Play Services
365
- 2. Check internet connection for first download
366
- 3. Clear app cache and retry
412
+ 1. Ensure device has Google Play Services installed
413
+ 2. Check internet connection (model downloads on first use)
414
+ 3. Wait 10-15 seconds - the library automatically retries during download
415
+ 4. Clear app cache and retry: `adb shell pm clear com.yourapp`
416
+
417
+ ### Web Not Working?
418
+
419
+ 1. Check browser console for errors
420
+ 2. Ensure your bundler supports WebAssembly
421
+ 3. The first call downloads a ~35MB WASM model
422
+ 4. CORS may block model download - check network tab
423
+
424
+ ### Native Module Not Found?
425
+
426
+ ```bash
427
+ # iOS
428
+ cd ios && pod install && cd ..
429
+ npx expo run:ios --device
430
+
431
+ # Android
432
+ npx expo run:android --device
433
+
434
+ # For Expo managed workflow
435
+ npx expo prebuild --clean
436
+ ```
437
+
438
+ ### TypeScript Errors?
439
+
440
+ Ensure peer dependencies match:
441
+ ```bash
442
+ npx expo install expo-file-system expo-image-manipulator
443
+ npm install react-native-nitro-modules@0.31.10
444
+ ```
367
445
 
368
446
  ---
369
447
 
@@ -1,13 +1,19 @@
1
1
  /**
2
- * Web implementation using @imgly/background-removal
3
- *
4
- * Provides real background removal on web using WebAssembly and ML models.
5
- * Falls back to no-op if the library fails to load.
2
+ * Web implementation using @huggingface/transformers
3
+ * Uses BRIAAI RMBG-1.4 model for background removal.
6
4
  */
5
+ export type OutputFormat = 'PNG' | 'WEBP';
6
+ export interface RemoveBgImageOptions {
7
+ format?: OutputFormat;
8
+ quality?: number;
9
+ onProgress?: (progress: number) => void;
10
+ debug?: boolean;
11
+ }
7
12
  /**
8
- * Output format for processed images
13
+ * Remove background from image
9
14
  */
10
- export type OutputFormat = 'PNG' | 'WEBP';
15
+ export declare function removeBgImage(uri: string, options?: RemoveBgImageOptions): Promise<string>;
16
+ export declare const removeBackground: typeof removeBgImage;
11
17
  export interface CompressImageOptions {
12
18
  maxSizeKB?: number;
13
19
  width?: number;
@@ -18,63 +24,12 @@ export interface CompressImageOptions {
18
24
  export interface GenerateThumbhashOptions {
19
25
  size?: number;
20
26
  }
21
- export interface RemoveBgImageOptions {
22
- maxDimension?: number;
23
- format?: OutputFormat;
24
- quality?: number;
25
- onProgress?: (progress: number) => void;
26
- useCache?: boolean;
27
- debug?: boolean;
28
- }
29
- /**
30
- * Compress image on web using canvas
31
- * @returns Compressed image as data URL
32
- */
33
- export declare function compressImage(uri: string, options?: CompressImageOptions): Promise<string>;
34
- /**
35
- * Generate thumbhash on web using canvas
36
- * @returns Base64 thumbhash string
37
- */
38
- export declare function generateThumbhash(imageUri: string, options?: GenerateThumbhashOptions): Promise<string>;
39
- /**
40
- * Remove background from image on web using @imgly/background-removal
41
- * @returns Data URL of processed image with transparent background
42
- */
43
- export declare function removeBgImage(uri: string, options?: RemoveBgImageOptions): Promise<string>;
44
- /**
45
- * Backward compatibility alias
46
- * @deprecated Use removeBgImage instead
47
- */
48
- export declare const removeBackground: typeof removeBgImage;
49
- /**
50
- * Clear the web background removal cache
51
- * @param _deleteFiles - Ignored on web (no disk cache)
52
- */
53
- export declare function clearCache(_deleteFiles?: boolean): Promise<void>;
54
- /**
55
- * Get the current cache size
56
- */
27
+ export declare function compressImage(uri: string, _options?: CompressImageOptions): Promise<string>;
28
+ export declare function generateThumbhash(_uri: string, _options?: GenerateThumbhashOptions): Promise<string>;
29
+ export declare function clearCache(): Promise<void>;
57
30
  export declare function getCacheSize(): number;
58
- /**
59
- * Handle low memory conditions by clearing the cache
60
- * On web, this simply clears the in-memory cache
61
- *
62
- * @param _deleteFiles - Ignored on web (no disk cache)
63
- * @returns Number of entries that were cleared
64
- */
65
- export declare function onLowMemory(_deleteFiles?: boolean): Promise<number>;
66
- /**
67
- * Configure the background removal cache
68
- * On web, maxEntries limits cache size. Disk persistence options are no-ops.
69
- */
70
- export declare function configureCache(config: {
31
+ export declare function onLowMemory(): Promise<number>;
32
+ export declare function configureCache(_config: {
71
33
  maxEntries?: number;
72
- maxAgeMinutes?: number;
73
- persistToDisk?: boolean;
74
- cacheDirectory?: string;
75
34
  }): void;
76
- /**
77
- * Get the cache directory path
78
- * On web, returns empty string as there is no disk cache
79
- */
80
35
  export declare function getCacheDirectory(): string;
@@ -1,249 +1,91 @@
1
1
  /**
2
- * Web implementation using @imgly/background-removal
3
- *
4
- * Provides real background removal on web using WebAssembly and ML models.
5
- * Falls back to no-op if the library fails to load.
2
+ * Web implementation using @huggingface/transformers
3
+ * Uses BRIAAI RMBG-1.4 model for background removal.
6
4
  */
7
- // Web cache configuration
8
- const webCacheConfig = {
9
- maxEntries: 50,
10
- maxAgeMinutes: 30,
11
- };
12
- // Simple in-memory LRU cache for web
13
- const webCache = new Map();
14
- /**
15
- * Add entry to cache with LRU eviction
16
- */
17
- function setCacheEntry(key, value) {
18
- // If key exists, delete it first (to update LRU order)
19
- if (webCache.has(key)) {
20
- webCache.delete(key);
21
- }
22
- // Evict oldest entries if at capacity
23
- while (webCache.size >= webCacheConfig.maxEntries) {
24
- const oldestKey = webCache.keys().next().value;
25
- if (oldestKey) {
26
- webCache.delete(oldestKey);
27
- }
28
- }
29
- webCache.set(key, value);
30
- }
31
- /**
32
- * Get entry from cache and update LRU order
33
- */
34
- function getCacheEntry(key) {
35
- const value = webCache.get(key);
36
- if (value !== undefined) {
37
- // Move to end (most recently used)
38
- webCache.delete(key);
39
- webCache.set(key, value);
40
- }
41
- return value;
42
- }
43
- /**
44
- * Compress image on web using canvas
45
- * @returns Compressed image as data URL
46
- */
47
- export async function compressImage(uri, options = {}) {
48
- const { maxSizeKB = 250, width = 1024, height = 1024, quality = 0.85, format = 'webp', } = options;
49
- try {
50
- // Load image
51
- const img = await loadImage(uri);
52
- // Calculate target dimensions maintaining aspect ratio
53
- const scale = Math.min(width / img.width, height / img.height, 1);
54
- const targetWidth = Math.round(img.width * scale);
55
- const targetHeight = Math.round(img.height * scale);
56
- // Create canvas and draw resized image
57
- const canvas = document.createElement('canvas');
58
- canvas.width = targetWidth;
59
- canvas.height = targetHeight;
60
- const ctx = canvas.getContext('2d');
61
- if (!ctx)
62
- throw new Error('Could not get canvas context');
63
- ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
64
- // Convert to data URL with compression
65
- const mimeType = format === 'png'
66
- ? 'image/png'
67
- : format === 'jpeg'
68
- ? 'image/jpeg'
69
- : 'image/webp';
70
- let dataUrl = canvas.toDataURL(mimeType, quality);
71
- // If still too large, reduce quality iteratively
72
- let currentQuality = quality;
73
- while (getDataUrlSizeKB(dataUrl) > maxSizeKB && currentQuality > 0.5) {
74
- currentQuality -= 0.1;
75
- dataUrl = canvas.toDataURL(mimeType, currentQuality);
76
- }
77
- return dataUrl;
78
- }
79
- catch (error) {
80
- console.warn('[rn-remove-image-bg] compressImage failed on web, returning original:', error);
81
- return uri;
5
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
+ let pipeline = null;
7
+ let loadPromise = null;
8
+ async function ensureLoaded(onProgress, debug) {
9
+ if (pipeline)
10
+ return pipeline;
11
+ if (loadPromise) {
12
+ await loadPromise;
13
+ return pipeline;
82
14
  }
83
- }
84
- /**
85
- * Generate thumbhash on web using canvas
86
- * @returns Base64 thumbhash string
87
- */
88
- export async function generateThumbhash(imageUri, options = {}) {
89
- const { size = 32 } = options;
90
- try {
91
- // Dynamically import thumbhash to avoid bundling issues
92
- const { rgbaToThumbHash } = await import('thumbhash');
93
- // Load and resize image
94
- const img = await loadImage(imageUri);
95
- const canvas = document.createElement('canvas');
96
- canvas.width = size;
97
- canvas.height = size;
98
- const ctx = canvas.getContext('2d');
99
- if (!ctx)
100
- throw new Error('Could not get canvas context');
101
- ctx.drawImage(img, 0, 0, size, size);
102
- // Get RGBA data
103
- const imageData = ctx.getImageData(0, 0, size, size);
104
- const hash = rgbaToThumbHash(size, size, imageData.data);
105
- // Convert to base64
106
- return btoa(String.fromCharCode(...hash));
107
- }
108
- catch (error) {
109
- console.warn('[rn-remove-image-bg] generateThumbhash failed on web:', error);
110
- return '';
111
- }
112
- }
113
- /**
114
- * Remove background from image on web using @imgly/background-removal
115
- * @returns Data URL of processed image with transparent background
116
- */
117
- export async function removeBgImage(uri, options = {}) {
118
- const { format = 'PNG', quality = 100, onProgress, useCache = true, debug = false, } = options;
119
- // Check cache
120
- const cacheKey = `${uri}::${format}::${quality}`;
121
- if (useCache) {
122
- const cachedResult = getCacheEntry(cacheKey);
123
- if (cachedResult) {
124
- if (debug) {
125
- console.log('[rn-remove-image-bg] Web cache hit');
126
- }
127
- onProgress?.(100);
128
- return cachedResult;
129
- }
130
- }
131
- if (debug) {
132
- console.log('[rn-remove-image-bg] Starting web background removal:', uri);
133
- }
134
- onProgress?.(5);
135
- try {
136
- // Dynamically import the library to prevent Metro from parsing onnxruntime-web at build time
137
- const { removeBackground: imglyRemoveBackground } = await import('@imgly/background-removal');
138
- // Call @imgly/background-removal
139
- const blob = await imglyRemoveBackground(uri, {
140
- progress: (key, current, total) => {
141
- if (onProgress && total > 0) {
142
- // Map progress to 10-90 range
143
- const progress = Math.round(10 + (current / total) * 80);
144
- onProgress(Math.min(progress, 90));
15
+ loadPromise = (async () => {
16
+ if (debug)
17
+ console.log('[rmbg] Loading model...');
18
+ const { pipeline: createPipeline, env } = await import('@huggingface/transformers');
19
+ env.allowLocalModels = false;
20
+ env.useBrowserCache = true;
21
+ onProgress?.(10);
22
+ pipeline = await createPipeline('image-segmentation', 'briaai/RMBG-1.4', {
23
+ dtype: 'q8',
24
+ progress_callback: (info) => {
25
+ if (onProgress && 'progress' in info && typeof info.progress === 'number') {
26
+ onProgress(Math.min(10 + (info.progress / 100) * 50, 60));
145
27
  }
146
- if (debug) {
147
- console.log(`[rn-remove-image-bg] ${key}: ${current}/${total}`);
148
- }
149
- },
150
- output: {
151
- format: format === 'WEBP' ? 'image/webp' : 'image/png',
152
- quality: quality / 100,
153
28
  },
154
29
  });
155
- onProgress?.(95);
156
- // Convert blob to data URL
157
- const dataUrl = await blobToDataUrl(blob);
158
- // Cache the result with LRU eviction
159
- if (useCache) {
160
- setCacheEntry(cacheKey, dataUrl);
161
- }
162
- if (debug) {
163
- console.log('[rn-remove-image-bg] Web background removal complete');
164
- }
165
- onProgress?.(100);
166
- return dataUrl;
167
- }
168
- catch (error) {
169
- console.error('[rn-remove-image-bg] Web background removal failed:', error);
170
- // Throw error instead of silent failure
171
- const message = error instanceof Error ? error.message : 'Web background removal failed';
172
- throw new Error(`Background removal failed: ${message}`);
173
- }
174
- }
175
- /**
176
- * Backward compatibility alias
177
- * @deprecated Use removeBgImage instead
178
- */
179
- export const removeBackground = removeBgImage;
180
- /**
181
- * Clear the web background removal cache
182
- * @param _deleteFiles - Ignored on web (no disk cache)
183
- */
184
- export async function clearCache(_deleteFiles = false) {
185
- webCache.clear();
30
+ if (debug)
31
+ console.log('[rmbg] Model ready');
32
+ })();
33
+ await loadPromise;
34
+ return pipeline;
186
35
  }
187
36
  /**
188
- * Get the current cache size
37
+ * Remove background from image
189
38
  */
190
- export function getCacheSize() {
191
- return webCache.size;
39
+ export async function removeBgImage(uri, options = {}) {
40
+ const { format = 'PNG', quality = 100, onProgress, debug = false } = options;
41
+ if (debug)
42
+ console.log('[rmbg] Processing:', uri);
43
+ onProgress?.(5);
44
+ const segmenter = await ensureLoaded(onProgress, debug);
45
+ onProgress?.(60);
46
+ const results = await segmenter(uri);
47
+ onProgress?.(90);
48
+ const result = Array.isArray(results) ? results[0] : results;
49
+ if (!result?.mask)
50
+ throw new Error('No mask returned');
51
+ // Convert to data URL
52
+ const dataUrl = typeof result.mask === 'string'
53
+ ? result.mask
54
+ : toDataUrl(result.mask, format, quality);
55
+ if (debug)
56
+ console.log('[rmbg] Done');
57
+ onProgress?.(100);
58
+ return dataUrl;
192
59
  }
193
- /**
194
- * Handle low memory conditions by clearing the cache
195
- * On web, this simply clears the in-memory cache
196
- *
197
- * @param _deleteFiles - Ignored on web (no disk cache)
198
- * @returns Number of entries that were cleared
199
- */
200
- export async function onLowMemory(_deleteFiles = true) {
201
- const size = webCache.size;
202
- webCache.clear();
203
- console.log(`[rn-remove-image-bg] Cleared ${size} web cache entries due to memory pressure`);
204
- return size;
60
+ function toDataUrl(mask, format, quality) {
61
+ const canvas = document.createElement('canvas');
62
+ canvas.width = mask.width;
63
+ canvas.height = mask.height;
64
+ const ctx = canvas.getContext('2d');
65
+ const imageData = ctx.createImageData(mask.width, mask.height);
66
+ imageData.data.set(mask.data);
67
+ ctx.putImageData(imageData, 0, 0);
68
+ return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
205
69
  }
206
- /**
207
- * Configure the background removal cache
208
- * On web, maxEntries limits cache size. Disk persistence options are no-ops.
209
- */
210
- export function configureCache(config) {
211
- if (config.maxEntries !== undefined && config.maxEntries > 0) {
212
- webCacheConfig.maxEntries = config.maxEntries;
213
- }
214
- if (config.maxAgeMinutes !== undefined && config.maxAgeMinutes > 0) {
215
- webCacheConfig.maxAgeMinutes = config.maxAgeMinutes;
216
- }
217
- // persistToDisk and cacheDirectory are no-ops on web
70
+ export const removeBackground = removeBgImage;
71
+ export async function compressImage(uri, _options) {
72
+ console.warn('[rmbg] compressImage not implemented on web, returning original');
73
+ return uri;
218
74
  }
219
- /**
220
- * Get the cache directory path
221
- * On web, returns empty string as there is no disk cache
222
- */
223
- export function getCacheDirectory() {
75
+ export async function generateThumbhash(_uri, _options) {
76
+ console.warn('[rmbg] generateThumbhash not implemented on web');
224
77
  return '';
225
78
  }
226
- // Helper functions
227
- function loadImage(src) {
228
- return new Promise((resolve, reject) => {
229
- const img = new Image();
230
- img.crossOrigin = 'anonymous';
231
- img.onload = () => resolve(img);
232
- img.onerror = reject;
233
- img.src = src;
234
- });
79
+ export async function clearCache() {
80
+ pipeline = null;
81
+ loadPromise = null;
235
82
  }
236
- function blobToDataUrl(blob) {
237
- return new Promise((resolve, reject) => {
238
- const reader = new FileReader();
239
- reader.onloadend = () => resolve(reader.result);
240
- reader.onerror = reject;
241
- reader.readAsDataURL(blob);
242
- });
83
+ export function getCacheSize() {
84
+ return 0;
243
85
  }
244
- function getDataUrlSizeKB(dataUrl) {
245
- // Data URL format: data:mime;base64,<base64data>
246
- const base64 = dataUrl.split(',')[1] || '';
247
- // Base64 encodes 3 bytes as 4 characters
248
- return (base64.length * 3) / 4 / 1024;
86
+ export async function onLowMemory() {
87
+ await clearCache();
88
+ return 0;
249
89
  }
90
+ export function configureCache(_config) { }
91
+ export function getCacheDirectory() { return ''; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-remove-image-bg",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "description": "rn-remove-image-bg",
5
5
  "homepage": "https://github.com/a-eid/rn-remove-image-bg",
6
6
  "main": "lib/index",
@@ -35,17 +35,6 @@
35
35
  "*.podspec",
36
36
  "README.md"
37
37
  ],
38
- "scripts": {
39
- "postinstall": "tsc || exit 0;",
40
- "typecheck": "tsc --noEmit",
41
- "clean": "rm -rf android/build node_modules/**/android/build lib",
42
- "lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
43
- "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions",
44
- "typescript": "tsc",
45
- "specs": "tsc --noEmit false && nitrogen --logLevel=\"debug\"",
46
- "prepare": "npm run typescript",
47
- "test": "vitest run"
48
- },
49
38
  "author": "Ahmed Eid <a.eid@yandex.com> (https://github.com/a-eid)",
50
39
  "license": "MIT",
51
40
  "devDependencies": {
@@ -96,9 +85,19 @@
96
85
  "lib/"
97
86
  ],
98
87
  "dependencies": {
99
- "@imgly/background-removal": "^1.7.0",
88
+ "@huggingface/transformers": "^3.8.1",
100
89
  "buffer": "^6.0.3",
101
90
  "thumbhash": "^0.1.1",
102
91
  "upng-js": "^2.1.0"
92
+ },
93
+ "scripts": {
94
+ "postinstall": "tsc || exit 0;",
95
+ "typecheck": "tsc --noEmit",
96
+ "clean": "rm -rf android/build node_modules/**/android/build lib",
97
+ "lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
98
+ "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions",
99
+ "typescript": "tsc",
100
+ "specs": "tsc --noEmit false && nitrogen --logLevel=\"debug\"",
101
+ "test": "vitest run"
103
102
  }
104
- }
103
+ }
@@ -1,347 +1,140 @@
1
1
  /**
2
- * Web implementation using @imgly/background-removal
3
- *
4
- * Provides real background removal on web using WebAssembly and ML models.
5
- * Falls back to no-op if the library fails to load.
2
+ * Web implementation using @huggingface/transformers
3
+ * Uses BRIAAI RMBG-1.4 model for background removal.
6
4
  */
7
5
 
8
- // @imgly/background-removal is dynamically imported at runtime in removeBgImage()
9
- // to prevent Metro from parsing onnxruntime-web at build time
10
-
11
- /**
12
- * Output format for processed images
13
- */
14
6
  export type OutputFormat = 'PNG' | 'WEBP';
15
7
 
16
- export interface CompressImageOptions {
17
- maxSizeKB?: number;
18
- width?: number;
19
- height?: number;
20
- quality?: number;
21
- format?: 'webp' | 'png' | 'jpeg';
22
- }
23
-
24
- export interface GenerateThumbhashOptions {
25
- size?: number;
26
- }
27
-
28
8
  export interface RemoveBgImageOptions {
29
- maxDimension?: number;
30
9
  format?: OutputFormat;
31
10
  quality?: number;
32
11
  onProgress?: (progress: number) => void;
33
- useCache?: boolean;
34
12
  debug?: boolean;
35
13
  }
36
- // Web cache configuration
37
- const webCacheConfig = {
38
- maxEntries: 50,
39
- maxAgeMinutes: 30,
40
- };
41
14
 
42
- // Simple in-memory LRU cache for web
43
- const webCache = new Map<string, string>();
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ let pipeline: any = null;
17
+ let loadPromise: Promise<void> | null = null;
44
18
 
45
- /**
46
- * Add entry to cache with LRU eviction
47
- */
48
- function setCacheEntry(key: string, value: string): void {
49
- // If key exists, delete it first (to update LRU order)
50
- if (webCache.has(key)) {
51
- webCache.delete(key);
19
+ async function ensureLoaded(onProgress?: (p: number) => void, debug?: boolean) {
20
+ if (pipeline) return pipeline;
21
+
22
+ if (loadPromise) {
23
+ await loadPromise;
24
+ return pipeline;
52
25
  }
53
26
 
54
- // Evict oldest entries if at capacity
55
- while (webCache.size >= webCacheConfig.maxEntries) {
56
- const oldestKey = webCache.keys().next().value;
57
- if (oldestKey) {
58
- webCache.delete(oldestKey);
59
- }
60
- }
61
-
62
- webCache.set(key, value);
63
- }
64
-
65
- /**
66
- * Get entry from cache and update LRU order
67
- */
68
- function getCacheEntry(key: string): string | undefined {
69
- const value = webCache.get(key);
70
- if (value !== undefined) {
71
- // Move to end (most recently used)
72
- webCache.delete(key);
73
- webCache.set(key, value);
74
- }
75
- return value;
76
- }
77
-
78
- /**
79
- * Compress image on web using canvas
80
- * @returns Compressed image as data URL
81
- */
82
- export async function compressImage(
83
- uri: string,
84
- options: CompressImageOptions = {}
85
- ): Promise<string> {
86
- const {
87
- maxSizeKB = 250,
88
- width = 1024,
89
- height = 1024,
90
- quality = 0.85,
91
- format = 'webp',
92
- } = options;
93
-
94
- try {
95
- // Load image
96
- const img = await loadImage(uri);
97
-
98
- // Calculate target dimensions maintaining aspect ratio
99
- const scale = Math.min(width / img.width, height / img.height, 1);
100
- const targetWidth = Math.round(img.width * scale);
101
- const targetHeight = Math.round(img.height * scale);
102
-
103
- // Create canvas and draw resized image
104
- const canvas = document.createElement('canvas');
105
- canvas.width = targetWidth;
106
- canvas.height = targetHeight;
107
- const ctx = canvas.getContext('2d');
108
- if (!ctx) throw new Error('Could not get canvas context');
109
- ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
110
-
111
- // Convert to data URL with compression
112
- const mimeType =
113
- format === 'png'
114
- ? 'image/png'
115
- : format === 'jpeg'
116
- ? 'image/jpeg'
117
- : 'image/webp';
118
- let dataUrl = canvas.toDataURL(mimeType, quality);
119
-
120
- // If still too large, reduce quality iteratively
121
- let currentQuality = quality;
122
- while (getDataUrlSizeKB(dataUrl) > maxSizeKB && currentQuality > 0.5) {
123
- currentQuality -= 0.1;
124
- dataUrl = canvas.toDataURL(mimeType, currentQuality);
125
- }
126
-
127
- return dataUrl;
128
- } catch (error) {
129
- console.warn(
130
- '[rn-remove-image-bg] compressImage failed on web, returning original:',
131
- error
132
- );
133
- return uri;
134
- }
135
- }
136
-
137
- /**
138
- * Generate thumbhash on web using canvas
139
- * @returns Base64 thumbhash string
140
- */
141
- export async function generateThumbhash(
142
- imageUri: string,
143
- options: GenerateThumbhashOptions = {}
144
- ): Promise<string> {
145
- const { size = 32 } = options;
146
-
147
- try {
148
- // Dynamically import thumbhash to avoid bundling issues
149
- const { rgbaToThumbHash } = await import('thumbhash');
150
-
151
- // Load and resize image
152
- const img = await loadImage(imageUri);
153
- const canvas = document.createElement('canvas');
154
- canvas.width = size;
155
- canvas.height = size;
156
- const ctx = canvas.getContext('2d');
157
- if (!ctx) throw new Error('Could not get canvas context');
158
- ctx.drawImage(img, 0, 0, size, size);
159
-
160
- // Get RGBA data
161
- const imageData = ctx.getImageData(0, 0, size, size);
162
- const hash = rgbaToThumbHash(size, size, imageData.data);
27
+ loadPromise = (async () => {
28
+ if (debug) console.log('[rmbg] Loading model...');
29
+
30
+ const { pipeline: createPipeline, env } = await import('@huggingface/transformers');
31
+
32
+ env.allowLocalModels = false;
33
+ env.useBrowserCache = true;
34
+
35
+ onProgress?.(10);
36
+
37
+ pipeline = await createPipeline('image-segmentation', 'briaai/RMBG-1.4', {
38
+ dtype: 'q8',
39
+ progress_callback: (info) => {
40
+ if (onProgress && 'progress' in info && typeof info.progress === 'number') {
41
+ onProgress(Math.min(10 + (info.progress / 100) * 50, 60));
42
+ }
43
+ },
44
+ });
45
+
46
+ if (debug) console.log('[rmbg] Model ready');
47
+ })();
163
48
 
164
- // Convert to base64
165
- return btoa(String.fromCharCode(...hash));
166
- } catch (error) {
167
- console.warn(
168
- '[rn-remove-image-bg] generateThumbhash failed on web:',
169
- error
170
- );
171
- return '';
172
- }
49
+ await loadPromise;
50
+ return pipeline;
173
51
  }
174
52
 
175
53
  /**
176
- * Remove background from image on web using @imgly/background-removal
177
- * @returns Data URL of processed image with transparent background
54
+ * Remove background from image
178
55
  */
179
56
  export async function removeBgImage(
180
57
  uri: string,
181
58
  options: RemoveBgImageOptions = {}
182
59
  ): Promise<string> {
183
- const {
184
- format = 'PNG',
185
- quality = 100,
186
- onProgress,
187
- useCache = true,
188
- debug = false,
189
- } = options;
190
-
191
- // Check cache
192
- const cacheKey = `${uri}::${format}::${quality}`;
193
- if (useCache) {
194
- const cachedResult = getCacheEntry(cacheKey);
195
- if (cachedResult) {
196
- if (debug) {
197
- console.log('[rn-remove-image-bg] Web cache hit');
198
- }
199
- onProgress?.(100);
200
- return cachedResult;
201
- }
202
- }
203
-
204
- if (debug) {
205
- console.log('[rn-remove-image-bg] Starting web background removal:', uri);
206
- }
60
+ const { format = 'PNG', quality = 100, onProgress, debug = false } = options;
207
61
 
62
+ if (debug) console.log('[rmbg] Processing:', uri);
208
63
  onProgress?.(5);
209
64
 
210
- try {
211
- // Dynamically import the library to prevent Metro from parsing onnxruntime-web at build time
212
- const { removeBackground: imglyRemoveBackground } =
213
- await import('@imgly/background-removal');
214
-
215
- // Call @imgly/background-removal
216
- const blob = await imglyRemoveBackground(uri, {
217
- progress: (key: string, current: number, total: number) => {
218
- if (onProgress && total > 0) {
219
- // Map progress to 10-90 range
220
- const progress = Math.round(10 + (current / total) * 80);
221
- onProgress(Math.min(progress, 90));
222
- }
223
- if (debug) {
224
- console.log(`[rn-remove-image-bg] ${key}: ${current}/${total}`);
225
- }
226
- },
227
- output: {
228
- format: format === 'WEBP' ? 'image/webp' : 'image/png',
229
- quality: quality / 100,
230
- },
231
- });
65
+ const segmenter = await ensureLoaded(onProgress, debug);
66
+ onProgress?.(60);
232
67
 
233
- onProgress?.(95);
68
+ const results = await segmenter(uri);
69
+ onProgress?.(90);
234
70
 
235
- // Convert blob to data URL
236
- const dataUrl = await blobToDataUrl(blob);
71
+ const result = Array.isArray(results) ? results[0] : results;
72
+ if (!result?.mask) throw new Error('No mask returned');
237
73
 
238
- // Cache the result with LRU eviction
239
- if (useCache) {
240
- setCacheEntry(cacheKey, dataUrl);
241
- }
74
+ // Convert to data URL
75
+ const dataUrl = typeof result.mask === 'string'
76
+ ? result.mask
77
+ : toDataUrl(result.mask, format, quality);
242
78
 
243
- if (debug) {
244
- console.log('[rn-remove-image-bg] Web background removal complete');
245
- }
79
+ if (debug) console.log('[rmbg] Done');
80
+ onProgress?.(100);
81
+
82
+ return dataUrl;
83
+ }
246
84
 
247
- onProgress?.(100);
248
- return dataUrl;
249
- } catch (error) {
250
- console.error('[rn-remove-image-bg] Web background removal failed:', error);
251
- // Throw error instead of silent failure
252
- const message = error instanceof Error ? error.message : 'Web background removal failed';
253
- throw new Error(`Background removal failed: ${message}`);
254
- }
85
+ function toDataUrl(
86
+ mask: { width: number; height: number; data: Uint8Array | Uint8ClampedArray },
87
+ format: OutputFormat,
88
+ quality: number
89
+ ): string {
90
+ const canvas = document.createElement('canvas');
91
+ canvas.width = mask.width;
92
+ canvas.height = mask.height;
93
+ const ctx = canvas.getContext('2d')!;
94
+ const imageData = ctx.createImageData(mask.width, mask.height);
95
+ imageData.data.set(mask.data);
96
+ ctx.putImageData(imageData, 0, 0);
97
+ return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
255
98
  }
256
99
 
257
- /**
258
- * Backward compatibility alias
259
- * @deprecated Use removeBgImage instead
260
- */
261
100
  export const removeBackground = removeBgImage;
262
101
 
263
- /**
264
- * Clear the web background removal cache
265
- * @param _deleteFiles - Ignored on web (no disk cache)
266
- */
267
- export async function clearCache(_deleteFiles = false): Promise<void> {
268
- webCache.clear();
269
- }
270
-
271
- /**
272
- * Get the current cache size
273
- */
274
- export function getCacheSize(): number {
275
- return webCache.size;
102
+ // Stub exports for API compatibility with native
103
+ export interface CompressImageOptions {
104
+ maxSizeKB?: number;
105
+ width?: number;
106
+ height?: number;
107
+ quality?: number;
108
+ format?: 'webp' | 'png' | 'jpeg';
276
109
  }
277
110
 
278
- /**
279
- * Handle low memory conditions by clearing the cache
280
- * On web, this simply clears the in-memory cache
281
- *
282
- * @param _deleteFiles - Ignored on web (no disk cache)
283
- * @returns Number of entries that were cleared
284
- */
285
- export async function onLowMemory(_deleteFiles = true): Promise<number> {
286
- const size = webCache.size;
287
- webCache.clear();
288
- console.log(
289
- `[rn-remove-image-bg] Cleared ${size} web cache entries due to memory pressure`
290
- );
291
- return size;
111
+ export interface GenerateThumbhashOptions {
112
+ size?: number;
292
113
  }
293
114
 
294
- /**
295
- * Configure the background removal cache
296
- * On web, maxEntries limits cache size. Disk persistence options are no-ops.
297
- */
298
- export function configureCache(config: {
299
- maxEntries?: number;
300
- maxAgeMinutes?: number;
301
- persistToDisk?: boolean;
302
- cacheDirectory?: string;
303
- }): void {
304
- if (config.maxEntries !== undefined && config.maxEntries > 0) {
305
- webCacheConfig.maxEntries = config.maxEntries;
306
- }
307
- if (config.maxAgeMinutes !== undefined && config.maxAgeMinutes > 0) {
308
- webCacheConfig.maxAgeMinutes = config.maxAgeMinutes;
309
- }
310
- // persistToDisk and cacheDirectory are no-ops on web
115
+ export async function compressImage(uri: string, _options?: CompressImageOptions): Promise<string> {
116
+ console.warn('[rmbg] compressImage not implemented on web, returning original');
117
+ return uri;
311
118
  }
312
119
 
313
- /**
314
- * Get the cache directory path
315
- * On web, returns empty string as there is no disk cache
316
- */
317
- export function getCacheDirectory(): string {
120
+ export async function generateThumbhash(_uri: string, _options?: GenerateThumbhashOptions): Promise<string> {
121
+ console.warn('[rmbg] generateThumbhash not implemented on web');
318
122
  return '';
319
123
  }
320
124
 
321
- // Helper functions
322
-
323
- function loadImage(src: string): Promise<HTMLImageElement> {
324
- return new Promise((resolve, reject) => {
325
- const img = new Image();
326
- img.crossOrigin = 'anonymous';
327
- img.onload = () => resolve(img);
328
- img.onerror = reject;
329
- img.src = src;
330
- });
125
+ export async function clearCache(): Promise<void> {
126
+ pipeline = null;
127
+ loadPromise = null;
331
128
  }
332
129
 
333
- function blobToDataUrl(blob: Blob): Promise<string> {
334
- return new Promise((resolve, reject) => {
335
- const reader = new FileReader();
336
- reader.onloadend = () => resolve(reader.result as string);
337
- reader.onerror = reject;
338
- reader.readAsDataURL(blob);
339
- });
130
+ export function getCacheSize(): number {
131
+ return 0;
340
132
  }
341
133
 
342
- function getDataUrlSizeKB(dataUrl: string): number {
343
- // Data URL format: data:mime;base64,<base64data>
344
- const base64 = dataUrl.split(',')[1] || '';
345
- // Base64 encodes 3 bytes as 4 characters
346
- return (base64.length * 3) / 4 / 1024;
134
+ export async function onLowMemory(): Promise<number> {
135
+ await clearCache();
136
+ return 0;
347
137
  }
138
+
139
+ export function configureCache(_config: { maxEntries?: number }): void {}
140
+ export function getCacheDirectory(): string { return ''; }