rn-remove-image-bg 0.0.23 → 0.0.24
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/README.md +39 -17
- package/lib/ImageProcessing.web.d.ts +18 -25
- package/lib/ImageProcessing.web.js +66 -112
- package/lib/web/core/BackgroundRemover.d.ts +8 -0
- package/lib/web/core/BackgroundRemover.js +33 -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 +32 -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 +18 -17
- package/src/ImageProcessing.web.ts +92 -158
- package/src/web/core/BackgroundRemover.ts +39 -0
- package/src/web/core/CacheManager.ts +76 -0
- package/src/web/core/types.ts +56 -0
- package/src/web/errors/WebErrorAdapter.ts +44 -0
- package/src/web/utils/CompressImage.ts +38 -0
- package/src/web/utils/ThumbhashGenerator.ts +40 -0
- package/src/web/utils/formatConverter.ts +17 -0
- package/src/web/utils/uriHelper.ts +51 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helper to normalize React Native URIs for Web consumption.
|
|
3
|
+
* Handles file://, asset://, and data: URIs.
|
|
4
|
+
*/
|
|
5
|
+
export async function normalizeUri(uri) {
|
|
6
|
+
if (!uri)
|
|
7
|
+
return '';
|
|
8
|
+
// Handle data URIs (return as is)
|
|
9
|
+
if (uri.startsWith('data:')) {
|
|
10
|
+
return uri;
|
|
11
|
+
}
|
|
12
|
+
// Handle http/https (return as is)
|
|
13
|
+
if (uri.startsWith('http')) {
|
|
14
|
+
return uri;
|
|
15
|
+
}
|
|
16
|
+
// Handle Expo asset:// URIs (convert to http relative path if needed, or pass through)
|
|
17
|
+
// In Expo Web, bundled assets are usually served from /assets/
|
|
18
|
+
if (uri.startsWith('asset://')) {
|
|
19
|
+
// NOTE: complex asset:// handling might require expo-asset,
|
|
20
|
+
// but often on web the uri passed is already resolved or a relative path.
|
|
21
|
+
// For now, we return it. If it fails, we might need 'expo-asset' module.
|
|
22
|
+
return uri.replace('asset://', '/assets/');
|
|
23
|
+
}
|
|
24
|
+
// Handle file:// URIs
|
|
25
|
+
// On web, file:// is blocked for security unless it's a blob url created by the app
|
|
26
|
+
if (uri.startsWith('file://')) {
|
|
27
|
+
// If it's a local file picked by user, it might be a blob: reference in disguise or invalid.
|
|
28
|
+
// We strip the protocol for relative paths check.
|
|
29
|
+
return uri;
|
|
30
|
+
}
|
|
31
|
+
// Relative paths
|
|
32
|
+
return uri;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Loads an image from a URI into an HTMLImageElement for processing.
|
|
36
|
+
* Handles CORS cross-origin issues.
|
|
37
|
+
*/
|
|
38
|
+
export function loadImage(uri) {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
const img = new Image();
|
|
41
|
+
img.crossOrigin = 'Anonymous'; // Enable CORS
|
|
42
|
+
img.onload = () => resolve(img);
|
|
43
|
+
img.onerror = (e) => reject(new Error(`Failed to load image at ${uri}: ${String(e)}`));
|
|
44
|
+
img.src = uri;
|
|
45
|
+
});
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rn-remove-image-bg",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "rn-remove-image-bg",
|
|
5
5
|
"homepage": "https://github.com/a-eid/rn-remove-image-bg",
|
|
6
6
|
"main": "lib/index",
|
|
@@ -35,11 +35,22 @@
|
|
|
35
35
|
"*.podspec",
|
|
36
36
|
"README.md"
|
|
37
37
|
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"postinstall": "tsc || exit 0;",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"clean": "rm -rf android/build node_modules/**/android/build lib",
|
|
42
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
|
|
43
|
+
"lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions",
|
|
44
|
+
"typescript": "tsc",
|
|
45
|
+
"specs": "tsc --noEmit false && nitrogen --logLevel=\"debug\"",
|
|
46
|
+
"prepare": "npm run typescript",
|
|
47
|
+
"test": "vitest run"
|
|
48
|
+
},
|
|
38
49
|
"author": "Ahmed Eid <a.eid@yandex.com> (https://github.com/a-eid)",
|
|
39
50
|
"license": "MIT",
|
|
40
51
|
"devDependencies": {
|
|
41
|
-
"@react-native/eslint-config": "0.82.0",
|
|
42
52
|
"@biomejs/biome": "^2.3.11",
|
|
53
|
+
"@react-native/eslint-config": "0.82.0",
|
|
43
54
|
"@types/react": "^19.1.03",
|
|
44
55
|
"eslint": "^8.57.0",
|
|
45
56
|
"eslint-config-prettier": "^9.1.0",
|
|
@@ -52,11 +63,11 @@
|
|
|
52
63
|
"vitest": "^3.0.0"
|
|
53
64
|
},
|
|
54
65
|
"peerDependencies": {
|
|
66
|
+
"expo-file-system": "*",
|
|
67
|
+
"expo-image-manipulator": "*",
|
|
55
68
|
"react": "*",
|
|
56
69
|
"react-native": "*",
|
|
57
|
-
"react-native-nitro-modules": "0.31.10"
|
|
58
|
-
"expo-file-system": "*",
|
|
59
|
-
"expo-image-manipulator": "*"
|
|
70
|
+
"react-native-nitro-modules": "0.31.10"
|
|
60
71
|
},
|
|
61
72
|
"eslintConfig": {
|
|
62
73
|
"root": true,
|
|
@@ -85,19 +96,9 @@
|
|
|
85
96
|
"lib/"
|
|
86
97
|
],
|
|
87
98
|
"dependencies": {
|
|
88
|
-
"@
|
|
99
|
+
"@imgly/background-removal": "^1.7.0",
|
|
89
100
|
"buffer": "^6.0.3",
|
|
90
101
|
"thumbhash": "^0.1.1",
|
|
91
102
|
"upng-js": "^2.1.0"
|
|
92
|
-
},
|
|
93
|
-
"scripts": {
|
|
94
|
-
"postinstall": "tsc || exit 0;",
|
|
95
|
-
"typecheck": "tsc --noEmit",
|
|
96
|
-
"clean": "rm -rf android/build node_modules/**/android/build lib",
|
|
97
|
-
"lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
|
|
98
|
-
"lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions",
|
|
99
|
-
"typescript": "tsc",
|
|
100
|
-
"specs": "tsc --noEmit false && nitrogen --logLevel=\"debug\"",
|
|
101
|
-
"test": "vitest run"
|
|
102
103
|
}
|
|
103
|
-
}
|
|
104
|
+
}
|
|
@@ -1,186 +1,120 @@
|
|
|
1
|
+
import { BackgroundRemover } from './web/core/BackgroundRemover';
|
|
2
|
+
import { cacheManager } from './web/core/CacheManager';
|
|
3
|
+
import { compressImage as compressImageWeb } from './web/utils/CompressImage';
|
|
4
|
+
import { generateThumbhash as generateThumbhashWeb } from './web/utils/ThumbhashGenerator';
|
|
5
|
+
import { blobToDataUrl } from './web/utils/formatConverter';
|
|
6
|
+
import type {
|
|
7
|
+
RemoveBgImageOptions,
|
|
8
|
+
CompressImageOptions,
|
|
9
|
+
GenerateThumbhashOptions,
|
|
10
|
+
OutputFormat
|
|
11
|
+
} from './web/core/types';
|
|
12
|
+
|
|
13
|
+
// Re-export types
|
|
14
|
+
export type {
|
|
15
|
+
RemoveBgImageOptions,
|
|
16
|
+
CompressImageOptions,
|
|
17
|
+
GenerateThumbhashOptions,
|
|
18
|
+
OutputFormat
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// Compatibility for NativeRemoveBackgroundOptions
|
|
22
|
+
export type NativeRemoveBackgroundOptions = RemoveBgImageOptions;
|
|
23
|
+
|
|
1
24
|
/**
|
|
2
|
-
*
|
|
3
|
-
* Requires the script to be added manually to index.html.
|
|
25
|
+
* Remove background from image (Web Implementation)
|
|
4
26
|
*/
|
|
5
|
-
|
|
6
|
-
export type OutputFormat = 'PNG' | 'WEBP';
|
|
7
|
-
|
|
8
|
-
export interface RemoveBgImageOptions {
|
|
9
|
-
format?: OutputFormat;
|
|
10
|
-
quality?: number;
|
|
11
|
-
onProgress?: (progress: number) => void;
|
|
12
|
-
debug?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Check for global variable
|
|
16
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
-
declare const imglyRemoveBackground: any;
|
|
18
|
-
|
|
19
27
|
export async function removeBgImage(
|
|
20
28
|
uri: string,
|
|
21
29
|
options: RemoveBgImageOptions = {}
|
|
22
30
|
): Promise<string> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
const { onProgress, useCache = true, debug = false } = options;
|
|
32
|
+
|
|
33
|
+
if (debug) console.log('[Web] removeBgImage called with:', uri, options);
|
|
34
|
+
|
|
35
|
+
// 1. Check Cache
|
|
36
|
+
if (useCache) {
|
|
37
|
+
const cached = cacheManager.get(uri, options);
|
|
38
|
+
if (cached) {
|
|
39
|
+
if (debug) console.log('[Web] Cache hit');
|
|
40
|
+
onProgress?.(100);
|
|
41
|
+
return cached;
|
|
34
42
|
}
|
|
43
|
+
}
|
|
35
44
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
45
|
+
// 2. Process
|
|
46
|
+
onProgress?.(10); // Start
|
|
47
|
+
const blob = await BackgroundRemover.remove(uri, {
|
|
48
|
+
...options,
|
|
49
|
+
onProgress: (p) => {
|
|
50
|
+
// Map progress to 10-90 range to leave room for start/end
|
|
51
|
+
const mapped = 10 + Math.round((p * 0.8));
|
|
52
|
+
onProgress?.(mapped);
|
|
45
53
|
}
|
|
46
|
-
|
|
47
|
-
if (debug) console.log('[rmbg] Starting...');
|
|
48
|
-
onProgress?.(1);
|
|
49
|
-
|
|
50
|
-
// Config for imgly
|
|
51
|
-
const config = {
|
|
52
|
-
debug: debug,
|
|
53
|
-
// Point publicPath to CDN for assets (wasm/models)
|
|
54
|
-
publicPath: 'https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/dist/',
|
|
55
|
-
progress: (_key: string, current: number, total: number) => {
|
|
56
|
-
if (onProgress && total > 0) {
|
|
57
|
-
onProgress((current / total) * 100);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// @ts-ignore
|
|
63
|
-
const blob = await window.imglyRemoveBackground(uri, config);
|
|
64
|
-
onProgress?.(100);
|
|
65
|
-
|
|
66
|
-
// Convert blob to DataURL
|
|
67
|
-
return new Promise((resolve, reject) => {
|
|
68
|
-
const reader = new FileReader();
|
|
69
|
-
reader.onloadend = () => resolve(reader.result as string);
|
|
70
|
-
reader.onerror = reject;
|
|
71
|
-
reader.readAsDataURL(blob);
|
|
72
|
-
});
|
|
73
|
-
}
|
|
54
|
+
});
|
|
74
55
|
|
|
75
|
-
|
|
56
|
+
// 3. Convert to Data URL
|
|
57
|
+
const dataUrl = await blobToDataUrl(blob);
|
|
58
|
+
onProgress?.(100);
|
|
76
59
|
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// Helper to load image
|
|
82
|
-
function loadImage(src: string): Promise<HTMLImageElement> {
|
|
83
|
-
return new Promise((resolve, reject) => {
|
|
84
|
-
const img = new Image();
|
|
85
|
-
img.crossOrigin = 'anonymous';
|
|
86
|
-
img.onload = () => resolve(img);
|
|
87
|
-
img.onerror = reject;
|
|
88
|
-
img.src = src;
|
|
89
|
-
});
|
|
90
|
-
}
|
|
60
|
+
// 4. Cache Result
|
|
61
|
+
if (useCache) {
|
|
62
|
+
cacheManager.set(uri, options, dataUrl);
|
|
63
|
+
}
|
|
91
64
|
|
|
92
|
-
|
|
93
|
-
maxSizeKB?: number;
|
|
94
|
-
width?: number;
|
|
95
|
-
height?: number;
|
|
96
|
-
quality?: number;
|
|
97
|
-
format?: 'webp' | 'png' | 'jpeg';
|
|
65
|
+
return dataUrl;
|
|
98
66
|
}
|
|
99
67
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Backward compatibility alias
|
|
70
|
+
* @deprecated Use removeBgImage
|
|
71
|
+
*/
|
|
72
|
+
export const removeBackground = removeBgImage;
|
|
103
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Compress image (Web Implementation)
|
|
76
|
+
*/
|
|
104
77
|
export async function compressImage(
|
|
105
78
|
uri: string,
|
|
106
79
|
options: CompressImageOptions = {}
|
|
107
80
|
): Promise<string> {
|
|
108
|
-
|
|
109
|
-
maxSizeKB = 250,
|
|
110
|
-
width = 1024,
|
|
111
|
-
height = 1024,
|
|
112
|
-
quality = 0.85,
|
|
113
|
-
format = 'webp',
|
|
114
|
-
} = options;
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
const img = await loadImage(uri);
|
|
118
|
-
const scale = Math.min(width / img.width, height / img.height, 1);
|
|
119
|
-
const targetWidth = Math.round(img.width * scale);
|
|
120
|
-
const targetHeight = Math.round(img.height * scale);
|
|
121
|
-
|
|
122
|
-
const canvas = document.createElement('canvas');
|
|
123
|
-
canvas.width = targetWidth;
|
|
124
|
-
canvas.height = targetHeight;
|
|
125
|
-
const ctx = canvas.getContext('2d');
|
|
126
|
-
if (!ctx) throw new Error('Could not get canvas context');
|
|
127
|
-
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
128
|
-
|
|
129
|
-
const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp';
|
|
130
|
-
let dataUrl = canvas.toDataURL(mimeType, quality);
|
|
131
|
-
|
|
132
|
-
// Reduce quality if over size limit
|
|
133
|
-
let currentQuality = quality;
|
|
134
|
-
const getSize = (url: string) => ((url.split(',')[1] || '').length * 3) / 4 / 1024;
|
|
135
|
-
while (getSize(dataUrl) > maxSizeKB && currentQuality > 0.5) {
|
|
136
|
-
currentQuality -= 0.1;
|
|
137
|
-
dataUrl = canvas.toDataURL(mimeType, currentQuality);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return dataUrl;
|
|
141
|
-
} catch (error) {
|
|
142
|
-
console.warn('[rmbg] compressImage failed:', error);
|
|
143
|
-
return uri;
|
|
144
|
-
}
|
|
81
|
+
return compressImageWeb(uri, options);
|
|
145
82
|
}
|
|
146
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Generate thumbhash (Web Implementation)
|
|
86
|
+
*/
|
|
147
87
|
export async function generateThumbhash(
|
|
148
|
-
|
|
149
|
-
|
|
88
|
+
uri: string,
|
|
89
|
+
_options: GenerateThumbhashOptions = {}
|
|
150
90
|
): Promise<string> {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
console.warn('[rmbg] generateThumbhash failed:', error);
|
|
172
|
-
return '';
|
|
173
|
-
}
|
|
91
|
+
// Web implementation currently doesn't use options (size hardcoded or auto-scaled)
|
|
92
|
+
return generateThumbhashWeb(uri);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Cache Management APIs
|
|
96
|
+
|
|
97
|
+
export async function clearCache(_deleteFiles = false): Promise<void> {
|
|
98
|
+
cacheManager.clear();
|
|
99
|
+
console.log('[Web] Cache cleared');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getCacheSize(): number {
|
|
103
|
+
return cacheManager.size();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function onLowMemory(_deleteFiles = true): Promise<number> {
|
|
107
|
+
const size = cacheManager.size();
|
|
108
|
+
cacheManager.clear();
|
|
109
|
+
console.log(`[Web] Cleared ${size} items due to low memory`);
|
|
110
|
+
return size;
|
|
174
111
|
}
|
|
175
112
|
|
|
176
|
-
export
|
|
177
|
-
//
|
|
113
|
+
export function configureCache(_config: Record<string, unknown>): void {
|
|
114
|
+
// Web cache is simple in-memory LRU, config not fully supported yet but stubbed
|
|
115
|
+
console.log('[Web] Cache configuration updated (no-op on web)');
|
|
178
116
|
}
|
|
179
117
|
|
|
180
|
-
export function
|
|
181
|
-
|
|
182
|
-
await clearCache();
|
|
183
|
-
return 0;
|
|
118
|
+
export function getCacheDirectory(): string {
|
|
119
|
+
return ''; // No file system on web
|
|
184
120
|
}
|
|
185
|
-
export function configureCache(_config: { maxEntries?: number }): void {}
|
|
186
|
-
export function getCacheDirectory(): string { return ''; }
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { removeBackground as imglyRemove, type Config } from '@imgly/background-removal';
|
|
2
|
+
import type { RemoveBgImageOptions } from './types';
|
|
3
|
+
import { mapErrorToBackgroundRemovalError } from '../errors/WebErrorAdapter';
|
|
4
|
+
import { normalizeUri } from '../utils/uriHelper';
|
|
5
|
+
|
|
6
|
+
export const BackgroundRemover = {
|
|
7
|
+
/**
|
|
8
|
+
* Removes background from an image.
|
|
9
|
+
* Returns a Blobl of the processed image (PNG).
|
|
10
|
+
*/
|
|
11
|
+
async remove(uri: string, options: RemoveBgImageOptions): Promise<Blob> {
|
|
12
|
+
try {
|
|
13
|
+
const normalizedUri = await normalizeUri(uri);
|
|
14
|
+
|
|
15
|
+
const config: Config = {
|
|
16
|
+
// Pass publicPath if provided (for self-hosted assets)
|
|
17
|
+
publicPath: options.publicPath,
|
|
18
|
+
|
|
19
|
+
// Map progress callback
|
|
20
|
+
progress: (_key: string, current: number, total: number) => {
|
|
21
|
+
if (options.onProgress && total > 0) {
|
|
22
|
+
const p = Math.min(100, Math.round((current / total) * 100));
|
|
23
|
+
options.onProgress(p);
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
// Enable debug logging if requested
|
|
28
|
+
debug: options.debug ?? false,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Execute removal
|
|
32
|
+
const blob = await imglyRemove(normalizedUri, config);
|
|
33
|
+
return blob;
|
|
34
|
+
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw mapErrorToBackgroundRemovalError(error);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { RemoveBgImageOptions } from './types';
|
|
2
|
+
|
|
3
|
+
interface CacheEntry {
|
|
4
|
+
dataUrl: string; // The processed image as Data URL
|
|
5
|
+
timestamp: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class CacheManager {
|
|
9
|
+
private cache: Map<string, CacheEntry>;
|
|
10
|
+
private readonly MAX_SIZE = 50; // Limit to 50 items to avoid memory leaks
|
|
11
|
+
|
|
12
|
+
constructor() {
|
|
13
|
+
this.cache = new Map();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generates a unique cache key based on input URI and processing options
|
|
18
|
+
*/
|
|
19
|
+
private generateKey(uri: string, options: RemoveBgImageOptions): string {
|
|
20
|
+
// We include relevant options that affect output
|
|
21
|
+
const { format = 'PNG', quality = 100, maxDimension = 0 } = options;
|
|
22
|
+
return `${uri}|${format}|${quality}|${maxDimension}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
public get(uri: string, options: RemoveBgImageOptions): string | null {
|
|
26
|
+
const key = this.generateKey(uri, options);
|
|
27
|
+
const entry = this.cache.get(key);
|
|
28
|
+
if (entry) {
|
|
29
|
+
entry.timestamp = Date.now(); // Update usage timestamp (simple LRU)
|
|
30
|
+
return entry.dataUrl;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public set(uri: string, options: RemoveBgImageOptions, dataUrl: string): void {
|
|
36
|
+
const key = this.generateKey(uri, options);
|
|
37
|
+
|
|
38
|
+
// Evict if full
|
|
39
|
+
if (this.cache.size >= this.MAX_SIZE) {
|
|
40
|
+
this.evictOldest();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.cache.set(key, {
|
|
44
|
+
dataUrl,
|
|
45
|
+
timestamp: Date.now()
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public clear(): void {
|
|
50
|
+
this.cache.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public size(): number {
|
|
54
|
+
return this.cache.size;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private evictOldest(): void {
|
|
58
|
+
// Find oldest entry
|
|
59
|
+
let oldestKey: string | null = null;
|
|
60
|
+
let oldestTime = Infinity;
|
|
61
|
+
|
|
62
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
63
|
+
if (entry.timestamp < oldestTime) {
|
|
64
|
+
oldestTime = entry.timestamp;
|
|
65
|
+
oldestKey = key;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (oldestKey) {
|
|
70
|
+
this.cache.delete(oldestKey);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Singleton instance
|
|
76
|
+
export const cacheManager = new CacheManager();
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type OutputFormat = 'PNG' | 'WEBP';
|
|
2
|
+
|
|
3
|
+
export interface RemoveBgImageOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Output format of the processed image.
|
|
6
|
+
* Default: 'PNG'
|
|
7
|
+
*/
|
|
8
|
+
format?: OutputFormat;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Quality of the output image (0-100). Only applies to WEBP/JPEG.
|
|
12
|
+
* Default: 100
|
|
13
|
+
*/
|
|
14
|
+
quality?: number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Callback to track download and processing progress (0-100).
|
|
18
|
+
*/
|
|
19
|
+
onProgress?: (progress: number) => void;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Enable debug logging.
|
|
23
|
+
* Default: false
|
|
24
|
+
*/
|
|
25
|
+
debug?: boolean;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Maximum dimension for the output image. Use this to resize large images before processing.
|
|
29
|
+
*/
|
|
30
|
+
maxDimension?: number;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Public path to serve the model assets from.
|
|
34
|
+
* If not provided, it will attempt to fetch from @imgly CDN.
|
|
35
|
+
* Important for Metro bundler compatibility if not using CDN.
|
|
36
|
+
*/
|
|
37
|
+
publicPath?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether to use the cache for this request.
|
|
41
|
+
* Default: true
|
|
42
|
+
*/
|
|
43
|
+
useCache?: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CompressImageOptions {
|
|
47
|
+
maxSizeKB?: number;
|
|
48
|
+
width?: number;
|
|
49
|
+
height?: number;
|
|
50
|
+
quality?: number;
|
|
51
|
+
format?: 'webp' | 'png' | 'jpeg';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface GenerateThumbhashOptions {
|
|
55
|
+
size?: number;
|
|
56
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export class BackgroundRemovalError extends Error {
|
|
2
|
+
code: string;
|
|
3
|
+
|
|
4
|
+
constructor(message: string, code: string = 'UNKNOWN_ERROR') {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'BackgroundRemovalError';
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Maps library errors to standardized BackgroundRemovalError
|
|
13
|
+
*/
|
|
14
|
+
export function mapErrorToBackgroundRemovalError(error: unknown): BackgroundRemovalError {
|
|
15
|
+
if (error instanceof BackgroundRemovalError) return error;
|
|
16
|
+
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
|
|
19
|
+
// Model loading errors
|
|
20
|
+
if (message.includes('fetch') || message.includes('network') || message.includes('Failed to load resource')) {
|
|
21
|
+
return new BackgroundRemovalError(
|
|
22
|
+
`Failed to download AI model. Please check your internet connection. (Details: ${message})`,
|
|
23
|
+
'MODEL_DOWNLOAD_ERROR'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// WASM errors
|
|
28
|
+
if (message.includes('wasm') || message.includes('WebAssembly')) {
|
|
29
|
+
return new BackgroundRemovalError(
|
|
30
|
+
`WebAssembly failed to initialize. Your browser might not support it. (Details: ${message})`,
|
|
31
|
+
'WASM_INIT_ERROR'
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Processing errors
|
|
36
|
+
if (message.includes('memory') || message.includes('allocation')) {
|
|
37
|
+
return new BackgroundRemovalError(
|
|
38
|
+
'Out of memory. Try using a smaller maxDimension or closing other tabs.',
|
|
39
|
+
'MEMORY_ERROR'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return new BackgroundRemovalError(message);
|
|
44
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { CompressImageOptions } from '../core/types';
|
|
2
|
+
import { loadImage } from './uriHelper';
|
|
3
|
+
|
|
4
|
+
export async function compressImage(uri: string, options: CompressImageOptions): Promise<string> {
|
|
5
|
+
const img = await loadImage(uri);
|
|
6
|
+
|
|
7
|
+
let { width, height, quality = 0.8, format = 'jpeg' } = options;
|
|
8
|
+
|
|
9
|
+
// Default dimensions to original if not specified
|
|
10
|
+
if (!width) width = img.naturalWidth;
|
|
11
|
+
if (!height) height = img.naturalHeight;
|
|
12
|
+
|
|
13
|
+
// Calculate aspect ratio if one dimension is missing (though simpler to just use natural if both default)
|
|
14
|
+
const ratio = img.naturalWidth / img.naturalHeight;
|
|
15
|
+
if (options.width && !options.height) height = Math.round(width / ratio);
|
|
16
|
+
if (options.height && !options.width) width = Math.round(height * ratio);
|
|
17
|
+
|
|
18
|
+
// Normalize format
|
|
19
|
+
const mimeType = format === 'png' ? 'image/png' : format === 'webp' ? 'image/webp' : 'image/jpeg';
|
|
20
|
+
|
|
21
|
+
// Create canvas
|
|
22
|
+
const canvas = document.createElement('canvas');
|
|
23
|
+
canvas.width = width;
|
|
24
|
+
canvas.height = height;
|
|
25
|
+
const ctx = canvas.getContext('2d');
|
|
26
|
+
|
|
27
|
+
if (!ctx) {
|
|
28
|
+
throw new Error('Canvas 2D context not available');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Draw image
|
|
32
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
33
|
+
|
|
34
|
+
// Export
|
|
35
|
+
// Note: quality (0-1) is ignored for PNG
|
|
36
|
+
const dataUrl = canvas.toDataURL(mimeType, quality);
|
|
37
|
+
return dataUrl;
|
|
38
|
+
}
|