rn-remove-image-bg 0.0.12 → 0.0.14
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 +104 -26
- package/lib/ImageProcessing.web.d.ts +16 -60
- package/lib/ImageProcessing.web.js +103 -194
- package/package.json +13 -14
- package/src/ImageProcessing.web.ts +136 -258
package/README.md
CHANGED
|
@@ -15,28 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
## Installation
|
|
17
17
|
|
|
18
|
-
Install from GitHub:
|
|
19
|
-
|
|
20
18
|
```bash
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
366
|
-
3.
|
|
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,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web implementation using @
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
4
|
+
* Loads from CDN to bypass Metro bundler issues.
|
|
6
5
|
*/
|
|
6
|
+
export type OutputFormat = 'PNG' | 'WEBP';
|
|
7
|
+
export interface RemoveBgImageOptions {
|
|
8
|
+
format?: OutputFormat;
|
|
9
|
+
quality?: number;
|
|
10
|
+
onProgress?: (progress: number) => void;
|
|
11
|
+
debug?: boolean;
|
|
12
|
+
}
|
|
7
13
|
/**
|
|
8
|
-
*
|
|
14
|
+
* Remove background from image
|
|
9
15
|
*/
|
|
10
|
-
export
|
|
16
|
+
export declare function removeBgImage(uri: string, options?: RemoveBgImageOptions): Promise<string>;
|
|
17
|
+
export declare const removeBackground: typeof removeBgImage;
|
|
11
18
|
export interface CompressImageOptions {
|
|
12
19
|
maxSizeKB?: number;
|
|
13
20
|
width?: number;
|
|
@@ -18,63 +25,12 @@ export interface CompressImageOptions {
|
|
|
18
25
|
export interface GenerateThumbhashOptions {
|
|
19
26
|
size?: number;
|
|
20
27
|
}
|
|
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
28
|
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
29
|
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
|
-
*/
|
|
30
|
+
export declare function clearCache(): Promise<void>;
|
|
57
31
|
export declare function getCacheSize(): number;
|
|
58
|
-
|
|
59
|
-
|
|
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: {
|
|
32
|
+
export declare function onLowMemory(): Promise<number>;
|
|
33
|
+
export declare function configureCache(_config: {
|
|
71
34
|
maxEntries?: number;
|
|
72
|
-
maxAgeMinutes?: number;
|
|
73
|
-
persistToDisk?: boolean;
|
|
74
|
-
cacheDirectory?: string;
|
|
75
35
|
}): void;
|
|
76
|
-
/**
|
|
77
|
-
* Get the cache directory path
|
|
78
|
-
* On web, returns empty string as there is no disk cache
|
|
79
|
-
*/
|
|
80
36
|
export declare function getCacheDirectory(): string;
|
|
@@ -1,59 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web implementation using @
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
4
|
+
* Loads from CDN to bypass Metro bundler issues.
|
|
6
5
|
*/
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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);
|
|
6
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
7
|
+
let pipeline = null;
|
|
8
|
+
let loadPromise = null;
|
|
9
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
10
|
+
let transformersModule = null;
|
|
11
|
+
// Load transformers.js from CDN to avoid Metro bundling issues
|
|
12
|
+
async function loadTransformers() {
|
|
13
|
+
if (transformersModule)
|
|
14
|
+
return transformersModule;
|
|
15
|
+
// Use dynamic import from CDN
|
|
16
|
+
const cdnUrl = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3';
|
|
17
|
+
transformersModule = await import(/* webpackIgnore: true */ cdnUrl);
|
|
18
|
+
return transformersModule;
|
|
30
19
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Move to end (most recently used)
|
|
38
|
-
webCache.delete(key);
|
|
39
|
-
webCache.set(key, value);
|
|
20
|
+
async function ensureLoaded(onProgress, debug) {
|
|
21
|
+
if (pipeline)
|
|
22
|
+
return pipeline;
|
|
23
|
+
if (loadPromise) {
|
|
24
|
+
await loadPromise;
|
|
25
|
+
return pipeline;
|
|
40
26
|
}
|
|
41
|
-
|
|
27
|
+
loadPromise = (async () => {
|
|
28
|
+
if (debug)
|
|
29
|
+
console.log('[rmbg] Loading model...');
|
|
30
|
+
const { pipeline: createPipeline, env } = await loadTransformers();
|
|
31
|
+
env.allowLocalModels = false;
|
|
32
|
+
env.useBrowserCache = true;
|
|
33
|
+
onProgress?.(10);
|
|
34
|
+
pipeline = await createPipeline('image-segmentation', 'briaai/RMBG-1.4', {
|
|
35
|
+
dtype: 'q8',
|
|
36
|
+
progress_callback: (info) => {
|
|
37
|
+
if (onProgress && 'progress' in info && typeof info.progress === 'number') {
|
|
38
|
+
onProgress(Math.min(10 + (info.progress / 100) * 50, 60));
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
if (debug)
|
|
43
|
+
console.log('[rmbg] Model ready');
|
|
44
|
+
})();
|
|
45
|
+
await loadPromise;
|
|
46
|
+
return pipeline;
|
|
42
47
|
}
|
|
43
48
|
/**
|
|
44
|
-
*
|
|
45
|
-
* @returns Compressed image as data URL
|
|
49
|
+
* Remove background from image
|
|
46
50
|
*/
|
|
51
|
+
export async function removeBgImage(uri, options = {}) {
|
|
52
|
+
const { format = 'PNG', quality = 100, onProgress, debug = false } = options;
|
|
53
|
+
if (debug)
|
|
54
|
+
console.log('[rmbg] Processing:', uri);
|
|
55
|
+
onProgress?.(5);
|
|
56
|
+
const segmenter = await ensureLoaded(onProgress, debug);
|
|
57
|
+
onProgress?.(60);
|
|
58
|
+
const results = await segmenter(uri);
|
|
59
|
+
onProgress?.(90);
|
|
60
|
+
const result = Array.isArray(results) ? results[0] : results;
|
|
61
|
+
if (!result?.mask)
|
|
62
|
+
throw new Error('No mask returned');
|
|
63
|
+
// Convert to data URL
|
|
64
|
+
const dataUrl = typeof result.mask === 'string'
|
|
65
|
+
? result.mask
|
|
66
|
+
: toDataUrl(result.mask, format, quality);
|
|
67
|
+
if (debug)
|
|
68
|
+
console.log('[rmbg] Done');
|
|
69
|
+
onProgress?.(100);
|
|
70
|
+
return dataUrl;
|
|
71
|
+
}
|
|
72
|
+
function toDataUrl(mask, format, quality) {
|
|
73
|
+
const canvas = document.createElement('canvas');
|
|
74
|
+
canvas.width = mask.width;
|
|
75
|
+
canvas.height = mask.height;
|
|
76
|
+
const ctx = canvas.getContext('2d');
|
|
77
|
+
const imageData = ctx.createImageData(mask.width, mask.height);
|
|
78
|
+
imageData.data.set(mask.data);
|
|
79
|
+
ctx.putImageData(imageData, 0, 0);
|
|
80
|
+
return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
|
|
81
|
+
}
|
|
82
|
+
export const removeBackground = removeBgImage;
|
|
83
|
+
// Helper to load image
|
|
84
|
+
function loadImage(src) {
|
|
85
|
+
return new Promise((resolve, reject) => {
|
|
86
|
+
const img = new Image();
|
|
87
|
+
img.crossOrigin = 'anonymous';
|
|
88
|
+
img.onload = () => resolve(img);
|
|
89
|
+
img.onerror = reject;
|
|
90
|
+
img.src = src;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
47
93
|
export async function compressImage(uri, options = {}) {
|
|
48
94
|
const { maxSizeKB = 250, width = 1024, height = 1024, quality = 0.85, format = 'webp', } = options;
|
|
49
95
|
try {
|
|
50
|
-
// Load image
|
|
51
96
|
const img = await loadImage(uri);
|
|
52
|
-
// Calculate target dimensions maintaining aspect ratio
|
|
53
97
|
const scale = Math.min(width / img.width, height / img.height, 1);
|
|
54
98
|
const targetWidth = Math.round(img.width * scale);
|
|
55
99
|
const targetHeight = Math.round(img.height * scale);
|
|
56
|
-
// Create canvas and draw resized image
|
|
57
100
|
const canvas = document.createElement('canvas');
|
|
58
101
|
canvas.width = targetWidth;
|
|
59
102
|
canvas.height = targetHeight;
|
|
@@ -61,36 +104,25 @@ export async function compressImage(uri, options = {}) {
|
|
|
61
104
|
if (!ctx)
|
|
62
105
|
throw new Error('Could not get canvas context');
|
|
63
106
|
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
64
|
-
|
|
65
|
-
const mimeType = format === 'png'
|
|
66
|
-
? 'image/png'
|
|
67
|
-
: format === 'jpeg'
|
|
68
|
-
? 'image/jpeg'
|
|
69
|
-
: 'image/webp';
|
|
107
|
+
const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp';
|
|
70
108
|
let dataUrl = canvas.toDataURL(mimeType, quality);
|
|
71
|
-
//
|
|
109
|
+
// Reduce quality if over size limit
|
|
72
110
|
let currentQuality = quality;
|
|
73
|
-
|
|
111
|
+
const getSize = (url) => ((url.split(',')[1] || '').length * 3) / 4 / 1024;
|
|
112
|
+
while (getSize(dataUrl) > maxSizeKB && currentQuality > 0.5) {
|
|
74
113
|
currentQuality -= 0.1;
|
|
75
114
|
dataUrl = canvas.toDataURL(mimeType, currentQuality);
|
|
76
115
|
}
|
|
77
116
|
return dataUrl;
|
|
78
117
|
}
|
|
79
118
|
catch (error) {
|
|
80
|
-
console.warn('[
|
|
119
|
+
console.warn('[rmbg] compressImage failed:', error);
|
|
81
120
|
return uri;
|
|
82
121
|
}
|
|
83
122
|
}
|
|
84
|
-
/**
|
|
85
|
-
* Generate thumbhash on web using canvas
|
|
86
|
-
* @returns Base64 thumbhash string
|
|
87
|
-
*/
|
|
88
123
|
export async function generateThumbhash(imageUri, options = {}) {
|
|
89
124
|
const { size = 32 } = options;
|
|
90
125
|
try {
|
|
91
|
-
// Dynamically import thumbhash to avoid bundling issues
|
|
92
|
-
const { rgbaToThumbHash } = await import('thumbhash');
|
|
93
|
-
// Load and resize image
|
|
94
126
|
const img = await loadImage(imageUri);
|
|
95
127
|
const canvas = document.createElement('canvas');
|
|
96
128
|
canvas.width = size;
|
|
@@ -99,151 +131,28 @@ export async function generateThumbhash(imageUri, options = {}) {
|
|
|
99
131
|
if (!ctx)
|
|
100
132
|
throw new Error('Could not get canvas context');
|
|
101
133
|
ctx.drawImage(img, 0, 0, size, size);
|
|
102
|
-
// Get RGBA data
|
|
103
134
|
const imageData = ctx.getImageData(0, 0, size, size);
|
|
135
|
+
// Load thumbhash from CDN
|
|
136
|
+
// @ts-expect-error CDN import works at runtime
|
|
137
|
+
const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
|
|
104
138
|
const hash = rgbaToThumbHash(size, size, imageData.data);
|
|
105
|
-
// Convert to base64
|
|
106
139
|
return btoa(String.fromCharCode(...hash));
|
|
107
140
|
}
|
|
108
141
|
catch (error) {
|
|
109
|
-
console.warn('[
|
|
142
|
+
console.warn('[rmbg] generateThumbhash failed:', error);
|
|
110
143
|
return '';
|
|
111
144
|
}
|
|
112
145
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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));
|
|
145
|
-
}
|
|
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
|
-
},
|
|
154
|
-
});
|
|
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();
|
|
146
|
+
export async function clearCache() {
|
|
147
|
+
pipeline = null;
|
|
148
|
+
loadPromise = null;
|
|
186
149
|
}
|
|
187
|
-
/**
|
|
188
|
-
* Get the current cache size
|
|
189
|
-
*/
|
|
190
150
|
export function getCacheSize() {
|
|
191
|
-
return
|
|
192
|
-
}
|
|
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;
|
|
205
|
-
}
|
|
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
|
|
218
|
-
}
|
|
219
|
-
/**
|
|
220
|
-
* Get the cache directory path
|
|
221
|
-
* On web, returns empty string as there is no disk cache
|
|
222
|
-
*/
|
|
223
|
-
export function getCacheDirectory() {
|
|
224
|
-
return '';
|
|
225
|
-
}
|
|
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
|
-
});
|
|
235
|
-
}
|
|
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
|
-
});
|
|
151
|
+
return 0;
|
|
243
152
|
}
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// Base64 encodes 3 bytes as 4 characters
|
|
248
|
-
return (base64.length * 3) / 4 / 1024;
|
|
153
|
+
export async function onLowMemory() {
|
|
154
|
+
await clearCache();
|
|
155
|
+
return 0;
|
|
249
156
|
}
|
|
157
|
+
export function configureCache(_config) { }
|
|
158
|
+
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.
|
|
3
|
+
"version": "0.0.14",
|
|
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
|
-
"@
|
|
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,84 +1,140 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web implementation using @
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
4
|
+
* Loads from CDN to bypass Metro bundler issues.
|
|
6
5
|
*/
|
|
7
6
|
|
|
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
7
|
export type OutputFormat = 'PNG' | 'WEBP';
|
|
15
8
|
|
|
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
9
|
export interface RemoveBgImageOptions {
|
|
29
|
-
maxDimension?: number;
|
|
30
10
|
format?: OutputFormat;
|
|
31
11
|
quality?: number;
|
|
32
12
|
onProgress?: (progress: number) => void;
|
|
33
|
-
useCache?: boolean;
|
|
34
13
|
debug?: boolean;
|
|
35
14
|
}
|
|
36
|
-
// Web cache configuration
|
|
37
|
-
const webCacheConfig = {
|
|
38
|
-
maxEntries: 50,
|
|
39
|
-
maxAgeMinutes: 30,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Simple in-memory LRU cache for web
|
|
43
|
-
const webCache = new Map<string, string>();
|
|
44
15
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
let pipeline: any = null;
|
|
18
|
+
let loadPromise: Promise<void> | null = null;
|
|
19
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
20
|
+
let transformersModule: any = null;
|
|
21
|
+
|
|
22
|
+
// Load transformers.js from CDN to avoid Metro bundling issues
|
|
23
|
+
async function loadTransformers() {
|
|
24
|
+
if (transformersModule) return transformersModule;
|
|
25
|
+
|
|
26
|
+
// Use dynamic import from CDN
|
|
27
|
+
const cdnUrl = 'https://cdn.jsdelivr.net/npm/@huggingface/transformers@3';
|
|
28
|
+
transformersModule = await import(/* webpackIgnore: true */ cdnUrl);
|
|
29
|
+
return transformersModule;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function ensureLoaded(onProgress?: (p: number) => void, debug?: boolean) {
|
|
33
|
+
if (pipeline) return pipeline;
|
|
34
|
+
|
|
35
|
+
if (loadPromise) {
|
|
36
|
+
await loadPromise;
|
|
37
|
+
return pipeline;
|
|
52
38
|
}
|
|
53
39
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
40
|
+
loadPromise = (async () => {
|
|
41
|
+
if (debug) console.log('[rmbg] Loading model...');
|
|
42
|
+
|
|
43
|
+
const { pipeline: createPipeline, env } = await loadTransformers();
|
|
44
|
+
|
|
45
|
+
env.allowLocalModels = false;
|
|
46
|
+
env.useBrowserCache = true;
|
|
47
|
+
|
|
48
|
+
onProgress?.(10);
|
|
49
|
+
|
|
50
|
+
pipeline = await createPipeline('image-segmentation', 'briaai/RMBG-1.4', {
|
|
51
|
+
dtype: 'q8',
|
|
52
|
+
progress_callback: (info: { progress?: number }) => {
|
|
53
|
+
if (onProgress && 'progress' in info && typeof info.progress === 'number') {
|
|
54
|
+
onProgress(Math.min(10 + (info.progress / 100) * 50, 60));
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (debug) console.log('[rmbg] Model ready');
|
|
60
|
+
})();
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
await loadPromise;
|
|
63
|
+
return pipeline;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
|
-
*
|
|
67
|
+
* Remove background from image
|
|
67
68
|
*/
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
export async function removeBgImage(
|
|
70
|
+
uri: string,
|
|
71
|
+
options: RemoveBgImageOptions = {}
|
|
72
|
+
): Promise<string> {
|
|
73
|
+
const { format = 'PNG', quality = 100, onProgress, debug = false } = options;
|
|
74
|
+
|
|
75
|
+
if (debug) console.log('[rmbg] Processing:', uri);
|
|
76
|
+
onProgress?.(5);
|
|
77
|
+
|
|
78
|
+
const segmenter = await ensureLoaded(onProgress, debug);
|
|
79
|
+
onProgress?.(60);
|
|
80
|
+
|
|
81
|
+
const results = await segmenter(uri);
|
|
82
|
+
onProgress?.(90);
|
|
83
|
+
|
|
84
|
+
const result = Array.isArray(results) ? results[0] : results;
|
|
85
|
+
if (!result?.mask) throw new Error('No mask returned');
|
|
86
|
+
|
|
87
|
+
// Convert to data URL
|
|
88
|
+
const dataUrl = typeof result.mask === 'string'
|
|
89
|
+
? result.mask
|
|
90
|
+
: toDataUrl(result.mask, format, quality);
|
|
91
|
+
|
|
92
|
+
if (debug) console.log('[rmbg] Done');
|
|
93
|
+
onProgress?.(100);
|
|
94
|
+
|
|
95
|
+
return dataUrl;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function toDataUrl(
|
|
99
|
+
mask: { width: number; height: number; data: Uint8Array | Uint8ClampedArray },
|
|
100
|
+
format: OutputFormat,
|
|
101
|
+
quality: number
|
|
102
|
+
): string {
|
|
103
|
+
const canvas = document.createElement('canvas');
|
|
104
|
+
canvas.width = mask.width;
|
|
105
|
+
canvas.height = mask.height;
|
|
106
|
+
const ctx = canvas.getContext('2d')!;
|
|
107
|
+
const imageData = ctx.createImageData(mask.width, mask.height);
|
|
108
|
+
imageData.data.set(mask.data);
|
|
109
|
+
ctx.putImageData(imageData, 0, 0);
|
|
110
|
+
return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const removeBackground = removeBgImage;
|
|
114
|
+
|
|
115
|
+
// Helper to load image
|
|
116
|
+
function loadImage(src: string): Promise<HTMLImageElement> {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
const img = new Image();
|
|
119
|
+
img.crossOrigin = 'anonymous';
|
|
120
|
+
img.onload = () => resolve(img);
|
|
121
|
+
img.onerror = reject;
|
|
122
|
+
img.src = src;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CompressImageOptions {
|
|
127
|
+
maxSizeKB?: number;
|
|
128
|
+
width?: number;
|
|
129
|
+
height?: number;
|
|
130
|
+
quality?: number;
|
|
131
|
+
format?: 'webp' | 'png' | 'jpeg';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface GenerateThumbhashOptions {
|
|
135
|
+
size?: number;
|
|
76
136
|
}
|
|
77
137
|
|
|
78
|
-
/**
|
|
79
|
-
* Compress image on web using canvas
|
|
80
|
-
* @returns Compressed image as data URL
|
|
81
|
-
*/
|
|
82
138
|
export async function compressImage(
|
|
83
139
|
uri: string,
|
|
84
140
|
options: CompressImageOptions = {}
|
|
@@ -92,15 +148,11 @@ export async function compressImage(
|
|
|
92
148
|
} = options;
|
|
93
149
|
|
|
94
150
|
try {
|
|
95
|
-
// Load image
|
|
96
151
|
const img = await loadImage(uri);
|
|
97
|
-
|
|
98
|
-
// Calculate target dimensions maintaining aspect ratio
|
|
99
152
|
const scale = Math.min(width / img.width, height / img.height, 1);
|
|
100
153
|
const targetWidth = Math.round(img.width * scale);
|
|
101
154
|
const targetHeight = Math.round(img.height * scale);
|
|
102
155
|
|
|
103
|
-
// Create canvas and draw resized image
|
|
104
156
|
const canvas = document.createElement('canvas');
|
|
105
157
|
canvas.width = targetWidth;
|
|
106
158
|
canvas.height = targetHeight;
|
|
@@ -108,36 +160,24 @@ export async function compressImage(
|
|
|
108
160
|
if (!ctx) throw new Error('Could not get canvas context');
|
|
109
161
|
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
110
162
|
|
|
111
|
-
|
|
112
|
-
const mimeType =
|
|
113
|
-
format === 'png'
|
|
114
|
-
? 'image/png'
|
|
115
|
-
: format === 'jpeg'
|
|
116
|
-
? 'image/jpeg'
|
|
117
|
-
: 'image/webp';
|
|
163
|
+
const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp';
|
|
118
164
|
let dataUrl = canvas.toDataURL(mimeType, quality);
|
|
119
165
|
|
|
120
|
-
//
|
|
166
|
+
// Reduce quality if over size limit
|
|
121
167
|
let currentQuality = quality;
|
|
122
|
-
|
|
168
|
+
const getSize = (url: string) => ((url.split(',')[1] || '').length * 3) / 4 / 1024;
|
|
169
|
+
while (getSize(dataUrl) > maxSizeKB && currentQuality > 0.5) {
|
|
123
170
|
currentQuality -= 0.1;
|
|
124
171
|
dataUrl = canvas.toDataURL(mimeType, currentQuality);
|
|
125
172
|
}
|
|
126
173
|
|
|
127
174
|
return dataUrl;
|
|
128
175
|
} catch (error) {
|
|
129
|
-
console.warn(
|
|
130
|
-
'[rn-remove-image-bg] compressImage failed on web, returning original:',
|
|
131
|
-
error
|
|
132
|
-
);
|
|
176
|
+
console.warn('[rmbg] compressImage failed:', error);
|
|
133
177
|
return uri;
|
|
134
178
|
}
|
|
135
179
|
}
|
|
136
180
|
|
|
137
|
-
/**
|
|
138
|
-
* Generate thumbhash on web using canvas
|
|
139
|
-
* @returns Base64 thumbhash string
|
|
140
|
-
*/
|
|
141
181
|
export async function generateThumbhash(
|
|
142
182
|
imageUri: string,
|
|
143
183
|
options: GenerateThumbhashOptions = {}
|
|
@@ -145,11 +185,8 @@ export async function generateThumbhash(
|
|
|
145
185
|
const { size = 32 } = options;
|
|
146
186
|
|
|
147
187
|
try {
|
|
148
|
-
// Dynamically import thumbhash to avoid bundling issues
|
|
149
|
-
const { rgbaToThumbHash } = await import('thumbhash');
|
|
150
|
-
|
|
151
|
-
// Load and resize image
|
|
152
188
|
const img = await loadImage(imageUri);
|
|
189
|
+
|
|
153
190
|
const canvas = document.createElement('canvas');
|
|
154
191
|
canvas.width = size;
|
|
155
192
|
canvas.height = size;
|
|
@@ -157,191 +194,32 @@ export async function generateThumbhash(
|
|
|
157
194
|
if (!ctx) throw new Error('Could not get canvas context');
|
|
158
195
|
ctx.drawImage(img, 0, 0, size, size);
|
|
159
196
|
|
|
160
|
-
// Get RGBA data
|
|
161
197
|
const imageData = ctx.getImageData(0, 0, size, size);
|
|
198
|
+
|
|
199
|
+
// Load thumbhash from CDN
|
|
200
|
+
// @ts-expect-error CDN import works at runtime
|
|
201
|
+
const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
|
|
162
202
|
const hash = rgbaToThumbHash(size, size, imageData.data);
|
|
163
|
-
|
|
164
|
-
// Convert to base64
|
|
165
203
|
return btoa(String.fromCharCode(...hash));
|
|
166
204
|
} catch (error) {
|
|
167
|
-
console.warn(
|
|
168
|
-
'[rn-remove-image-bg] generateThumbhash failed on web:',
|
|
169
|
-
error
|
|
170
|
-
);
|
|
205
|
+
console.warn('[rmbg] generateThumbhash failed:', error);
|
|
171
206
|
return '';
|
|
172
207
|
}
|
|
173
208
|
}
|
|
174
209
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
*/
|
|
179
|
-
export async function removeBgImage(
|
|
180
|
-
uri: string,
|
|
181
|
-
options: RemoveBgImageOptions = {}
|
|
182
|
-
): 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
|
-
}
|
|
207
|
-
|
|
208
|
-
onProgress?.(5);
|
|
209
|
-
|
|
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
|
-
});
|
|
232
|
-
|
|
233
|
-
onProgress?.(95);
|
|
234
|
-
|
|
235
|
-
// Convert blob to data URL
|
|
236
|
-
const dataUrl = await blobToDataUrl(blob);
|
|
237
|
-
|
|
238
|
-
// Cache the result with LRU eviction
|
|
239
|
-
if (useCache) {
|
|
240
|
-
setCacheEntry(cacheKey, dataUrl);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (debug) {
|
|
244
|
-
console.log('[rn-remove-image-bg] Web background removal complete');
|
|
245
|
-
}
|
|
246
|
-
|
|
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
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Backward compatibility alias
|
|
259
|
-
* @deprecated Use removeBgImage instead
|
|
260
|
-
*/
|
|
261
|
-
export const removeBackground = removeBgImage;
|
|
262
|
-
|
|
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();
|
|
210
|
+
export async function clearCache(): Promise<void> {
|
|
211
|
+
pipeline = null;
|
|
212
|
+
loadPromise = null;
|
|
269
213
|
}
|
|
270
214
|
|
|
271
|
-
/**
|
|
272
|
-
* Get the current cache size
|
|
273
|
-
*/
|
|
274
215
|
export function getCacheSize(): number {
|
|
275
|
-
return
|
|
276
|
-
}
|
|
277
|
-
|
|
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;
|
|
292
|
-
}
|
|
293
|
-
|
|
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
|
|
311
|
-
}
|
|
312
|
-
|
|
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 {
|
|
318
|
-
return '';
|
|
216
|
+
return 0;
|
|
319
217
|
}
|
|
320
218
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
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
|
-
});
|
|
219
|
+
export async function onLowMemory(): Promise<number> {
|
|
220
|
+
await clearCache();
|
|
221
|
+
return 0;
|
|
340
222
|
}
|
|
341
223
|
|
|
342
|
-
function
|
|
343
|
-
|
|
344
|
-
const base64 = dataUrl.split(',')[1] || '';
|
|
345
|
-
// Base64 encodes 3 bytes as 4 characters
|
|
346
|
-
return (base64.length * 3) / 4 / 1024;
|
|
347
|
-
}
|
|
224
|
+
export function configureCache(_config: { maxEntries?: number }): void {}
|
|
225
|
+
export function getCacheDirectory(): string { return ''; }
|