rn-remove-image-bg 0.0.10
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/NitroRnRemoveImageBg.podspec +33 -0
- package/README.md +386 -0
- package/android/CMakeLists.txt +28 -0
- package/android/build.gradle +142 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt +189 -0
- package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgPackage.kt +31 -0
- package/app.plugin.js +12 -0
- package/ios/Bridge.h +8 -0
- package/ios/HybridImageBackgroundRemover.swift +224 -0
- package/ios/NitroRnRemoveImageBgOnLoad.mm +22 -0
- package/lib/ImageProcessing.d.ts +167 -0
- package/lib/ImageProcessing.js +323 -0
- package/lib/ImageProcessing.web.d.ts +80 -0
- package/lib/ImageProcessing.web.js +248 -0
- package/lib/__tests__/cache.test.d.ts +1 -0
- package/lib/__tests__/cache.test.js +87 -0
- package/lib/__tests__/errors.test.d.ts +1 -0
- package/lib/__tests__/errors.test.js +82 -0
- package/lib/cache.d.ts +72 -0
- package/lib/cache.js +228 -0
- package/lib/errors.d.ts +20 -0
- package/lib/errors.js +64 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +9 -0
- package/lib/specs/Example.nitro.d.ts +0 -0
- package/lib/specs/Example.nitro.js +2 -0
- package/lib/specs/ImageBackgroundRemover.nitro.d.ts +41 -0
- package/lib/specs/ImageBackgroundRemover.nitro.js +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.cpp +44 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.hpp +25 -0
- package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.cpp +72 -0
- package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.hpp +65 -0
- package/nitrogen/generated/android/c++/JNativeRemoveBackgroundOptions.hpp +66 -0
- package/nitrogen/generated/android/c++/JOutputFormat.hpp +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemoverSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NativeRemoveBackgroundOptions.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/OutputFormat.kt +21 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.cpp +49 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.hpp +111 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Umbrella.hpp +51 -0
- package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.hpp +82 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec_cxx.swift +138 -0
- package/nitrogen/generated/ios/swift/NativeRemoveBackgroundOptions.swift +58 -0
- package/nitrogen/generated/ios/swift/OutputFormat.swift +40 -0
- package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/NativeRemoveBackgroundOptions.hpp +84 -0
- package/nitrogen/generated/shared/c++/OutputFormat.hpp +76 -0
- package/package.json +104 -0
- package/react-native.config.js +16 -0
- package/src/ImageProcessing.ts +532 -0
- package/src/ImageProcessing.web.ts +342 -0
- package/src/__tests__/ImageProcessing.test.ts +278 -0
- package/src/__tests__/cache.test.ts +110 -0
- package/src/__tests__/errors.test.ts +117 -0
- package/src/cache.ts +305 -0
- package/src/errors.ts +93 -0
- package/src/index.ts +49 -0
- package/src/specs/Example.nitro.ts +1 -0
- package/src/specs/ImageBackgroundRemover.nitro.ts +49 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { Image } from 'react-native';
|
|
2
|
+
import * as ImageManipulator from 'expo-image-manipulator';
|
|
3
|
+
import * as FileSystem from 'expo-file-system/legacy';
|
|
4
|
+
import { Buffer } from 'buffer';
|
|
5
|
+
import { rgbaToThumbHash } from 'thumbhash';
|
|
6
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
7
|
+
import { BackgroundRemovalError, wrapNativeError } from './errors';
|
|
8
|
+
import { bgRemovalCache } from './cache';
|
|
9
|
+
let nativeRemover;
|
|
10
|
+
function getNativeRemover() {
|
|
11
|
+
if (!nativeRemover) {
|
|
12
|
+
nativeRemover = NitroModules.createHybridObject('ImageBackgroundRemover');
|
|
13
|
+
}
|
|
14
|
+
return nativeRemover;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Validate image path format
|
|
18
|
+
*/
|
|
19
|
+
function validateImagePath(uri) {
|
|
20
|
+
if (!uri || uri.trim().length === 0) {
|
|
21
|
+
throw new BackgroundRemovalError('Image path cannot be empty', 'INVALID_PATH');
|
|
22
|
+
}
|
|
23
|
+
// Must be a file path or file:// URI
|
|
24
|
+
const isValidPath = uri.startsWith('file://') ||
|
|
25
|
+
uri.startsWith('/') ||
|
|
26
|
+
uri.match(/^[a-zA-Z]:\\/);
|
|
27
|
+
if (!isValidPath) {
|
|
28
|
+
throw new BackgroundRemovalError(`Invalid file path format: ${uri}. Expected file:// URI or absolute path.`, 'INVALID_PATH');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Validate options
|
|
33
|
+
*/
|
|
34
|
+
function validateOptions(options) {
|
|
35
|
+
if (options.maxDimension !== undefined) {
|
|
36
|
+
if (options.maxDimension < 100 || options.maxDimension > 8192) {
|
|
37
|
+
throw new BackgroundRemovalError('maxDimension must be between 100 and 8192', 'INVALID_OPTIONS');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (options.quality !== undefined) {
|
|
41
|
+
if (options.quality < 0 || options.quality > 100) {
|
|
42
|
+
throw new BackgroundRemovalError('quality must be between 0 and 100', 'INVALID_OPTIONS');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (options.format !== undefined &&
|
|
46
|
+
!['PNG', 'WEBP'].includes(options.format)) {
|
|
47
|
+
throw new BackgroundRemovalError('format must be either "PNG" or "WEBP"', 'INVALID_OPTIONS');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Default options for background removal */
|
|
51
|
+
const DEFAULT_OPTIONS = {
|
|
52
|
+
maxDimension: 2048,
|
|
53
|
+
format: 'PNG',
|
|
54
|
+
quality: 100,
|
|
55
|
+
useCache: true,
|
|
56
|
+
debug: false,
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Compress image to WebP format with configurable options
|
|
60
|
+
*/
|
|
61
|
+
export async function compressImage(uri, options = {}) {
|
|
62
|
+
const { maxSizeKB = 250, width = 1024, height = 1024, quality = 0.85, format = ImageManipulator.SaveFormat.WEBP, } = options;
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
const maxSize = maxSizeKB * 1024;
|
|
65
|
+
// Get original file size and dimensions
|
|
66
|
+
const originalInfo = await FileSystem.getInfoAsync(uri);
|
|
67
|
+
const originalSize = 'size' in originalInfo ? originalInfo.size : 0;
|
|
68
|
+
const { width: originalWidth, height: originalHeight } = await new Promise((resolve, reject) => {
|
|
69
|
+
Image.getSize(uri, (w, h) => resolve({ width: w, height: h }), reject);
|
|
70
|
+
});
|
|
71
|
+
// Calculate target dimensions maintaining aspect ratio
|
|
72
|
+
// We want the image to fit WITHIN the bounding box defined by width x height
|
|
73
|
+
const scale = Math.min(width / originalWidth, height / originalHeight);
|
|
74
|
+
// If image is smaller than target box, we can keep original size (scale = 1) if we don't want to upscale.
|
|
75
|
+
// Generally "compress" implies making smaller or equal.
|
|
76
|
+
// If the image is larger, scale < 1. If smaller, scale >= 1.
|
|
77
|
+
// Let's cap scale at 1 to prevent upscaling unless explicitly desired (usually not for compression).
|
|
78
|
+
const finalScale = Math.min(scale, 1);
|
|
79
|
+
const resizeWidth = Math.round(originalWidth * finalScale);
|
|
80
|
+
const resizeHeight = Math.round(originalHeight * finalScale);
|
|
81
|
+
// Start with calculated dimensions and quality
|
|
82
|
+
let result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: resizeWidth, height: resizeHeight } }], {
|
|
83
|
+
compress: quality,
|
|
84
|
+
format,
|
|
85
|
+
});
|
|
86
|
+
let fileInfo = await FileSystem.getInfoAsync(result.uri);
|
|
87
|
+
// If still too large, reduce quality
|
|
88
|
+
if ('size' in fileInfo && fileInfo.size > maxSize) {
|
|
89
|
+
let currentQuality = quality * 0.9;
|
|
90
|
+
while (currentQuality > 0.5 &&
|
|
91
|
+
'size' in fileInfo &&
|
|
92
|
+
fileInfo.size > maxSize) {
|
|
93
|
+
result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: resizeWidth, height: resizeHeight } }], {
|
|
94
|
+
compress: currentQuality,
|
|
95
|
+
format,
|
|
96
|
+
});
|
|
97
|
+
fileInfo = await FileSystem.getInfoAsync(result.uri);
|
|
98
|
+
if ('size' in fileInfo && fileInfo.size <= maxSize)
|
|
99
|
+
break;
|
|
100
|
+
currentQuality -= 0.05;
|
|
101
|
+
}
|
|
102
|
+
// If still too large, reduce dimensions
|
|
103
|
+
if ('size' in fileInfo && fileInfo.size > maxSize) {
|
|
104
|
+
const smallerWidth = Math.floor(resizeWidth * 0.75);
|
|
105
|
+
const smallerHeight = Math.floor(resizeHeight * 0.75);
|
|
106
|
+
result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: smallerWidth, height: smallerHeight } }], {
|
|
107
|
+
compress: 0.75,
|
|
108
|
+
format,
|
|
109
|
+
});
|
|
110
|
+
fileInfo = await FileSystem.getInfoAsync(result.uri);
|
|
111
|
+
// Final quality reduction if needed
|
|
112
|
+
if ('size' in fileInfo && fileInfo.size > maxSize) {
|
|
113
|
+
let finalQuality = 0.7;
|
|
114
|
+
while (finalQuality > 0.5 &&
|
|
115
|
+
'size' in fileInfo &&
|
|
116
|
+
fileInfo.size > maxSize) {
|
|
117
|
+
result = await ImageManipulator.manipulateAsync(uri, [{ resize: { width: smallerWidth, height: smallerHeight } }], {
|
|
118
|
+
compress: finalQuality,
|
|
119
|
+
format,
|
|
120
|
+
});
|
|
121
|
+
fileInfo = await FileSystem.getInfoAsync(result.uri);
|
|
122
|
+
if ('size' in fileInfo && fileInfo.size <= maxSize)
|
|
123
|
+
break;
|
|
124
|
+
finalQuality -= 0.05;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const finalSize = 'size' in fileInfo ? fileInfo.size : 0;
|
|
130
|
+
const duration = Date.now() - startTime;
|
|
131
|
+
console.log(`[Native] Image Compression:`, {
|
|
132
|
+
originalSize: `${(originalSize / 1024).toFixed(2)} KB`,
|
|
133
|
+
compressedSize: `${(finalSize / 1024).toFixed(2)} KB`,
|
|
134
|
+
reduction: `${(((originalSize - finalSize) / originalSize) * 100).toFixed(1)}%`,
|
|
135
|
+
duration: `${duration}ms`,
|
|
136
|
+
dimensions: `${resizeWidth}x${resizeHeight}`,
|
|
137
|
+
});
|
|
138
|
+
return result.uri;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Generate thumbhash from image URI (Native/Mobile)
|
|
142
|
+
*/
|
|
143
|
+
export async function generateThumbhash(imageUri, options = {}) {
|
|
144
|
+
const { size = 32 } = options;
|
|
145
|
+
// 1. Create tiny PNG
|
|
146
|
+
const tiny = await ImageManipulator.manipulateAsync(imageUri, [{ resize: { width: size, height: size } }], { format: ImageManipulator.SaveFormat.PNG });
|
|
147
|
+
// 2. Read as base64
|
|
148
|
+
const base64 = await FileSystem.readAsStringAsync(tiny.uri, {
|
|
149
|
+
encoding: 'base64',
|
|
150
|
+
});
|
|
151
|
+
// 3. Decode PNG and generate thumbhash
|
|
152
|
+
const UPNG = require('upng-js');
|
|
153
|
+
const buffer = Buffer.from(base64, 'base64');
|
|
154
|
+
const img = UPNG.decode(buffer);
|
|
155
|
+
const rgba = UPNG.toRGBA8(img)[0];
|
|
156
|
+
const hash = rgbaToThumbHash(size, size, new Uint8Array(rgba));
|
|
157
|
+
// 4. Convert to base64
|
|
158
|
+
return Buffer.from(hash).toString('base64');
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Remove background from image using native ML models
|
|
162
|
+
*
|
|
163
|
+
* @param uri - File path or file:// URI to the source image
|
|
164
|
+
* @param options - Processing options
|
|
165
|
+
* @returns Promise resolving to a URI suitable for use with `<Image>` component.
|
|
166
|
+
* - **iOS/Android**: File path (`file:///path/to/cache/bg_removed_xxx.png`)
|
|
167
|
+
* - **Web**: Data URL (`data:image/png;base64,...`)
|
|
168
|
+
*
|
|
169
|
+
* @throws {BackgroundRemovalError} When image cannot be processed
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```typescript
|
|
173
|
+
* const result = await removeBgImage('file:///path/to/photo.jpg')
|
|
174
|
+
* // Use directly in Image component
|
|
175
|
+
* <Image source={{ uri: result }} />
|
|
176
|
+
* ```
|
|
177
|
+
*
|
|
178
|
+
* @example
|
|
179
|
+
* ```typescript
|
|
180
|
+
* // With options
|
|
181
|
+
* const result = await removeBgImage('file:///path/to/photo.jpg', {
|
|
182
|
+
* maxDimension: 1024,
|
|
183
|
+
* format: 'WEBP',
|
|
184
|
+
* quality: 90,
|
|
185
|
+
* onProgress: (p) => console.log(`Progress: ${p}%`)
|
|
186
|
+
* })
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export async function removeBgImage(uri, options = {}) {
|
|
190
|
+
const startTime = Date.now();
|
|
191
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
192
|
+
const { onProgress, debug } = opts;
|
|
193
|
+
// Validate inputs
|
|
194
|
+
validateImagePath(uri);
|
|
195
|
+
validateOptions(options);
|
|
196
|
+
if (debug) {
|
|
197
|
+
console.log('[rn-remove-image-bg] Starting background removal:', uri);
|
|
198
|
+
console.log('[rn-remove-image-bg] Options:', opts);
|
|
199
|
+
}
|
|
200
|
+
// Report initial progress
|
|
201
|
+
onProgress?.(5);
|
|
202
|
+
// Check cache if enabled
|
|
203
|
+
if (opts.useCache) {
|
|
204
|
+
const optionsHash = bgRemovalCache.hashOptions({
|
|
205
|
+
maxDimension: opts.maxDimension,
|
|
206
|
+
format: opts.format,
|
|
207
|
+
quality: opts.quality,
|
|
208
|
+
});
|
|
209
|
+
const cached = await bgRemovalCache.get(uri, optionsHash);
|
|
210
|
+
if (cached) {
|
|
211
|
+
if (debug) {
|
|
212
|
+
console.log('[rn-remove-image-bg] Cache hit:', cached);
|
|
213
|
+
}
|
|
214
|
+
onProgress?.(100);
|
|
215
|
+
return cached.startsWith('file://') ? cached : `file://${cached}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
onProgress?.(10);
|
|
219
|
+
try {
|
|
220
|
+
// Prepare native options
|
|
221
|
+
const nativeOptions = {
|
|
222
|
+
maxDimension: opts.maxDimension,
|
|
223
|
+
format: opts.format,
|
|
224
|
+
quality: opts.quality,
|
|
225
|
+
};
|
|
226
|
+
onProgress?.(20);
|
|
227
|
+
// Call native implementation
|
|
228
|
+
const result = await getNativeRemover().removeBackground(uri, nativeOptions);
|
|
229
|
+
onProgress?.(90);
|
|
230
|
+
// Normalize result path
|
|
231
|
+
const resultPath = result.startsWith('file://')
|
|
232
|
+
? result
|
|
233
|
+
: `file://${result}`;
|
|
234
|
+
// Cache the result
|
|
235
|
+
if (opts.useCache) {
|
|
236
|
+
const optionsHash = bgRemovalCache.hashOptions({
|
|
237
|
+
maxDimension: opts.maxDimension,
|
|
238
|
+
format: opts.format,
|
|
239
|
+
quality: opts.quality,
|
|
240
|
+
});
|
|
241
|
+
bgRemovalCache.set(uri, optionsHash, resultPath);
|
|
242
|
+
}
|
|
243
|
+
if (debug) {
|
|
244
|
+
console.log('[rn-remove-image-bg] Completed in', Date.now() - startTime, 'ms');
|
|
245
|
+
console.log('[rn-remove-image-bg] Result:', resultPath);
|
|
246
|
+
}
|
|
247
|
+
onProgress?.(100);
|
|
248
|
+
return resultPath;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (debug) {
|
|
252
|
+
console.error('[rn-remove-image-bg] Failed:', error);
|
|
253
|
+
}
|
|
254
|
+
throw wrapNativeError(error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Backward compatibility alias for removeBgImage
|
|
259
|
+
* @deprecated Use removeBgImage instead
|
|
260
|
+
*/
|
|
261
|
+
export const removeBackground = removeBgImage;
|
|
262
|
+
/**
|
|
263
|
+
* Clear the background removal cache
|
|
264
|
+
* @param deleteFiles - Also delete cached files from disk (default: false)
|
|
265
|
+
*/
|
|
266
|
+
export async function clearCache(deleteFiles = false) {
|
|
267
|
+
await bgRemovalCache.clear(deleteFiles);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get the current cache size
|
|
271
|
+
*/
|
|
272
|
+
export function getCacheSize() {
|
|
273
|
+
return bgRemovalCache.size;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Handle low memory conditions by clearing the cache
|
|
277
|
+
* Call this when your app receives memory warnings
|
|
278
|
+
*
|
|
279
|
+
* @param deleteFiles - Also delete cached files from disk (default: true)
|
|
280
|
+
* @returns Number of entries that were cleared
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* import { AppState } from 'react-native'
|
|
285
|
+
* import { onLowMemory } from 'rn-remove-image-bg'
|
|
286
|
+
*
|
|
287
|
+
* // In your app initialization
|
|
288
|
+
* AppState.addEventListener('memoryWarning', () => {
|
|
289
|
+
* onLowMemory()
|
|
290
|
+
* })
|
|
291
|
+
* ```
|
|
292
|
+
*/
|
|
293
|
+
export async function onLowMemory(deleteFiles = true) {
|
|
294
|
+
const size = bgRemovalCache.size;
|
|
295
|
+
await bgRemovalCache.clear(deleteFiles);
|
|
296
|
+
console.log(`[rn-remove-image-bg] Cleared ${size} cache entries due to memory pressure`);
|
|
297
|
+
return size;
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Configure the background removal cache
|
|
301
|
+
* Call this early in your app lifecycle to customize cache behavior
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* import { configureCache } from 'rn-remove-image-bg'
|
|
306
|
+
*
|
|
307
|
+
* configureCache({
|
|
308
|
+
* maxEntries: 100,
|
|
309
|
+
* maxAgeMinutes: 60,
|
|
310
|
+
* persistToDisk: true
|
|
311
|
+
* })
|
|
312
|
+
* ```
|
|
313
|
+
*/
|
|
314
|
+
export function configureCache(config) {
|
|
315
|
+
bgRemovalCache.configure(config);
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Get the cache directory path
|
|
319
|
+
* Useful for debugging or manual cache management
|
|
320
|
+
*/
|
|
321
|
+
export function getCacheDirectory() {
|
|
322
|
+
return bgRemovalCache.getCacheDirectory();
|
|
323
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web implementation using @imgly/background-removal
|
|
3
|
+
*
|
|
4
|
+
* Provides real background removal on web using WebAssembly and ML models.
|
|
5
|
+
* Falls back to no-op if the library fails to load.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Output format for processed images
|
|
9
|
+
*/
|
|
10
|
+
export type OutputFormat = 'PNG' | 'WEBP';
|
|
11
|
+
export interface CompressImageOptions {
|
|
12
|
+
maxSizeKB?: number;
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
quality?: number;
|
|
16
|
+
format?: 'webp' | 'png' | 'jpeg';
|
|
17
|
+
}
|
|
18
|
+
export interface GenerateThumbhashOptions {
|
|
19
|
+
size?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface RemoveBgImageOptions {
|
|
22
|
+
maxDimension?: number;
|
|
23
|
+
format?: OutputFormat;
|
|
24
|
+
quality?: number;
|
|
25
|
+
onProgress?: (progress: number) => void;
|
|
26
|
+
useCache?: boolean;
|
|
27
|
+
debug?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Compress image on web using canvas
|
|
31
|
+
* @returns Compressed image as data URL
|
|
32
|
+
*/
|
|
33
|
+
export declare function compressImage(uri: string, options?: CompressImageOptions): Promise<string>;
|
|
34
|
+
/**
|
|
35
|
+
* Generate thumbhash on web using canvas
|
|
36
|
+
* @returns Base64 thumbhash string
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateThumbhash(imageUri: string, options?: GenerateThumbhashOptions): Promise<string>;
|
|
39
|
+
/**
|
|
40
|
+
* Remove background from image on web using @imgly/background-removal
|
|
41
|
+
* @returns Data URL of processed image with transparent background
|
|
42
|
+
*/
|
|
43
|
+
export declare function removeBgImage(uri: string, options?: RemoveBgImageOptions): Promise<string>;
|
|
44
|
+
/**
|
|
45
|
+
* Backward compatibility alias
|
|
46
|
+
* @deprecated Use removeBgImage instead
|
|
47
|
+
*/
|
|
48
|
+
export declare const removeBackground: typeof removeBgImage;
|
|
49
|
+
/**
|
|
50
|
+
* Clear the web background removal cache
|
|
51
|
+
* @param _deleteFiles - Ignored on web (no disk cache)
|
|
52
|
+
*/
|
|
53
|
+
export declare function clearCache(_deleteFiles?: boolean): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Get the current cache size
|
|
56
|
+
*/
|
|
57
|
+
export declare function getCacheSize(): number;
|
|
58
|
+
/**
|
|
59
|
+
* Handle low memory conditions by clearing the cache
|
|
60
|
+
* On web, this simply clears the in-memory cache
|
|
61
|
+
*
|
|
62
|
+
* @param _deleteFiles - Ignored on web (no disk cache)
|
|
63
|
+
* @returns Number of entries that were cleared
|
|
64
|
+
*/
|
|
65
|
+
export declare function onLowMemory(_deleteFiles?: boolean): Promise<number>;
|
|
66
|
+
/**
|
|
67
|
+
* Configure the background removal cache
|
|
68
|
+
* On web, maxEntries limits cache size. Disk persistence options are no-ops.
|
|
69
|
+
*/
|
|
70
|
+
export declare function configureCache(config: {
|
|
71
|
+
maxEntries?: number;
|
|
72
|
+
maxAgeMinutes?: number;
|
|
73
|
+
persistToDisk?: boolean;
|
|
74
|
+
cacheDirectory?: string;
|
|
75
|
+
}): void;
|
|
76
|
+
/**
|
|
77
|
+
* Get the cache directory path
|
|
78
|
+
* On web, returns empty string as there is no disk cache
|
|
79
|
+
*/
|
|
80
|
+
export declare function getCacheDirectory(): string;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web implementation using @imgly/background-removal
|
|
3
|
+
*
|
|
4
|
+
* Provides real background removal on web using WebAssembly and ML models.
|
|
5
|
+
* Falls back to no-op if the library fails to load.
|
|
6
|
+
*/
|
|
7
|
+
import { removeBackground as imglyRemoveBackground } from '@imgly/background-removal';
|
|
8
|
+
// Web cache configuration
|
|
9
|
+
const webCacheConfig = {
|
|
10
|
+
maxEntries: 50,
|
|
11
|
+
maxAgeMinutes: 30,
|
|
12
|
+
};
|
|
13
|
+
// Simple in-memory LRU cache for web
|
|
14
|
+
const webCache = new Map();
|
|
15
|
+
/**
|
|
16
|
+
* Add entry to cache with LRU eviction
|
|
17
|
+
*/
|
|
18
|
+
function setCacheEntry(key, value) {
|
|
19
|
+
// If key exists, delete it first (to update LRU order)
|
|
20
|
+
if (webCache.has(key)) {
|
|
21
|
+
webCache.delete(key);
|
|
22
|
+
}
|
|
23
|
+
// Evict oldest entries if at capacity
|
|
24
|
+
while (webCache.size >= webCacheConfig.maxEntries) {
|
|
25
|
+
const oldestKey = webCache.keys().next().value;
|
|
26
|
+
if (oldestKey) {
|
|
27
|
+
webCache.delete(oldestKey);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
webCache.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get entry from cache and update LRU order
|
|
34
|
+
*/
|
|
35
|
+
function getCacheEntry(key) {
|
|
36
|
+
const value = webCache.get(key);
|
|
37
|
+
if (value !== undefined) {
|
|
38
|
+
// Move to end (most recently used)
|
|
39
|
+
webCache.delete(key);
|
|
40
|
+
webCache.set(key, value);
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Compress image on web using canvas
|
|
46
|
+
* @returns Compressed image as data URL
|
|
47
|
+
*/
|
|
48
|
+
export async function compressImage(uri, options = {}) {
|
|
49
|
+
const { maxSizeKB = 250, width = 1024, height = 1024, quality = 0.85, format = 'webp', } = options;
|
|
50
|
+
try {
|
|
51
|
+
// Load image
|
|
52
|
+
const img = await loadImage(uri);
|
|
53
|
+
// Calculate target dimensions maintaining aspect ratio
|
|
54
|
+
const scale = Math.min(width / img.width, height / img.height, 1);
|
|
55
|
+
const targetWidth = Math.round(img.width * scale);
|
|
56
|
+
const targetHeight = Math.round(img.height * scale);
|
|
57
|
+
// Create canvas and draw resized image
|
|
58
|
+
const canvas = document.createElement('canvas');
|
|
59
|
+
canvas.width = targetWidth;
|
|
60
|
+
canvas.height = targetHeight;
|
|
61
|
+
const ctx = canvas.getContext('2d');
|
|
62
|
+
if (!ctx)
|
|
63
|
+
throw new Error('Could not get canvas context');
|
|
64
|
+
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
65
|
+
// Convert to data URL with compression
|
|
66
|
+
const mimeType = format === 'png'
|
|
67
|
+
? 'image/png'
|
|
68
|
+
: format === 'jpeg'
|
|
69
|
+
? 'image/jpeg'
|
|
70
|
+
: 'image/webp';
|
|
71
|
+
let dataUrl = canvas.toDataURL(mimeType, quality);
|
|
72
|
+
// If still too large, reduce quality iteratively
|
|
73
|
+
let currentQuality = quality;
|
|
74
|
+
while (getDataUrlSizeKB(dataUrl) > maxSizeKB && currentQuality > 0.5) {
|
|
75
|
+
currentQuality -= 0.1;
|
|
76
|
+
dataUrl = canvas.toDataURL(mimeType, currentQuality);
|
|
77
|
+
}
|
|
78
|
+
return dataUrl;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
console.warn('[rn-remove-image-bg] compressImage failed on web, returning original:', error);
|
|
82
|
+
return uri;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Generate thumbhash on web using canvas
|
|
87
|
+
* @returns Base64 thumbhash string
|
|
88
|
+
*/
|
|
89
|
+
export async function generateThumbhash(imageUri, options = {}) {
|
|
90
|
+
const { size = 32 } = options;
|
|
91
|
+
try {
|
|
92
|
+
// Dynamically import thumbhash to avoid bundling issues
|
|
93
|
+
const { rgbaToThumbHash } = await import('thumbhash');
|
|
94
|
+
// Load and resize image
|
|
95
|
+
const img = await loadImage(imageUri);
|
|
96
|
+
const canvas = document.createElement('canvas');
|
|
97
|
+
canvas.width = size;
|
|
98
|
+
canvas.height = size;
|
|
99
|
+
const ctx = canvas.getContext('2d');
|
|
100
|
+
if (!ctx)
|
|
101
|
+
throw new Error('Could not get canvas context');
|
|
102
|
+
ctx.drawImage(img, 0, 0, size, size);
|
|
103
|
+
// Get RGBA data
|
|
104
|
+
const imageData = ctx.getImageData(0, 0, size, size);
|
|
105
|
+
const hash = rgbaToThumbHash(size, size, imageData.data);
|
|
106
|
+
// Convert to base64
|
|
107
|
+
return btoa(String.fromCharCode(...hash));
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.warn('[rn-remove-image-bg] generateThumbhash failed on web:', error);
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Remove background from image on web using @imgly/background-removal
|
|
116
|
+
* @returns Data URL of processed image with transparent background
|
|
117
|
+
*/
|
|
118
|
+
export async function removeBgImage(uri, options = {}) {
|
|
119
|
+
const { format = 'PNG', quality = 100, onProgress, useCache = true, debug = false, } = options;
|
|
120
|
+
// Check cache
|
|
121
|
+
const cacheKey = `${uri}::${format}::${quality}`;
|
|
122
|
+
if (useCache) {
|
|
123
|
+
const cachedResult = getCacheEntry(cacheKey);
|
|
124
|
+
if (cachedResult) {
|
|
125
|
+
if (debug) {
|
|
126
|
+
console.log('[rn-remove-image-bg] Web cache hit');
|
|
127
|
+
}
|
|
128
|
+
onProgress?.(100);
|
|
129
|
+
return cachedResult;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (debug) {
|
|
133
|
+
console.log('[rn-remove-image-bg] Starting web background removal:', uri);
|
|
134
|
+
}
|
|
135
|
+
onProgress?.(5);
|
|
136
|
+
try {
|
|
137
|
+
// Call @imgly/background-removal
|
|
138
|
+
const blob = await imglyRemoveBackground(uri, {
|
|
139
|
+
progress: (key, current, total) => {
|
|
140
|
+
if (onProgress && total > 0) {
|
|
141
|
+
// Map progress to 10-90 range
|
|
142
|
+
const progress = Math.round(10 + (current / total) * 80);
|
|
143
|
+
onProgress(Math.min(progress, 90));
|
|
144
|
+
}
|
|
145
|
+
if (debug) {
|
|
146
|
+
console.log(`[rn-remove-image-bg] ${key}: ${current}/${total}`);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
output: {
|
|
150
|
+
format: format === 'WEBP' ? 'image/webp' : 'image/png',
|
|
151
|
+
quality: quality / 100,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
onProgress?.(95);
|
|
155
|
+
// Convert blob to data URL
|
|
156
|
+
const dataUrl = await blobToDataUrl(blob);
|
|
157
|
+
// Cache the result with LRU eviction
|
|
158
|
+
if (useCache) {
|
|
159
|
+
setCacheEntry(cacheKey, dataUrl);
|
|
160
|
+
}
|
|
161
|
+
if (debug) {
|
|
162
|
+
console.log('[rn-remove-image-bg] Web background removal complete');
|
|
163
|
+
}
|
|
164
|
+
onProgress?.(100);
|
|
165
|
+
return dataUrl;
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
console.error('[rn-remove-image-bg] Web background removal failed:', error);
|
|
169
|
+
// Return original URI on failure
|
|
170
|
+
onProgress?.(100);
|
|
171
|
+
return uri;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Backward compatibility alias
|
|
176
|
+
* @deprecated Use removeBgImage instead
|
|
177
|
+
*/
|
|
178
|
+
export const removeBackground = removeBgImage;
|
|
179
|
+
/**
|
|
180
|
+
* Clear the web background removal cache
|
|
181
|
+
* @param _deleteFiles - Ignored on web (no disk cache)
|
|
182
|
+
*/
|
|
183
|
+
export async function clearCache(_deleteFiles = false) {
|
|
184
|
+
webCache.clear();
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Get the current cache size
|
|
188
|
+
*/
|
|
189
|
+
export function getCacheSize() {
|
|
190
|
+
return webCache.size;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Handle low memory conditions by clearing the cache
|
|
194
|
+
* On web, this simply clears the in-memory cache
|
|
195
|
+
*
|
|
196
|
+
* @param _deleteFiles - Ignored on web (no disk cache)
|
|
197
|
+
* @returns Number of entries that were cleared
|
|
198
|
+
*/
|
|
199
|
+
export async function onLowMemory(_deleteFiles = true) {
|
|
200
|
+
const size = webCache.size;
|
|
201
|
+
webCache.clear();
|
|
202
|
+
console.log(`[rn-remove-image-bg] Cleared ${size} web cache entries due to memory pressure`);
|
|
203
|
+
return size;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Configure the background removal cache
|
|
207
|
+
* On web, maxEntries limits cache size. Disk persistence options are no-ops.
|
|
208
|
+
*/
|
|
209
|
+
export function configureCache(config) {
|
|
210
|
+
if (config.maxEntries !== undefined && config.maxEntries > 0) {
|
|
211
|
+
webCacheConfig.maxEntries = config.maxEntries;
|
|
212
|
+
}
|
|
213
|
+
if (config.maxAgeMinutes !== undefined && config.maxAgeMinutes > 0) {
|
|
214
|
+
webCacheConfig.maxAgeMinutes = config.maxAgeMinutes;
|
|
215
|
+
}
|
|
216
|
+
// persistToDisk and cacheDirectory are no-ops on web
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Get the cache directory path
|
|
220
|
+
* On web, returns empty string as there is no disk cache
|
|
221
|
+
*/
|
|
222
|
+
export function getCacheDirectory() {
|
|
223
|
+
return '';
|
|
224
|
+
}
|
|
225
|
+
// Helper functions
|
|
226
|
+
function loadImage(src) {
|
|
227
|
+
return new Promise((resolve, reject) => {
|
|
228
|
+
const img = new Image();
|
|
229
|
+
img.crossOrigin = 'anonymous';
|
|
230
|
+
img.onload = () => resolve(img);
|
|
231
|
+
img.onerror = reject;
|
|
232
|
+
img.src = src;
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
function blobToDataUrl(blob) {
|
|
236
|
+
return new Promise((resolve, reject) => {
|
|
237
|
+
const reader = new FileReader();
|
|
238
|
+
reader.onloadend = () => resolve(reader.result);
|
|
239
|
+
reader.onerror = reject;
|
|
240
|
+
reader.readAsDataURL(blob);
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
function getDataUrlSizeKB(dataUrl) {
|
|
244
|
+
// Data URL format: data:mime;base64,<base64data>
|
|
245
|
+
const base64 = dataUrl.split(',')[1] || '';
|
|
246
|
+
// Base64 encodes 3 bytes as 4 characters
|
|
247
|
+
return (base64.length * 3) / 4 / 1024;
|
|
248
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|