rn-remove-image-bg 0.0.14 → 0.0.16

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 Inline Web Worker & WebGPU.
3
+ * Moves all heavy processing to a background thread to prevent UI freezing.
4
+ * Loads @huggingface/transformers 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,85 +1,194 @@
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 Inline Web Worker & WebGPU.
3
+ * Moves all heavy processing to a background thread to prevent UI freezing.
4
+ * Loads @huggingface/transformers 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',
6
+ // ==========================================
7
+ // INLINE WORKER CODE (Run in background)
8
+ // ==========================================
9
+ const WORKER_CODE = `
10
+ let pipeline = null;
11
+ let env = null;
12
+
13
+ // Helper to load image bitmap
14
+ async function loadImageBitmapFromUrl(url) {
15
+ const response = await fetch(url);
16
+ const blob = await response.blob();
17
+ return await createImageBitmap(blob);
18
+ }
19
+
20
+ self.onmessage = async (e) => {
21
+ const { id, type, payload } = e.data;
22
+
23
+ try {
24
+ if (type === 'init') {
25
+ if (!pipeline) {
26
+ // Dynamic import from CDN
27
+ const transformers = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.3.0/+esm');
28
+ env = transformers.env;
29
+
30
+ // Configure environment
31
+ env.allowLocalModels = false;
32
+ env.useBrowserCache = true;
33
+ // Try WebGPU, fallback to WASM
34
+ // env.backends.onnx.wasm.numThreads = 1; // Limit threads if needed
35
+
36
+ pipeline = await transformers.pipeline('image-segmentation', 'briaai/RMBG-1.4', {
37
+ device: 'webgpu', // Attempt WebGPU first
38
+ dtype: 'q8', // Quantized for speed
36
39
  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
- },
40
+ self.postMessage({ id, type: 'progress', payload: info });
41
+ }
42
+ });
43
+ }
44
+ self.postMessage({ id, type: 'success', payload: true });
45
+ return;
46
+ }
47
+
48
+ if (type === 'removeBg') {
49
+ const { uri, format, quality } = payload;
50
+
51
+ if (!pipeline) throw new Error('Pipeline not initialized');
52
+
53
+ // 1. Run Inference
54
+ const results = await pipeline(uri);
55
+ const result = Array.isArray(results) ? results[0] : results;
56
+ if (!result || !result.mask) throw new Error('No mask generated');
57
+
58
+ // 2. Apply Mask (Pixel Manipulation)
59
+ // We use OffscreenCanvas if available, or just pixel math
60
+ // Since we are in a worker, we can't use DOM Image, but we can use ImageBitmap
61
+
62
+ const originalBitmap = await loadImageBitmapFromUrl(uri);
63
+ const { width, height } = originalBitmap;
64
+
65
+ const offscreen = new OffscreenCanvas(width, height);
66
+ const ctx = offscreen.getContext('2d');
67
+ ctx.drawImage(originalBitmap, 0, 0);
68
+
69
+ const imageData = ctx.getImageData(0, 0, width, height);
70
+ const pixelData = imageData.data;
71
+
72
+ // Handle Mask
73
+ // mask.data is usually 1-channel or 3-channel
74
+ const mask = result.mask;
75
+ const maskData = mask.data;
76
+
77
+ // Simple resizing logic if dimensions differ (Nearest Neighbor)
78
+ // (Preprocessing usually resizes input, so output mask matches input size typically?
79
+ // Actually RMBG-1.4 output is fixed size 1024x1024 usually, need resize)
80
+
81
+ const maskW = mask.width;
82
+ const maskH = mask.height;
83
+
84
+ for (let i = 0; i < pixelData.length; i += 4) {
85
+ const pixelIndex = i / 4;
86
+ const x = pixelIndex % width;
87
+ const y = Math.floor(pixelIndex / width);
88
+
89
+ // Map to mask coordinates
90
+ const mx = Math.floor(x * (maskW / width));
91
+ const my = Math.floor(y * (maskH / height));
92
+ const maskIdx = (my * maskW + mx);
93
+
94
+ // Get Alpha
95
+ let alpha = 255;
96
+ if (maskData.length === maskW * maskH) {
97
+ alpha = maskData[maskIdx];
98
+ } else {
99
+ alpha = maskData[maskIdx * mask.channels]; // Assuming channels property or stride
100
+ // Fallback if channels undefined:
101
+ if (!mask.channels) alpha = maskData[maskIdx * 3]; // RGB assumption
102
+ }
103
+
104
+ pixelData[i + 3] = alpha;
105
+ }
106
+
107
+ ctx.putImageData(imageData, 0, 0);
108
+
109
+ // 3. Convert to Blob/DataURL
110
+ const blob = await offscreen.convertToBlob({
111
+ type: format === 'WEBP' ? 'image/webp' : 'image/png',
112
+ quality: quality / 100
41
113
  });
42
- if (debug)
43
- console.log('[rmbg] Model ready');
44
- })();
45
- await loadPromise;
46
- return pipeline;
114
+
115
+ // Convert blob to DataURL for return
116
+ const reader = new FileReader();
117
+ reader.onloadend = () => {
118
+ self.postMessage({ id, type: 'success', payload: reader.result });
119
+ };
120
+ reader.readAsDataURL(blob);
121
+ }
122
+ } catch (err) {
123
+ self.postMessage({ id, type: 'error', payload: err.message });
124
+ }
125
+ };
126
+ `;
127
+ // ==========================================
128
+ // MAIN THREAD BRIDGE
129
+ // ==========================================
130
+ let worker = null;
131
+ const pendingMessages = new Map();
132
+ function getWorker() {
133
+ if (!worker) {
134
+ const blob = new Blob([WORKER_CODE], { type: 'application/javascript' });
135
+ const url = URL.createObjectURL(blob);
136
+ worker = new Worker(url);
137
+ worker.onmessage = (e) => {
138
+ const { id, type, payload } = e.data;
139
+ const deferred = pendingMessages.get(id);
140
+ if (!deferred)
141
+ return;
142
+ if (type === 'progress') {
143
+ if (deferred.onProgress && payload.progress) {
144
+ // Map 0-100 progress
145
+ deferred.onProgress(payload.progress);
146
+ }
147
+ }
148
+ else if (type === 'success') {
149
+ deferred.resolve(payload);
150
+ pendingMessages.delete(id);
151
+ }
152
+ else if (type === 'error') {
153
+ deferred.reject(new Error(payload));
154
+ pendingMessages.delete(id);
155
+ }
156
+ };
157
+ }
158
+ return worker;
159
+ }
160
+ function sendToWorker(type, payload, onProgress) {
161
+ return new Promise((resolve, reject) => {
162
+ const id = Math.random().toString(36).substring(7);
163
+ pendingMessages.set(id, { resolve, reject, onProgress });
164
+ getWorker().postMessage({ id, type, payload });
165
+ });
166
+ }
167
+ // Initialize model
168
+ let initPromise = null;
169
+ async function ensureInit() {
170
+ if (!initPromise) {
171
+ initPromise = sendToWorker('init', {});
172
+ }
173
+ return initPromise;
47
174
  }
