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.
- package/lib/ImageProcessing.web.d.ts +3 -6
- package/lib/ImageProcessing.web.js +126 -145
- package/package.json +1 -1
- package/src/ImageProcessing.web.ts +131 -163
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web implementation using @
|
|
3
|
-
*
|
|
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 @
|
|
3
|
-
*
|
|
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
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
let
|
|
11
|
-
|
|
12
|
-
async
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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 (
|
|
119
|
-
|
|
82
|
+
else if (type === 'success') {
|
|
83
|
+
deferred.resolve(payload);
|
|
84
|
+
pendingMessages.delete(id);
|
|
120
85
|
}
|
|
121
|
-
else {
|
|
122
|
-
|
|
86
|
+
else if (type === 'error') {
|
|
87
|
+
deferred.reject(new Error(payload));
|
|
88
|
+
pendingMessages.delete(id);
|
|
123
89
|
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Web implementation using @
|
|
3
|
-
*
|
|
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
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
let
|
|
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
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (loadPromise) {
|
|
36
|
-
await loadPromise;
|
|
37
|
-
return pipeline;
|
|
38
|
-
}
|
|
73
|
+
// ==========================================
|
|
74
|
+
// MAIN THREAD BRIDGE
|
|
75
|
+
// ==========================================
|
|
39
76
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
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
|
-
//
|
|
159
|
-
|
|
160
|
-
const y = Math.floor((index / 4) / targetWidth);
|
|
134
|
+
// Ensure worker is ready
|
|
135
|
+
await ensureInit();
|
|
161
136
|
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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 ''; }
|