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.
- package/lib/ImageProcessing.web.d.ts +3 -2
- package/lib/ImageProcessing.web.js +150 -16
- package/package.json +1 -1
- package/src/ImageProcessing.web.ts +178 -21
|
@@ -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,
|
|
28
|
-
export declare function generateThumbhash(
|
|
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
|
|
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
|
-
//
|
|
52
|
-
const
|
|
53
|
-
|
|
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
|
|
71
|
+
async function applyMask(image, mask, format, quality) {
|
|
61
72
|
const canvas = document.createElement('canvas');
|
|
62
|
-
|
|
63
|
-
canvas.
|
|
73
|
+
// Use original image dimensions
|
|
74
|
+
canvas.width = image.width;
|
|
75
|
+
canvas.height = image.height;
|
|
64
76
|
const ctx = canvas.getContext('2d');
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
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
|
|
76
|
-
|
|
77
|
-
|
|
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,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
|
|
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
|
-
//
|
|
75
|
-
const
|
|
76
|
-
|
|
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
|
|
86
|
-
|
|
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
|
-
|
|
92
|
-
canvas.
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
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
|
-
//
|
|
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(
|
|
116
|
-
|
|
117
|
-
|
|
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(
|
|
121
|
-
|
|
122
|
-
|
|
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> {
|