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.
@@ -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.23",
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
- "@huggingface/transformers": "^3.8.1",
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
- * Web implementation using @imgly/background-removal via Script Tag.
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
- const { onProgress, debug = false } = options;
24
-
25
- // Safety check
26
- // @ts-ignore
27
- if (typeof window !== 'undefined' && !window.imglyRemoveBackground) {
28
- // Also check if user defined it as imglyRemoveBackground globally
29
- // @ts-ignore
30
- if (typeof imglyRemoveBackground !== 'undefined') {
31
- // @ts-ignore
32
- window.imglyRemoveBackground = imglyRemoveBackground;
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
- // @ts-ignore
37
- if (typeof window.imglyRemoveBackground === 'undefined') {
38
- throw new Error(
39
- '[rn-remove-image-bg] Library not found. Please add the following script to your web index.html:\n' +
40
- '<script type="module">\n' +
41
- ' import { removeBackground } from "https://cdn.jsdelivr.net/npm/@imgly/background-removal@1.7.0/+esm";\n' +
42
- ' window.imglyRemoveBackground = removeBackground;\n' +
43
- '</script>'
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
- export const removeBackground = removeBgImage;
56
+ // 3. Convert to Data URL
57
+ const dataUrl = await blobToDataUrl(blob);
58
+ onProgress?.(100);
76
59
 
77
- // ==========================================
78
- // UTILITIES (Main Thread - lightweight)
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
- export interface CompressImageOptions {
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
- export interface GenerateThumbhashOptions {
101
- size?: number;
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
- const {
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
- imageUri: string,
149
- options: GenerateThumbhashOptions = {}
88
+ uri: string,
89
+ _options: GenerateThumbhashOptions = {}
150
90
  ): Promise<string> {
151
- const { size = 32 } = options;
152
-
153
- try {
154
- const img = await loadImage(imageUri);
155
-
156
- const canvas = document.createElement('canvas');
157
- canvas.width = size;
158
- canvas.height = size;
159
- const ctx = canvas.getContext('2d');
160
- if (!ctx) throw new Error('Could not get canvas context');
161
- ctx.drawImage(img, 0, 0, size, size);
162
-
163
- const imageData = ctx.getImageData(0, 0, size, size);
164
-
165
- // Load thumbhash from CDN
166
- // @ts-ignore
167
- const { rgbaToThumbHash } = await import(/* webpackIgnore: true */ 'https://cdn.jsdelivr.net/npm/thumbhash@0.1/+esm');
168
- const hash = rgbaToThumbHash(size, size, imageData.data);
169
- return btoa(String.fromCharCode(...hash));
170
- } catch (error) {
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 async function clearCache(): Promise<void> {
177
- // Caching is handled internally by the library
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 getCacheSize(): number { return 0; }
181
- export async function onLowMemory(): Promise<number> {
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
+ }