rn-remove-image-bg 0.0.13 → 0.0.15

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,6 +1,7 @@
1
1
  /**
2
2
  * Web implementation using @huggingface/transformers
3
3
  * Uses BRIAAI RMBG-1.4 model for background removal.
4
+ * Loads from CDN to bypass Metro bundler issues.
4
5
  */
5
6
  export type OutputFormat = 'PNG' | 'WEBP';
6
7
  export interface RemoveBgImageOptions {
@@ -24,8 +25,8 @@ export interface CompressImageOptions {
24
25
  export interface GenerateThumbhashOptions {
25
26
  size?: number;
26
27
  }
27
- export declare function compressImage(uri: string, _options?: CompressImageOptions): Promise<string>;
28
- export declare function generateThumbhash(_uri: string, _options?: GenerateThumbhashOptions): Promise<string>;
28
+ export declare function compressImage(uri: string, options?: CompressImageOptions): Promise<string>;
29
+ export declare function generateThumbhash(imageUri: string, options?: GenerateThumbhashOptions): Promise<string>;
29
30
  export declare function clearCache(): Promise<void>;
30
31
  export declare function getCacheSize(): number;
31
32
  export declare function onLowMemory(): Promise<number>;
@@ -1,10 +1,22 @@
1
1
  /**
2
2
  * Web implementation using @huggingface/transformers
3
3
  * Uses BRIAAI RMBG-1.4 model for background removal.
4
+ * Loads from CDN to bypass Metro bundler issues.
4
5
  */
5
6
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
6
7
  let pipeline = null;
7
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;
19
+ }
8
20
  async function ensureLoaded(onProgress, debug) {
9
21
  if (pipeline)
10
22
  return pipeline;
@@ -15,7 +27,7 @@ async function ensureLoaded(onProgress, debug) {
15
27
  loadPromise = (async () => {
16
28
  if (debug)
17
29
  console.log('[rmbg] Loading model...');
18
- const { pipeline: createPipeline, env } = await import('@huggingface/transformers');
30
+ const { pipeline: createPipeline, env } = await loadTransformers();
19
31
  env.allowLocalModels = false;
20
32
  env.useBrowserCache = true;
21
33
  onProgress?.(10);
@@ -48,33 +60,155 @@ export async function removeBgImage(uri, options = {}) {
48
60
  const result = Array.isArray(results) ? results[0] : results;
49
61
  if (!result?.mask)
50
62
  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);
63
+ // Apply mask to original image
64
+ const original = await loadImage(uri);
65
+ const dataUrl = await applyMask(original, result.mask, format, quality);
55
66
  if (debug)
56
67
  console.log('[rmbg] Done');
57
68
  onProgress?.(100);
58
69
  return dataUrl;
59
70
  }
60
- function toDataUrl(mask, format, quality) {
71
+ async function applyMask(image, mask, format, quality) {
61
72
  const canvas = document.createElement('canvas');
62
- canvas.width = mask.width;
63
- canvas.height = mask.height;
73
+ // Use original image dimensions
74
+ canvas.width = image.width;
75
+ canvas.height = image.height;
64
76
  const ctx = canvas.getContext('2d');
65
- const imageData = ctx.createImageData(mask.width, mask.height);
66
- imageData.data.set(mask.data);
77
+ if (!ctx)
78
+ throw new Error('Could not get canvas context');
79
+ // Draw original image
80
+ ctx.drawImage(image, 0, 0);
81
+ // Get image data to manipulate pixels
82
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
83
+ const pixelData = imageData.data;
84
+ // Process mask
85
+ let maskData;
86
+ let maskWidth;
87
+ let maskHeight;
88
+ if (typeof mask === 'string') {
89
+ // If mask is a URL, load it
90
+ const maskImg = await loadImage(mask);
91
+ const maskCanvas = document.createElement('canvas');
92
+ maskCanvas.width = canvas.width;
93
+ maskCanvas.height = canvas.height;
94
+ const maskCtx = maskCanvas.getContext('2d');
95
+ if (!maskCtx)
96
+ throw new Error('Could not get mask context');
97
+ // Draw and resize mask to match image
98
+ maskCtx.drawImage(maskImg, 0, 0, canvas.width, canvas.height);
99
+ const maskImageData = maskCtx.getImageData(0, 0, canvas.width, canvas.height);
100
+ maskData = maskImageData.data;
101
+ maskWidth = canvas.width;
102
+ maskHeight = canvas.height;
103
+ }
104
+ else {
105
+ // @ts-ignore - Transformers.js types are loose
106
+ maskData = mask.data;
107
+ maskWidth = mask.width;
108
+ maskHeight = mask.height;
109
+ }
110
+ // Helper to get alpha value from mask data
111
+ const getAlpha = (index, data, width, height, targetWidth, targetHeight) => {
112
+ // If dimensions match
113
+ if (width === targetWidth && height === targetHeight) {
114
+ // Check if mask is single channel (grayscale) or RGBA
115
+ if (data.length === width * height) {
116
+ return data[index / 4];
117
+ }
118
+ else if (data.length === width * height * 3) {
119
+ return data[Math.floor(index / 4) * 3];
120
+ }
121
+ else {
122
+ return data[index]; // Assume RGBA red channel or alpha channel usage
123
+ }
124
+ }
125
+ // Nearest neighbor resizing
126
+ const x = (index / 4) % targetWidth;
127
+ const y = Math.floor((index / 4) / targetWidth);
128
+ const maskX = Math.floor(x * (width / targetWidth));
129
+ const maskY = Math.floor(y * (height / targetHeight));
130
+ const maskIndex = (maskY * width + maskX);
131
+ if (data.length === width * height) {
132
+ return data[maskIndex];
133
+ }
134
+ else if (data.length === width * height * 3) {
135
+ return data[maskIndex * 3];
136
+ }
137
+ else {
138
+ return data[maskIndex * 4];
139
+ }
140
+ };
141
+ // Apply alpha
142
+ for (let i = 0; i < pixelData.length; i += 4) {
143
+ const alpha = getAlpha(i, maskData, maskWidth, maskHeight, canvas.width, canvas.height) ?? 255;
144
+ pixelData[i + 3] = alpha;
145
+ }
67
146
  ctx.putImageData(imageData, 0, 0);
68
147
  return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
69
148
  }
70
149
  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;
150
+ // Helper to load image
151
+ function loadImage(src) {
152
+ return new Promise((resolve, reject) => {
153
+ const img = new Image();
154
+ img.crossOrigin = 'anonymous';
155
+ img.onload = () => resolve(img);
156
+ img.onerror = reject;
157
+ img.src = src;
158
+ });
74
159
  }
75
- export async function generateThumbhash(_uri, _options) {
76
- console.warn('[rmbg] generateThumbhash not implemented on web');
77
- return '';
160
+ export async function compressImage(uri, options = {}) {
161
+ const { maxSizeKB = 250, width = 1024, height = 1024, quality = 0.85, format = 'webp', } = options;
162
+ try {
163
+ const img = await loadImage(uri);
164
+ const scale = Math.min(width / img.width, height / img.height, 1);
165
+ const targetWidth = Math.round(img.width * scale);
166
+ const targetHeight = Math.round(img.height * scale);
167
+ const canvas = document.createElement('canvas');
168
+ canvas.width = targetWidth;
169
+ canvas.height = targetHeight;
170
+ const ctx = canvas.getContext('2d');
171
+ if (!ctx)
172
+ throw new Error('Could not get canvas context');
173
+ ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
174
+ const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp';
175
+ let dataUrl = canvas.toDataURL(mimeType, quality);
176
+ // Reduce quality if over size limit
177
+ let currentQuality = quality;
178
+ const getSize = (url) => ((url.split(',')[1] || '').length * 3) / 4 / 1024;
179
+ while (getSize(dataUrl) > maxSizeKB && currentQuality > 0.5) {
180
+ currentQuality -= 0.1;
181
+ dataUrl = canvas.toDataURL(mimeType, currentQuality);
182
+ }
183
+ return dataUrl;
184
+ }
185
+ catch (error) {
186
+ console.warn('[rmbg] compressImage failed:', error);
187
+ return uri;
188
+ }
189
+ }
190
+ export async function generateThumbhash(imageUri, options = {}) {
191
+ const { size = 32 } = options;
192
+ try {
193
+ const img = await loadImage(imageUri);
194
+ const canvas = document.createElement('canvas');
195
+ canvas.width = size;
196
+ canvas.height = size;
197
+ const ctx = canvas.getContext('2d');
198
+ if (!ctx)
199
+ throw new Error('Could not get canvas context');
200
+ ctx.drawImage(img, 0, 0, size, size);
201
+ const imageData = ctx.getImageData(0, 0, size, size);
202
+ // Load thumbhash from CDN
203
+ // @ts-expect-error CDN import works at runtime
204
+ const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
205
+ const hash = rgbaToThumbHash(size, size, imageData.data);
206
+ return btoa(String.fromCharCode(...hash));
207
+ }
208
+ catch (error) {
209
+ console.warn('[rmbg] generateThumbhash failed:', error);
210
+ return '';
211
+ }
78
212
  }
79
213
  export async function clearCache() {
80
214
  pipeline = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-remove-image-bg",
3
- "version": "0.0.13",
3
+ "version": "0.0.15",
4
4
  "description": "rn-remove-image-bg",
5
5
  "homepage": "https://github.com/a-eid/rn-remove-image-bg",
6
6
  "main": "lib/index",
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Web implementation using @huggingface/transformers
3
3
  * Uses BRIAAI RMBG-1.4 model for background removal.
4
+ * Loads from CDN to bypass Metro bundler issues.
4
5
  */
5
6
 
6
7
  export type OutputFormat = 'PNG' | 'WEBP';
@@ -15,6 +16,18 @@ export interface RemoveBgImageOptions {
15
16
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
17
  let pipeline: any = null;
17
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
+ }
18
31
 
19
32
  async function ensureLoaded(onProgress?: (p: number) => void, debug?: boolean) {
20
33
  if (pipeline) return pipeline;
@@ -27,7 +40,7 @@ async function ensureLoaded(onProgress?: (p: number) => void, debug?: boolean) {
27
40
  loadPromise = (async () => {
28
41
  if (debug) console.log('[rmbg] Loading model...');
29
42
 
30
- const { pipeline: createPipeline, env } = await import('@huggingface/transformers');
43
+ const { pipeline: createPipeline, env } = await loadTransformers();
31
44
 
32
45
  env.allowLocalModels = false;
33
46
  env.useBrowserCache = true;
@@ -36,7 +49,7 @@ async function ensureLoaded(onProgress?: (p: number) => void, debug?: boolean) {
36
49
 
37
50
  pipeline = await createPipeline('image-segmentation', 'briaai/RMBG-1.4', {
38
51
  dtype: 'q8',
39
- progress_callback: (info) => {
52
+ progress_callback: (info: { progress?: number }) => {
40
53
  if (onProgress && 'progress' in info && typeof info.progress === 'number') {
41
54
  onProgress(Math.min(10 + (info.progress / 100) * 50, 60));
42
55
  }
@@ -71,10 +84,9 @@ export async function removeBgImage(
71
84
  const result = Array.isArray(results) ? results[0] : results;
72
85
  if (!result?.mask) throw new Error('No mask returned');
73
86
 
74
- // Convert to data URL
75
- const dataUrl = typeof result.mask === 'string'
76
- ? result.mask
77
- : toDataUrl(result.mask, format, quality);
87
+ // Apply mask to original image
88
+ const original = await loadImage(uri);
89
+ const dataUrl = await applyMask(original, result.mask, format, quality);
78
90
 
79
91
  if (debug) console.log('[rmbg] Done');
80
92
  onProgress?.(100);
@@ -82,24 +94,107 @@ export async function removeBgImage(
82
94
  return dataUrl;
83
95
  }
84
96
 
85
- function toDataUrl(
86
- mask: { width: number; height: number; data: Uint8Array | Uint8ClampedArray },
97
+ async function applyMask(
98
+ image: HTMLImageElement,
99
+ mask: { width: number; height: number; data: Uint8Array | Uint8ClampedArray } | string,
87
100
  format: OutputFormat,
88
101
  quality: number
89
- ): string {
102
+ ): Promise<string> {
90
103
  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);
104
+ // Use original image dimensions
105
+ canvas.width = image.width;
106
+ canvas.height = image.height;
107
+ const ctx = canvas.getContext('2d');
108
+ if (!ctx) throw new Error('Could not get canvas context');
109
+
110
+ // Draw original image
111
+ ctx.drawImage(image, 0, 0);
112
+
113
+ // Get image data to manipulate pixels
114
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
115
+ const pixelData = imageData.data;
116
+
117
+ // Process mask
118
+ let maskData: Uint8ClampedArray | Uint8Array;
119
+ let maskWidth: number;
120
+ let maskHeight: number;
121
+
122
+ if (typeof mask === 'string') {
123
+ // If mask is a URL, load it
124
+ const maskImg = await loadImage(mask);
125
+ const maskCanvas = document.createElement('canvas');
126
+ maskCanvas.width = canvas.width;
127
+ maskCanvas.height = canvas.height;
128
+ const maskCtx = maskCanvas.getContext('2d');
129
+ if (!maskCtx) throw new Error('Could not get mask context');
130
+
131
+ // Draw and resize mask to match image
132
+ maskCtx.drawImage(maskImg, 0, 0, canvas.width, canvas.height);
133
+ const maskImageData = maskCtx.getImageData(0, 0, canvas.width, canvas.height);
134
+ maskData = maskImageData.data;
135
+ maskWidth = canvas.width;
136
+ maskHeight = canvas.height;
137
+ } else {
138
+ // @ts-ignore - Transformers.js types are loose
139
+ maskData = mask.data;
140
+ maskWidth = mask.width;
141
+ maskHeight = mask.height;
142
+ }
143
+
144
+ // Helper to get alpha value from mask data
145
+ const getAlpha = (index: number, data: Uint8ClampedArray | Uint8Array, width: number, height: number, targetWidth: number, targetHeight: number) => {
146
+ // If dimensions match
147
+ if (width === targetWidth && height === targetHeight) {
148
+ // Check if mask is single channel (grayscale) or RGBA
149
+ if (data.length === width * height) {
150
+ return data[index / 4];
151
+ } else if (data.length === width * height * 3) {
152
+ return data[Math.floor(index / 4) * 3];
153
+ } else {
154
+ return data[index]; // Assume RGBA red channel or alpha channel usage
155
+ }
156
+ }
157
+
158
+ // Nearest neighbor resizing
159
+ const x = (index / 4) % targetWidth;
160
+ const y = Math.floor((index / 4) / targetWidth);
161
+
162
+ const maskX = Math.floor(x * (width / targetWidth));
163
+ const maskY = Math.floor(y * (height / targetHeight));
164
+ const maskIndex = (maskY * width + maskX);
165
+
166
+ if (data.length === width * height) {
167
+ return data[maskIndex];
168
+ } else if (data.length === width * height * 3) {
169
+ return data[maskIndex * 3];
170
+ } else {
171
+ return data[maskIndex * 4];
172
+ }
173
+ };
174
+
175
+ // Apply alpha
176
+ for (let i = 0; i < pixelData.length; i += 4) {
177
+ const alpha = getAlpha(i, maskData, maskWidth, maskHeight, canvas.width, canvas.height) ?? 255;
178
+ pixelData[i + 3] = alpha;
179
+ }
180
+
96
181
  ctx.putImageData(imageData, 0, 0);
97
182
  return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
98
183
  }
99
184
 
100
185
  export const removeBackground = removeBgImage;
101
186
 
102
- // Stub exports for API compatibility with native
187
+ // Helper to load image
188
+ function loadImage(src: string): Promise<HTMLImageElement> {
189
+ return new Promise((resolve, reject) => {
190
+ const img = new Image();
191
+ img.crossOrigin = 'anonymous';
192
+ img.onload = () => resolve(img);
193
+ img.onerror = reject;
194
+ img.src = src;
195
+ });
196
+ }
197
+
103
198
  export interface CompressImageOptions {
104
199
  maxSizeKB?: number;
105
200
  width?: number;
@@ -112,14 +207,76 @@ export interface GenerateThumbhashOptions {
112
207
  size?: number;
113
208
  }
114
209
 
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;
210
+ export async function compressImage(
211
+ uri: string,
212
+ options: CompressImageOptions = {}
213
+ ): Promise<string> {
214
+ const {
215
+ maxSizeKB = 250,
216
+ width = 1024,
217
+ height = 1024,
218
+ quality = 0.85,
219
+ format = 'webp',
220
+ } = options;
221
+
222
+ try {
223
+ const img = await loadImage(uri);
224
+ const scale = Math.min(width / img.width, height / img.height, 1);
225
+ const targetWidth = Math.round(img.width * scale);
226
+ const targetHeight = Math.round(img.height * scale);
227
+
228
+ const canvas = document.createElement('canvas');
229
+ canvas.width = targetWidth;
230
+ canvas.height = targetHeight;
231
+ const ctx = canvas.getContext('2d');
232
+ if (!ctx) throw new Error('Could not get canvas context');
233
+ ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
234
+
235
+ const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp';
236
+ let dataUrl = canvas.toDataURL(mimeType, quality);
237
+
238
+ // Reduce quality if over size limit
239
+ let currentQuality = quality;
240
+ const getSize = (url: string) => ((url.split(',')[1] || '').length * 3) / 4 / 1024;
241
+ while (getSize(dataUrl) > maxSizeKB && currentQuality > 0.5) {
242
+ currentQuality -= 0.1;
243
+ dataUrl = canvas.toDataURL(mimeType, currentQuality);
244
+ }
245
+
246
+ return dataUrl;
247
+ } catch (error) {
248
+ console.warn('[rmbg] compressImage failed:', error);
249
+ return uri;
250
+ }
118
251
  }
119
252
 
120
- export async function generateThumbhash(_uri: string, _options?: GenerateThumbhashOptions): Promise<string> {
121
- console.warn('[rmbg] generateThumbhash not implemented on web');
122
- return '';
253
+ export async function generateThumbhash(
254
+ imageUri: string,
255
+ options: GenerateThumbhashOptions = {}
256
+ ): Promise<string> {
257
+ const { size = 32 } = options;
258
+
259
+ try {
260
+ const img = await loadImage(imageUri);
261
+
262
+ const canvas = document.createElement('canvas');
263
+ canvas.width = size;
264
+ canvas.height = size;
265
+ const ctx = canvas.getContext('2d');
266
+ if (!ctx) throw new Error('Could not get canvas context');
267
+ ctx.drawImage(img, 0, 0, size, size);
268
+
269
+ const imageData = ctx.getImageData(0, 0, size, size);
270
+
271
+ // Load thumbhash from CDN
272
+ // @ts-expect-error CDN import works at runtime
273
+ const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
274
+ const hash = rgbaToThumbHash(size, size, imageData.data);
275
+ return btoa(String.fromCharCode(...hash));
276
+ } catch (error) {
277
+ console.warn('[rmbg] generateThumbhash failed:', error);
278
+ return '';
279
+ }
123
280
  }
124
281
 
125
282
  export async function clearCache(): Promise<void> {