48
- /**
49
- * Remove background from image
50
- */
51
175
  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
- // Convert to data URL
64
- const dataUrl = typeof result.mask === 'string'
65
- ? result.mask
66
- : toDataUrl(result.mask, format, quality);
67
- if (debug)
68
- console.log('[rmbg] Done');
176
+ const { format = 'PNG', quality = 100, onProgress } = options;
177
+ onProgress?.(1); // Start
178
+ await ensureInit();
179
+ // The worker handles the heavy calculation
180
+ const result = await sendToWorker('removeBg', { uri, format, quality }, (p) => {
181
+ // Transformers.js progress is model downloading mainly
182
+ // We can map it: 0-90% download/load, 90-100% inference
183
+ onProgress?.(p * 0.9);
184
+ });
69
185
  onProgress?.(100);
70
- return dataUrl;
71
- }
72
- function toDataUrl(mask, format, quality) {
73
- const canvas = document.createElement('canvas');
74
- canvas.width = mask.width;
75
- canvas.height = mask.height;
76
- const ctx = canvas.getContext('2d');
77
- const imageData = ctx.createImageData(mask.width, mask.height);
78
- imageData.data.set(mask.data);
79
- ctx.putImageData(imageData, 0, 0);
80
- return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
186
+ return result;
81
187
  }
