rn-remove-image-bg 0.0.10 → 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)
@@ -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
- segmenter.process(inputImage)
59
- .addOnSuccessListener { result ->
60
- val foregroundBitmap = result.foregroundBitmap
61
- if (foregroundBitmap != null) {
62
- try {
63
- val context = NitroModules.applicationContext
64
- if (context == null) {
65
- bitmap.recycle()
66
- foregroundBitmap.recycle()
67
- continuation.resumeWithException(Exception("Application Context is null"))
68
- return@addOnSuccessListener
69
- }
70
-
71
- val outputPath = saveImage(
72
- foregroundBitmap,
73
- context.cacheDir,
74
- options.format,
75
- options.quality.toInt()
76
- )
77
-
78
- bitmap.recycle()
79
- foregroundBitmap.recycle()
80
- continuation.resume(outputPath)
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(e)
93
+ continuation.resumeWithException(Exception("Application Context is null"))
94
+ return@addOnSuccessListener
85
95
  }
86
- } else {
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
- continuation.resumeWithException(Exception("Could not generate foreground bitmap"))
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
- .addOnFailureListener { e ->
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
- continuation.resumeWithException(e)
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
  /**
@@ -4,7 +4,6 @@
4
4
  * Provides real background removal on web using WebAssembly and ML models.
5
5
  * Falls back to no-op if the library fails to load.
6
6
  */
7
- import { removeBackground as imglyRemoveBackground } from '@imgly/background-removal';
8
7
  // Web cache configuration
9
8
  const webCacheConfig = {
10
9
  maxEntries: 50,
@@ -134,6 +133,8 @@ export async function removeBgImage(uri, options = {}) {
134
133
  }
135
134
  onProgress?.(5);
136
135
  try {
136
+ // Dynamically import the library to prevent Metro from parsing onnxruntime-web at build time
137
+ const { removeBackground: imglyRemoveBackground } = await import('@imgly/background-removal');
137
138
  // Call @imgly/background-removal
138
139
  const blob = await imglyRemoveBackground(uri, {
139
140
  progress: (key, current, total) => {
@@ -166,9 +167,9 @@ export async function removeBgImage(uri, options = {}) {
166
167
  }
167
168
  catch (error) {
168
169
  console.error('[rn-remove-image-bg] Web background removal failed:', error);
169
- // Return original URI on failure
170
- onProgress?.(100);
171
- return uri;
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}`);
172
173
  }
173
174
  }
174
175
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-remove-image-bg",
3
- "version": "0.0.10",
3
+ "version": "0.0.12",
4
4
  "description": "rn-remove-image-bg",
5
5
  "homepage": "https://github.com/a-eid/rn-remove-image-bg",
6
6
  "main": "lib/index",
@@ -5,7 +5,8 @@
5
5
  * Falls back to no-op if the library fails to load.
6
6
  */
7
7
 
8
- import { removeBackground as imglyRemoveBackground } from '@imgly/background-removal';
8
+ // @imgly/background-removal is dynamically imported at runtime in removeBgImage()
9
+ // to prevent Metro from parsing onnxruntime-web at build time
9
10
 
10
11
  /**
11
12
  * Output format for processed images
@@ -34,8 +35,8 @@ export interface RemoveBgImageOptions {
34
35
  }
35
36
  // Web cache configuration
36
37
  const webCacheConfig = {
37
- maxEntries: 50,
38
- maxAgeMinutes: 30,
38
+ maxEntries: 50,
39
+ maxAgeMinutes: 30,
39
40
  };
40
41
 
41
42
  // Simple in-memory LRU cache for web
@@ -45,33 +46,33 @@ const webCache = new Map<string, string>();
45
46
  * Add entry to cache with LRU eviction
46
47
  */
47
48
  function setCacheEntry(key: string, value: string): void {
48
- // If key exists, delete it first (to update LRU order)
49
- if (webCache.has(key)) {
50
- webCache.delete(key);
51
- }
52
-
53
- // Evict oldest entries if at capacity
54
- while (webCache.size >= webCacheConfig.maxEntries) {
55
- const oldestKey = webCache.keys().next().value;
56
- if (oldestKey) {
57
- webCache.delete(oldestKey);
58
- }
59
- }
60
-
61
- webCache.set(key, value);
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);
62
63
  }
63
64
 
64
65
  /**
65
66
  * Get entry from cache and update LRU order
66
67
  */
67
68
  function getCacheEntry(key: string): string | undefined {
68
- const value = webCache.get(key);
69
- if (value !== undefined) {
70
- // Move to end (most recently used)
71
- webCache.delete(key);
72
- webCache.set(key, value);
73
- }
74
- return value;
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;
75
76
  }
76
77
 
77
78
  /**
@@ -207,6 +208,10 @@ export async function removeBgImage(
207
208
  onProgress?.(5);
208
209
 
209
210
  try {
211
+ // Dynamically import the library to prevent Metro from parsing onnxruntime-web at build time
212
+ const { removeBackground: imglyRemoveBackground } =
213
+ await import('@imgly/background-removal');
214
+
210
215
  // Call @imgly/background-removal
211
216
  const blob = await imglyRemoveBackground(uri, {
212
217
  progress: (key: string, current: number, total: number) => {
@@ -243,9 +248,9 @@ export async function removeBgImage(
243
248
  return dataUrl;
244
249
  } catch (error) {
245
250
  console.error('[rn-remove-image-bg] Web background removal failed:', error);
246
- // Return original URI on failure
247
- onProgress?.(100);
248
- return uri;
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}`);
249
254
  }
250
255
  }
251
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.fn().mockResolvedValue({ uri: 'file:///mock/result.png' }),
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.fn().mockResolvedValue('file:///mock/bg_removed.png')
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(removeBgImage('http://example.com/image.jpg')).rejects.toMatchObject({
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(removeBgImage('https://example.com/image.jpg')).rejects.toMatchObject({
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(removeBgImage('file:///path/to/image.jpg')).resolves.toBeDefined()
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(removeBgImage('/path/to/image.jpg')).resolves.toBeDefined()
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', { useCache: true })
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', { useCache: true })
180
-
181
- expect(result1).toBe(result2)
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', { useCache: false })
210
-
211
- expect(result).toBe('file:///path/without/scheme.png')
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
+ });