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.
Files changed (74) hide show
  1. package/NitroRnRemoveImageBg.podspec +33 -0
  2. package/README.md +386 -0
  3. package/android/CMakeLists.txt +28 -0
  4. package/android/build.gradle +142 -0
  5. package/android/fix-prefab.gradle +51 -0
  6. package/android/gradle.properties +5 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/cpp-adapter.cpp +6 -0
  9. package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt +189 -0
  10. package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgPackage.kt +31 -0
  11. package/app.plugin.js +12 -0
  12. package/ios/Bridge.h +8 -0
  13. package/ios/HybridImageBackgroundRemover.swift +224 -0
  14. package/ios/NitroRnRemoveImageBgOnLoad.mm +22 -0
  15. package/lib/ImageProcessing.d.ts +167 -0
  16. package/lib/ImageProcessing.js +323 -0
  17. package/lib/ImageProcessing.web.d.ts +80 -0
  18. package/lib/ImageProcessing.web.js +248 -0
  19. package/lib/__tests__/cache.test.d.ts +1 -0
  20. package/lib/__tests__/cache.test.js +87 -0
  21. package/lib/__tests__/errors.test.d.ts +1 -0
  22. package/lib/__tests__/errors.test.js +82 -0
  23. package/lib/cache.d.ts +72 -0
  24. package/lib/cache.js +228 -0
  25. package/lib/errors.d.ts +20 -0
  26. package/lib/errors.js +64 -0
  27. package/lib/index.d.ts +6 -0
  28. package/lib/index.js +9 -0
  29. package/lib/specs/Example.nitro.d.ts +0 -0
  30. package/lib/specs/Example.nitro.js +2 -0
  31. package/lib/specs/ImageBackgroundRemover.nitro.d.ts +41 -0
  32. package/lib/specs/ImageBackgroundRemover.nitro.js +1 -0
  33. package/nitro.json +17 -0
  34. package/nitrogen/generated/.gitattributes +1 -0
  35. package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.cmake +81 -0
  36. package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.gradle +27 -0
  37. package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.cpp +44 -0
  38. package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.hpp +25 -0
  39. package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.cpp +72 -0
  40. package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.hpp +65 -0
  41. package/nitrogen/generated/android/c++/JNativeRemoveBackgroundOptions.hpp +66 -0
  42. package/nitrogen/generated/android/c++/JOutputFormat.hpp +59 -0
  43. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemoverSpec.kt +58 -0
  44. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NativeRemoveBackgroundOptions.kt +44 -0
  45. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgOnLoad.kt +35 -0
  46. package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/OutputFormat.kt +21 -0
  47. package/nitrogen/generated/ios/NitroRnRemoveImageBg+autolinking.rb +60 -0
  48. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.cpp +49 -0
  49. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.hpp +111 -0
  50. package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Umbrella.hpp +51 -0
  51. package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.cpp +11 -0
  52. package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.hpp +82 -0
  53. package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
  54. package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
  55. package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec.swift +56 -0
  56. package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec_cxx.swift +138 -0
  57. package/nitrogen/generated/ios/swift/NativeRemoveBackgroundOptions.swift +58 -0
  58. package/nitrogen/generated/ios/swift/OutputFormat.swift +40 -0
  59. package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.cpp +21 -0
  60. package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.hpp +65 -0
  61. package/nitrogen/generated/shared/c++/NativeRemoveBackgroundOptions.hpp +84 -0
  62. package/nitrogen/generated/shared/c++/OutputFormat.hpp +76 -0
  63. package/package.json +104 -0
  64. package/react-native.config.js +16 -0
  65. package/src/ImageProcessing.ts +532 -0
  66. package/src/ImageProcessing.web.ts +342 -0
  67. package/src/__tests__/ImageProcessing.test.ts +278 -0
  68. package/src/__tests__/cache.test.ts +110 -0
  69. package/src/__tests__/errors.test.ts +117 -0
  70. package/src/cache.ts +305 -0
  71. package/src/errors.ts +93 -0
  72. package/src/index.ts +49 -0
  73. package/src/specs/Example.nitro.ts +1 -0
  74. 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 {};