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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-remove-image-bg",
3
- "version": "0.0.22",
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,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
- * 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 imglyBackgroundRemoval: 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
- 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
- export const removeBackground = removeBgImage;
33
+ if (debug) console.log('[Web] removeBgImage called with:', uri, options);
62
34
 
63
- // ==========================================
64
- // UTILITIES (Main Thread - lightweight)
65
- // ==========================================
66
-
67
- // Helper to load image
68
- function loadImage(src: string): Promise<HTMLImageElement> {
69
- return new Promise((resolve, reject) => {
70
- const img = new Image();
71
- img.crossOrigin = 'anonymous';
72
- img.onload = () => resolve(img);
73
- img.onerror = reject;
74
- img.src = src;
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
- export interface CompressImageOptions {
79
- maxSizeKB?: number;
80
- width?: number;
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
- export interface GenerateThumbhashOptions {
87
- size?: number;
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
- const {
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
- imageUri: string,
135
- options: GenerateThumbhashOptions = {}
88
+ uri: string,
89
+ _options: GenerateThumbhashOptions = {}
136
90
  ): Promise<string> {
137
- const { size = 32 } = options;
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
- export async function clearCache(): Promise<void> {
163
- // @ts-ignore
164
- if (typeof imglyBackgroundRemoval !== 'undefined' && imglyBackgroundRemoval.preload) {
165
- // no explicit clear cache in uMD version usually
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 getCacheSize(): number { return 0; }
170
- export async function onLowMemory(): Promise<number> {
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
+ }