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,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
- // 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
- * 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.11",
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
+ }