rn-remove-image-bg 0.0.10
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/NitroRnRemoveImageBg.podspec +33 -0
- package/README.md +386 -0
- package/android/CMakeLists.txt +28 -0
- package/android/build.gradle +142 -0
- package/android/fix-prefab.gradle +51 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt +189 -0
- package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgPackage.kt +31 -0
- package/app.plugin.js +12 -0
- package/ios/Bridge.h +8 -0
- package/ios/HybridImageBackgroundRemover.swift +224 -0
- package/ios/NitroRnRemoveImageBgOnLoad.mm +22 -0
- package/lib/ImageProcessing.d.ts +167 -0
- package/lib/ImageProcessing.js +323 -0
- package/lib/ImageProcessing.web.d.ts +80 -0
- package/lib/ImageProcessing.web.js +248 -0
- package/lib/__tests__/cache.test.d.ts +1 -0
- package/lib/__tests__/cache.test.js +87 -0
- package/lib/__tests__/errors.test.d.ts +1 -0
- package/lib/__tests__/errors.test.js +82 -0
- package/lib/cache.d.ts +72 -0
- package/lib/cache.js +228 -0
- package/lib/errors.d.ts +20 -0
- package/lib/errors.js +64 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +9 -0
- package/lib/specs/Example.nitro.d.ts +0 -0
- package/lib/specs/Example.nitro.js +2 -0
- package/lib/specs/ImageBackgroundRemover.nitro.d.ts +41 -0
- package/lib/specs/ImageBackgroundRemover.nitro.js +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/.gitattributes +1 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.cmake +81 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBg+autolinking.gradle +27 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.cpp +44 -0
- package/nitrogen/generated/android/NitroRnRemoveImageBgOnLoad.hpp +25 -0
- package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.cpp +72 -0
- package/nitrogen/generated/android/c++/JHybridImageBackgroundRemoverSpec.hpp +65 -0
- package/nitrogen/generated/android/c++/JNativeRemoveBackgroundOptions.hpp +66 -0
- package/nitrogen/generated/android/c++/JOutputFormat.hpp +59 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemoverSpec.kt +58 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NativeRemoveBackgroundOptions.kt +44 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/NitroRnRemoveImageBgOnLoad.kt +35 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/rnremoveimagebg/OutputFormat.kt +21 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.cpp +49 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Bridge.hpp +111 -0
- package/nitrogen/generated/ios/NitroRnRemoveImageBg-Swift-Cxx-Umbrella.hpp +51 -0
- package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridImageBackgroundRemoverSpecSwift.hpp +82 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +47 -0
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridImageBackgroundRemoverSpec_cxx.swift +138 -0
- package/nitrogen/generated/ios/swift/NativeRemoveBackgroundOptions.swift +58 -0
- package/nitrogen/generated/ios/swift/OutputFormat.swift +40 -0
- package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.cpp +21 -0
- package/nitrogen/generated/shared/c++/HybridImageBackgroundRemoverSpec.hpp +65 -0
- package/nitrogen/generated/shared/c++/NativeRemoveBackgroundOptions.hpp +84 -0
- package/nitrogen/generated/shared/c++/OutputFormat.hpp +76 -0
- package/package.json +104 -0
- package/react-native.config.js +16 -0
- package/src/ImageProcessing.ts +532 -0
- package/src/ImageProcessing.web.ts +342 -0
- package/src/__tests__/ImageProcessing.test.ts +278 -0
- package/src/__tests__/cache.test.ts +110 -0
- package/src/__tests__/errors.test.ts +117 -0
- package/src/cache.ts +305 -0
- package/src/errors.ts +93 -0
- package/src/index.ts +49 -0
- package/src/specs/Example.nitro.ts +1 -0
- package/src/specs/ImageBackgroundRemover.nitro.ts +49 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
// Mock expo-file-system before importing cache
|
|
3
|
+
vi.mock('expo-file-system/legacy', () => ({
|
|
4
|
+
cacheDirectory: '/mock/cache/',
|
|
5
|
+
getInfoAsync: vi.fn().mockResolvedValue({ exists: false }),
|
|
6
|
+
makeDirectoryAsync: vi.fn().mockResolvedValue(undefined),
|
|
7
|
+
readAsStringAsync: vi.fn().mockResolvedValue('{}'),
|
|
8
|
+
writeAsStringAsync: vi.fn().mockResolvedValue(undefined),
|
|
9
|
+
deleteAsync: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
}));
|
|
11
|
+
// Import after mocking
|
|
12
|
+
import { bgRemovalCache } from '../cache';
|
|
13
|
+
describe('BackgroundRemovalCache', () => {
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
// Clear cache before each test
|
|
16
|
+
await bgRemovalCache.clear();
|
|
17
|
+
});
|
|
18
|
+
describe('hashOptions', () => {
|
|
19
|
+
it('should hash options to JSON string', () => {
|
|
20
|
+
const hash = bgRemovalCache.hashOptions({
|
|
21
|
+
format: 'PNG',
|
|
22
|
+
maxDimension: 1024,
|
|
23
|
+
quality: 100
|
|
24
|
+
});
|
|
25
|
+
expect(hash).toBe('{"format":"PNG","maxDimension":1024,"quality":100}');
|
|
26
|
+
});
|
|
27
|
+
it('should produce consistent hash regardless of key order', () => {
|
|
28
|
+
const hash1 = bgRemovalCache.hashOptions({ a: 1, b: 2 });
|
|
29
|
+
const hash2 = bgRemovalCache.hashOptions({ b: 2, a: 1 });
|
|
30
|
+
expect(hash1).toBe(hash2);
|
|
31
|
+
});
|
|
32
|
+
it('should exclude functions from hash', () => {
|
|
33
|
+
const hash = bgRemovalCache.hashOptions({
|
|
34
|
+
value: 1,
|
|
35
|
+
callback: () => { }
|
|
36
|
+
});
|
|
37
|
+
expect(hash).toBe('{"value":1}');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
describe('size', () => {
|
|
41
|
+
it('should return 0 for empty cache', () => {
|
|
42
|
+
expect(bgRemovalCache.size).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe('set and get', () => {
|
|
46
|
+
it('should store and retrieve cached results', async () => {
|
|
47
|
+
const path = 'file:///test/image.jpg';
|
|
48
|
+
const optionsHash = bgRemovalCache.hashOptions({ format: 'PNG' });
|
|
49
|
+
const resultPath = 'file:///cache/result.png';
|
|
50
|
+
bgRemovalCache.set(path, optionsHash, resultPath);
|
|
51
|
+
expect(bgRemovalCache.size).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('clear', () => {
|
|
55
|
+
it('should clear all cache entries', async () => {
|
|
56
|
+
bgRemovalCache.set('path1', 'hash1', 'result1');
|
|
57
|
+
bgRemovalCache.set('path2', 'hash2', 'result2');
|
|
58
|
+
expect(bgRemovalCache.size).toBe(2);
|
|
59
|
+
await bgRemovalCache.clear();
|
|
60
|
+
expect(bgRemovalCache.size).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('getCacheDirectory', () => {
|
|
64
|
+
it('should return the cache directory path', () => {
|
|
65
|
+
const dir = bgRemovalCache.getCacheDirectory();
|
|
66
|
+
expect(dir).toContain('bg-removal');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe('configure', () => {
|
|
70
|
+
it('should allow updating cache configuration', () => {
|
|
71
|
+
bgRemovalCache.configure({
|
|
72
|
+
maxEntries: 100,
|
|
73
|
+
maxAgeMinutes: 60,
|
|
74
|
+
persistToDisk: true,
|
|
75
|
+
});
|
|
76
|
+
// Configuration applied - no error thrown
|
|
77
|
+
expect(true).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe('prune', () => {
|
|
81
|
+
it('should return 0 when no entries are expired', () => {
|
|
82
|
+
bgRemovalCache.set('path', 'hash', 'result');
|
|
83
|
+
const removed = bgRemovalCache.prune();
|
|
84
|
+
expect(removed).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { BackgroundRemovalError, wrapNativeError } from '../errors';
|
|
3
|
+
describe('BackgroundRemovalError', () => {
|
|
4
|
+
describe('constructor', () => {
|
|
5
|
+
it('should create an error with message and code', () => {
|
|
6
|
+
const error = new BackgroundRemovalError('Test message', 'INVALID_PATH');
|
|
7
|
+
expect(error.message).toBe('Test message');
|
|
8
|
+
expect(error.code).toBe('INVALID_PATH');
|
|
9
|
+
expect(error.name).toBe('BackgroundRemovalError');
|
|
10
|
+
expect(error.originalError).toBeUndefined();
|
|
11
|
+
});
|
|
12
|
+
it('should preserve original error', () => {
|
|
13
|
+
const originalError = new Error('Original error');
|
|
14
|
+
const error = new BackgroundRemovalError('Wrapped', 'UNKNOWN', originalError);
|
|
15
|
+
expect(error.originalError).toBe(originalError);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('toUserMessage', () => {
|
|
19
|
+
it('should return user-friendly message for INVALID_PATH', () => {
|
|
20
|
+
const error = new BackgroundRemovalError('test', 'INVALID_PATH');
|
|
21
|
+
expect(error.toUserMessage()).toBe('The image path provided is invalid.');
|
|
22
|
+
});
|
|
23
|
+
it('should return user-friendly message for FILE_NOT_FOUND', () => {
|
|
24
|
+
const error = new BackgroundRemovalError('test', 'FILE_NOT_FOUND');
|
|
25
|
+
expect(error.toUserMessage()).toBe('The image file could not be found.');
|
|
26
|
+
});
|
|
27
|
+
it('should return user-friendly message for DECODE_FAILED', () => {
|
|
28
|
+
const error = new BackgroundRemovalError('test', 'DECODE_FAILED');
|
|
29
|
+
expect(error.toUserMessage()).toBe('The image could not be read. Please ensure it is a valid image file.');
|
|
30
|
+
});
|
|
31
|
+
it('should return user-friendly message for ML_PROCESSING_FAILED', () => {
|
|
32
|
+
const error = new BackgroundRemovalError('test', 'ML_PROCESSING_FAILED');
|
|
33
|
+
expect(error.toUserMessage()).toBe('Background removal failed. Please try with a different image.');
|
|
34
|
+
});
|
|
35
|
+
it('should return user-friendly message for SAVE_FAILED', () => {
|
|
36
|
+
const error = new BackgroundRemovalError('test', 'SAVE_FAILED');
|
|
37
|
+
expect(error.toUserMessage()).toBe('Could not save the processed image.');
|
|
38
|
+
});
|
|
39
|
+
it('should return user-friendly message for INVALID_OPTIONS', () => {
|
|
40
|
+
const error = new BackgroundRemovalError('test', 'INVALID_OPTIONS');
|
|
41
|
+
expect(error.toUserMessage()).toBe('Invalid options provided for background removal.');
|
|
42
|
+
});
|
|
43
|
+
it('should return default message for UNKNOWN', () => {
|
|
44
|
+
const error = new BackgroundRemovalError('test', 'UNKNOWN');
|
|
45
|
+
expect(error.toUserMessage()).toBe('An unexpected error occurred during background removal.');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('wrapNativeError', () => {
|
|
50
|
+
it('should return same error if already BackgroundRemovalError', () => {
|
|
51
|
+
const error = new BackgroundRemovalError('test', 'INVALID_PATH');
|
|
52
|
+
const wrapped = wrapNativeError(error);
|
|
53
|
+
expect(wrapped).toBe(error);
|
|
54
|
+
});
|
|
55
|
+
it('should wrap string error', () => {
|
|
56
|
+
const wrapped = wrapNativeError('Something went wrong');
|
|
57
|
+
expect(wrapped).toBeInstanceOf(BackgroundRemovalError);
|
|
58
|
+
expect(wrapped.message).toBe('Something went wrong');
|
|
59
|
+
expect(wrapped.code).toBe('UNKNOWN');
|
|
60
|
+
});
|
|
61
|
+
it('should detect FILE_NOT_FOUND from message', () => {
|
|
62
|
+
const error = new Error('File does not exist at path');
|
|
63
|
+
const wrapped = wrapNativeError(error);
|
|
64
|
+
expect(wrapped.code).toBe('FILE_NOT_FOUND');
|
|
65
|
+
expect(wrapped.originalError).toBe(error);
|
|
66
|
+
});
|
|
67
|
+
it('should detect DECODE_FAILED from message', () => {
|
|
68
|
+
const error = new Error('Could not decode image');
|
|
69
|
+
const wrapped = wrapNativeError(error);
|
|
70
|
+
expect(wrapped.code).toBe('DECODE_FAILED');
|
|
71
|
+
});
|
|
72
|
+
it('should detect ML_PROCESSING_FAILED from message', () => {
|
|
73
|
+
const error = new Error('Failed to generate mask');
|
|
74
|
+
const wrapped = wrapNativeError(error);
|
|
75
|
+
expect(wrapped.code).toBe('ML_PROCESSING_FAILED');
|
|
76
|
+
});
|
|
77
|
+
it('should detect SAVE_FAILED from message', () => {
|
|
78
|
+
const error = new Error('Could not save output file');
|
|
79
|
+
const wrapped = wrapNativeError(error);
|
|
80
|
+
expect(wrapped.code).toBe('SAVE_FAILED');
|
|
81
|
+
});
|
|
82
|
+
});
|
package/lib/cache.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the background removal cache
|
|
3
|
+
*/
|
|
4
|
+
export interface CacheConfig {
|
|
5
|
+
/** Maximum number of entries in memory (default: 50) */
|
|
6
|
+
maxEntries?: number;
|
|
7
|
+
/** Maximum age of cache entries in minutes (default: 30) */
|
|
8
|
+
maxAgeMinutes?: number;
|
|
9
|
+
/** Enable disk persistence (default: false) */
|
|
10
|
+
persistToDisk?: boolean;
|
|
11
|
+
/** Custom cache directory (default: FileSystem.cacheDirectory + 'bg-removal/') */
|
|
12
|
+
cacheDirectory?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* LRU cache for background removal results with optional disk persistence
|
|
16
|
+
*/
|
|
17
|
+
declare class BackgroundRemovalCache {
|
|
18
|
+
private cache;
|
|
19
|
+
private _maxEntries;
|
|
20
|
+
private _maxAgeMs;
|
|
21
|
+
private persistToDisk;
|
|
22
|
+
private cacheDirectory;
|
|
23
|
+
private initialized;
|
|
24
|
+
constructor(config?: CacheConfig);
|
|
25
|
+
/**
|
|
26
|
+
* Configure cache settings
|
|
27
|
+
*/
|
|
28
|
+
configure(config: CacheConfig): void;
|
|
29
|
+
/**
|
|
30
|
+
* Initialize disk cache (load manifest if exists)
|
|
31
|
+
*/
|
|
32
|
+
initialize(): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* Save cache manifest to disk
|
|
35
|
+
*/
|
|
36
|
+
private saveManifest;
|
|
37
|
+
/**
|
|
38
|
+
* Generate a cache key from path and options
|
|
39
|
+
*/
|
|
40
|
+
private generateKey;
|
|
41
|
+
/**
|
|
42
|
+
* Hash options object to string for cache key
|
|
43
|
+
*/
|
|
44
|
+
hashOptions(options: Record<string, unknown>): string;
|
|
45
|
+
/**
|
|
46
|
+
* Get cached result if valid
|
|
47
|
+
*/
|
|
48
|
+
get(path: string, optionsHash: string): Promise<string | null>;
|
|
49
|
+
/**
|
|
50
|
+
* Store result in cache
|
|
51
|
+
*/
|
|
52
|
+
set(path: string, optionsHash: string, resultPath: string): void;
|
|
53
|
+
/**
|
|
54
|
+
* Clear all cached entries
|
|
55
|
+
* @param deleteFiles - Also delete cached files from disk (default: false)
|
|
56
|
+
*/
|
|
57
|
+
clear(deleteFiles?: boolean): Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Get current cache size
|
|
60
|
+
*/
|
|
61
|
+
get size(): number;
|
|
62
|
+
/**
|
|
63
|
+
* Remove expired entries
|
|
64
|
+
*/
|
|
65
|
+
prune(): number;
|
|
66
|
+
/**
|
|
67
|
+
* Get cache directory path
|
|
68
|
+
*/
|
|
69
|
+
getCacheDirectory(): string;
|
|
70
|
+
}
|
|
71
|
+
export declare const bgRemovalCache: BackgroundRemovalCache;
|
|
72
|
+
export {};
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import * as FileSystem from 'expo-file-system/legacy';
|
|
2
|
+
const MANIFEST_FILENAME = 'cache-manifest.json';
|
|
3
|
+
const CACHE_VERSION = 1;
|
|
4
|
+
/**
|
|
5
|
+
* LRU cache for background removal results with optional disk persistence
|
|
6
|
+
*/
|
|
7
|
+
class BackgroundRemovalCache {
|
|
8
|
+
cache = new Map();
|
|
9
|
+
_maxEntries;
|
|
10
|
+
_maxAgeMs;
|
|
11
|
+
persistToDisk;
|
|
12
|
+
cacheDirectory;
|
|
13
|
+
initialized = false;
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
this._maxEntries = config.maxEntries ?? 50;
|
|
16
|
+
this._maxAgeMs = (config.maxAgeMinutes ?? 30) * 60 * 1000;
|
|
17
|
+
this.persistToDisk = config.persistToDisk ?? false;
|
|
18
|
+
this.cacheDirectory =
|
|
19
|
+
config.cacheDirectory ?? `${FileSystem.cacheDirectory}bg-removal/`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Configure cache settings
|
|
23
|
+
*/
|
|
24
|
+
configure(config) {
|
|
25
|
+
if (config.maxEntries !== undefined) {
|
|
26
|
+
this._maxEntries = config.maxEntries;
|
|
27
|
+
}
|
|
28
|
+
if (config.maxAgeMinutes !== undefined) {
|
|
29
|
+
this._maxAgeMs = config.maxAgeMinutes * 60 * 1000;
|
|
30
|
+
}
|
|
31
|
+
if (config.persistToDisk !== undefined) {
|
|
32
|
+
this.persistToDisk = config.persistToDisk;
|
|
33
|
+
}
|
|
34
|
+
if (config.cacheDirectory !== undefined) {
|
|
35
|
+
this.cacheDirectory = config.cacheDirectory;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Initialize disk cache (load manifest if exists)
|
|
40
|
+
*/
|
|
41
|
+
async initialize() {
|
|
42
|
+
if (this.initialized || !this.persistToDisk)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
// Ensure cache directory exists
|
|
46
|
+
const dirInfo = await FileSystem.getInfoAsync(this.cacheDirectory);
|
|
47
|
+
if (!dirInfo.exists) {
|
|
48
|
+
await FileSystem.makeDirectoryAsync(this.cacheDirectory, {
|
|
49
|
+
intermediates: true,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// Load manifest if exists
|
|
53
|
+
const manifestPath = `${this.cacheDirectory}${MANIFEST_FILENAME}`;
|
|
54
|
+
const manifestInfo = await FileSystem.getInfoAsync(manifestPath);
|
|
55
|
+
if (manifestInfo.exists) {
|
|
56
|
+
const manifestJson = await FileSystem.readAsStringAsync(manifestPath);
|
|
57
|
+
const manifest = JSON.parse(manifestJson);
|
|
58
|
+
if (manifest.version === CACHE_VERSION) {
|
|
59
|
+
// Validate and load entries
|
|
60
|
+
for (const [key, entry] of Object.entries(manifest.entries)) {
|
|
61
|
+
const fileInfo = await FileSystem.getInfoAsync(entry.resultPath);
|
|
62
|
+
if (fileInfo.exists) {
|
|
63
|
+
this.cache.set(key, entry);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
console.warn('[rn-remove-image-bg] Failed to load cache manifest:', error);
|
|
71
|
+
}
|
|
72
|
+
this.initialized = true;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Save cache manifest to disk
|
|
76
|
+
*/
|
|
77
|
+
async saveManifest() {
|
|
78
|
+
if (!this.persistToDisk)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const manifest = {
|
|
82
|
+
version: CACHE_VERSION,
|
|
83
|
+
entries: Object.fromEntries(this.cache.entries()),
|
|
84
|
+
};
|
|
85
|
+
const manifestPath = `${this.cacheDirectory}${MANIFEST_FILENAME}`;
|
|
86
|
+
await FileSystem.writeAsStringAsync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.warn('[rn-remove-image-bg] Failed to save cache manifest:', error);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Generate a cache key from path and options
|
|
94
|
+
*/
|
|
95
|
+
generateKey(path, optionsHash) {
|
|
96
|
+
return `${path}::${optionsHash}`;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Hash options object to string for cache key
|
|
100
|
+
*/
|
|
101
|
+
hashOptions(options) {
|
|
102
|
+
const sorted = Object.keys(options)
|
|
103
|
+
.sort()
|
|
104
|
+
.reduce((acc, key) => {
|
|
105
|
+
const value = options[key];
|
|
106
|
+
// Exclude functions from hash
|
|
107
|
+
if (typeof value !== 'function') {
|
|
108
|
+
acc[key] = value;
|
|
109
|
+
}
|
|
110
|
+
return acc;
|
|
111
|
+
}, {});
|
|
112
|
+
return JSON.stringify(sorted);
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Get cached result if valid
|
|
116
|
+
*/
|
|
117
|
+
async get(path, optionsHash) {
|
|
118
|
+
await this.initialize();
|
|
119
|
+
const key = this.generateKey(path, optionsHash);
|
|
120
|
+
const entry = this.cache.get(key);
|
|
121
|
+
if (!entry) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Check if expired
|
|
125
|
+
if (Date.now() - entry.timestamp > this._maxAgeMs) {
|
|
126
|
+
this.cache.delete(key);
|
|
127
|
+
this.saveManifest();
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
// Verify result file still exists
|
|
131
|
+
try {
|
|
132
|
+
const info = await FileSystem.getInfoAsync(entry.resultPath);
|
|
133
|
+
if (!info.exists) {
|
|
134
|
+
this.cache.delete(key);
|
|
135
|
+
this.saveManifest();
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
this.cache.delete(key);
|
|
141
|
+
this.saveManifest();
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
// Move to end (most recently used)
|
|
145
|
+
this.cache.delete(key);
|
|
146
|
+
this.cache.set(key, entry);
|
|
147
|
+
return entry.resultPath;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Store result in cache
|
|
151
|
+
*/
|
|
152
|
+
set(path, optionsHash, resultPath) {
|
|
153
|
+
const key = this.generateKey(path, optionsHash);
|
|
154
|
+
// Evict oldest entries if at capacity
|
|
155
|
+
while (this.cache.size >= this._maxEntries) {
|
|
156
|
+
const oldestKey = this.cache.keys().next().value;
|
|
157
|
+
if (oldestKey) {
|
|
158
|
+
this.cache.delete(oldestKey);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.cache.set(key, {
|
|
162
|
+
originalPath: path,
|
|
163
|
+
resultPath,
|
|
164
|
+
timestamp: Date.now(),
|
|
165
|
+
optionsHash,
|
|
166
|
+
});
|
|
167
|
+
this.saveManifest();
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Clear all cached entries
|
|
171
|
+
* @param deleteFiles - Also delete cached files from disk (default: false)
|
|
172
|
+
*/
|
|
173
|
+
async clear(deleteFiles = false) {
|
|
174
|
+
if (deleteFiles && this.persistToDisk) {
|
|
175
|
+
try {
|
|
176
|
+
// Delete all cached result files
|
|
177
|
+
for (const entry of this.cache.values()) {
|
|
178
|
+
try {
|
|
179
|
+
await FileSystem.deleteAsync(entry.resultPath, {
|
|
180
|
+
idempotent: true,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// Ignore individual file deletion errors
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Delete manifest
|
|
188
|
+
const manifestPath = `${this.cacheDirectory}${MANIFEST_FILENAME}`;
|
|
189
|
+
await FileSystem.deleteAsync(manifestPath, { idempotent: true });
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
console.warn('[rn-remove-image-bg] Error clearing cache files:', error);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.cache.clear();
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Get current cache size
|
|
199
|
+
*/
|
|
200
|
+
get size() {
|
|
201
|
+
return this.cache.size;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Remove expired entries
|
|
205
|
+
*/
|
|
206
|
+
prune() {
|
|
207
|
+
const now = Date.now();
|
|
208
|
+
let removed = 0;
|
|
209
|
+
for (const [key, entry] of this.cache.entries()) {
|
|
210
|
+
if (now - entry.timestamp > this._maxAgeMs) {
|
|
211
|
+
this.cache.delete(key);
|
|
212
|
+
removed++;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (removed > 0) {
|
|
216
|
+
this.saveManifest();
|
|
217
|
+
}
|
|
218
|
+
return removed;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get cache directory path
|
|
222
|
+
*/
|
|
223
|
+
getCacheDirectory() {
|
|
224
|
+
return this.cacheDirectory;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Export singleton instance
|
|
228
|
+
export const bgRemovalCache = new BackgroundRemovalCache();
|
package/lib/errors.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error codes for background removal operations
|
|
3
|
+
*/
|
|
4
|
+
export type BackgroundRemovalErrorCode = 'INVALID_PATH' | 'FILE_NOT_FOUND' | 'DECODE_FAILED' | 'ML_PROCESSING_FAILED' | 'SAVE_FAILED' | 'INVALID_OPTIONS' | 'UNKNOWN';
|
|
5
|
+
/**
|
|
6
|
+
* Custom error class for background removal operations
|
|
7
|
+
*/
|
|
8
|
+
export declare class BackgroundRemovalError extends Error {
|
|
9
|
+
readonly code: BackgroundRemovalErrorCode;
|
|
10
|
+
readonly originalError?: Error;
|
|
11
|
+
constructor(message: string, code: BackgroundRemovalErrorCode, originalError?: Error);
|
|
12
|
+
/**
|
|
13
|
+
* Create a user-friendly error message
|
|
14
|
+
*/
|
|
15
|
+
toUserMessage(): string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Helper to wrap native errors with proper typing
|
|
19
|
+
*/
|
|
20
|
+
export declare function wrapNativeError(error: unknown): BackgroundRemovalError;
|
package/lib/errors.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error class for background removal operations
|
|
3
|
+
*/
|
|
4
|
+
export class BackgroundRemovalError extends Error {
|
|
5
|
+
code;
|
|
6
|
+
originalError;
|
|
7
|
+
constructor(message, code, originalError) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'BackgroundRemovalError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.originalError = originalError;
|
|
12
|
+
// Maintain proper stack trace in V8 environments
|
|
13
|
+
if ('captureStackTrace' in Error) {
|
|
14
|
+
Error.captureStackTrace(this, BackgroundRemovalError);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a user-friendly error message
|
|
19
|
+
*/
|
|
20
|
+
toUserMessage() {
|
|
21
|
+
switch (this.code) {
|
|
22
|
+
case 'INVALID_PATH':
|
|
23
|
+
return 'The image path provided is invalid.';
|
|
24
|
+
case 'FILE_NOT_FOUND':
|
|
25
|
+
return 'The image file could not be found.';
|
|
26
|
+
case 'DECODE_FAILED':
|
|
27
|
+
return 'The image could not be read. Please ensure it is a valid image file.';
|
|
28
|
+
case 'ML_PROCESSING_FAILED':
|
|
29
|
+
return 'Background removal failed. Please try with a different image.';
|
|
30
|
+
case 'SAVE_FAILED':
|
|
31
|
+
return 'Could not save the processed image.';
|
|
32
|
+
case 'INVALID_OPTIONS':
|
|
33
|
+
return 'Invalid options provided for background removal.';
|
|
34
|
+
default:
|
|
35
|
+
return 'An unexpected error occurred during background removal.';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Helper to wrap native errors with proper typing
|
|
41
|
+
*/
|
|
42
|
+
export function wrapNativeError(error) {
|
|
43
|
+
if (error instanceof BackgroundRemovalError) {
|
|
44
|
+
return error;
|
|
45
|
+
}
|
|
46
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
+
const originalError = error instanceof Error ? error : undefined;
|
|
48
|
+
// Try to determine error code from message
|
|
49
|
+
if (message.includes('does not exist') || message.includes('not found')) {
|
|
50
|
+
return new BackgroundRemovalError(message, 'FILE_NOT_FOUND', originalError);
|
|
51
|
+
}
|
|
52
|
+
if (message.includes('decode') || message.includes('load image')) {
|
|
53
|
+
return new BackgroundRemovalError(message, 'DECODE_FAILED', originalError);
|
|
54
|
+
}
|
|
55
|
+
if (message.includes('mask') ||
|
|
56
|
+
message.includes('segment') ||
|
|
57
|
+
message.includes('ML')) {
|
|
58
|
+
return new BackgroundRemovalError(message, 'ML_PROCESSING_FAILED', originalError);
|
|
59
|
+
}
|
|
60
|
+
if (message.includes('save') || message.includes('write')) {
|
|
61
|
+
return new BackgroundRemovalError(message, 'SAVE_FAILED', originalError);
|
|
62
|
+
}
|
|
63
|
+
return new BackgroundRemovalError(message, 'UNKNOWN', originalError);
|
|
64
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { compressImage, generateThumbhash, removeBgImage, removeBackground, clearCache, getCacheSize, onLowMemory, configureCache, getCacheDirectory } from './ImageProcessing';
|
|
2
|
+
import type { CompressImageOptions, GenerateThumbhashOptions, RemoveBgImageOptions, OutputFormat } from './ImageProcessing';
|
|
3
|
+
import { BackgroundRemovalError, type BackgroundRemovalErrorCode } from './errors';
|
|
4
|
+
import type { CacheConfig } from './cache';
|
|
5
|
+
export { compressImage, generateThumbhash, removeBgImage, removeBackground, clearCache, getCacheSize, onLowMemory, configureCache, getCacheDirectory, BackgroundRemovalError, };
|
|
6
|
+
export type { CompressImageOptions, GenerateThumbhashOptions, RemoveBgImageOptions, OutputFormat, BackgroundRemovalErrorCode, CacheConfig, };
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { compressImage, generateThumbhash, removeBgImage, removeBackground, clearCache, getCacheSize, onLowMemory, configureCache, getCacheDirectory, } from './ImageProcessing';
|
|
2
|
+
import { BackgroundRemovalError, } from './errors';
|
|
3
|
+
export {
|
|
4
|
+
// Functions
|
|
5
|
+
compressImage, generateThumbhash, removeBgImage, removeBackground, clearCache, getCacheSize,
|
|
6
|
+
// Memory management
|
|
7
|
+
onLowMemory, configureCache, getCacheDirectory,
|
|
8
|
+
// Errors
|
|
9
|
+
BackgroundRemovalError, };
|
|
File without changes
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
/**
|
|
3
|
+
* Output format for processed images
|
|
4
|
+
*/
|
|
5
|
+
export type OutputFormat = 'PNG' | 'WEBP';
|
|
6
|
+
/**
|
|
7
|
+
* Native options for background removal
|
|
8
|
+
*/
|
|
9
|
+
export interface NativeRemoveBackgroundOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Maximum dimension (width or height) for processing
|
|
12
|
+
* Larger images will be downsampled for better performance
|
|
13
|
+
* Default: 2048
|
|
14
|
+
*/
|
|
15
|
+
maxDimension: number;
|
|
16
|
+
/**
|
|
17
|
+
* Output image format
|
|
18
|
+
* PNG: Lossless, larger file size
|
|
19
|
+
* WEBP: Smaller file size, good quality
|
|
20
|
+
* Default: PNG
|
|
21
|
+
*/
|
|
22
|
+
format: OutputFormat;
|
|
23
|
+
/**
|
|
24
|
+
* Quality for WEBP format (0-100)
|
|
25
|
+
* Ignored for PNG format
|
|
26
|
+
* Default: 100
|
|
27
|
+
*/
|
|
28
|
+
quality: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ImageBackgroundRemover extends HybridObject<{
|
|
31
|
+
ios: 'swift';
|
|
32
|
+
android: 'kotlin';
|
|
33
|
+
}> {
|
|
34
|
+
/**
|
|
35
|
+
* Remove background from an image
|
|
36
|
+
* @param imagePath - File path or file:// URI to the source image
|
|
37
|
+
* @param options - Processing options
|
|
38
|
+
* @returns Promise resolving to the output file path
|
|
39
|
+
*/
|
|
40
|
+
removeBackground(imagePath: string, options: NativeRemoveBackgroundOptions): Promise<string>;
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/nitro.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://nitro.margelo.com/nitro.schema.json",
|
|
3
|
+
"cxxNamespace": ["rnremoveimagebg"],
|
|
4
|
+
"ios": {
|
|
5
|
+
"iosModuleName": "NitroRnRemoveImageBg"
|
|
6
|
+
},
|
|
7
|
+
"android": {
|
|
8
|
+
"androidNamespace": ["rnremoveimagebg"],
|
|
9
|
+
"androidCxxLibName": "NitroRnRemoveImageBg"
|
|
10
|
+
},
|
|
11
|
+
"autolinking": {
|
|
12
|
+
"ImageBackgroundRemover": {
|
|
13
|
+
"kotlin": "HybridImageBackgroundRemover"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"ignorePaths": ["**/node_modules"]
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
** linguist-generated=true
|