rn-remove-image-bg 0.0.31 → 0.0.33
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/ios/HybridImageBackgroundRemover.swift +1 -0
- package/lib/ImageProcessing.d.ts +167 -0
- package/lib/ImageProcessing.js +323 -0
- package/lib/ImageProcessing.web.d.ts +25 -0
- package/lib/ImageProcessing.web.js +89 -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/lib/web/core/BackgroundRemover.d.ts +31 -0
- package/lib/web/core/BackgroundRemover.js +64 -0
- package/lib/web/core/CacheManager.d.ts +16 -0
- package/lib/web/core/CacheManager.js +57 -0
- package/lib/web/core/types.d.ts +47 -0
- package/lib/web/core/types.js +1 -0
- package/lib/web/errors/WebErrorAdapter.d.ts +8 -0
- package/lib/web/errors/WebErrorAdapter.js +29 -0
- package/lib/web/utils/CompressImage.d.ts +2 -0
- package/lib/web/utils/CompressImage.js +34 -0
- package/lib/web/utils/ThumbhashGenerator.d.ts +1 -0
- package/lib/web/utils/ThumbhashGenerator.js +31 -0
- package/lib/web/utils/formatConverter.d.ts +4 -0
- package/lib/web/utils/formatConverter.js +18 -0
- package/lib/web/utils/uriHelper.d.ts +10 -0
- package/lib/web/utils/uriHelper.js +46 -0
- package/package.json +1 -1
package/lib/cache.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as FileSystem from 'expo-file-system/legacy';
|
|
2
|
+
const MANIFEST_FILENAME = 'cache-manifest.json';
|
|
3
|
+
const CACHE_VERSION = 1;
|
|
4
|
+
/**
|
|
5
|
+
* LRU cache for background removal results with optional disk persistence
|
|
6
|
+
*/
|
|
7
|
+
class BackgroundRemovalCache {
|
|
8
|
+
cache = new Map();
|
|
9
|
+
_maxEntries;
|
|
10
|
+
_maxAgeMs;
|
|
11
|
+
persistToDisk;
|
|
12
|
+
cacheDirectory;
|
|
13
|
+
initialized = false;
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this._maxEntries = config.maxEntries ?? 50;
|
|
16
|
+
this._maxAgeMs = (config.maxAgeMinutes ?? 30) * 60 * 1000;
|
|
17
|
+
this.persistToDisk = config.persistToDisk ?? false;
|
|
18
|
+
this.cacheDirectory =
|
|
19
|
+
config.cacheDirectory ?? `${FileSystem.cacheDirectory}bg-removal/`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Configure cache settings
|
|
23
|
+
*/
|
|
24
|
+
configure(config) {
|
|
25
|
+
if (config.maxEntries !== undefined) {
|
|
26
|
+
this._maxEntries = config.maxEntries;
|
|
27
|
+
}
|
|
28
|
+
if (config.maxAgeMinutes !== undefined) {
|
|
29
|
+
this._maxAgeMs = config.maxAgeMinutes * 60 * 1000;
|
|
30
|
+
}
|
|
31
|
+
if (config.persistToDisk !== undefined) {
|
|
32
|
+
this.persistToDisk = config.persistToDisk;
|
|
33
|
+
}
|
|
34
|
+
if (config.cacheDirectory !== undefined) {
|
|
35
|
+
this.cacheDirectory = config.cacheDirectory;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Initialize disk cache (load manifest if exists)
|
|
40
|
+
*/
|
|
41
|
+
async initialize() {
|
|
42
|
+
if (this.initialized || !this.persistToDisk)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
// Ensure cache directory exists
|
|
46
|
+
const dirInfo = await FileSystem.getInfoAsync(this.cacheDirectory);
|
|
47
|
+
if (!dirInfo.exists) {
|
|
48
|
+
await FileSystem.makeDirectoryAsync(this.cacheDirectory, {
|
|
49
|
+
intermediates: true,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Load manifest if exists
|
|
53
|
+
const manifestPath = `${this.cacheDirectory}${MANIFEST_FILENAME}`;
|
|
54
|
+
const manifestInfo = await FileSystem.getInfoAsync(manifestPath);
|
|
55
|
+
if (manifestInfo.exists) {
|
|
56
|
+
const manifestJson = await FileSystem.readAsStringAsync(manifestPath);
|
|
57
|
+
const manifest = JSON.parse(manifestJson);
|
|
58
|
+
if (manifest.version === CACHE_VERSION) {
|
|
59
|
+
// Validate and load entries
|
|
60
|
+
for (const [key, entry] of Object.entries(manifest.entries)) {
|
|
61
|
+
const fileInfo = await FileSystem.getInfoAsync(entry.resultPath);
|
|
62
|
+
if (fileInfo.exists) {
|
|
63
|
+
this.cache.set(key, entry);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn('[rn-remove-image-bg] Failed to load cache manifest:', error);
|
|
71
|
+
}
|
|
72
|
+
this.initialized = true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Save cache manifest to disk
|
|
76
|
+
*/
|
|
77
|
+
async saveManifest() {
|
|
78
|
+
if (!this.persistToDisk)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const manifest = {
|
|
82
|
+
version: CACHE_VERSION,
|
|
83
|
+
entries: Object.fromEntries(this.cache.entries()),
|
|
84
|
+
};
|
|
85
|
+
const manifestPath = `${this.cacheDirectory}${MANIFEST_FILENAME}`;
|
|
86
|
+
await FileSystem.writeAsStringAsync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.warn('[rn-remove-image-bg] Failed to save cache manifest:', error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Generate a cache key from path and options
|
|
94
|
+
*/
|
|
95
|
+
generateKey(path, optionsHash) {
|
|
96
|
+
return `${path}::${optionsHash}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Hash options object to string for cache key
|
|
100
|
+
*/
|
|
101
|
+
hashOptions(options) {
|
|
102
|
+
const sorted = Object.keys(options)
|
|
103
|
+
.sort()
|
|
104
|
+
.reduce((acc, key) => {
|
|
105
|
+
const value = options[key];
|
|
106
|
+
// Exclude functions from hash
|
|
107
|
+
if (typeof value !== 'function') {
|
|
108
|
+
acc[key] = value;
|
|
109
|
+
}
|
|
110
|
+
return acc;
|
|
111
|
+
}, {});
|
|
112
|
+
return JSON.stringify(sorted);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get cached result if valid
|
|
116
|
+
*/
|
|
117
|
+
async get(path, optionsHash) {
|
|
118
|
+
await this.initialize();
|
|
119
|
+
const key = this.generateKey(path, optionsHash);
|
|
120
|
+
const entry = this.cache.get(key);
|
|
121
|
+
if (!entry) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Check if expired
|
|
125
|
+
if (Date.now() - entry.timestamp > this._maxAgeMs) {
|
|
126
|
+
this.cache.delete(key);
|
|
127
|
+
this.saveManifest();
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
// Verify result file still exists
|
|
131
|
+
try {
|
|
132
|
+
const info = await FileSystem.getInfoAsync(entry.resultPath);
|
|
133
|
+
if (!info.exists) {
|
|
134
|
+
this.cache.delete(key);
|
|
135
|
+
this.saveManifest();
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
this.cache.delete(key);
|
|
141
|
+
this.saveManifest();
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
// Move to end (most recently used)
|
|
145
|
+
this.cache.delete(key);
|
|
146
|
+
this.cache.set(key, entry);
|
|
147
|
+
return entry.resultPath;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Store result in cache
|
|
151
|
+
*/
|
|
152
|
+
set(path, optionsHash, resultPath) {
|
|
153
|
+
const key = this.generateKey(path, optionsHash);
|
|
154
|
+
// Evict oldest entries if at capacity
|
|
155
|
+
while (this.cache.size >= this._maxEntries) {
|
|
156
|
+
const oldestKey = this.cache.keys().next().value;
|
|
157
|
+
if (oldestKey) {
|
|
158
|
+
this.cache.delete(oldestKey);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.cache.set(key, {
|
|
162
|
+
originalPath: path,
|
|
163
|
+
resultPath,
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
optionsHash,
|
|
166
|
+
});
|
|
167
|
+
this.saveManifest();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Clear all cached entries
|
|
171
|
+
* @param deleteFiles - Also delete cached files from disk (default: false)
|
|
172
|
+
*/
|
|
173
|
+
async clear(deleteFiles = false) {
|
|
174
|
+
if (deleteFiles && this.persistToDisk) {
|
|
175
|
+
try {
|
|
176
|
+
// Delete all cached result files
|
|
177
|
+
for (const entry of this.cache.values()) {
|
|
178
|
+
try {
|
|
179
|
+
await FileSystem.deleteAsync(entry.resultPath, {
|
|
180
|
+
idempotent: true,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Ignore individual file deletion errors
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Delete manifest
|
|
188
|
+
const manifestPath = `${this.cacheDirectory}${MANIFEST_FILENAME}`;
|
|
189
|
+
await FileSystem.deleteAsync(manifestPath, { idempotent: true });
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.warn('[rn-remove-image-bg] Error clearing cache files:', error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.cache.clear();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get current cache size
|
|
199
|
+
*/
|
|
200
|
+
get size() {
|
|
201
|
+
return this.cache.size;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Remove expired entries
|
|
205
|
+
*/
|
|
206
|
+
prune() {
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
let removed = 0;
|
|
209
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
210
|
+
if (now - entry.timestamp > this._maxAgeMs) {
|
|
211
|
+
this.cache.delete(key);
|
|
212
|
+
removed++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (removed > 0) {
|
|
216
|
+
this.saveManifest();
|
|
217
|
+
}
|
|
218
|
+
return removed;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get cache directory path
|
|
222
|
+
*/
|
|
223
|
+
getCacheDirectory() {
|
|
224
|
+
return this.cacheDirectory;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Export singleton instance
|
|
228
|
+
export const bgRemovalCache = new BackgroundRemovalCache();
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for background removal operations
|
|
3
|
+
*/
|
|
4
|
+
export type BackgroundRemovalErrorCode = 'INVALID_PATH' | 'FILE_NOT_FOUND' | 'DECODE_FAILED' | 'ML_PROCESSING_FAILED' | 'SAVE_FAILED' | 'INVALID_OPTIONS' | 'UNKNOWN';
|
|
5
|
+
/**
|
|
6
|
+
* Custom error class for background removal operations
|
|
7
|
+
*/
|
|
8
|
+
export declare class BackgroundRemovalError extends Error {
|
|
9
|
+
readonly code: BackgroundRemovalErrorCode;
|
|
10
|
+
readonly originalError?: Error;
|
|
11
|
+
constructor(message: string, code: BackgroundRemovalErrorCode, originalError?: Error);
|
|
12
|
+
/**
|
|
13
|
+
* Create a user-friendly error message
|
|
14
|
+
*/
|
|
15
|
+
toUserMessage(): string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Helper to wrap native errors with proper typing
|
|
19
|
+
*/
|
|
20
|
+
export declare function wrapNativeError(error: unknown): BackgroundRemovalError;
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error class for background removal operations
|
|
3
|
+
*/
|
|
4
|
+
export class BackgroundRemovalError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
originalError;
|
|
7
|
+
constructor(message, code, originalError) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'BackgroundRemovalError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.originalError = originalError;
|
|
12
|
+
// Maintain proper stack trace in V8 environments
|
|
13
|
+
if ('captureStackTrace' in Error) {
|
|
14
|
+
Error.captureStackTrace(this, BackgroundRemovalError);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a user-friendly error message
|
|
19
|
+
*/
|
|
20
|
+
toUserMessage() {
|
|
21
|
+
switch (this.code) {
|
|
22
|
+
case 'INVALID_PATH':
|
|
23
|
+
return 'The image path provided is invalid.';
|
|
24
|
+
case 'FILE_NOT_FOUND':
|
|
25
|
+
return 'The image file could not be found.';
|
|
26
|
+
case 'DECODE_FAILED':
|
|
27
|
+
return 'The image could not be read. Please ensure it is a valid image file.';
|
|
28
|
+
case 'ML_PROCESSING_FAILED':
|
|
29
|
+
return 'Background removal failed. Please try with a different image.';
|
|
30
|
+
case 'SAVE_FAILED':
|
|
31
|
+
return 'Could not save the processed image.';
|
|
32
|
+
case 'INVALID_OPTIONS':
|
|
33
|
+
return 'Invalid options provided for background removal.';
|
|
34
|
+
default:
|
|
35
|
+
return 'An unexpected error occurred during background removal.';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Helper to wrap native errors with proper typing
|
|
41
|
+
*/
|
|
42
|
+
export function wrapNativeError(error) {
|
|
43
|
+
if (error instanceof BackgroundRemovalError) {
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
const originalError = error instanceof Error ? error : undefined;
|
|
48
|
+
// Try to determine error code from message
|
|
49
|
+
if (message.includes('does not exist') || message.includes('not found')) {
|
|
50
|
+
return new BackgroundRemovalError(message, 'FILE_NOT_FOUND', originalError);
|
|
51
|
+
}
|
|
52
|
+
if (message.includes('decode') || message.includes('load image')) {
|
|
53
|
+
return new BackgroundRemovalError(message, 'DECODE_FAILED', originalError);
|
|
54
|
+
}
|
|
55
|
+
if (message.includes('mask') ||
|
|
56
|
+
message.includes('segment') ||
|
|
57
|
+
message.includes('ML')) {
|
|
58
|
+
return new BackgroundRemovalError(message, 'ML_PROCESSING_FAILED', originalError);
|
|
59
|
+
}
|
|
60
|
+
if (message.includes('save') || message.includes('write')) {
|
|
61
|
+
return new BackgroundRemovalError(message, 'SAVE_FAILED', originalError);
|
|
62
|
+
}
|
|
63
|
+
return new BackgroundRemovalError(message, 'UNKNOWN', originalError);
|
|
64
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { compressImage, generateThumbhash, removeBgImage, removeBackground, clearCache, getCacheSize, onLowMemory, configureCache, getCacheDirectory } from './ImageProcessing';
|
|
2
|
+
import type { CompressImageOptions, GenerateThumbhashOptions, RemoveBgImageOptions, OutputFormat } from './ImageProcessing';
|
|
3
|
+
import { BackgroundRemovalError, type BackgroundRemovalErrorCode } from './errors';
|
|
4
|
+
import type { CacheConfig } from './cache';
|
|
5
|
+
export { compressImage, generateThumbhash, removeBgImage, removeBackground, clearCache, getCacheSize, onLowMemory, configureCache, getCacheDirectory, BackgroundRemovalError, };
|
|
6
|
+
export type { CompressImageOptions, GenerateThumbhashOptions, RemoveBgImageOptions, OutputFormat, BackgroundRemovalErrorCode, CacheConfig, };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { compressImage, generateThumbhash, removeBgImage, removeBackground, clearCache, getCacheSize, onLowMemory, configureCache, getCacheDirectory, } from './ImageProcessing';
|
|
2
|
+
import { BackgroundRemovalError, } from './errors';
|
|
3
|
+
export {
|
|
4
|
+
// Functions
|
|
5
|
+
compressImage, generateThumbhash, removeBgImage, removeBackground, clearCache, getCacheSize,
|
|
6
|
+
// Memory management
|
|
7
|
+
onLowMemory, configureCache, getCacheDirectory,
|
|
8
|
+
// Errors
|
|
9
|
+
BackgroundRemovalError, };
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
/**
|
|
3
|
+
* Output format for processed images
|
|
4
|
+
*/
|
|
5
|
+
export type OutputFormat = 'PNG' | 'WEBP';
|
|
6
|
+
/**
|
|
7
|
+
* Native options for background removal
|
|
8
|
+
*/
|
|
9
|
+
export interface NativeRemoveBackgroundOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Maximum dimension (width or height) for processing
|
|
12
|
+
* Larger images will be downsampled for better performance
|
|
13
|
+
* Default: 2048
|
|
14
|
+
*/
|
|
15
|
+
maxDimension: number;
|
|
16
|
+
/**
|
|
17
|
+
* Output image format
|
|
18
|
+
* PNG: Lossless, larger file size
|
|
19
|
+
* WEBP: Smaller file size, good quality
|
|
20
|
+
* Default: PNG
|
|
21
|
+
*/
|
|
22
|
+
format: OutputFormat;
|
|
23
|
+
/**
|
|
24
|
+
* Quality for WEBP format (0-100)
|
|
25
|
+
* Ignored for PNG format
|
|
26
|
+
* Default: 100
|
|
27
|
+
*/
|
|
28
|
+
quality: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ImageBackgroundRemover extends HybridObject<{
|
|
31
|
+
ios: 'swift';
|
|
32
|
+
android: 'kotlin';
|
|
33
|
+
}> {
|
|
34
|
+
/**
|
|
35
|
+
* Remove background from an image
|
|
36
|
+
* @param imagePath - File path or file:// URI to the source image
|
|
37
|
+
* @param options - Processing options
|
|
38
|
+
* @returns Promise resolving to the output file path
|
|
39
|
+
*/
|
|
40
|
+
removeBackground(imagePath: string, options: NativeRemoveBackgroundOptions): Promise<string>;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RemoveBgImageOptions } from './types';
|
|
2
|
+
type ImglyConfig = {
|
|
3
|
+
publicPath?: string;
|
|
4
|
+
progress?: (key: string, current: number, total: number) => void;
|
|
5
|
+
debug?: boolean;
|
|
6
|
+
output?: {
|
|
7
|
+
format?: 'image/png' | 'image/jpeg' | 'image/webp';
|
|
8
|
+
quality?: number;
|
|
9
|
+
type?: 'foreground' | 'background' | 'mask';
|
|
10
|
+
};
|
|
11
|
+
};
|
|
12
|
+
type ImglyRemoveBackground = (image: string | Blob | ArrayBuffer, config?: ImglyConfig) => Promise<Blob>;
|
|
13
|
+
declare global {
|
|
14
|
+
interface Window {
|
|
15
|
+
imglyRemoveBackground?: ImglyRemoveBackground;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export declare const BackgroundRemover: {
|
|
19
|
+
/**
|
|
20
|
+
* Checks if the background removal library is loaded.
|
|
21
|
+
*/
|
|
22
|
+
isAvailable(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Removes background from an image.
|
|
25
|
+
* Returns a Blob of the processed image (PNG with transparency).
|
|
26
|
+
*
|
|
27
|
+
* Requires @imgly/background-removal to be loaded via CDN script tag.
|
|
28
|
+
*/
|
|
29
|
+
remove(uri: string, options: RemoveBgImageOptions): Promise<Blob>;
|
|
30
|
+
};
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { mapErrorToBackgroundRemovalError, BackgroundRemovalError } from '../errors/WebErrorAdapter';
|
|
2
|
+
import { normalizeUri } from '../utils/uriHelper';
|
|
3
|
+
/**
|
|
4
|
+
* Gets the @imgly removeBackground function from window.
|
|
5
|
+
* This must be loaded via CDN script tag before use.
|
|
6
|
+
*/
|
|
7
|
+
function getImglyRemoveBackground() {
|
|
8
|
+
if (typeof window === 'undefined') {
|
|
9
|
+
throw new BackgroundRemovalError('Background removal is only available in browser environment.', 'ENVIRONMENT_ERROR');
|
|
10
|
+
}
|
|
11
|
+
if (!window.imglyRemoveBackground) {
|
|
12
|
+
throw new BackgroundRemovalError('Background removal library not loaded. Please add the following script to your HTML:\n' +
|
|
13
|
+
'<script type="module">\n' +
|
|
14
|
+
' import { removeBackground } from "https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/+esm";\n' +
|
|
15
|
+
' window.imglyRemoveBackground = removeBackground;\n' +
|
|
16
|
+
'</script>', 'LIBRARY_NOT_LOADED');
|
|
17
|
+
}
|
|
18
|
+
return window.imglyRemoveBackground;
|
|
19
|
+
}
|
|
20
|
+
export const BackgroundRemover = {
|
|
21
|
+
/**
|
|
22
|
+
* Checks if the background removal library is loaded.
|
|
23
|
+
*/
|
|
24
|
+
isAvailable() {
|
|
25
|
+
return typeof window !== 'undefined' && typeof window.imglyRemoveBackground === 'function';
|
|
26
|
+
},
|
|
27
|
+
/**
|
|
28
|
+
* Removes background from an image.
|
|
29
|
+
* Returns a Blob of the processed image (PNG with transparency).
|
|
30
|
+
*
|
|
31
|
+
* Requires @imgly/background-removal to be loaded via CDN script tag.
|
|
32
|
+
*/
|
|
33
|
+
async remove(uri, options) {
|
|
34
|
+
try {
|
|
35
|
+
const imglyRemove = getImglyRemoveBackground();
|
|
36
|
+
const normalizedUri = await normalizeUri(uri);
|
|
37
|
+
const config = {
|
|
38
|
+
// Pass publicPath if provided (for self-hosted assets)
|
|
39
|
+
publicPath: options.publicPath,
|
|
40
|
+
// Map progress callback
|
|
41
|
+
progress: (_key, current, total) => {
|
|
42
|
+
if (options.onProgress && total > 0) {
|
|
43
|
+
const p = Math.min(100, Math.round((current / total) * 100));
|
|
44
|
+
options.onProgress(p);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
// Enable debug logging if requested
|
|
48
|
+
debug: options.debug ?? false,
|
|
49
|
+
// Explicit output configuration for transparent PNG
|
|
50
|
+
output: {
|
|
51
|
+
format: 'image/png',
|
|
52
|
+
quality: 1.0,
|
|
53
|
+
type: 'foreground', // Extract foreground with transparency
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
// Execute removal
|
|
57
|
+
const blob = await imglyRemove(normalizedUri, config);
|
|
58
|
+
return blob;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw mapErrorToBackgroundRemovalError(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RemoveBgImageOptions } from './types';
|
|
2
|
+
export declare class CacheManager {
|
|
3
|
+
private cache;
|
|
4
|
+
private readonly MAX_SIZE;
|
|
5
|
+
constructor();
|
|
6
|
+
/**
|
|
7
|
+
* Generates a unique cache key based on input URI and processing options
|
|
8
|
+
*/
|
|
9
|
+
private generateKey;
|
|
10
|
+
get(uri: string, options: RemoveBgImageOptions): string | null;
|
|
11
|
+
set(uri: string, options: RemoveBgImageOptions, dataUrl: string): void;
|
|
12
|
+
clear(): void;
|
|
13
|
+
size(): number;
|
|
14
|
+
private evictOldest;
|
|
15
|
+
}
|
|
16
|
+
export declare const cacheManager: CacheManager;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export class CacheManager {
|
|
2
|
+
cache;
|
|
3
|
+
MAX_SIZE = 50; // Limit to 50 items to avoid memory leaks
|
|
4
|
+
constructor() {
|
|
5
|
+
this.cache = new Map();
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Generates a unique cache key based on input URI and processing options
|
|
9
|
+
*/
|
|
10
|
+
generateKey(uri, options) {
|
|
11
|
+
// We include relevant options that affect output
|
|
12
|
+
const { format = 'PNG', quality = 100, maxDimension = 0 } = options;
|
|
13
|
+
return `${uri}|${format}|${quality}|${maxDimension}`;
|
|
14
|
+
}
|
|
15
|
+
get(uri, options) {
|
|
16
|
+
const key = this.generateKey(uri, options);
|
|
17
|
+
const entry = this.cache.get(key);
|
|
18
|
+
if (entry) {
|
|
19
|
+
entry.timestamp = Date.now(); // Update usage timestamp (simple LRU)
|
|
20
|
+
return entry.dataUrl;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
set(uri, options, dataUrl) {
|
|
25
|
+
const key = this.generateKey(uri, options);
|
|
26
|
+
// Evict if full
|
|
27
|
+
if (this.cache.size >= this.MAX_SIZE) {
|
|
28
|
+
this.evictOldest();
|
|
29
|
+
}
|
|
30
|
+
this.cache.set(key, {
|
|
31
|
+
dataUrl,
|
|
32
|
+
timestamp: Date.now()
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
clear() {
|
|
36
|
+
this.cache.clear();
|
|
37
|
+
}
|
|
38
|
+
size() {
|
|
39
|
+
return this.cache.size;
|
|
40
|
+
}
|
|
41
|
+
evictOldest() {
|
|
42
|
+
// Find oldest entry
|
|
43
|
+
let oldestKey = null;
|
|
44
|
+
let oldestTime = Infinity;
|
|
45
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
46
|
+
if (entry.timestamp < oldestTime) {
|
|
47
|
+
oldestTime = entry.timestamp;
|
|
48
|
+
oldestKey = key;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (oldestKey) {
|
|
52
|
+
this.cache.delete(oldestKey);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Singleton instance
|
|
57
|
+
export const cacheManager = new CacheManager();
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type OutputFormat = 'PNG' | 'WEBP';
|
|
2
|
+
export interface RemoveBgImageOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Output format of the processed image.
|
|
5
|
+
* Default: 'PNG'
|
|
6
|
+
*/
|
|
7
|
+
format?: OutputFormat;
|
|
8
|
+
/**
|
|
9
|
+
* Quality of the output image (0-100). Only applies to WEBP/JPEG.
|
|
10
|
+
* Default: 100
|
|
11
|
+
*/
|
|
12
|
+
quality?: number;
|
|
13
|
+
/**
|
|
14
|
+
* Callback to track download and processing progress (0-100).
|
|
15
|
+
*/
|
|
16
|
+
onProgress?: (progress: number) => void;
|
|
17
|
+
/**
|
|
18
|
+
* Enable debug logging.
|
|
19
|
+
* Default: false
|
|
20
|
+
*/
|
|
21
|
+
debug?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Maximum dimension for the output image. Use this to resize large images before processing.
|
|
24
|
+
*/
|
|
25
|
+
maxDimension?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Public path to serve the model assets from.
|
|
28
|
+
* If not provided, it will attempt to fetch from @imgly CDN.
|
|
29
|
+
* Important for Metro bundler compatibility if not using CDN.
|
|
30
|
+
*/
|
|
31
|
+
publicPath?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Whether to use the cache for this request.
|
|
34
|
+
* Default: true
|
|
35
|
+
*/
|
|
36
|
+
useCache?: boolean;
|
|
37
|
+
}
|
|
38
|
+
export interface CompressImageOptions {
|
|
39
|
+
maxSizeKB?: number;
|
|
40
|
+
width?: number;
|
|
41
|
+
height?: number;
|
|
42
|
+
quality?: number;
|
|
43
|
+
format?: 'webp' | 'png' | 'jpeg';
|
|
44
|
+
}
|
|
45
|
+
export interface GenerateThumbhashOptions {
|
|
46
|
+
size?: number;
|
|
47
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class BackgroundRemovalError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
constructor(message: string, code?: string);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Maps library errors to standardized BackgroundRemovalError
|
|
7
|
+
*/
|
|
8
|
+
export declare function mapErrorToBackgroundRemovalError(error: unknown): BackgroundRemovalError;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export class BackgroundRemovalError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code = 'UNKNOWN_ERROR') {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'BackgroundRemovalError';
|
|
6
|
+
this.code = code;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Maps library errors to standardized BackgroundRemovalError
|
|
11
|
+
*/
|
|
12
|
+
export function mapErrorToBackgroundRemovalError(error) {
|
|
13
|
+
if (error instanceof BackgroundRemovalError)
|
|
14
|
+
return error;
|
|
15
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
16
|
+
// Model loading errors
|
|
17
|
+
if (message.includes('fetch') || message.includes('network') || message.includes('Failed to load resource')) {
|
|
18
|
+
return new BackgroundRemovalError(`Failed to download AI model. Please check your internet connection. (Details: ${message})`, 'MODEL_DOWNLOAD_ERROR');
|
|
19
|
+
}
|
|
20
|
+
// WASM errors
|
|
21
|
+
if (message.includes('wasm') || message.includes('WebAssembly')) {
|
|
22
|
+
return new BackgroundRemovalError(`WebAssembly failed to initialize. Your browser might not support it. (Details: ${message})`, 'WASM_INIT_ERROR');
|
|
23
|
+
}
|
|
24
|
+
// Processing errors
|
|
25
|
+
if (message.includes('memory') || message.includes('allocation')) {
|
|
26
|
+
return new BackgroundRemovalError('Out of memory. Try using a smaller maxDimension or closing other tabs.', 'MEMORY_ERROR');
|
|
27
|
+
}
|
|
28
|
+
return new BackgroundRemovalError(message);
|
|
29
|
+
}
|