82
188
  export const removeBackground = removeBgImage;
189
+ // ==========================================
190
+ // UTILITIES (Main Thread - lightweight)
191
+ // ==========================================
83
192
  // Helper to load image
84
193
  function loadImage(src) {
85
194
  return new Promise((resolve, reject) => {
@@ -133,7 +242,7 @@ export async function generateThumbhash(imageUri, options = {}) {
133
242
  ctx.drawImage(img, 0, 0, size, size);
134
243
  const imageData = ctx.getImageData(0, 0, size, size);
135
244
  // Load thumbhash from CDN
136
- // @ts-expect-error CDN import works at runtime
245
+ // @ts-ignore
137
246
  const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
138
247
  const hash = rgbaToThumbHash(size, size, imageData.data);
139
248
  return btoa(String.fromCharCode(...hash));
@@ -144,12 +253,13 @@ export async function generateThumbhash(imageUri, options = {}) {
144
253
  }
145
254
  }
146
255
  export async function clearCache() {
147
- pipeline = null;
148
- loadPromise = null;
149
- }
150
- export function getCacheSize() {
151
- return 0;
256
+ if (worker) {
257
+ worker.terminate();
258
+ worker = null;
259
+ }
260
+ initPromise = null;
152
261
  }
262
+ export function getCacheSize() { return 0; }
153
263
  export async function onLowMemory() {
154
264
  await clearCache();
155
265
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-remove-image-bg",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
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 Inline Web Worker & WebGPU.
3
+ * Moves all heavy processing to a background thread to prevent UI freezing.
4
+ * Loads @huggingface/transformers from CDN to bypass Metro bundler issues.
5
5
  */
6
6
 
7
7
  export type OutputFormat = 'PNG' | 'WEBP';
@@ -13,105 +13,207 @@ 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;
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
- }
31
-
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;
16
+ // ==========================================
17
+ // INLINE WORKER CODE (Run in background)
18
+ // ==========================================
19
+ const WORKER_CODE = `
20
+ let pipeline = null;
21
+ let env = null;
22
+
23
+ // Helper to load image bitmap
24
+ async function loadImageBitmapFromUrl(url) {
25
+ const response = await fetch(url);
26
+ const blob = await response.blob();
27
+ return await createImageBitmap(blob);
38
28
  }
39
29
 
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);
49
-
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));
30
+ self.onmessage = async (e) => {
31
+ const { id, type, payload } = e.data;
32
+
33
+ try {
34
+ if (type === 'init') {
35
+ if (!pipeline) {
36
+ // Dynamic import from CDN
37
+ const transformers = await import('https://cdn.jsdelivr.net/npm/@huggingface/transformers@3.3.0/+esm');
38
+ env = transformers.env;
39
+
40
+ // Configure environment
41
+ env.allowLocalModels = false;
42
+ env.useBrowserCache = true;
43
+ // Try WebGPU, fallback to WASM
44
+ // env.backends.onnx.wasm.numThreads = 1; // Limit threads if needed
45
+
46
+ pipeline = await transformers.pipeline('image-segmentation', 'briaai/RMBG-1.4', {
47
+ device: 'webgpu', // Attempt WebGPU first
48
+ dtype: 'q8', // Quantized for speed
49
+ progress_callback: (info) => {
50
+ self.postMessage({ id, type: 'progress', payload: info });
51
+ }
52
+ });
55
53
  }
56
- },
57
- });
54
+ self.postMessage({ id, type: 'success', payload: true });
55
+ return;
56
+ }
57
+
58
+ if (type === 'removeBg') {
59
+ const { uri, format, quality } = payload;
60
+
61
+ if (!pipeline) throw new Error('Pipeline not initialized');
62
+
63
+ // 1. Run Inference
64
+ const results = await pipeline(uri);
65
+ const result = Array.isArray(results) ? results[0] : results;
66
+ if (!result || !result.mask) throw new Error('No mask generated');
67
+
68
+ // 2. Apply Mask (Pixel Manipulation)
69
+ // We use OffscreenCanvas if available, or just pixel math
70
+ // Since we are in a worker, we can't use DOM Image, but we can use ImageBitmap
71
+
72
+ const originalBitmap = await loadImageBitmapFromUrl(uri);
73
+ const { width, height } = originalBitmap;
74
+
75
+ const offscreen = new OffscreenCanvas(width, height);
76
+ const ctx = offscreen.getContext('2d');
77
+ ctx.drawImage(originalBitmap, 0, 0);
78
+
79
+ const imageData = ctx.getImageData(0, 0, width, height);
80
+ const pixelData = imageData.data;
81
+
82
+ // Handle Mask
83
+ // mask.data is usually 1-channel or 3-channel
84
+ const mask = result.mask;
85
+ const maskData = mask.data;
86
+
87
+ // Simple resizing logic if dimensions differ (Nearest Neighbor)
88
+ // (Preprocessing usually resizes input, so output mask matches input size typically?
89
+ // Actually RMBG-1.4 output is fixed size 1024x1024 usually, need resize)
90
+
91
+ const maskW = mask.width;
92
+ const maskH = mask.height;
93
+
94
+ for (let i = 0; i < pixelData.length; i += 4) {
95
+ const pixelIndex = i / 4;
96
+ const x = pixelIndex % width;
97
+ const y = Math.floor(pixelIndex / width);
98
+
99
+ // Map to mask coordinates
100
+ const mx = Math.floor(x * (maskW / width));
101
+ const my = Math.floor(y * (maskH / height));
102
+ const maskIdx = (my * maskW + mx);
103
+
104
+ // Get Alpha
105
+ let alpha = 255;
106
+ if (maskData.length === maskW * maskH) {
107
+ alpha = maskData[maskIdx];
108
+ } else {
109
+ alpha = maskData[maskIdx * mask.channels]; // Assuming channels property or stride
110
+ // Fallback if channels undefined:
111
+ if (!mask.channels) alpha = maskData[maskIdx * 3]; // RGB assumption
112
+ }
113
+
114
+ pixelData[i + 3] = alpha;
115
+ }
116
+
117
+ ctx.putImageData(imageData, 0, 0);
118
+
119
+ // 3. Convert to Blob/DataURL
120
+ const blob = await offscreen.convertToBlob({
121
+ type: format === 'WEBP' ? 'image/webp' : 'image/png',
122
+ quality: quality / 100
123
+ });
124
+
125
+ // Convert blob to DataURL for return
126
+ const reader = new FileReader();
127
+ reader.onloadend = () => {
128
+ self.postMessage({ id, type: 'success', payload: reader.result });
129
+ };
130
+ reader.readAsDataURL(blob);
131
+ }
132
+ } catch (err) {
133
+ self.postMessage({ id, type: 'error', payload: err.message });
134
+ }
135
+ };
136
+ `;
137
+
138
+ // ==========================================
139
+ // MAIN THREAD BRIDGE
140
+ // ==========================================
141
+
142
+ let worker: Worker | null = null;
143
+ const pendingMessages = new Map<string, { resolve: (v: any) => void; reject: (e: any) => void; onProgress?: (p: number) => void }>();
144
+
145
+ function getWorker() {
146
+ if (!worker) {
147
+ const blob = new Blob([WORKER_CODE], { type: 'application/javascript' });
148
+ const url = URL.createObjectURL(blob);
149
+ worker = new Worker(url);
58
150
 
59
- if (debug) console.log('[rmbg] Model ready');
60
- })();
151
+ worker.onmessage = (e) => {
152
+ const { id, type, payload } = e.data;
153
+ const deferred = pendingMessages.get(id);
154
+
155
+ if (!deferred) return;
156
+
157
+ if (type === 'progress') {
158
+ if (deferred.onProgress && payload.progress) {
159
+ // Map 0-100 progress
160
+ deferred.onProgress(payload.progress);
161
+ }
162
+ } else if (type === 'success') {
163
+ deferred.resolve(payload);
164
+ pendingMessages.delete(id);
165
+ } else if (type === 'error') {
166
+ deferred.reject(new Error(payload));
167
+ pendingMessages.delete(id);
168
+ }
169
+ };
170
+ }
171
+ return worker;
172
+ }
61
173
 
62
- await loadPromise;
63
- return pipeline;
174
+ function sendToWorker(type: string, payload: any, onProgress?: (p: number) => void): Promise<any> {
175
+ return new Promise((resolve, reject) => {
176
+ const id = Math.random().toString(36).substring(7);
177
+ pendingMessages.set(id, { resolve, reject, onProgress });
178
+ getWorker().postMessage({ id, type, payload });
179
+ });
180
+ }
181
+
182
+ // Initialize model
183
+ let initPromise: Promise<void> | null = null;
184
+ async function ensureInit() {
185
+ if (!initPromise) {
186
+ initPromise = sendToWorker('init', {});
187
+ }
188
+ return initPromise;
64
189
  }
65
190
 
66
- /**
67
- * Remove background from image
68
- */
69
191
  export async function removeBgImage(
70
192
  uri: string,
71
193
  options: RemoveBgImageOptions = {}
72
194
  ): 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
- // Convert to data URL
88
- const dataUrl = typeof result.mask === 'string'
89
- ? result.mask
90
- : toDataUrl(result.mask, format, quality);
91
-
92
- if (debug) console.log('[rmbg] Done');
93
- onProgress?.(100);
94
-
95
- return dataUrl;
96
- }
97
-
98
- function toDataUrl(
99
- mask: { width: number; height: number; data: Uint8Array | Uint8ClampedArray },
100
- format: OutputFormat,
101
- quality: number
102
- ): string {
103
- const canvas = document.createElement('canvas');
104
- canvas.width = mask.width;
105
- canvas.height = mask.height;
106
- const ctx = canvas.getContext('2d')!;
107
- const imageData = ctx.createImageData(mask.width, mask.height);
108
- imageData.data.set(mask.data);
109
- ctx.putImageData(imageData, 0, 0);
110
- return canvas.toDataURL(format === 'WEBP' ? 'image/webp' : 'image/png', quality / 100);
195
+ const { format = 'PNG', quality = 100, onProgress } = options;
196
+
197
+ onProgress?.(1); // Start
198
+ await ensureInit();
199
+
200
+ // The worker handles the heavy calculation
201
+ const result = await sendToWorker('removeBg', { uri, format, quality }, (p) => {
202
+ // Transformers.js progress is model downloading mainly
203
+ // We can map it: 0-90% download/load, 90-100% inference
204
+ onProgress?.(p * 0.9);
205
+ });
206
+
207
+ onProgress?.(100);
208
+ return result as string;
111
209
  }
