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,346 +1,140 @@
|
|
|
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
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
|
-
|
|
42
|
-
// Simple in-memory LRU cache for web
|
|
43
|
-
const webCache = new Map<string, string>();
|
|
44
|
-
|
|
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);
|
|
52
|
-
}
|
|
53
|
-
|
|
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
14
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
currentQuality -= 0.1;
|
|
124
|
-
dataUrl = canvas.toDataURL(mimeType, currentQuality);
|
|
125
|
-
}
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
let pipeline: any = null;
|
|
17
|
+
let loadPromise: Promise<void> | null = null;
|
|
126
18
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return uri;
|
|
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;
|
|
134
25
|
}
|
|
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
26
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
const { removeBackground: imglyRemoveBackground } = await import('@imgly/background-removal');
|
|
213
|
-
|
|
214
|
-
// Call @imgly/background-removal
|
|
215
|
-
const blob = await imglyRemoveBackground(uri, {
|
|
216
|
-
progress: (key: string, current: number, total: number) => {
|
|
217
|
-
if (onProgress && total > 0) {
|
|
218
|
-
// Map progress to 10-90 range
|
|
219
|
-
const progress = Math.round(10 + (current / total) * 80);
|
|
220
|
-
onProgress(Math.min(progress, 90));
|
|
221
|
-
}
|
|
222
|
-
if (debug) {
|
|
223
|
-
console.log(`[rn-remove-image-bg] ${key}: ${current}/${total}`);
|
|
224
|
-
}
|
|
225
|
-
},
|
|
226
|
-
output: {
|
|
227
|
-
format: format === 'WEBP' ? 'image/webp' : 'image/png',
|
|
228
|
-
quality: quality / 100,
|
|
229
|
-
},
|
|
230
|
-
});
|
|
65
|
+
const segmenter = await ensureLoaded(onProgress, debug);
|
|
66
|
+
onProgress?.(60);
|
|
231
67
|
|
|
232
|
-
|
|
68
|
+
const results = await segmenter(uri);
|
|
69
|
+
onProgress?.(90);
|
|
233
70
|
|
|
234
|
-
|
|
235
|
-
|
|
71
|
+
const result = Array.isArray(results) ? results[0] : results;
|
|
72
|
+
if (!result?.mask) throw new Error('No mask returned');
|
|
236
73
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
74
|
+
// Convert to data URL
|
|
75
|
+
const dataUrl = typeof result.mask === 'string'
|
|
76
|
+
? result.mask
|
|
77
|
+
: toDataUrl(result.mask, format, quality);
|
|
241
78
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
79
|
+
if (debug) console.log('[rmbg] Done');
|
|
80
|
+
onProgress?.(100);
|
|
81
|
+
|
|
82
|
+
return dataUrl;
|
|
83
|
+
}
|
|
245
84
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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);
|
|
254
98
|
}
|
|
255
99
|
|
|
256
|
-
/**
|
|
257
|
-
* Backward compatibility alias
|
|
258
|
-
* @deprecated Use removeBgImage instead
|
|
259
|
-
*/
|
|
260
100
|
export const removeBackground = removeBgImage;
|
|
261
101
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Get the current cache size
|
|
272
|
-
*/
|
|
273
|
-
export function getCacheSize(): number {
|
|
274
|
-
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';
|
|
275
109
|
}
|
|
276
110
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
* On web, this simply clears the in-memory cache
|
|
280
|
-
*
|
|
281
|
-
* @param _deleteFiles - Ignored on web (no disk cache)
|
|
282
|
-
* @returns Number of entries that were cleared
|
|
283
|
-
*/
|
|
284
|
-
export async function onLowMemory(_deleteFiles = true): Promise<number> {
|
|
285
|
-
const size = webCache.size;
|
|
286
|
-
webCache.clear();
|
|
287
|
-
console.log(
|
|
288
|
-
`[rn-remove-image-bg] Cleared ${size} web cache entries due to memory pressure`
|
|
289
|
-
);
|
|
290
|
-
return size;
|
|
111
|
+
export interface GenerateThumbhashOptions {
|
|
112
|
+
size?: number;
|
|
291
113
|
}
|
|
292
114
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
*/
|
|
297
|
-
export function configureCache(config: {
|
|
298
|
-
maxEntries?: number;
|
|
299
|
-
maxAgeMinutes?: number;
|
|
300
|
-
persistToDisk?: boolean;
|
|
301
|
-
cacheDirectory?: string;
|
|
302
|
-
}): void {
|
|
303
|
-
if (config.maxEntries !== undefined && config.maxEntries > 0) {
|
|
304
|
-
webCacheConfig.maxEntries = config.maxEntries;
|
|
305
|
-
}
|
|
306
|
-
if (config.maxAgeMinutes !== undefined && config.maxAgeMinutes > 0) {
|
|
307
|
-
webCacheConfig.maxAgeMinutes = config.maxAgeMinutes;
|
|
308
|
-
}
|
|
309
|
-
// 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;
|
|
310
118
|
}
|
|
311
119
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
* On web, returns empty string as there is no disk cache
|
|
315
|
-
*/
|
|
316
|
-
export function getCacheDirectory(): string {
|
|
120
|
+
export async function generateThumbhash(_uri: string, _options?: GenerateThumbhashOptions): Promise<string> {
|
|
121
|
+
console.warn('[rmbg] generateThumbhash not implemented on web');
|
|
317
122
|
return '';
|
|
318
123
|
}
|
|
319
124
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
return new Promise((resolve, reject) => {
|
|
324
|
-
const img = new Image();
|
|
325
|
-
img.crossOrigin = 'anonymous';
|
|
326
|
-
img.onload = () => resolve(img);
|
|
327
|
-
img.onerror = reject;
|
|
328
|
-
img.src = src;
|
|
329
|
-
});
|
|
125
|
+
export async function clearCache(): Promise<void> {
|
|
126
|
+
pipeline = null;
|
|
127
|
+
loadPromise = null;
|
|
330
128
|
}
|
|
331
129
|
|
|
332
|
-
function
|
|
333
|
-
return
|
|
334
|
-
const reader = new FileReader();
|
|
335
|
-
reader.onloadend = () => resolve(reader.result as string);
|
|
336
|
-
reader.onerror = reject;
|
|
337
|
-
reader.readAsDataURL(blob);
|
|
338
|
-
});
|
|
130
|
+
export function getCacheSize(): number {
|
|
131
|
+
return 0;
|
|
339
132
|
}
|
|
340
133
|
|
|
341
|
-
function
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
// Base64 encodes 3 bytes as 4 characters
|
|
345
|
-
return (base64.length * 3) / 4 / 1024;
|
|
134
|
+
export async function onLowMemory(): Promise<number> {
|
|
135
|
+
await clearCache();
|
|
136
|
+
return 0;
|
|
346
137
|
}
|
|
138
|
+
|
|
139
|
+
export function configureCache(_config: { maxEntries?: number }): void {}
|
|
140
|
+
export function getCacheDirectory(): string { return ''; }
|