rn-remove-image-bg 0.0.22 → 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 +38 -12
- package/lib/ImageProcessing.web.d.ts +18 -25
- package/lib/ImageProcessing.web.js +67 -103
- 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 +94 -149
- 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
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,175 +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 imglyBackgroundRemoval: any;
|
|
18
|
-
|
|
19
27
|
export async function removeBgImage(
|
|
20
28
|
uri: string,
|
|
21
29
|
options: RemoveBgImageOptions = {}
|
|
22
30
|
): Promise<string> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// Safety check
|
|
26
|
-
if (typeof imglyBackgroundRemoval === 'undefined') {
|
|
27
|
-
throw new Error(
|
|
28
|
-
'[rn-remove-image-bg] Library not found. Please add the following script to your web index.html:\n' +
|
|
29
|
-
'<script src="https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/dist/imgly-background-removal.min.js"></script>'
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (debug) console.log('[rmbg] Starting...');
|
|
34
|
-
onProgress?.(1);
|
|
35
|
-
|
|
36
|
-
// Config for imgly
|
|
37
|
-
const config = {
|
|
38
|
-
debug: debug,
|
|
39
|
-
// Point publicPath to CDN for assets (wasm/models)
|
|
40
|
-
// This is crucial for UMD build to find its dependencies on the CDN
|
|
41
|
-
publicPath: 'https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/dist/',
|
|
42
|
-
progress: (_key: string, current: number, total: number) => {
|
|
43
|
-
if (onProgress && total > 0) {
|
|
44
|
-
onProgress((current / total) * 100);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const blob = await imglyBackgroundRemoval.removeBackground(uri, config);
|
|
50
|
-
onProgress?.(100);
|
|
51
|
-
|
|
52
|
-
// Convert blob to DataURL
|
|
53
|
-
return new Promise((resolve, reject) => {
|
|
54
|
-
const reader = new FileReader();
|
|
55
|
-
reader.onloadend = () => resolve(reader.result as string);
|
|
56
|
-
reader.onerror = reject;
|
|
57
|
-
reader.readAsDataURL(blob);
|
|
58
|
-
});
|
|
59
|
-
}
|
|
31
|
+
const { onProgress, useCache = true, debug = false } = options;
|
|
60
32
|
|
|
61
|
-
|
|
33
|
+
if (debug) console.log('[Web] removeBgImage called with:', uri, options);
|
|
62
34
|
|
|
63
|
-
//
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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;
|
|
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);
|
|
53
|
+
}
|
|
75
54
|
});
|
|
76
|
-
}
|
|
77
55
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
height?: number;
|
|
82
|
-
quality?: number;
|
|
83
|
-
format?: 'webp' | 'png' | 'jpeg';
|
|
84
|
-
}
|
|
56
|
+
// 3. Convert to Data URL
|
|
57
|
+
const dataUrl = await blobToDataUrl(blob);
|
|
58
|
+
onProgress?.(100);
|
|
85
59
|
|
|
86
|
-
|
|
87
|
-
|
|
60
|
+
// 4. Cache Result
|
|
61
|
+
if (useCache) {
|
|
62
|
+
cacheManager.set(uri, options, dataUrl);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return dataUrl;
|
|
88
66
|
}
|
|
89
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Backward compatibility alias
|
|
70
|
+
* @deprecated Use removeBgImage
|
|
71
|
+
*/
|
|
72
|
+
export const removeBackground = removeBgImage;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compress image (Web Implementation)
|
|
76
|
+
*/
|
|
90
77
|
export async function compressImage(
|
|
91
78
|
uri: string,
|
|
92
79
|
options: CompressImageOptions = {}
|
|
93
80
|
): Promise<string> {
|
|
94
|
-
|
|
95
|
-
maxSizeKB = 250,
|
|
96
|
-
width = 1024,
|
|
97
|
-
height = 1024,
|
|
98
|
-
quality = 0.85,
|
|
99
|
-
format = 'webp',
|
|
100
|
-
} = options;
|
|
101
|
-
|
|
102
|
-
try {
|
|
103
|
-
const img = await loadImage(uri);
|
|
104
|
-
const scale = Math.min(width / img.width, height / img.height, 1);
|
|
105
|
-
const targetWidth = Math.round(img.width * scale);
|
|
106
|
-
const targetHeight = Math.round(img.height * scale);
|
|
107
|
-
|
|
108
|
-
const canvas = document.createElement('canvas');
|
|
109
|
-
canvas.width = targetWidth;
|
|
110
|
-
canvas.height = targetHeight;
|
|
111
|
-
const ctx = canvas.getContext('2d');
|
|
112
|
-
if (!ctx) throw new Error('Could not get canvas context');
|
|
113
|
-
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
|
|
114
|
-
|
|
115
|
-
const mimeType = format === 'png' ? 'image/png' : format === 'jpeg' ? 'image/jpeg' : 'image/webp';
|
|
116
|
-
let dataUrl = canvas.toDataURL(mimeType, quality);
|
|
117
|
-
|
|
118
|
-
// Reduce quality if over size limit
|
|
119
|
-
let currentQuality = quality;
|
|
120
|
-
const getSize = (url: string) => ((url.split(',')[1] || '').length * 3) / 4 / 1024;
|
|
121
|
-
while (getSize(dataUrl) > maxSizeKB && currentQuality > 0.5) {
|
|
122
|
-
currentQuality -= 0.1;
|
|
123
|
-
dataUrl = canvas.toDataURL(mimeType, currentQuality);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return dataUrl;
|
|
127
|
-
} catch (error) {
|
|
128
|
-
console.warn('[rmbg] compressImage failed:', error);
|
|
129
|
-
return uri;
|
|
130
|
-
}
|
|
81
|
+
return compressImageWeb(uri, options);
|
|
131
82
|
}
|
|
132
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Generate thumbhash (Web Implementation)
|
|
86
|
+
*/
|
|
133
87
|
export async function generateThumbhash(
|
|
134
|
-
|
|
135
|
-
|
|
88
|
+
uri: string,
|
|
89
|
+
_options: GenerateThumbhashOptions = {}
|
|
136
90
|
): Promise<string> {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
try {
|
|
140
|
-
const img = await loadImage(imageUri);
|
|
141
|
-
|
|
142
|
-
const canvas = document.createElement('canvas');
|
|
143
|
-
canvas.width = size;
|
|
144
|
-
canvas.height = size;
|
|
145
|
-
const ctx = canvas.getContext('2d');
|
|
146
|
-
if (!ctx) throw new Error('Could not get canvas context');
|
|
147
|
-
ctx.drawImage(img, 0, 0, size, size);
|
|
148
|
-
|
|
149
|
-
const imageData = ctx.getImageData(0, 0, size, size);
|
|
150
|
-
|
|
151
|
-
// Load thumbhash from CDN
|
|
152
|
-
// @ts-ignore
|
|
153
|
-
const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
|
|
154
|
-
const hash = rgbaToThumbHash(size, size, imageData.data);
|
|
155
|
-
return btoa(String.fromCharCode(...hash));
|
|
156
|
-
} catch (error) {
|
|
157
|
-
console.warn('[rmbg] generateThumbhash failed:', error);
|
|
158
|
-
return '';
|
|
159
|
-
}
|
|
91
|
+
// Web implementation currently doesn't use options (size hardcoded or auto-scaled)
|
|
92
|
+
return generateThumbhashWeb(uri);
|
|
160
93
|
}
|
|
161
94
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
|
111
|
+
}
|
|
112
|
+
|
|
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)');
|
|
167
116
|
}
|
|
168
117
|
|
|
169
|
-
export function
|
|
170
|
-
|
|
171
|
-
await clearCache();
|
|
172
|
-
return 0;
|
|
118
|
+
export function getCacheDirectory(): string {
|
|
119
|
+
return ''; // No file system on web
|
|
173
120
|
}
|
|
174
|
-
export function configureCache(_config: { maxEntries?: number }): void {}
|
|
175
|
-
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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as ThumbHash from 'thumbhash';
|
|
2
|
+
import { loadImage } from './uriHelper';
|
|
3
|
+
|
|
4
|
+
export async function generateThumbhash(uri: string): Promise<string> {
|
|
5
|
+
const img = await loadImage(uri);
|
|
6
|
+
|
|
7
|
+
// Thumbhash works best with images < 100x100
|
|
8
|
+
const maxSize = 100;
|
|
9
|
+
let width = img.naturalWidth;
|
|
10
|
+
let height = img.naturalHeight;
|
|
11
|
+
|
|
12
|
+
const scale = Math.min(maxSize / width, maxSize / height);
|
|
13
|
+
if (scale < 1) {
|
|
14
|
+
width = Math.round(width * scale);
|
|
15
|
+
height = Math.round(height * scale);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const canvas = document.createElement('canvas');
|
|
19
|
+
canvas.width = width;
|
|
20
|
+
canvas.height = height;
|
|
21
|
+
const ctx = canvas.getContext('2d');
|
|
22
|
+
|
|
23
|
+
if (!ctx) {
|
|
24
|
+
throw new Error('Canvas 2D context not available');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
ctx.drawImage(img, 0, 0, width, height);
|
|
28
|
+
|
|
29
|
+
// Get RGBA data
|
|
30
|
+
const imageData = ctx.getImageData(0, 0, width, height);
|
|
31
|
+
const rgba = imageData.data;
|
|
32
|
+
|
|
33
|
+
// Generate binary hash
|
|
34
|
+
const hash = ThumbHash.rgbaToThumbHash(width, height, rgba);
|
|
35
|
+
|
|
36
|
+
// Convert to base64 using browser API
|
|
37
|
+
// hash is Uint8Array, spread into String.fromCharCode is safe for small thumbhashes (~30 bytes)
|
|
38
|
+
const binary = String.fromCharCode(...hash);
|
|
39
|
+
return window.btoa(binary);
|
|
40
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a Blob to a Data URL string (base64)
|
|
3
|
+
*/
|
|
4
|
+
export function blobToDataUrl(blob: Blob): Promise<string> {
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const reader = new FileReader();
|
|
7
|
+
reader.onload = () => {
|
|
8
|
+
if (typeof reader.result === 'string') {
|
|
9
|
+
resolve(reader.result);
|
|
10
|
+
} else {
|
|
11
|
+
reject(new Error('Failed to convert blob to data URL'));
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
reader.onerror = () => reject(reader.error);
|
|
15
|
+
reader.readAsDataURL(blob);
|
|
16
|
+
});
|
|
17
|
+
}
|