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.
@@ -1,346 +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
-
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
- // 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
- }
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
- 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;
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
- // 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 } = 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
- onProgress?.(95);
68
+ const results = await segmenter(uri);
69
+ onProgress?.(90);
233
70
 
234
- // Convert blob to data URL
235
- const dataUrl = await blobToDataUrl(blob);
71
+ const result = Array.isArray(results) ? results[0] : results;
72
+ if (!result?.mask) throw new Error('No mask returned');
236
73
 
237
- // Cache the result with LRU eviction
238
- if (useCache) {
239
- setCacheEntry(cacheKey, dataUrl);
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
- if (debug) {
243
- console.log('[rn-remove-image-bg] Web background removal complete');
244
- }
79
+ if (debug) console.log('[rmbg] Done');
80
+ onProgress?.(100);
81
+
82
+ return dataUrl;
83
+ }
245
84
 
246
- onProgress?.(100);
247
- return dataUrl;
248
- } catch (error) {
249
- console.error('[rn-remove-image-bg] Web background removal failed:', error);
250
- // Return original URI on failure
251
- onProgress?.(100);
252
- return uri;
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
- * Clear the web background removal cache
264
- * @param _deleteFiles - Ignored on web (no disk cache)
265
- */
266
- export async function clearCache(_deleteFiles = false): Promise<void> {
267
- webCache.clear();
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
- * Handle low memory conditions by clearing the cache
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
- * Configure the background removal cache
295
- * On web, maxEntries limits cache size. Disk persistence options are no-ops.
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
- * Get the cache directory path
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
- // Helper functions
321
-
322
- function loadImage(src: string): Promise<HTMLImageElement> {
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 blobToDataUrl(blob: Blob): Promise<string> {
333
- return new Promise((resolve, reject) => {
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 getDataUrlSizeKB(dataUrl: string): number {
342
- // Data URL format: data:mime;base64,<base64data>
343
- const base64 = dataUrl.split(',')[1] || '';
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 ''; }