112
210
 
113
211
  export const removeBackground = removeBgImage;
114
212
 
213
+ // ==========================================
214
+ // UTILITIES (Main Thread - lightweight)
215
+ // ==========================================
216
+
115
217
  // Helper to load image
116
218
  function loadImage(src: string): Promise<HTMLImageElement> {
117
219
  return new Promise((resolve, reject) => {
@@ -197,7 +299,7 @@ export async function generateThumbhash(
197
299
  const imageData = ctx.getImageData(0, 0, size, size);
198
300
 
199
301
  // Load thumbhash from CDN
200
- // @ts-expect-error CDN import works at runtime
302
+ // @ts-ignore
201
303
  const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
202
304
  const hash = rgbaToThumbHash(size, size, imageData.data);
203
305
  return btoa(String.fromCharCode(...hash));
@@ -208,18 +310,17 @@ export async function generateThumbhash(
208
310
  }
209
311
 
210
312
  export async function clearCache(): Promise<void> {
211
- pipeline = null;
212
- loadPromise = null;
213
- }
214
-
215
- export function getCacheSize(): number {
216
- return 0;
313
+ if (worker) {
314
+ worker.terminate();
315
+ worker = null;
316
+ }
317
+ initPromise = null;
217
318
  }
218
319
 
320
+ export function getCacheSize(): number { return 0; }
219
321
  export async function onLowMemory(): Promise<number> {
220
322
  await clearCache();
221
323
  return 0;
222
324
  }
223
-
224
325
  export function configureCache(_config: { maxEntries?: number }): void {}
225
326
  export function getCacheDirectory(): string { return ''; }