rn-remove-image-bg 0.0.11 → 0.0.13
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 +107 -27
- package/android/src/main/java/com/margelo/nitro/rnremoveimagebg/HybridImageBackgroundRemover.kt +76 -30
- package/lib/ImageProcessing.web.d.ts +17 -62
- package/lib/ImageProcessing.web.js +74 -232
- package/package.json +13 -14
- package/src/ImageProcessing.web.ts +90 -296
- package/src/__tests__/ImageProcessing.test.ts +132 -114
|
@@ -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
|
+
});
|