rn-remove-image-bg 0.0.11 → 0.0.12
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
CHANGED
|
@@ -270,10 +270,12 @@ try {
|
|
|
270
270
|
### Android
|
|
271
271
|
|
|
272
272
|
- **Technology**: ML Kit Subject Segmentation (beta)
|
|
273
|
-
- **Model**: Downloads ~10MB on first use
|
|
273
|
+
- **Model**: Downloads ~10MB on first use (handled automatically)
|
|
274
274
|
- **Output**: PNG or WEBP (lossy/lossless based on quality)
|
|
275
275
|
- **Requires**: Google Play Services
|
|
276
276
|
|
|
277
|
+
> **First-time Use**: On Android, the ML Kit model downloads automatically on first use. The library waits for the download to complete (up to ~15 seconds with retries) before processing. Subsequent calls are instant.
|
|
278
|
+
|
|
277
279
|
### Web
|
|
278
280
|
|
|
279
281
|
- **Technology**: @imgly/background-removal (WebAssembly + ONNX)
|
package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt
CHANGED
|
@@ -3,6 +3,9 @@ package com.margelo.nitro.rnremoveimagebg
|
|
|
3
3
|
import android.graphics.Bitmap
|
|
4
4
|
import android.graphics.BitmapFactory
|
|
5
5
|
import android.os.Build
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.Looper
|
|
8
|
+
import android.util.Log
|
|
6
9
|
import com.google.mlkit.vision.common.InputImage
|
|
7
10
|
import com.google.mlkit.vision.segmentation.subject.SubjectSegmentation
|
|
8
11
|
import com.google.mlkit.vision.segmentation.subject.SubjectSegmenterOptions
|
|
@@ -14,6 +17,7 @@ import com.margelo.nitro.NitroModules
|
|
|
14
17
|
import java.io.File
|
|
15
18
|
import java.io.FileOutputStream
|
|
16
19
|
import java.util.UUID
|
|
20
|
+
import kotlin.coroutines.Continuation
|
|
17
21
|
import kotlin.coroutines.resume
|
|
18
22
|
import kotlin.coroutines.resumeWithException
|
|
19
23
|
import kotlin.coroutines.suspendCoroutine
|
|
@@ -25,6 +29,12 @@ class HybridImageBackgroundRemover : HybridImageBackgroundRemoverSpec() {
|
|
|
25
29
|
override val memorySize: Long
|
|
26
30
|
get() = 0L
|
|
27
31
|
|
|
32
|
+
companion object {
|
|
33
|
+
private const val TAG = "RNRemoveImageBg"
|
|
34
|
+
private const val MAX_RETRIES = 10
|
|
35
|
+
private const val BASE_DELAY_MS = 1500L
|
|
36
|
+
}
|
|
37
|
+
|
|
28
38
|
private val segmenter by lazy {
|
|
29
39
|
val options = SubjectSegmenterOptions.Builder()
|
|
30
40
|
.enableForegroundBitmap()
|
|
@@ -55,44 +65,80 @@ class HybridImageBackgroundRemover : HybridImageBackgroundRemoverSpec() {
|
|
|
55
65
|
val inputImage = InputImage.fromBitmap(bitmap, 0)
|
|
56
66
|
|
|
57
67
|
return suspendCoroutine { continuation ->
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
} catch (e: Exception) {
|
|
68
|
+
processWithRetry(inputImage, bitmap, options, continuation, retryCount = 0)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Process image with retry logic for ML Kit model download.
|
|
74
|
+
* ML Kit Subject Segmentation downloads ~10MB model on first use.
|
|
75
|
+
* This method waits and retries if the model is still downloading.
|
|
76
|
+
*/
|
|
77
|
+
private fun processWithRetry(
|
|
78
|
+
inputImage: InputImage,
|
|
79
|
+
bitmap: Bitmap,
|
|
80
|
+
options: NativeRemoveBackgroundOptions,
|
|
81
|
+
continuation: Continuation<String>,
|
|
82
|
+
retryCount: Int
|
|
83
|
+
) {
|
|
84
|
+
segmenter.process(inputImage)
|
|
85
|
+
.addOnSuccessListener { result ->
|
|
86
|
+
val foregroundBitmap = result.foregroundBitmap
|
|
87
|
+
if (foregroundBitmap != null) {
|
|
88
|
+
try {
|
|
89
|
+
val context = NitroModules.applicationContext
|
|
90
|
+
if (context == null) {
|
|
82
91
|
bitmap.recycle()
|
|
83
92
|
foregroundBitmap.recycle()
|
|
84
|
-
continuation.resumeWithException(
|
|
93
|
+
continuation.resumeWithException(Exception("Application Context is null"))
|
|
94
|
+
return@addOnSuccessListener
|
|
85
95
|
}
|
|
86
|
-
|
|
96
|
+
|
|
97
|
+
val outputPath = saveImage(
|
|
98
|
+
foregroundBitmap,
|
|
99
|
+
context.cacheDir,
|
|
100
|
+
options.format,
|
|
101
|
+
options.quality.toInt()
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
bitmap.recycle()
|
|
105
|
+
foregroundBitmap.recycle()
|
|
106
|
+
continuation.resume(outputPath)
|
|
107
|
+
} catch (e: Exception) {
|
|
87
108
|
bitmap.recycle()
|
|
88
|
-
|
|
109
|
+
foregroundBitmap.recycle()
|
|
110
|
+
continuation.resumeWithException(e)
|
|
89
111
|
}
|
|
112
|
+
} else {
|
|
113
|
+
bitmap.recycle()
|
|
114
|
+
continuation.resumeWithException(Exception("Could not generate foreground bitmap"))
|
|
90
115
|
}
|
|
91
|
-
|
|
116
|
+
}
|
|
117
|
+
.addOnFailureListener { e ->
|
|
118
|
+
// Check if error is due to model still downloading
|
|
119
|
+
val isModelDownloading = e.message?.contains("Waiting for", ignoreCase = true) == true ||
|
|
120
|
+
e.message?.contains("downloading", ignoreCase = true) == true ||
|
|
121
|
+
e.message?.contains("not yet available", ignoreCase = true) == true
|
|
122
|
+
|
|
123
|
+
if (isModelDownloading && retryCount < MAX_RETRIES) {
|
|
124
|
+
// Calculate delay with exponential backoff (1.5s, 3s, 4.5s, ...)
|
|
125
|
+
val delayMs = BASE_DELAY_MS * (retryCount + 1)
|
|
126
|
+
Log.d(TAG, "ML Kit model downloading, retry ${retryCount + 1}/$MAX_RETRIES in ${delayMs}ms")
|
|
127
|
+
|
|
128
|
+
Handler(Looper.getMainLooper()).postDelayed({
|
|
129
|
+
processWithRetry(inputImage, bitmap, options, continuation, retryCount + 1)
|
|
130
|
+
}, delayMs)
|
|
131
|
+
} else {
|
|
92
132
|
bitmap.recycle()
|
|
93
|
-
|
|
133
|
+
if (isModelDownloading) {
|
|
134
|
+
continuation.resumeWithException(
|
|
135
|
+
Exception("ML Kit model download timed out. Please check your internet connection and try again.")
|
|
136
|
+
)
|
|
137
|
+
} else {
|
|
138
|
+
continuation.resumeWithException(e)
|
|
139
|
+
}
|
|
94
140
|
}
|
|
95
|
-
|
|
141
|
+
}
|
|
96
142
|
}
|
|
97
143
|
|
|
98
144
|
/**
|
|
@@ -167,9 +167,9 @@ export async function removeBgImage(uri, options = {}) {
|
|
|
167
167
|
}
|
|
168
168
|
catch (error) {
|
|
169
169
|
console.error('[rn-remove-image-bg] Web background removal failed:', error);
|
|
170
|
-
//
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
// Throw error instead of silent failure
|
|
171
|
+
const message = error instanceof Error ? error.message : 'Web background removal failed';
|
|
172
|
+
throw new Error(`Background removal failed: ${message}`);
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
175
|
/**
|
package/package.json
CHANGED
|
@@ -35,8 +35,8 @@ export interface RemoveBgImageOptions {
|
|
|
35
35
|
}
|
|
36
36
|
// Web cache configuration
|
|
37
37
|
const webCacheConfig = {
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
maxEntries: 50,
|
|
39
|
+
maxAgeMinutes: 30,
|
|
40
40
|
};
|
|
41
41
|
|
|
42
42
|
// Simple in-memory LRU cache for web
|
|
@@ -46,33 +46,33 @@ const webCache = new Map<string, string>();
|
|
|
46
46
|
* Add entry to cache with LRU eviction
|
|
47
47
|
*/
|
|
48
48
|
function setCacheEntry(key: string, value: string): void {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
49
|
+
// If key exists, delete it first (to update LRU order)
|
|
50
|
+
if (webCache.has(key)) {
|
|
51
|
+
webCache.delete(key);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Evict oldest entries if at capacity
|
|
55
|
+
while (webCache.size >= webCacheConfig.maxEntries) {
|
|
56
|
+
const oldestKey = webCache.keys().next().value;
|
|
57
|
+
if (oldestKey) {
|
|
58
|
+
webCache.delete(oldestKey);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
webCache.set(key, value);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
66
|
* Get entry from cache and update LRU order
|
|
67
67
|
*/
|
|
68
68
|
function getCacheEntry(key: string): string | undefined {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
const value = webCache.get(key);
|
|
70
|
+
if (value !== undefined) {
|
|
71
|
+
// Move to end (most recently used)
|
|
72
|
+
webCache.delete(key);
|
|
73
|
+
webCache.set(key, value);
|
|
74
|
+
}
|
|
75
|
+
return value;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
@@ -209,8 +209,9 @@ export async function removeBgImage(
|
|
|
209
209
|
|
|
210
210
|
try {
|
|
211
211
|
// Dynamically import the library to prevent Metro from parsing onnxruntime-web at build time
|
|
212
|
-
const { removeBackground: imglyRemoveBackground } =
|
|
213
|
-
|
|
212
|
+
const { removeBackground: imglyRemoveBackground } =
|
|
213
|
+
await import('@imgly/background-removal');
|
|
214
|
+
|
|
214
215
|
// Call @imgly/background-removal
|
|
215
216
|
const blob = await imglyRemoveBackground(uri, {
|
|
216
217
|
progress: (key: string, current: number, total: number) => {
|
|
@@ -247,9 +248,9 @@ export async function removeBgImage(
|
|
|
247
248
|
return dataUrl;
|
|
248
249
|
} catch (error) {
|
|
249
250
|
console.error('[rn-remove-image-bg] Web background removal failed:', error);
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
// Throw error instead of silent failure
|
|
252
|
+
const message = error instanceof Error ? error.message : 'Web background removal failed';
|
|
253
|
+
throw new Error(`Background removal failed: ${message}`);
|
|
253
254
|
}
|
|
254
255
|
}
|
|
255
256
|
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
|
|
3
3
|
// Mock all dependencies before importing
|
|
4
4
|
vi.mock('react-native', () => ({
|
|
5
5
|
Image: {
|
|
6
6
|
getSize: vi.fn((uri, success) => success(1024, 768)),
|
|
7
7
|
},
|
|
8
|
-
}))
|
|
8
|
+
}));
|
|
9
9
|
|
|
10
10
|
vi.mock('expo-image-manipulator', () => ({
|
|
11
|
-
manipulateAsync: vi
|
|
11
|
+
manipulateAsync: vi
|
|
12
|
+
.fn()
|
|
13
|
+
.mockResolvedValue({ uri: 'file:///mock/result.png' }),
|
|
12
14
|
SaveFormat: { WEBP: 'webp', PNG: 'png', JPEG: 'jpeg' },
|
|
13
|
-
}))
|
|
15
|
+
}));
|
|
14
16
|
|
|
15
17
|
vi.mock('expo-file-system/legacy', () => ({
|
|
16
18
|
cacheDirectory: '/mock/cache/',
|
|
@@ -19,26 +21,28 @@ vi.mock('expo-file-system/legacy', () => ({
|
|
|
19
21
|
readAsStringAsync: vi.fn().mockResolvedValue('{}'),
|
|
20
22
|
writeAsStringAsync: vi.fn().mockResolvedValue(undefined),
|
|
21
23
|
deleteAsync: vi.fn().mockResolvedValue(undefined),
|
|
22
|
-
}))
|
|
24
|
+
}));
|
|
23
25
|
|
|
24
26
|
vi.mock('thumbhash', () => ({
|
|
25
27
|
rgbaToThumbHash: vi.fn().mockReturnValue(new Uint8Array([1, 2, 3, 4])),
|
|
26
|
-
}))
|
|
28
|
+
}));
|
|
27
29
|
|
|
28
30
|
vi.mock('upng-js', () => ({
|
|
29
31
|
decode: vi.fn().mockReturnValue({ width: 32, height: 32 }),
|
|
30
32
|
toRGBA8: vi.fn().mockReturnValue([new Uint8Array(32 * 32 * 4)]),
|
|
31
|
-
}))
|
|
33
|
+
}));
|
|
32
34
|
|
|
33
35
|
// Mock Nitro modules
|
|
34
|
-
const mockRemoveBackground = vi
|
|
36
|
+
const mockRemoveBackground = vi
|
|
37
|
+
.fn()
|
|
38
|
+
.mockResolvedValue('file:///mock/bg_removed.png');
|
|
35
39
|
vi.mock('react-native-nitro-modules', () => ({
|
|
36
40
|
NitroModules: {
|
|
37
41
|
createHybridObject: vi.fn(() => ({
|
|
38
42
|
removeBackground: mockRemoveBackground,
|
|
39
43
|
})),
|
|
40
44
|
},
|
|
41
|
-
}))
|
|
45
|
+
}));
|
|
42
46
|
|
|
43
47
|
// Import after mocking
|
|
44
48
|
import {
|
|
@@ -48,53 +52,61 @@ import {
|
|
|
48
52
|
onLowMemory,
|
|
49
53
|
configureCache,
|
|
50
54
|
getCacheDirectory,
|
|
51
|
-
} from '../ImageProcessing'
|
|
52
|
-
import { BackgroundRemovalError } from '../errors'
|
|
55
|
+
} from '../ImageProcessing';
|
|
56
|
+
import { BackgroundRemovalError } from '../errors';
|
|
53
57
|
|
|
54
58
|
describe('ImageProcessing', () => {
|
|
55
59
|
beforeEach(() => {
|
|
56
|
-
vi.clearAllMocks()
|
|
57
|
-
})
|
|
60
|
+
vi.clearAllMocks();
|
|
61
|
+
});
|
|
58
62
|
|
|
59
63
|
afterEach(async () => {
|
|
60
|
-
await clearCache()
|
|
61
|
-
})
|
|
64
|
+
await clearCache();
|
|
65
|
+
});
|
|
62
66
|
|
|
63
67
|
describe('removeBgImage', () => {
|
|
64
68
|
describe('input validation', () => {
|
|
65
69
|
it('should throw INVALID_PATH for empty string', async () => {
|
|
66
|
-
await expect(removeBgImage('')).rejects.toThrow(BackgroundRemovalError)
|
|
70
|
+
await expect(removeBgImage('')).rejects.toThrow(BackgroundRemovalError);
|
|
67
71
|
await expect(removeBgImage('')).rejects.toMatchObject({
|
|
68
72
|
code: 'INVALID_PATH',
|
|
69
|
-
})
|
|
70
|
-
})
|
|
73
|
+
});
|
|
74
|
+
});
|
|
71
75
|
|
|
72
76
|
it('should throw INVALID_PATH for whitespace-only string', async () => {
|
|
73
77
|
await expect(removeBgImage(' ')).rejects.toMatchObject({
|
|
74
78
|
code: 'INVALID_PATH',
|
|
75
|
-
})
|
|
76
|
-
})
|
|
79
|
+
});
|
|
80
|
+
});
|
|
77
81
|
|
|
78
82
|
it('should throw INVALID_PATH for http URLs', async () => {
|
|
79
|
-
await expect(
|
|
83
|
+
await expect(
|
|
84
|
+
removeBgImage('http://example.com/image.jpg')
|
|
85
|
+
).rejects.toMatchObject({
|
|
80
86
|
code: 'INVALID_PATH',
|
|
81
|
-
})
|
|
82
|
-
})
|
|
87
|
+
});
|
|
88
|
+
});
|
|
83
89
|
|
|
84
90
|
it('should throw INVALID_PATH for https URLs', async () => {
|
|
85
|
-
await expect(
|
|
91
|
+
await expect(
|
|
92
|
+
removeBgImage('https://example.com/image.jpg')
|
|
93
|
+
).rejects.toMatchObject({
|
|
86
94
|
code: 'INVALID_PATH',
|
|
87
|
-
})
|
|
88
|
-
})
|
|
95
|
+
});
|
|
96
|
+
});
|
|
89
97
|
|
|
90
98
|
it('should accept file:// URIs', async () => {
|
|
91
|
-
await expect(
|
|
92
|
-
|
|
99
|
+
await expect(
|
|
100
|
+
removeBgImage('file:///path/to/image.jpg')
|
|
101
|
+
).resolves.toBeDefined();
|
|
102
|
+
});
|
|
93
103
|
|
|
94
104
|
it('should accept absolute paths starting with /', async () => {
|
|
95
|
-
await expect(
|
|
96
|
-
|
|
97
|
-
|
|
105
|
+
await expect(
|
|
106
|
+
removeBgImage('/path/to/image.jpg')
|
|
107
|
+
).resolves.toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
98
110
|
|
|
99
111
|
describe('options validation', () => {
|
|
100
112
|
it('should throw INVALID_OPTIONS for maxDimension < 100', async () => {
|
|
@@ -102,40 +114,40 @@ describe('ImageProcessing', () => {
|
|
|
102
114
|
removeBgImage('file:///test.jpg', { maxDimension: 50 })
|
|
103
115
|
).rejects.toMatchObject({
|
|
104
116
|
code: 'INVALID_OPTIONS',
|
|
105
|
-
})
|
|
106
|
-
})
|
|
117
|
+
});
|
|
118
|
+
});
|
|
107
119
|
|
|
108
120
|
it('should throw INVALID_OPTIONS for maxDimension > 8192', async () => {
|
|
109
121
|
await expect(
|
|
110
122
|
removeBgImage('file:///test.jpg', { maxDimension: 10000 })
|
|
111
123
|
).rejects.toMatchObject({
|
|
112
124
|
code: 'INVALID_OPTIONS',
|
|
113
|
-
})
|
|
114
|
-
})
|
|
125
|
+
});
|
|
126
|
+
});
|
|
115
127
|
|
|
116
128
|
it('should throw INVALID_OPTIONS for quality < 0', async () => {
|
|
117
129
|
await expect(
|
|
118
130
|
removeBgImage('file:///test.jpg', { quality: -10 })
|
|
119
131
|
).rejects.toMatchObject({
|
|
120
132
|
code: 'INVALID_OPTIONS',
|
|
121
|
-
})
|
|
122
|
-
})
|
|
133
|
+
});
|
|
134
|
+
});
|
|
123
135
|
|
|
124
136
|
it('should throw INVALID_OPTIONS for quality > 100', async () => {
|
|
125
137
|
await expect(
|
|
126
138
|
removeBgImage('file:///test.jpg', { quality: 150 })
|
|
127
139
|
).rejects.toMatchObject({
|
|
128
140
|
code: 'INVALID_OPTIONS',
|
|
129
|
-
})
|
|
130
|
-
})
|
|
141
|
+
});
|
|
142
|
+
});
|
|
131
143
|
|
|
132
144
|
it('should throw INVALID_OPTIONS for invalid format', async () => {
|
|
133
145
|
await expect(
|
|
134
146
|
removeBgImage('file:///test.jpg', { format: 'JPEG' as any })
|
|
135
147
|
).rejects.toMatchObject({
|
|
136
148
|
code: 'INVALID_OPTIONS',
|
|
137
|
-
})
|
|
138
|
-
})
|
|
149
|
+
});
|
|
150
|
+
});
|
|
139
151
|
|
|
140
152
|
it('should accept valid options', async () => {
|
|
141
153
|
await expect(
|
|
@@ -144,45 +156,49 @@ describe('ImageProcessing', () => {
|
|
|
144
156
|
quality: 90,
|
|
145
157
|
format: 'WEBP',
|
|
146
158
|
})
|
|
147
|
-
).resolves.toBeDefined()
|
|
148
|
-
})
|
|
149
|
-
})
|
|
159
|
+
).resolves.toBeDefined();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
150
162
|
|
|
151
163
|
describe('progress callback', () => {
|
|
152
164
|
it('should call onProgress during processing', async () => {
|
|
153
|
-
const onProgress = vi.fn()
|
|
154
|
-
await removeBgImage('file:///test.jpg', { onProgress })
|
|
165
|
+
const onProgress = vi.fn();
|
|
166
|
+
await removeBgImage('file:///test.jpg', { onProgress });
|
|
155
167
|
|
|
156
168
|
// Should be called at least for start and end
|
|
157
|
-
expect(onProgress).toHaveBeenCalled()
|
|
158
|
-
expect(onProgress).toHaveBeenCalledWith(expect.any(Number))
|
|
159
|
-
})
|
|
160
|
-
})
|
|
169
|
+
expect(onProgress).toHaveBeenCalled();
|
|
170
|
+
expect(onProgress).toHaveBeenCalledWith(expect.any(Number));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
161
173
|
|
|
162
174
|
describe('caching', () => {
|
|
163
175
|
it('should cache results when useCache is true', async () => {
|
|
164
|
-
await removeBgImage('file:///test.jpg', { useCache: true })
|
|
165
|
-
expect(getCacheSize()).toBe(1)
|
|
166
|
-
})
|
|
176
|
+
await removeBgImage('file:///test.jpg', { useCache: true });
|
|
177
|
+
expect(getCacheSize()).toBe(1);
|
|
178
|
+
});
|
|
167
179
|
|
|
168
180
|
it('should not cache results when useCache is false', async () => {
|
|
169
|
-
await removeBgImage('file:///test.jpg', { useCache: false })
|
|
170
|
-
expect(getCacheSize()).toBe(0)
|
|
171
|
-
})
|
|
181
|
+
await removeBgImage('file:///test.jpg', { useCache: false });
|
|
182
|
+
expect(getCacheSize()).toBe(0);
|
|
183
|
+
});
|
|
172
184
|
|
|
173
185
|
it('should return cached result on second call', async () => {
|
|
174
|
-
const result1 = await removeBgImage('file:///test.jpg', {
|
|
175
|
-
|
|
186
|
+
const result1 = await removeBgImage('file:///test.jpg', {
|
|
187
|
+
useCache: true,
|
|
188
|
+
});
|
|
189
|
+
|
|
176
190
|
// Reset mock to verify it's not called again
|
|
177
|
-
mockRemoveBackground.mockClear()
|
|
178
|
-
|
|
179
|
-
const result2 = await removeBgImage('file:///test.jpg', {
|
|
180
|
-
|
|
181
|
-
|
|
191
|
+
mockRemoveBackground.mockClear();
|
|
192
|
+
|
|
193
|
+
const result2 = await removeBgImage('file:///test.jpg', {
|
|
194
|
+
useCache: true,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(result1).toBe(result2);
|
|
182
198
|
// Native should not be called on second request (cache hit)
|
|
183
|
-
expect(mockRemoveBackground).not.toHaveBeenCalled()
|
|
184
|
-
})
|
|
185
|
-
})
|
|
199
|
+
expect(mockRemoveBackground).not.toHaveBeenCalled();
|
|
200
|
+
});
|
|
201
|
+
});
|
|
186
202
|
|
|
187
203
|
describe('native call', () => {
|
|
188
204
|
it('should call native removeBackground with correct options', async () => {
|
|
@@ -191,7 +207,7 @@ describe('ImageProcessing', () => {
|
|
|
191
207
|
format: 'WEBP',
|
|
192
208
|
quality: 85,
|
|
193
209
|
useCache: false, // Don't cache so we can verify the call
|
|
194
|
-
})
|
|
210
|
+
});
|
|
195
211
|
|
|
196
212
|
expect(mockRemoveBackground).toHaveBeenCalledWith(
|
|
197
213
|
'file:///test.jpg',
|
|
@@ -200,61 +216,63 @@ describe('ImageProcessing', () => {
|
|
|
200
216
|
format: 'WEBP',
|
|
201
217
|
quality: 85,
|
|
202
218
|
})
|
|
203
|
-
)
|
|
204
|
-
})
|
|
219
|
+
);
|
|
220
|
+
});
|
|
205
221
|
|
|
206
222
|
it('should normalize result path to file:// URI', async () => {
|
|
207
|
-
mockRemoveBackground.mockResolvedValueOnce('/path/without/scheme.png')
|
|
208
|
-
|
|
209
|
-
const result = await removeBgImage('file:///test.jpg', {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
223
|
+
mockRemoveBackground.mockResolvedValueOnce('/path/without/scheme.png');
|
|
224
|
+
|
|
225
|
+
const result = await removeBgImage('file:///test.jpg', {
|
|
226
|
+
useCache: false,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
expect(result).toBe('file:///path/without/scheme.png');
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
});
|
|
215
233
|
|
|
216
234
|
describe('cache management functions', () => {
|
|
217
235
|
describe('clearCache', () => {
|
|
218
236
|
it('should clear all cache entries', async () => {
|
|
219
|
-
await removeBgImage('file:///test1.jpg', { useCache: true })
|
|
220
|
-
await removeBgImage('file:///test2.jpg', { useCache: true })
|
|
221
|
-
expect(getCacheSize()).toBe(2)
|
|
237
|
+
await removeBgImage('file:///test1.jpg', { useCache: true });
|
|
238
|
+
await removeBgImage('file:///test2.jpg', { useCache: true });
|
|
239
|
+
expect(getCacheSize()).toBe(2);
|
|
222
240
|
|
|
223
|
-
await clearCache()
|
|
224
|
-
expect(getCacheSize()).toBe(0)
|
|
225
|
-
})
|
|
226
|
-
})
|
|
241
|
+
await clearCache();
|
|
242
|
+
expect(getCacheSize()).toBe(0);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
227
245
|
|
|
228
246
|
describe('getCacheSize', () => {
|
|
229
247
|
it('should return 0 for empty cache', () => {
|
|
230
|
-
expect(getCacheSize()).toBe(0)
|
|
231
|
-
})
|
|
248
|
+
expect(getCacheSize()).toBe(0);
|
|
249
|
+
});
|
|
232
250
|
|
|
233
251
|
it('should return correct count after adding entries', async () => {
|
|
234
|
-
await removeBgImage('file:///test1.jpg', { useCache: true })
|
|
235
|
-
expect(getCacheSize()).toBe(1)
|
|
236
|
-
|
|
237
|
-
await removeBgImage('file:///test2.jpg', { useCache: true })
|
|
238
|
-
expect(getCacheSize()).toBe(2)
|
|
239
|
-
})
|
|
240
|
-
})
|
|
252
|
+
await removeBgImage('file:///test1.jpg', { useCache: true });
|
|
253
|
+
expect(getCacheSize()).toBe(1);
|
|
254
|
+
|
|
255
|
+
await removeBgImage('file:///test2.jpg', { useCache: true });
|
|
256
|
+
expect(getCacheSize()).toBe(2);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
241
259
|
|
|
242
260
|
describe('onLowMemory', () => {
|
|
243
261
|
it('should clear cache and return count', async () => {
|
|
244
|
-
await removeBgImage('file:///test1.jpg', { useCache: true })
|
|
245
|
-
await removeBgImage('file:///test2.jpg', { useCache: true })
|
|
246
|
-
|
|
247
|
-
const cleared = await onLowMemory()
|
|
248
|
-
|
|
249
|
-
expect(cleared).toBe(2)
|
|
250
|
-
expect(getCacheSize()).toBe(0)
|
|
251
|
-
})
|
|
262
|
+
await removeBgImage('file:///test1.jpg', { useCache: true });
|
|
263
|
+
await removeBgImage('file:///test2.jpg', { useCache: true });
|
|
264
|
+
|
|
265
|
+
const cleared = await onLowMemory();
|
|
266
|
+
|
|
267
|
+
expect(cleared).toBe(2);
|
|
268
|
+
expect(getCacheSize()).toBe(0);
|
|
269
|
+
});
|
|
252
270
|
|
|
253
271
|
it('should return 0 when cache is empty', async () => {
|
|
254
|
-
const cleared = await onLowMemory()
|
|
255
|
-
expect(cleared).toBe(0)
|
|
256
|
-
})
|
|
257
|
-
})
|
|
272
|
+
const cleared = await onLowMemory();
|
|
273
|
+
expect(cleared).toBe(0);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
258
276
|
|
|
259
277
|
describe('configureCache', () => {
|
|
260
278
|
it('should not throw when configuring cache', () => {
|
|
@@ -264,15 +282,15 @@ describe('ImageProcessing', () => {
|
|
|
264
282
|
maxAgeMinutes: 60,
|
|
265
283
|
persistToDisk: true,
|
|
266
284
|
})
|
|
267
|
-
).not.toThrow()
|
|
268
|
-
})
|
|
269
|
-
})
|
|
285
|
+
).not.toThrow();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
270
288
|
|
|
271
289
|
describe('getCacheDirectory', () => {
|
|
272
290
|
it('should return a string containing bg-removal', () => {
|
|
273
|
-
const dir = getCacheDirectory()
|
|
274
|
-
expect(dir).toContain('bg-removal')
|
|
275
|
-
})
|
|
276
|
-
})
|
|
277
|
-
})
|
|
278
|
-
})
|
|
291
|
+
const dir = getCacheDirectory();
|
|
292
|
+
expect(dir).toContain('bg-removal');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
});
|