rn-remove-image-bg 0.0.15 → 0.0.18

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,7 +1,7 @@
1
1
  /**
2
- * Web implementation using @huggingface/transformers
3
- * Uses BRIAAI RMBG-1.4 model for background removal.
4
- * Loads from CDN to bypass Metro bundler issues.
2
+ * Web implementation using @imgly/background-removal via Inline Web Worker.
3
+ * Moves all heavy processing to a background thread to prevent UI freezing.
4
+ * Loads library from CDN to bypass Metro bundler issues.
5
5
  */
6
6
  export type OutputFormat = 'PNG' | 'WEBP';
7
7
  export interface RemoveBgImageOptions {
@@ -10,9 +10,6 @@ export interface RemoveBgImageOptions {
10
10
  onProgress?: (progress: number) => void;
11
11
  debug?: boolean;
12
12
  }
13
- /**
14
- * Remove background from image
15
- */
16
13
  export declare function removeBgImage(uri: string, options?: RemoveBgImageOptions): Promise<string>;
17
14
  export declare const removeBackground: typeof removeBgImage;
18
15
  export interface CompressImageOptions {
@@ -1,152 +1,132 @@
1
1
  /**
2
- * Web implementation using @huggingface/transformers
3
- * Uses BRIAAI RMBG-1.4 model for background removal.
4
- * Loads from CDN to bypass Metro bundler issues.
2
+ * Web implementation using @imgly/background-removal via Inline Web Worker.
3
+ * Moves all heavy processing to a background thread to prevent UI freezing.
4
+ * Loads library from CDN to bypass Metro bundler issues.
5
5
  */
6
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- let pipeline = null;
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
- }
20
- async function ensureLoaded(onProgress, debug) {
21
- if (pipeline)
22
- return pipeline;
23
- if (loadPromise) {
24
- await loadPromise;
25
- return pipeline;
26
- }
27
- loadPromise = (async () => {
28
- if (debug)
29
- console.log('[rmbg] Loading model...');
30
- const { pipeline: createPipeline, env } = await loadTransformers();
31
- env.allowLocalModels = false;
32
- env.useBrowserCache = true;
33
- onProgress?.(10);
34
- pipeline = await createPipeline('image-segmentation', 'briaai/RMBG-1.4', {
35
- dtype: 'q8',
36
- progress_callback: (info) => {
37
- if (onProgress && 'progress' in info && typeof info.progress === 'number') {
38
- onProgress(Math.min(10 + (info.progress / 100) * 50, 60));
39
- }
40
- },
6
+ // ==========================================
7
+ // INLINE WORKER CODE (Run in background)
8
+ // ==========================================
9
+ const WORKER_CODE = `
10
+ let removeBackground = null;
11
+
12
+ self.onmessage = async (e) => {
13
+ const { id, type, payload } = e.data;
14
+
15
+ try {
16
+ if (type === 'init') {
17
+ if (!removeBackground) {
18
+ // Dynamic import from CDN
19
+ // Using v1.3.0 which is stable
20
+ const module = await import('https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.3.0/+esm');
21
+ removeBackground = module.removeBackground;
22
+
23
+ // Preload/Init logic could go here if exposed, but imgly inits on first run usually
24
+ // Unless we want to preload the wasm.
25
+ // self.postMessage({ id, type: 'progress', payload: { progress: 0.1 } });
26
+ }
27
+ self.postMessage({ id, type: 'success', payload: true });
28
+ return;
29
+ }
30
+
31
+ if (type === 'removeBg') {
32
+ const { uri, config } = payload;
33
+
34
+ if (!removeBackground) {
35
+ const module = await import('https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.3.0/+esm');
36
+ removeBackground = module.removeBackground;
37
+ }
38
+
39
+ // Run Inference
40
+ // imgly config: { progress: (key, current, total) => ... }
41
+ const blob = await removeBackground(uri, {
42
+ progress: (key, current, total) => {
43
+ // Map progress roughly
44
+ const p = current / total;
45
+ self.postMessage({ id, type: 'progress', payload: { progress: p } });
46
+ },
47
+ ...config
41
48
  });
42
- if (debug)
43
- console.log('[rmbg] Model ready');
44
- })();
45
- await loadPromise;
46
- return pipeline;
47
- }
48
- /**
49
- * Remove background from image
50
- */
51
- export async function removeBgImage(uri, options = {}) {
52
- const { format = 'PNG', quality = 100, onProgress, debug = false } = options;
53
- if (debug)
54
- console.log('[rmbg] Processing:', uri);
55
- onProgress?.(5);
56
- const segmenter = await ensureLoaded(onProgress, debug);
57
- onProgress?.(60);
58
- const results = await segmenter(uri);
59
- onProgress?.(90);
60
- const result = Array.isArray(results) ? results[0] : results;
61
- if (!result?.mask)
62
- throw new Error('No mask returned');
63
- // Apply mask to original image
64
- const original = await loadImage(uri);
65
- const dataUrl = await applyMask(original, result.mask, format, quality);
66
- if (debug)
67
- console.log('[rmbg] Done');
68
- onProgress?.(100);
69
- return dataUrl;
70
- }
71
- async function applyMask(image, mask, format, quality) {
72
- const canvas = document.createElement('canvas');
73
- // Use original image dimensions
74
- canvas.width = image.width;
75
- canvas.height = image.height;
76
- const ctx = canvas.getContext('2d');
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;
49
+
50
+ // Convert blob to DataURL for return
51
+ const reader = new FileReader();
52
+ reader.onloadend = () => {
53
+ self.postMessage({ id, type: 'success', payload: reader.result });
54
+ };
55
+ reader.readAsDataURL(blob);
56
+ }
57
+ } catch (err) {
58
+ self.postMessage({ id, type: 'error', payload: err.message || JSON.stringify(err) });
109
59
  }
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];
60
+ };
61
+ `;
62
+ // ==========================================
63
+ // MAIN THREAD BRIDGE
64
+ // ==========================================
65
+ let worker = null;
66
+ const pendingMessages = new Map();
67
+ function getWorker() {
68
+ if (!worker) {
69
+ const blob = new Blob([WORKER_CODE], { type: 'application/javascript' });
70
+ const url = URL.createObjectURL(blob);
71
+ worker = new Worker(url);
72
+ worker.onmessage = (e) => {
73
+ const { id, type, payload } = e.data;
74
+ const deferred = pendingMessages.get(id);
75
+ if (!deferred)
76
+ return;
77
+ if (type === 'progress') {
78
+ if (deferred.onProgress && payload.progress) {
79
+ deferred.onProgress(payload.progress * 100);
80
+ }
117
81
  }
118
- else if (data.length === width * height * 3) {
119
- return data[Math.floor(index / 4) * 3];
82
+ else if (type === 'success') {
83
+ deferred.resolve(payload);
84
+ pendingMessages.delete(id);
120
85
  }
121
- else {
122
- return data[index]; // Assume RGBA red channel or alpha channel usage
86
+ else if (type === 'error') {
87
+ deferred.reject(new Error(payload));
88
+ pendingMessages.delete(id);
123
89
  }
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;
90
+ };
91
+ }
92
+ return worker;
93
+ }
94
+ function sendToWorker(type, payload, onProgress) {
95
+ return new Promise((resolve, reject) => {
96
+ const id = Math.random().toString(36).substring(7);
97
+ pendingMessages.set(id, { resolve, reject, onProgress });
98
+ getWorker().postMessage({ id, type, payload });
99
+ });
100
+ }
101
+ // Initialize
102
+ let initPromise = null;
103
+ async function ensureInit() {
104
+ if (!initPromise) {
105
+ initPromise = sendToWorker('init', {});
145
106
  }
146
- ctx.putImageData(imageData, 0, 0);
147
- return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
107
+ return initPromise;
108
+ }
109
+ export async function removeBgImage(uri, options = {}) {
110
+ const { onProgress, debug = false } = options;
111
+ if (debug)
112
+ console.log('[rmbg] Starting...');
113
+ onProgress?.(1);
114
+ // Ensure worker is ready
115
+ await ensureInit();
116
+ // Config for imgly
117
+ const config = {
118
+ debug: debug,
119
+ // We can pass other options supported by imgly if needed
120
+ // device: 'gpu' is auto-detected usually
121
+ };
122
+ const result = await sendToWorker('removeBg', { uri, config }, onProgress);
123
+ onProgress?.(100);
124
+ return result;
148
125
  }
149
126
  export const removeBackground = removeBgImage;
127
+ // ==========================================
128
+ // UTILITIES (Main Thread - lightweight)
129
+ // ==========================================
150
130
  // Helper to load image
151
131
  function loadImage(src) {
152
132
  return new Promise((resolve, reject) => {
@@ -200,7 +180,7 @@ export async function generateThumbhash(imageUri, options = {}) {
200
180
  ctx.drawImage(img, 0, 0, size, size);
201
181
  const imageData = ctx.getImageData(0, 0, size, size);
202
182
  // Load thumbhash from CDN
203
- // @ts-expect-error CDN import works at runtime
183
+ // @ts-ignore
204
184
  const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
205
185
  const hash = rgbaToThumbHash(size, size, imageData.data);
206
186
  return btoa(String.fromCharCode(...hash));
@@ -211,12 +191,13 @@ export async function generateThumbhash(imageUri, options = {}) {
211
191
  }
212
192
  }
213
193
  export async function clearCache() {
214
- pipeline = null;
215
- loadPromise = null;
216
- }
217
- export function getCacheSize() {
218
- return 0;
194
+ if (worker) {
195
+ worker.terminate();
196
+ worker = null;
197
+ }
198
+ initPromise = null;
219
199
  }
200
+ export function getCacheSize() { return 0; }
220
201
  export async function onLowMemory() {
221
202
  await clearCache();
222
203
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-remove-image-bg",
3
- "version": "0.0.15",
3
+ "version": "0.0.18",
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,7 +1,7 @@
1
1
  /**
2
- * Web implementation using @huggingface/transformers
3
- * Uses BRIAAI RMBG-1.4 model for background removal.
4
- * Loads from CDN to bypass Metro bundler issues.
2
+ * Web implementation using @imgly/background-removal via Inline Web Worker.
3
+ * Moves all heavy processing to a background thread to prevent UI freezing.
4
+ * Loads library from CDN to bypass Metro bundler issues.
5
5
  */
6
6
 
7
7
  export type OutputFormat = 'PNG' | 'WEBP';
@@ -13,177 +13,146 @@ export interface RemoveBgImageOptions {
13
13
  debug?: boolean;
14
14
  }
15
15
 
16
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
- let pipeline: any = null;
18
- let loadPromise: Promise<void> | null = null;
19
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
- let transformersModule: any = null;
16
+ // ==========================================
17
+ // INLINE WORKER CODE (Run in background)
18
+ // ==========================================
19
+ const WORKER_CODE = `
20
+ let removeBackground = null;
21
+
22
+ self.onmessage = async (e) => {
23
+ const { id, type, payload } = e.data;
24
+
25
+ try {
26
+ if (type === 'init') {
27
+ if (!removeBackground) {
28
+ // Dynamic import from CDN
29
+ // Using v1.3.0 which is stable
30
+ const module = await import('https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.3.0/+esm');
31
+ removeBackground = module.removeBackground;
32
+
33
+ // Preload/Init logic could go here if exposed, but imgly inits on first run usually
34
+ // Unless we want to preload the wasm.
35
+ // self.postMessage({ id, type: 'progress', payload: { progress: 0.1 } });
36
+ }
37
+ self.postMessage({ id, type: 'success', payload: true });
38
+ return;
39
+ }
40
+
41
+ if (type === 'removeBg') {
42
+ const { uri, config } = payload;
43
+
44
+ if (!removeBackground) {
45
+ const module = await import('https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.3.0/+esm');
46
+ removeBackground = module.removeBackground;
47
+ }
21
48
 
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
- }
49
+ // Run Inference
50
+ // imgly config: { progress: (key, current, total) => ... }
51
+ const blob = await removeBackground(uri, {
52
+ progress: (key, current, total) => {
53
+ // Map progress roughly
54
+ const p = current / total;
55
+ self.postMessage({ id, type: 'progress', payload: { progress: p } });
56
+ },
57
+ ...config
58
+ });
59
+
60
+ // Convert blob to DataURL for return
61
+ const reader = new FileReader();
62
+ reader.onloadend = () => {
63
+ self.postMessage({ id, type: 'success', payload: reader.result });
64
+ };
65
+ reader.readAsDataURL(blob);
66
+ }
67
+ } catch (err) {
68
+ self.postMessage({ id, type: 'error', payload: err.message || JSON.stringify(err) });
69
+ }
70
+ };
71
+ `;
31
72
 
32
- async function ensureLoaded(onProgress?: (p: number) => void, debug?: boolean) {
33
- if (pipeline) return pipeline;
34
-
35
- if (loadPromise) {
36
- await loadPromise;
37
- return pipeline;
38
- }
73
+ // ==========================================
74
+ // MAIN THREAD BRIDGE
75
+ // ==========================================
39
76
 
40
- loadPromise = (async () => {
41
- if (debug) console.log('[rmbg] Loading model...');
42
-
43
- const { pipeline: createPipeline, env } = await loadTransformers();
44
-
45
- env.allowLocalModels = false;
46
- env.useBrowserCache = true;
47
-
48
- onProgress?.(10);
77
+ let worker: Worker | null = null;
78
+ const pendingMessages = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void; onProgress?: (p: number) => void }>();
79
+
80
+ function getWorker() {
81
+ if (!worker) {
82
+ const blob = new Blob([WORKER_CODE], { type: 'application/javascript' });
83
+ const url = URL.createObjectURL(blob);
84
+ worker = new Worker(url);
49
85
 
50
- pipeline = await createPipeline('image-segmentation', 'briaai/RMBG-1.4', {
51
- dtype: 'q8',
52
- progress_callback: (info: { progress?: number }) => {
53
- if (onProgress && 'progress' in info && typeof info.progress === 'number') {
54
- onProgress(Math.min(10 + (info.progress / 100) * 50, 60));
86
+ worker.onmessage = (e) => {
87
+ const { id, type, payload } = e.data;
88
+ const deferred = pendingMessages.get(id);
89
+
90
+ if (!deferred) return;
91
+
92
+ if (type === 'progress') {
93
+ if (deferred.onProgress && payload.progress) {
94
+ deferred.onProgress(payload.progress * 100);
55
95
  }
56
- },
57
- });
58
-
59
- if (debug) console.log('[rmbg] Model ready');
60
- })();
96
+ } else if (type === 'success') {
97
+ deferred.resolve(payload);
98
+ pendingMessages.delete(id);
99
+ } else if (type === 'error') {
100
+ deferred.reject(new Error(payload));
101
+ pendingMessages.delete(id);
102
+ }
103
+ };
104
+ }
105
+ return worker;
106
+ }
61
107
 
62
- await loadPromise;
63
- return pipeline;
108
+ function sendToWorker(type: string, payload: any, onProgress?: (p: number) => void): Promise<any> {
109
+ return new Promise((resolve, reject) => {
110
+ const id = Math.random().toString(36).substring(7);
111
+ pendingMessages.set(id, { resolve, reject, onProgress });
112
+ getWorker().postMessage({ id, type, payload });
113
+ });
114
+ }
115
+
116
+ // Initialize
117
+ let initPromise: Promise<void> | null = null;
118
+ async function ensureInit() {
119
+ if (!initPromise) {
120
+ initPromise = sendToWorker('init', {});
121
+ }
122
+ return initPromise;
64
123
  }
65
124
 
66
- /**
67
- * Remove background from image
68
- */
69
125
  export async function removeBgImage(
70
126
  uri: string,
71
127
  options: RemoveBgImageOptions = {}
72
128
  ): Promise<string> {
73
- const { format = 'PNG', quality = 100, onProgress, debug = false } = options;
74
-
75
- if (debug) console.log('[rmbg] Processing:', uri);
76
- onProgress?.(5);
77
-
78
- const segmenter = await ensureLoaded(onProgress, debug);
79
- onProgress?.(60);
80
-
81
- const results = await segmenter(uri);
82
- onProgress?.(90);
83
-
84
- const result = Array.isArray(results) ? results[0] : results;
85
- if (!result?.mask) throw new Error('No mask returned');
86
-
87
- // Apply mask to original image
88
- const original = await loadImage(uri);
89
- const dataUrl = await applyMask(original, result.mask, format, quality);
90
-
91
- if (debug) console.log('[rmbg] Done');
92
- onProgress?.(100);
93
-
94
- return dataUrl;
95
- }
96
-
97
- async function applyMask(
98
- image: HTMLImageElement,
99
- mask: { width: number; height: number; data: Uint8Array | Uint8ClampedArray } | string,
100
- format: OutputFormat,
101
- quality: number
102
- ): Promise<string> {
103
- const canvas = document.createElement('canvas');
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');
129
+ const { onProgress, debug = false } = options;
130
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
- }
131
+ if (debug) console.log('[rmbg] Starting...');
132
+ onProgress?.(1);
157
133
 
158
- // Nearest neighbor resizing
159
- const x = (index / 4) % targetWidth;
160
- const y = Math.floor((index / 4) / targetWidth);
134
+ // Ensure worker is ready
135
+ await ensureInit();
161
136
 
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
-
181
- ctx.putImageData(imageData, 0, 0);
182
- return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
137
+ // Config for imgly
138
+ const config = {
139
+ debug: debug,
140
+ // We can pass other options supported by imgly if needed
141
+ // device: 'gpu' is auto-detected usually
142
+ };
143
+
144
+ const result = await sendToWorker('removeBg', { uri, config }, onProgress);
145
+
146
+ onProgress?.(100);
147
+ return result as string;
183
148
  }
184
149
 
185
150
  export const removeBackground = removeBgImage;
186
151
 
152
+ // ==========================================
153
+ // UTILITIES (Main Thread - lightweight)
154
+ // ==========================================
155
+
187
156
  // Helper to load image
188
157
  function loadImage(src: string): Promise<HTMLImageElement> {
189
158
  return new Promise((resolve, reject) => {
@@ -269,7 +238,7 @@ export async function generateThumbhash(
269
238
  const imageData = ctx.getImageData(0, 0, size, size);
270
239
 
271
240
  // Load thumbhash from CDN
272
- // @ts-expect-error CDN import works at runtime
241
+ // @ts-ignore
273
242
  const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
274
243
  const hash = rgbaToThumbHash(size, size, imageData.data);
275
244
  return btoa(String.fromCharCode(...hash));
@@ -280,18 +249,17 @@ export async function generateThumbhash(
280
249
  }
281
250
 
282
251
  export async function clearCache(): Promise<void> {
283
- pipeline = null;
284
- loadPromise = null;
285
- }
286
-
287
- export function getCacheSize(): number {
288
- return 0;
252
+ if (worker) {
253
+ worker.terminate();
254
+ worker = null;
255
+ }
256
+ initPromise = null;
289
257
  }
290
258
 
259
+ export function getCacheSize(): number { return 0; }
291
260
  export async function onLowMemory(): Promise<number> {
292
261
  await clearCache();
293
262
  return 0;
294
263
  }
295
-
296
264
  export function configureCache(_config: { maxEntries?: number }): void {}
297
265
  export function getCacheDirectory(): string { return ''; }