rn-remove-image-bg 0.0.11 → 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 +107 -27
- package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt +76 -30
- package/lib/ImageProcessing.web.d.ts +17 -62
- package/lib/ImageProcessing.web.js +74 -232
- package/package.json +13 -14
- package/src/ImageProcessing.web.ts +90 -296
- package/src/__tests__/ImageProcessing.test.ts +132 -114
|
@@ -1,249 +1,91 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web implementation using @
|
|
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
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
// Return original URI on failure
|
|
171
|
-
onProgress?.(100);
|
|
172
|
-
return uri;
|
|
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
|
-
*
|
|
37
|
+
* Remove background from image
|
|
189
38
|
*/
|
|
190
|
-
export function
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
208
|
-
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
237
|
-
return
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
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.
|
|
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
|
-
"@
|
|
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
|
+
}
|