@resizebox/background-remover 1.0.0
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/LICENSE +21 -0
- package/README.md +149 -0
- package/dist/index.cjs +381 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +337 -0
- package/dist/index.js.map +1 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ResizeBox.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# @resizebox/background-remover
|
|
2
|
+
|
|
3
|
+
Browser-first AI background removal for JavaScript and TypeScript.
|
|
4
|
+
|
|
5
|
+
Built by [ResizeBox.com](https://resizebox.com/remove-background), a privacy-focused image toolkit that processes images locally on the user's device.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @resizebox/background-remover
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { removeBackground } from '@resizebox/background-remover';
|
|
17
|
+
|
|
18
|
+
const input = fileInput.files?.[0];
|
|
19
|
+
if (!input) throw new Error('Select an image first');
|
|
20
|
+
|
|
21
|
+
const result = await removeBackground(input, {
|
|
22
|
+
onProgress(progress) {
|
|
23
|
+
if (progress.phase === 'downloading-model') {
|
|
24
|
+
console.log(`Downloading AI model: ${progress.progress}%`);
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const url = URL.createObjectURL(result.blob);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The first run downloads the AI model from Hugging Face and can take longer. The browser cache is used for later runs.
|
|
33
|
+
|
|
34
|
+
## Background colors
|
|
35
|
+
|
|
36
|
+
Transparent PNG is the default output. You can also composite the cutout onto a solid six-digit hex color:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const whiteBackground = await removeBackground(file, {
|
|
40
|
+
background: '#ffffff',
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Preload the model
|
|
45
|
+
|
|
46
|
+
Use `preloadBackgroundRemovalModel()` when you want to download and initialize the model before the user's first image is processed.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import { preloadBackgroundRemovalModel } from '@resizebox/background-remover';
|
|
50
|
+
|
|
51
|
+
await preloadBackgroundRemovalModel({
|
|
52
|
+
onProgress(progress) {
|
|
53
|
+
console.log(progress.phase, progress.progress);
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Backend selection
|
|
59
|
+
|
|
60
|
+
The default device is `auto`. When WebGPU is available, the package tries WebGPU first and falls back to WebAssembly if model loading or inference fails.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
await removeBackground(file, { device: 'webgpu' });
|
|
64
|
+
await removeBackground(file, { device: 'wasm' });
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Supported device values:
|
|
68
|
+
|
|
69
|
+
- `auto`
|
|
70
|
+
- `webgpu`
|
|
71
|
+
- `wasm`
|
|
72
|
+
|
|
73
|
+
## Progress states
|
|
74
|
+
|
|
75
|
+
The `onProgress` callback can receive:
|
|
76
|
+
|
|
77
|
+
- `loading-model`
|
|
78
|
+
- `downloading-model`
|
|
79
|
+
- `initializing`
|
|
80
|
+
- `fallback`
|
|
81
|
+
- `ready`
|
|
82
|
+
- `processing`
|
|
83
|
+
|
|
84
|
+
`downloading-model` includes a `progress` percentage when the model host provides it.
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
### `removeBackground(input, options?)`
|
|
89
|
+
|
|
90
|
+
Accepts a JPEG, PNG, or WebP `Blob` or `File` and returns a PNG `Blob` plus output metadata.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
type RemoveBackgroundOptions = {
|
|
94
|
+
device?: 'auto' | 'webgpu' | 'wasm';
|
|
95
|
+
background?: 'transparent' | string;
|
|
96
|
+
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
|
97
|
+
};
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
The package validates MIME type, file signature, file size, image decoding, and decoded pixel count before model processing. Current limits are exported through `backgroundRemoverConfig`.
|
|
101
|
+
|
|
102
|
+
### `preloadBackgroundRemovalModel(options?)`
|
|
103
|
+
|
|
104
|
+
Downloads and initializes the model without processing an image. Returns the selected backend.
|
|
105
|
+
|
|
106
|
+
### `resetBackgroundRemovalModel()`
|
|
107
|
+
|
|
108
|
+
Clears the in-memory model instance so a later call can load it again. Browser-cached model data is not deleted.
|
|
109
|
+
|
|
110
|
+
### `BackgroundRemoverError`
|
|
111
|
+
|
|
112
|
+
Validation, model loading, processing, canvas, and encoding failures throw `BackgroundRemoverError`. Use its `code` property for application-specific messages.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
import {
|
|
116
|
+
BackgroundRemoverError,
|
|
117
|
+
removeBackground,
|
|
118
|
+
} from '@resizebox/background-remover';
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
await removeBackground(file);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof BackgroundRemoverError) {
|
|
124
|
+
console.error(error.code);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Browser requirements
|
|
130
|
+
|
|
131
|
+
The package is intended for modern browsers and uses browser APIs such as:
|
|
132
|
+
|
|
133
|
+
- `Blob`
|
|
134
|
+
- `createImageBitmap`
|
|
135
|
+
- Canvas 2D or `OffscreenCanvas`
|
|
136
|
+
- WebAssembly
|
|
137
|
+
- WebGPU when available
|
|
138
|
+
|
|
139
|
+
The AI model is not bundled into this npm package. Model data is downloaded from `onnx-community/ormbg-ONNX` on Hugging Face on first use.
|
|
140
|
+
|
|
141
|
+
## Privacy
|
|
142
|
+
|
|
143
|
+
Selected images are processed locally on the user's device. Image content is not uploaded to ResizeBox or Hugging Face. Hugging Face is used only to download AI model data.
|
|
144
|
+
|
|
145
|
+
For the interactive tool, visit [ResizeBox.com](https://resizebox.com/remove-background).
|
|
146
|
+
|
|
147
|
+
## License
|
|
148
|
+
|
|
149
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
BackgroundRemoverError: () => BackgroundRemoverError,
|
|
34
|
+
backgroundRemoverConfig: () => backgroundRemoverConfig,
|
|
35
|
+
matchesImageSignature: () => matchesImageSignature,
|
|
36
|
+
normalizeBackground: () => normalizeBackground,
|
|
37
|
+
preloadBackgroundRemovalModel: () => preloadBackgroundRemovalModel,
|
|
38
|
+
removeBackground: () => removeBackground,
|
|
39
|
+
resetBackgroundRemovalModel: () => resetBackgroundRemovalModel,
|
|
40
|
+
validateInputImage: () => validateInputImage
|
|
41
|
+
});
|
|
42
|
+
module.exports = __toCommonJS(index_exports);
|
|
43
|
+
|
|
44
|
+
// src/config.ts
|
|
45
|
+
var backgroundRemoverConfig = {
|
|
46
|
+
model: {
|
|
47
|
+
id: "onnx-community/ormbg-ONNX",
|
|
48
|
+
dtype: "q8",
|
|
49
|
+
defaultDevice: "auto"
|
|
50
|
+
},
|
|
51
|
+
input: {
|
|
52
|
+
maxFileSizeBytes: 10 * 1024 * 1024,
|
|
53
|
+
maxMegapixels: 36,
|
|
54
|
+
supportedMimeTypes: ["image/jpeg", "image/png", "image/webp"]
|
|
55
|
+
},
|
|
56
|
+
output: {
|
|
57
|
+
mimeType: "image/png",
|
|
58
|
+
defaultBackground: "transparent"
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/errors.ts
|
|
63
|
+
var BackgroundRemoverError = class extends Error {
|
|
64
|
+
constructor(code, message, options) {
|
|
65
|
+
super(message, options);
|
|
66
|
+
this.code = code;
|
|
67
|
+
this.name = "BackgroundRemoverError";
|
|
68
|
+
}
|
|
69
|
+
code;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// src/model.ts
|
|
73
|
+
var modelPromises = /* @__PURE__ */ new Map();
|
|
74
|
+
var progressListeners = /* @__PURE__ */ new Map();
|
|
75
|
+
var transformersPromise = null;
|
|
76
|
+
async function preloadBackgroundRemovalModel(options = {}) {
|
|
77
|
+
const loaded = await resolveModel(options.device, options.onProgress);
|
|
78
|
+
return loaded.backend;
|
|
79
|
+
}
|
|
80
|
+
function resetBackgroundRemovalModel() {
|
|
81
|
+
modelPromises.clear();
|
|
82
|
+
}
|
|
83
|
+
async function runBackgroundRemovalModel(input, options = {}) {
|
|
84
|
+
const device = options.device ?? backgroundRemoverConfig.model.defaultDevice;
|
|
85
|
+
const loaded = await resolveModel(device, options.onProgress);
|
|
86
|
+
options.onProgress?.({ phase: "processing", backend: loaded.backend });
|
|
87
|
+
try {
|
|
88
|
+
return { output: await loaded.runner(input), backend: loaded.backend };
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (device === "auto" && loaded.backend === "webgpu") {
|
|
91
|
+
modelPromises.delete("webgpu");
|
|
92
|
+
options.onProgress?.({ phase: "fallback", backend: "wasm" });
|
|
93
|
+
const fallback = await resolveModel("wasm", options.onProgress);
|
|
94
|
+
options.onProgress?.({ phase: "processing", backend: "wasm" });
|
|
95
|
+
try {
|
|
96
|
+
return { output: await fallback.runner(input), backend: "wasm" };
|
|
97
|
+
} catch (fallbackError) {
|
|
98
|
+
throw new BackgroundRemoverError(
|
|
99
|
+
"processing-failed",
|
|
100
|
+
"Background removal failed.",
|
|
101
|
+
{ cause: fallbackError }
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw new BackgroundRemoverError("processing-failed", "Background removal failed.", {
|
|
106
|
+
cause: error
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function resolveModel(device, onProgress) {
|
|
111
|
+
const requested = device ?? backgroundRemoverConfig.model.defaultDevice;
|
|
112
|
+
if (requested === "auto") {
|
|
113
|
+
if (supportsWebGpu()) {
|
|
114
|
+
try {
|
|
115
|
+
return { backend: "webgpu", runner: await getModel("webgpu", onProgress) };
|
|
116
|
+
} catch {
|
|
117
|
+
onProgress?.({ phase: "fallback", backend: "wasm" });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { backend: "wasm", runner: await getModel("wasm", onProgress) };
|
|
121
|
+
}
|
|
122
|
+
return { backend: requested, runner: await getModel(requested, onProgress) };
|
|
123
|
+
}
|
|
124
|
+
async function getModel(backend, onProgress) {
|
|
125
|
+
const listener = onProgress ?? void 0;
|
|
126
|
+
if (listener) {
|
|
127
|
+
getProgressListeners(backend).add(listener);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
if (!modelPromises.has(backend)) {
|
|
131
|
+
modelPromises.set(backend, createModel(backend));
|
|
132
|
+
}
|
|
133
|
+
return await modelPromises.get(backend);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
modelPromises.delete(backend);
|
|
136
|
+
if (error instanceof BackgroundRemoverError) {
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
throw new BackgroundRemoverError("model-load-failed", "The AI model could not be loaded.", {
|
|
140
|
+
cause: error
|
|
141
|
+
});
|
|
142
|
+
} finally {
|
|
143
|
+
if (listener) {
|
|
144
|
+
getProgressListeners(backend).delete(listener);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
async function createModel(backend) {
|
|
149
|
+
const transformers = await getTransformers();
|
|
150
|
+
emitProgress(backend, { phase: "loading-model", backend });
|
|
151
|
+
try {
|
|
152
|
+
const model = await transformers.pipeline(
|
|
153
|
+
"background-removal",
|
|
154
|
+
backgroundRemoverConfig.model.id,
|
|
155
|
+
{
|
|
156
|
+
dtype: backgroundRemoverConfig.model.dtype,
|
|
157
|
+
device: backend,
|
|
158
|
+
progress_callback: (progress) => handleModelProgress(backend, progress)
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
emitProgress(backend, { phase: "ready", backend, progress: 100 });
|
|
162
|
+
return model;
|
|
163
|
+
} catch (error) {
|
|
164
|
+
throw new BackgroundRemoverError("model-load-failed", "The AI model could not be loaded.", {
|
|
165
|
+
cause: error
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function getTransformers() {
|
|
170
|
+
if (!transformersPromise) {
|
|
171
|
+
transformersPromise = import("@huggingface/transformers").then((transformers) => {
|
|
172
|
+
transformers.env.allowLocalModels = false;
|
|
173
|
+
transformers.env.allowRemoteModels = true;
|
|
174
|
+
if ("useBrowserCache" in transformers.env) {
|
|
175
|
+
transformers.env.useBrowserCache = true;
|
|
176
|
+
}
|
|
177
|
+
return transformers;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return transformersPromise;
|
|
181
|
+
}
|
|
182
|
+
function handleModelProgress(backend, value) {
|
|
183
|
+
if (!value || typeof value !== "object") {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const progress = value;
|
|
187
|
+
if (progress.status === "progress" && Number.isFinite(Number(progress.progress))) {
|
|
188
|
+
emitProgress(backend, {
|
|
189
|
+
phase: "downloading-model",
|
|
190
|
+
backend,
|
|
191
|
+
progress: Math.max(0, Math.min(100, Math.round(Number(progress.progress))))
|
|
192
|
+
});
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (progress.status === "ready" || progress.status === "done") {
|
|
196
|
+
emitProgress(backend, { phase: "initializing", backend });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function supportsWebGpu() {
|
|
200
|
+
if (typeof navigator === "undefined") {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
return Boolean(navigator.gpu);
|
|
204
|
+
}
|
|
205
|
+
function getProgressListeners(backend) {
|
|
206
|
+
let listeners = progressListeners.get(backend);
|
|
207
|
+
if (!listeners) {
|
|
208
|
+
listeners = /* @__PURE__ */ new Set();
|
|
209
|
+
progressListeners.set(backend, listeners);
|
|
210
|
+
}
|
|
211
|
+
return listeners;
|
|
212
|
+
}
|
|
213
|
+
function emitProgress(backend, progress) {
|
|
214
|
+
for (const listener of getProgressListeners(backend)) {
|
|
215
|
+
listener(progress);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// src/validation.ts
|
|
220
|
+
async function validateInputImage(input) {
|
|
221
|
+
const mimeType = String(input.type || "").toLowerCase();
|
|
222
|
+
if (!backgroundRemoverConfig.input.supportedMimeTypes.includes(
|
|
223
|
+
mimeType
|
|
224
|
+
)) {
|
|
225
|
+
throw new BackgroundRemoverError(
|
|
226
|
+
"unsupported-input",
|
|
227
|
+
"Only JPEG, PNG, and WebP images are supported."
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (input.size > backgroundRemoverConfig.input.maxFileSizeBytes) {
|
|
231
|
+
throw new BackgroundRemoverError(
|
|
232
|
+
"file-too-large",
|
|
233
|
+
"The image exceeds the configured file size limit."
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
const signature = new Uint8Array(await input.slice(0, 12).arrayBuffer());
|
|
237
|
+
if (!matchesImageSignature(signature, mimeType)) {
|
|
238
|
+
throw new BackgroundRemoverError(
|
|
239
|
+
"invalid-signature",
|
|
240
|
+
"The image file signature does not match its MIME type."
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
let bitmap;
|
|
244
|
+
try {
|
|
245
|
+
bitmap = await createImageBitmap(input);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
throw new BackgroundRemoverError("decode-failed", "The image could not be decoded.", {
|
|
248
|
+
cause: error
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const megapixels = bitmap.width * bitmap.height / 1e6;
|
|
253
|
+
if (megapixels > backgroundRemoverConfig.input.maxMegapixels) {
|
|
254
|
+
throw new BackgroundRemoverError(
|
|
255
|
+
"too-many-pixels",
|
|
256
|
+
"The image dimensions exceed the configured pixel limit."
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
return { width: bitmap.width, height: bitmap.height };
|
|
260
|
+
} finally {
|
|
261
|
+
bitmap.close();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function matchesImageSignature(bytes, mimeType) {
|
|
265
|
+
if (mimeType === "image/png") {
|
|
266
|
+
return bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10;
|
|
267
|
+
}
|
|
268
|
+
if (mimeType === "image/webp") {
|
|
269
|
+
return bytes.length >= 12 && String.fromCharCode(...bytes.subarray(0, 4)) === "RIFF" && String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP";
|
|
270
|
+
}
|
|
271
|
+
return bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
|
|
272
|
+
}
|
|
273
|
+
function normalizeBackground(background) {
|
|
274
|
+
if (background === "transparent") {
|
|
275
|
+
return background;
|
|
276
|
+
}
|
|
277
|
+
if (!/^#[0-9a-f]{6}$/i.test(background)) {
|
|
278
|
+
throw new BackgroundRemoverError(
|
|
279
|
+
"invalid-background",
|
|
280
|
+
'Background must be "transparent" or a six-digit hex color.'
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
return background.toUpperCase();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/image.ts
|
|
287
|
+
async function removeBackground(input, options = {}) {
|
|
288
|
+
await validateInputImage(input);
|
|
289
|
+
const background = normalizeBackground(
|
|
290
|
+
options.background ?? backgroundRemoverConfig.output.defaultBackground
|
|
291
|
+
);
|
|
292
|
+
const { output, backend } = await runBackgroundRemovalModel(input, options);
|
|
293
|
+
let transparentBlob;
|
|
294
|
+
try {
|
|
295
|
+
transparentBlob = await output.toBlob(backgroundRemoverConfig.output.mimeType);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw new BackgroundRemoverError(
|
|
298
|
+
"processing-failed",
|
|
299
|
+
"The background-removed image could not be created.",
|
|
300
|
+
{ cause: error }
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
const blob = background === "transparent" ? transparentBlob : await composeBackground(transparentBlob, background);
|
|
304
|
+
return {
|
|
305
|
+
blob,
|
|
306
|
+
width: output.width,
|
|
307
|
+
height: output.height,
|
|
308
|
+
mimeType: backgroundRemoverConfig.output.mimeType,
|
|
309
|
+
backend
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async function composeBackground(input, color) {
|
|
313
|
+
let bitmap;
|
|
314
|
+
try {
|
|
315
|
+
bitmap = await createImageBitmap(input);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
throw new BackgroundRemoverError(
|
|
318
|
+
"decode-failed",
|
|
319
|
+
"The background-removed image could not be decoded.",
|
|
320
|
+
{ cause: error }
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
325
|
+
const canvas2 = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
326
|
+
const context2 = canvas2.getContext("2d", { alpha: false });
|
|
327
|
+
if (!context2) {
|
|
328
|
+
throw new BackgroundRemoverError("canvas-unavailable", "Canvas 2D is unavailable.");
|
|
329
|
+
}
|
|
330
|
+
context2.fillStyle = color;
|
|
331
|
+
context2.fillRect(0, 0, bitmap.width, bitmap.height);
|
|
332
|
+
context2.drawImage(bitmap, 0, 0);
|
|
333
|
+
try {
|
|
334
|
+
return await canvas2.convertToBlob({ type: backgroundRemoverConfig.output.mimeType });
|
|
335
|
+
} catch (error) {
|
|
336
|
+
throw new BackgroundRemoverError("encode-failed", "PNG encoding failed.", {
|
|
337
|
+
cause: error
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
if (typeof document === "undefined") {
|
|
342
|
+
throw new BackgroundRemoverError(
|
|
343
|
+
"canvas-unavailable",
|
|
344
|
+
"A browser canvas is required for a solid background."
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const canvas = document.createElement("canvas");
|
|
348
|
+
canvas.width = bitmap.width;
|
|
349
|
+
canvas.height = bitmap.height;
|
|
350
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
351
|
+
if (!context) {
|
|
352
|
+
throw new BackgroundRemoverError("canvas-unavailable", "Canvas 2D is unavailable.");
|
|
353
|
+
}
|
|
354
|
+
context.fillStyle = color;
|
|
355
|
+
context.fillRect(0, 0, bitmap.width, bitmap.height);
|
|
356
|
+
context.drawImage(bitmap, 0, 0);
|
|
357
|
+
return await new Promise((resolve, reject) => {
|
|
358
|
+
canvas.toBlob((blob) => {
|
|
359
|
+
if (blob) {
|
|
360
|
+
resolve(blob);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
reject(new BackgroundRemoverError("encode-failed", "PNG encoding failed."));
|
|
364
|
+
}, backgroundRemoverConfig.output.mimeType);
|
|
365
|
+
});
|
|
366
|
+
} finally {
|
|
367
|
+
bitmap.close();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
371
|
+
0 && (module.exports = {
|
|
372
|
+
BackgroundRemoverError,
|
|
373
|
+
backgroundRemoverConfig,
|
|
374
|
+
matchesImageSignature,
|
|
375
|
+
normalizeBackground,
|
|
376
|
+
preloadBackgroundRemovalModel,
|
|
377
|
+
removeBackground,
|
|
378
|
+
resetBackgroundRemovalModel,
|
|
379
|
+
validateInputImage
|
|
380
|
+
});
|
|
381
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config.ts","../src/errors.ts","../src/model.ts","../src/validation.ts","../src/image.ts"],"sourcesContent":["export { backgroundRemoverConfig } from './config';\nexport { BackgroundRemoverError } from './errors';\nexport { removeBackground } from './image';\nexport {\n preloadBackgroundRemovalModel,\n resetBackgroundRemovalModel,\n} from './model';\nexport { matchesImageSignature, normalizeBackground, validateInputImage } from './validation';\nexport type {\n BackgroundRemovalPhase,\n BackgroundRemovalProgress,\n BackgroundRemoverBackend,\n BackgroundRemoverDevice,\n ModelLoadOptions,\n RemoveBackgroundOptions,\n RemoveBackgroundResult,\n} from './types';\n","import type { BackgroundRemoverDevice } from './types';\n\nexport const backgroundRemoverConfig = {\n model: {\n id: 'onnx-community/ormbg-ONNX',\n dtype: 'q8',\n defaultDevice: 'auto' as BackgroundRemoverDevice,\n },\n input: {\n maxFileSizeBytes: 10 * 1024 * 1024,\n maxMegapixels: 36,\n supportedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'] as const,\n },\n output: {\n mimeType: 'image/png' as const,\n defaultBackground: 'transparent' as const,\n },\n} as const;\n","export type BackgroundRemoverErrorCode =\n | 'unsupported-input'\n | 'file-too-large'\n | 'invalid-signature'\n | 'decode-failed'\n | 'too-many-pixels'\n | 'invalid-background'\n | 'model-load-failed'\n | 'processing-failed'\n | 'canvas-unavailable'\n | 'encode-failed';\n\nexport class BackgroundRemoverError extends Error {\n constructor(\n public readonly code: BackgroundRemoverErrorCode,\n message: string,\n options?: ErrorOptions\n ) {\n super(message, options);\n this.name = 'BackgroundRemoverError';\n }\n}\n","import { backgroundRemoverConfig } from './config';\nimport { BackgroundRemoverError } from './errors';\nimport type {\n BackgroundRemovalProgress,\n BackgroundRemoverBackend,\n BackgroundRemoverDevice,\n ModelLoadOptions,\n} from './types';\n\ntype ProgressCallback = ModelLoadOptions['onProgress'];\n\ntype ModelOutput = {\n width: number;\n height: number;\n toBlob: (type?: string) => Promise<Blob>;\n};\n\ntype ModelRunner = (input: Blob) => Promise<ModelOutput>;\n\ntype LoadedModel = {\n backend: BackgroundRemoverBackend;\n runner: ModelRunner;\n};\n\ntype TransformersModule = typeof import('@huggingface/transformers');\n\nconst modelPromises = new Map<BackgroundRemoverBackend, Promise<ModelRunner>>();\nconst progressListeners = new Map<BackgroundRemoverBackend, Set<NonNullable<ProgressCallback>>>();\nlet transformersPromise: Promise<TransformersModule> | null = null;\n\nexport async function preloadBackgroundRemovalModel(\n options: ModelLoadOptions = {}\n): Promise<BackgroundRemoverBackend> {\n const loaded = await resolveModel(options.device, options.onProgress);\n return loaded.backend;\n}\n\nexport function resetBackgroundRemovalModel(): void {\n modelPromises.clear();\n}\n\nexport async function runBackgroundRemovalModel(\n input: Blob,\n options: ModelLoadOptions = {}\n): Promise<{ output: ModelOutput; backend: BackgroundRemoverBackend }> {\n const device = options.device ?? backgroundRemoverConfig.model.defaultDevice;\n const loaded = await resolveModel(device, options.onProgress);\n options.onProgress?.({ phase: 'processing', backend: loaded.backend });\n\n try {\n return { output: await loaded.runner(input), backend: loaded.backend };\n } catch (error) {\n if (device === 'auto' && loaded.backend === 'webgpu') {\n modelPromises.delete('webgpu');\n options.onProgress?.({ phase: 'fallback', backend: 'wasm' });\n const fallback = await resolveModel('wasm', options.onProgress);\n options.onProgress?.({ phase: 'processing', backend: 'wasm' });\n try {\n return { output: await fallback.runner(input), backend: 'wasm' };\n } catch (fallbackError) {\n throw new BackgroundRemoverError(\n 'processing-failed',\n 'Background removal failed.',\n { cause: fallbackError }\n );\n }\n }\n\n throw new BackgroundRemoverError('processing-failed', 'Background removal failed.', {\n cause: error,\n });\n }\n}\n\nasync function resolveModel(\n device: BackgroundRemoverDevice | undefined,\n onProgress?: ProgressCallback\n): Promise<LoadedModel> {\n const requested = device ?? backgroundRemoverConfig.model.defaultDevice;\n if (requested === 'auto') {\n if (supportsWebGpu()) {\n try {\n return { backend: 'webgpu', runner: await getModel('webgpu', onProgress) };\n } catch {\n onProgress?.({ phase: 'fallback', backend: 'wasm' });\n }\n }\n return { backend: 'wasm', runner: await getModel('wasm', onProgress) };\n }\n\n return { backend: requested, runner: await getModel(requested, onProgress) };\n}\n\nasync function getModel(\n backend: BackgroundRemoverBackend,\n onProgress?: ProgressCallback\n): Promise<ModelRunner> {\n const listener = onProgress ?? undefined;\n if (listener) {\n getProgressListeners(backend).add(listener);\n }\n\n try {\n if (!modelPromises.has(backend)) {\n modelPromises.set(backend, createModel(backend));\n }\n return await modelPromises.get(backend)!;\n } catch (error) {\n modelPromises.delete(backend);\n if (error instanceof BackgroundRemoverError) {\n throw error;\n }\n throw new BackgroundRemoverError('model-load-failed', 'The AI model could not be loaded.', {\n cause: error,\n });\n } finally {\n if (listener) {\n getProgressListeners(backend).delete(listener);\n }\n }\n}\n\nasync function createModel(backend: BackgroundRemoverBackend): Promise<ModelRunner> {\n const transformers = await getTransformers();\n emitProgress(backend, { phase: 'loading-model', backend });\n\n try {\n const model = await transformers.pipeline(\n 'background-removal',\n backgroundRemoverConfig.model.id,\n {\n dtype: backgroundRemoverConfig.model.dtype,\n device: backend,\n progress_callback: (progress: unknown) => handleModelProgress(backend, progress),\n }\n );\n emitProgress(backend, { phase: 'ready', backend, progress: 100 });\n return model as unknown as ModelRunner;\n } catch (error) {\n throw new BackgroundRemoverError('model-load-failed', 'The AI model could not be loaded.', {\n cause: error,\n });\n }\n}\n\nasync function getTransformers(): Promise<TransformersModule> {\n if (!transformersPromise) {\n transformersPromise = import('@huggingface/transformers').then((transformers) => {\n transformers.env.allowLocalModels = false;\n transformers.env.allowRemoteModels = true;\n if ('useBrowserCache' in transformers.env) {\n transformers.env.useBrowserCache = true;\n }\n return transformers;\n });\n }\n return transformersPromise;\n}\n\nfunction handleModelProgress(backend: BackgroundRemoverBackend, value: unknown): void {\n if (!value || typeof value !== 'object') {\n return;\n }\n\n const progress = value as { status?: unknown; progress?: unknown };\n if (progress.status === 'progress' && Number.isFinite(Number(progress.progress))) {\n emitProgress(backend, {\n phase: 'downloading-model',\n backend,\n progress: Math.max(0, Math.min(100, Math.round(Number(progress.progress)))),\n });\n return;\n }\n\n if (progress.status === 'ready' || progress.status === 'done') {\n emitProgress(backend, { phase: 'initializing', backend });\n }\n}\n\nfunction supportsWebGpu(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n return Boolean((navigator as Navigator & { gpu?: unknown }).gpu);\n}\n\nfunction getProgressListeners(\n backend: BackgroundRemoverBackend\n): Set<NonNullable<ProgressCallback>> {\n let listeners = progressListeners.get(backend);\n if (!listeners) {\n listeners = new Set();\n progressListeners.set(backend, listeners);\n }\n return listeners;\n}\n\nfunction emitProgress(\n backend: BackgroundRemoverBackend,\n progress: BackgroundRemovalProgress\n): void {\n for (const listener of getProgressListeners(backend)) {\n listener(progress);\n }\n}\n","import { backgroundRemoverConfig } from './config';\nimport { BackgroundRemoverError } from './errors';\n\nexport async function validateInputImage(input: Blob): Promise<{ width: number; height: number }> {\n const mimeType = String(input.type || '').toLowerCase();\n if (!backgroundRemoverConfig.input.supportedMimeTypes.includes(\n mimeType as (typeof backgroundRemoverConfig.input.supportedMimeTypes)[number]\n )) {\n throw new BackgroundRemoverError(\n 'unsupported-input',\n 'Only JPEG, PNG, and WebP images are supported.'\n );\n }\n\n if (input.size > backgroundRemoverConfig.input.maxFileSizeBytes) {\n throw new BackgroundRemoverError(\n 'file-too-large',\n 'The image exceeds the configured file size limit.'\n );\n }\n\n const signature = new Uint8Array(await input.slice(0, 12).arrayBuffer());\n if (!matchesImageSignature(signature, mimeType)) {\n throw new BackgroundRemoverError(\n 'invalid-signature',\n 'The image file signature does not match its MIME type.'\n );\n }\n\n let bitmap: ImageBitmap;\n try {\n bitmap = await createImageBitmap(input);\n } catch (error) {\n throw new BackgroundRemoverError('decode-failed', 'The image could not be decoded.', {\n cause: error,\n });\n }\n\n try {\n const megapixels = (bitmap.width * bitmap.height) / 1_000_000;\n if (megapixels > backgroundRemoverConfig.input.maxMegapixels) {\n throw new BackgroundRemoverError(\n 'too-many-pixels',\n 'The image dimensions exceed the configured pixel limit.'\n );\n }\n return { width: bitmap.width, height: bitmap.height };\n } finally {\n bitmap.close();\n }\n}\n\nexport function matchesImageSignature(bytes: Uint8Array, mimeType: string): boolean {\n if (mimeType === 'image/png') {\n return bytes.length >= 8\n && bytes[0] === 0x89\n && bytes[1] === 0x50\n && bytes[2] === 0x4e\n && bytes[3] === 0x47\n && bytes[4] === 0x0d\n && bytes[5] === 0x0a\n && bytes[6] === 0x1a\n && bytes[7] === 0x0a;\n }\n\n if (mimeType === 'image/webp') {\n return bytes.length >= 12\n && String.fromCharCode(...bytes.subarray(0, 4)) === 'RIFF'\n && String.fromCharCode(...bytes.subarray(8, 12)) === 'WEBP';\n }\n\n return bytes.length >= 3\n && bytes[0] === 0xff\n && bytes[1] === 0xd8\n && bytes[2] === 0xff;\n}\n\nexport function normalizeBackground(background: string): 'transparent' | string {\n if (background === 'transparent') {\n return background;\n }\n if (!/^#[0-9a-f]{6}$/i.test(background)) {\n throw new BackgroundRemoverError(\n 'invalid-background',\n 'Background must be \"transparent\" or a six-digit hex color.'\n );\n }\n return background.toUpperCase();\n}\n","import { backgroundRemoverConfig } from './config';\nimport { BackgroundRemoverError } from './errors';\nimport { runBackgroundRemovalModel } from './model';\nimport type { RemoveBackgroundOptions, RemoveBackgroundResult } from './types';\nimport { normalizeBackground, validateInputImage } from './validation';\n\nexport async function removeBackground(\n input: Blob,\n options: RemoveBackgroundOptions = {}\n): Promise<RemoveBackgroundResult> {\n await validateInputImage(input);\n\n const background = normalizeBackground(\n options.background ?? backgroundRemoverConfig.output.defaultBackground\n );\n\n const { output, backend } = await runBackgroundRemovalModel(input, options);\n\n let transparentBlob: Blob;\n try {\n transparentBlob = await output.toBlob(backgroundRemoverConfig.output.mimeType);\n } catch (error) {\n throw new BackgroundRemoverError(\n 'processing-failed',\n 'The background-removed image could not be created.',\n { cause: error }\n );\n }\n\n const blob = background === 'transparent'\n ? transparentBlob\n : await composeBackground(transparentBlob, background);\n\n return {\n blob,\n width: output.width,\n height: output.height,\n mimeType: backgroundRemoverConfig.output.mimeType,\n backend,\n };\n}\n\nasync function composeBackground(input: Blob, color: string): Promise<Blob> {\n let bitmap: ImageBitmap;\n try {\n bitmap = await createImageBitmap(input);\n } catch (error) {\n throw new BackgroundRemoverError(\n 'decode-failed',\n 'The background-removed image could not be decoded.',\n { cause: error }\n );\n }\n\n try {\n if (typeof OffscreenCanvas !== 'undefined') {\n const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);\n const context = canvas.getContext('2d', { alpha: false });\n if (!context) {\n throw new BackgroundRemoverError('canvas-unavailable', 'Canvas 2D is unavailable.');\n }\n context.fillStyle = color;\n context.fillRect(0, 0, bitmap.width, bitmap.height);\n context.drawImage(bitmap, 0, 0);\n try {\n return await canvas.convertToBlob({ type: backgroundRemoverConfig.output.mimeType });\n } catch (error) {\n throw new BackgroundRemoverError('encode-failed', 'PNG encoding failed.', {\n cause: error,\n });\n }\n }\n\n if (typeof document === 'undefined') {\n throw new BackgroundRemoverError(\n 'canvas-unavailable',\n 'A browser canvas is required for a solid background.'\n );\n }\n\n const canvas = document.createElement('canvas');\n canvas.width = bitmap.width;\n canvas.height = bitmap.height;\n const context = canvas.getContext('2d', { alpha: false });\n if (!context) {\n throw new BackgroundRemoverError('canvas-unavailable', 'Canvas 2D is unavailable.');\n }\n\n context.fillStyle = color;\n context.fillRect(0, 0, bitmap.width, bitmap.height);\n context.drawImage(bitmap, 0, 0);\n\n return await new Promise<Blob>((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob) {\n resolve(blob);\n return;\n }\n reject(new BackgroundRemoverError('encode-failed', 'PNG encoding failed.'));\n }, backgroundRemoverConfig.output.mimeType);\n });\n } finally {\n bitmap.close();\n }\n}\n"],"mappings":"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","names":["canvas","context"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
type BackgroundRemoverBackend = 'webgpu' | 'wasm';
|
|
2
|
+
type BackgroundRemoverDevice = 'auto' | BackgroundRemoverBackend;
|
|
3
|
+
type BackgroundRemovalPhase = 'loading-model' | 'downloading-model' | 'initializing' | 'fallback' | 'ready' | 'processing';
|
|
4
|
+
interface BackgroundRemovalProgress {
|
|
5
|
+
phase: BackgroundRemovalPhase;
|
|
6
|
+
backend: BackgroundRemoverBackend;
|
|
7
|
+
progress?: number;
|
|
8
|
+
}
|
|
9
|
+
interface ModelLoadOptions {
|
|
10
|
+
device?: BackgroundRemoverDevice;
|
|
11
|
+
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
|
12
|
+
}
|
|
13
|
+
interface RemoveBackgroundOptions extends ModelLoadOptions {
|
|
14
|
+
background?: 'transparent' | string;
|
|
15
|
+
}
|
|
16
|
+
interface RemoveBackgroundResult {
|
|
17
|
+
blob: Blob;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
mimeType: 'image/png';
|
|
21
|
+
backend: BackgroundRemoverBackend;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare const backgroundRemoverConfig: {
|
|
25
|
+
readonly model: {
|
|
26
|
+
readonly id: "onnx-community/ormbg-ONNX";
|
|
27
|
+
readonly dtype: "q8";
|
|
28
|
+
readonly defaultDevice: BackgroundRemoverDevice;
|
|
29
|
+
};
|
|
30
|
+
readonly input: {
|
|
31
|
+
readonly maxFileSizeBytes: number;
|
|
32
|
+
readonly maxMegapixels: 36;
|
|
33
|
+
readonly supportedMimeTypes: readonly ["image/jpeg", "image/png", "image/webp"];
|
|
34
|
+
};
|
|
35
|
+
readonly output: {
|
|
36
|
+
readonly mimeType: "image/png";
|
|
37
|
+
readonly defaultBackground: "transparent";
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type BackgroundRemoverErrorCode = 'unsupported-input' | 'file-too-large' | 'invalid-signature' | 'decode-failed' | 'too-many-pixels' | 'invalid-background' | 'model-load-failed' | 'processing-failed' | 'canvas-unavailable' | 'encode-failed';
|
|
42
|
+
declare class BackgroundRemoverError extends Error {
|
|
43
|
+
readonly code: BackgroundRemoverErrorCode;
|
|
44
|
+
constructor(code: BackgroundRemoverErrorCode, message: string, options?: ErrorOptions);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare function removeBackground(input: Blob, options?: RemoveBackgroundOptions): Promise<RemoveBackgroundResult>;
|
|
48
|
+
|
|
49
|
+
declare function preloadBackgroundRemovalModel(options?: ModelLoadOptions): Promise<BackgroundRemoverBackend>;
|
|
50
|
+
declare function resetBackgroundRemovalModel(): void;
|
|
51
|
+
|
|
52
|
+
declare function validateInputImage(input: Blob): Promise<{
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
}>;
|
|
56
|
+
declare function matchesImageSignature(bytes: Uint8Array, mimeType: string): boolean;
|
|
57
|
+
declare function normalizeBackground(background: string): 'transparent' | string;
|
|
58
|
+
|
|
59
|
+
export { type BackgroundRemovalPhase, type BackgroundRemovalProgress, type BackgroundRemoverBackend, type BackgroundRemoverDevice, BackgroundRemoverError, type ModelLoadOptions, type RemoveBackgroundOptions, type RemoveBackgroundResult, backgroundRemoverConfig, matchesImageSignature, normalizeBackground, preloadBackgroundRemovalModel, removeBackground, resetBackgroundRemovalModel, validateInputImage };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
type BackgroundRemoverBackend = 'webgpu' | 'wasm';
|
|
2
|
+
type BackgroundRemoverDevice = 'auto' | BackgroundRemoverBackend;
|
|
3
|
+
type BackgroundRemovalPhase = 'loading-model' | 'downloading-model' | 'initializing' | 'fallback' | 'ready' | 'processing';
|
|
4
|
+
interface BackgroundRemovalProgress {
|
|
5
|
+
phase: BackgroundRemovalPhase;
|
|
6
|
+
backend: BackgroundRemoverBackend;
|
|
7
|
+
progress?: number;
|
|
8
|
+
}
|
|
9
|
+
interface ModelLoadOptions {
|
|
10
|
+
device?: BackgroundRemoverDevice;
|
|
11
|
+
onProgress?: (progress: BackgroundRemovalProgress) => void;
|
|
12
|
+
}
|
|
13
|
+
interface RemoveBackgroundOptions extends ModelLoadOptions {
|
|
14
|
+
background?: 'transparent' | string;
|
|
15
|
+
}
|
|
16
|
+
interface RemoveBackgroundResult {
|
|
17
|
+
blob: Blob;
|
|
18
|
+
width: number;
|
|
19
|
+
height: number;
|
|
20
|
+
mimeType: 'image/png';
|
|
21
|
+
backend: BackgroundRemoverBackend;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
declare const backgroundRemoverConfig: {
|
|
25
|
+
readonly model: {
|
|
26
|
+
readonly id: "onnx-community/ormbg-ONNX";
|
|
27
|
+
readonly dtype: "q8";
|
|
28
|
+
readonly defaultDevice: BackgroundRemoverDevice;
|
|
29
|
+
};
|
|
30
|
+
readonly input: {
|
|
31
|
+
readonly maxFileSizeBytes: number;
|
|
32
|
+
readonly maxMegapixels: 36;
|
|
33
|
+
readonly supportedMimeTypes: readonly ["image/jpeg", "image/png", "image/webp"];
|
|
34
|
+
};
|
|
35
|
+
readonly output: {
|
|
36
|
+
readonly mimeType: "image/png";
|
|
37
|
+
readonly defaultBackground: "transparent";
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type BackgroundRemoverErrorCode = 'unsupported-input' | 'file-too-large' | 'invalid-signature' | 'decode-failed' | 'too-many-pixels' | 'invalid-background' | 'model-load-failed' | 'processing-failed' | 'canvas-unavailable' | 'encode-failed';
|
|
42
|
+
declare class BackgroundRemoverError extends Error {
|
|
43
|
+
readonly code: BackgroundRemoverErrorCode;
|
|
44
|
+
constructor(code: BackgroundRemoverErrorCode, message: string, options?: ErrorOptions);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
declare function removeBackground(input: Blob, options?: RemoveBackgroundOptions): Promise<RemoveBackgroundResult>;
|
|
48
|
+
|
|
49
|
+
declare function preloadBackgroundRemovalModel(options?: ModelLoadOptions): Promise<BackgroundRemoverBackend>;
|
|
50
|
+
declare function resetBackgroundRemovalModel(): void;
|
|
51
|
+
|
|
52
|
+
declare function validateInputImage(input: Blob): Promise<{
|
|
53
|
+
width: number;
|
|
54
|
+
height: number;
|
|
55
|
+
}>;
|
|
56
|
+
declare function matchesImageSignature(bytes: Uint8Array, mimeType: string): boolean;
|
|
57
|
+
declare function normalizeBackground(background: string): 'transparent' | string;
|
|
58
|
+
|
|
59
|
+
export { type BackgroundRemovalPhase, type BackgroundRemovalProgress, type BackgroundRemoverBackend, type BackgroundRemoverDevice, BackgroundRemoverError, type ModelLoadOptions, type RemoveBackgroundOptions, type RemoveBackgroundResult, backgroundRemoverConfig, matchesImageSignature, normalizeBackground, preloadBackgroundRemovalModel, removeBackground, resetBackgroundRemovalModel, validateInputImage };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// src/config.ts
|
|
2
|
+
var backgroundRemoverConfig = {
|
|
3
|
+
model: {
|
|
4
|
+
id: "onnx-community/ormbg-ONNX",
|
|
5
|
+
dtype: "q8",
|
|
6
|
+
defaultDevice: "auto"
|
|
7
|
+
},
|
|
8
|
+
input: {
|
|
9
|
+
maxFileSizeBytes: 10 * 1024 * 1024,
|
|
10
|
+
maxMegapixels: 36,
|
|
11
|
+
supportedMimeTypes: ["image/jpeg", "image/png", "image/webp"]
|
|
12
|
+
},
|
|
13
|
+
output: {
|
|
14
|
+
mimeType: "image/png",
|
|
15
|
+
defaultBackground: "transparent"
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// src/errors.ts
|
|
20
|
+
var BackgroundRemoverError = class extends Error {
|
|
21
|
+
constructor(code, message, options) {
|
|
22
|
+
super(message, options);
|
|
23
|
+
this.code = code;
|
|
24
|
+
this.name = "BackgroundRemoverError";
|
|
25
|
+
}
|
|
26
|
+
code;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/model.ts
|
|
30
|
+
var modelPromises = /* @__PURE__ */ new Map();
|
|
31
|
+
var progressListeners = /* @__PURE__ */ new Map();
|
|
32
|
+
var transformersPromise = null;
|
|
33
|
+
async function preloadBackgroundRemovalModel(options = {}) {
|
|
34
|
+
const loaded = await resolveModel(options.device, options.onProgress);
|
|
35
|
+
return loaded.backend;
|
|
36
|
+
}
|
|
37
|
+
function resetBackgroundRemovalModel() {
|
|
38
|
+
modelPromises.clear();
|
|
39
|
+
}
|
|
40
|
+
async function runBackgroundRemovalModel(input, options = {}) {
|
|
41
|
+
const device = options.device ?? backgroundRemoverConfig.model.defaultDevice;
|
|
42
|
+
const loaded = await resolveModel(device, options.onProgress);
|
|
43
|
+
options.onProgress?.({ phase: "processing", backend: loaded.backend });
|
|
44
|
+
try {
|
|
45
|
+
return { output: await loaded.runner(input), backend: loaded.backend };
|
|
46
|
+
} catch (error) {
|
|
47
|
+
if (device === "auto" && loaded.backend === "webgpu") {
|
|
48
|
+
modelPromises.delete("webgpu");
|
|
49
|
+
options.onProgress?.({ phase: "fallback", backend: "wasm" });
|
|
50
|
+
const fallback = await resolveModel("wasm", options.onProgress);
|
|
51
|
+
options.onProgress?.({ phase: "processing", backend: "wasm" });
|
|
52
|
+
try {
|
|
53
|
+
return { output: await fallback.runner(input), backend: "wasm" };
|
|
54
|
+
} catch (fallbackError) {
|
|
55
|
+
throw new BackgroundRemoverError(
|
|
56
|
+
"processing-failed",
|
|
57
|
+
"Background removal failed.",
|
|
58
|
+
{ cause: fallbackError }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw new BackgroundRemoverError("processing-failed", "Background removal failed.", {
|
|
63
|
+
cause: error
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function resolveModel(device, onProgress) {
|
|
68
|
+
const requested = device ?? backgroundRemoverConfig.model.defaultDevice;
|
|
69
|
+
if (requested === "auto") {
|
|
70
|
+
if (supportsWebGpu()) {
|
|
71
|
+
try {
|
|
72
|
+
return { backend: "webgpu", runner: await getModel("webgpu", onProgress) };
|
|
73
|
+
} catch {
|
|
74
|
+
onProgress?.({ phase: "fallback", backend: "wasm" });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { backend: "wasm", runner: await getModel("wasm", onProgress) };
|
|
78
|
+
}
|
|
79
|
+
return { backend: requested, runner: await getModel(requested, onProgress) };
|
|
80
|
+
}
|
|
81
|
+
async function getModel(backend, onProgress) {
|
|
82
|
+
const listener = onProgress ?? void 0;
|
|
83
|
+
if (listener) {
|
|
84
|
+
getProgressListeners(backend).add(listener);
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
if (!modelPromises.has(backend)) {
|
|
88
|
+
modelPromises.set(backend, createModel(backend));
|
|
89
|
+
}
|
|
90
|
+
return await modelPromises.get(backend);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
modelPromises.delete(backend);
|
|
93
|
+
if (error instanceof BackgroundRemoverError) {
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
throw new BackgroundRemoverError("model-load-failed", "The AI model could not be loaded.", {
|
|
97
|
+
cause: error
|
|
98
|
+
});
|
|
99
|
+
} finally {
|
|
100
|
+
if (listener) {
|
|
101
|
+
getProgressListeners(backend).delete(listener);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function createModel(backend) {
|
|
106
|
+
const transformers = await getTransformers();
|
|
107
|
+
emitProgress(backend, { phase: "loading-model", backend });
|
|
108
|
+
try {
|
|
109
|
+
const model = await transformers.pipeline(
|
|
110
|
+
"background-removal",
|
|
111
|
+
backgroundRemoverConfig.model.id,
|
|
112
|
+
{
|
|
113
|
+
dtype: backgroundRemoverConfig.model.dtype,
|
|
114
|
+
device: backend,
|
|
115
|
+
progress_callback: (progress) => handleModelProgress(backend, progress)
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
emitProgress(backend, { phase: "ready", backend, progress: 100 });
|
|
119
|
+
return model;
|
|
120
|
+
} catch (error) {
|
|
121
|
+
throw new BackgroundRemoverError("model-load-failed", "The AI model could not be loaded.", {
|
|
122
|
+
cause: error
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function getTransformers() {
|
|
127
|
+
if (!transformersPromise) {
|
|
128
|
+
transformersPromise = import("@huggingface/transformers").then((transformers) => {
|
|
129
|
+
transformers.env.allowLocalModels = false;
|
|
130
|
+
transformers.env.allowRemoteModels = true;
|
|
131
|
+
if ("useBrowserCache" in transformers.env) {
|
|
132
|
+
transformers.env.useBrowserCache = true;
|
|
133
|
+
}
|
|
134
|
+
return transformers;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return transformersPromise;
|
|
138
|
+
}
|
|
139
|
+
function handleModelProgress(backend, value) {
|
|
140
|
+
if (!value || typeof value !== "object") {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const progress = value;
|
|
144
|
+
if (progress.status === "progress" && Number.isFinite(Number(progress.progress))) {
|
|
145
|
+
emitProgress(backend, {
|
|
146
|
+
phase: "downloading-model",
|
|
147
|
+
backend,
|
|
148
|
+
progress: Math.max(0, Math.min(100, Math.round(Number(progress.progress))))
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (progress.status === "ready" || progress.status === "done") {
|
|
153
|
+
emitProgress(backend, { phase: "initializing", backend });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function supportsWebGpu() {
|
|
157
|
+
if (typeof navigator === "undefined") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return Boolean(navigator.gpu);
|
|
161
|
+
}
|
|
162
|
+
function getProgressListeners(backend) {
|
|
163
|
+
let listeners = progressListeners.get(backend);
|
|
164
|
+
if (!listeners) {
|
|
165
|
+
listeners = /* @__PURE__ */ new Set();
|
|
166
|
+
progressListeners.set(backend, listeners);
|
|
167
|
+
}
|
|
168
|
+
return listeners;
|
|
169
|
+
}
|
|
170
|
+
function emitProgress(backend, progress) {
|
|
171
|
+
for (const listener of getProgressListeners(backend)) {
|
|
172
|
+
listener(progress);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/validation.ts
|
|
177
|
+
async function validateInputImage(input) {
|
|
178
|
+
const mimeType = String(input.type || "").toLowerCase();
|
|
179
|
+
if (!backgroundRemoverConfig.input.supportedMimeTypes.includes(
|
|
180
|
+
mimeType
|
|
181
|
+
)) {
|
|
182
|
+
throw new BackgroundRemoverError(
|
|
183
|
+
"unsupported-input",
|
|
184
|
+
"Only JPEG, PNG, and WebP images are supported."
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (input.size > backgroundRemoverConfig.input.maxFileSizeBytes) {
|
|
188
|
+
throw new BackgroundRemoverError(
|
|
189
|
+
"file-too-large",
|
|
190
|
+
"The image exceeds the configured file size limit."
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
const signature = new Uint8Array(await input.slice(0, 12).arrayBuffer());
|
|
194
|
+
if (!matchesImageSignature(signature, mimeType)) {
|
|
195
|
+
throw new BackgroundRemoverError(
|
|
196
|
+
"invalid-signature",
|
|
197
|
+
"The image file signature does not match its MIME type."
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
let bitmap;
|
|
201
|
+
try {
|
|
202
|
+
bitmap = await createImageBitmap(input);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
throw new BackgroundRemoverError("decode-failed", "The image could not be decoded.", {
|
|
205
|
+
cause: error
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const megapixels = bitmap.width * bitmap.height / 1e6;
|
|
210
|
+
if (megapixels > backgroundRemoverConfig.input.maxMegapixels) {
|
|
211
|
+
throw new BackgroundRemoverError(
|
|
212
|
+
"too-many-pixels",
|
|
213
|
+
"The image dimensions exceed the configured pixel limit."
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
return { width: bitmap.width, height: bitmap.height };
|
|
217
|
+
} finally {
|
|
218
|
+
bitmap.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function matchesImageSignature(bytes, mimeType) {
|
|
222
|
+
if (mimeType === "image/png") {
|
|
223
|
+
return bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10;
|
|
224
|
+
}
|
|
225
|
+
if (mimeType === "image/webp") {
|
|
226
|
+
return bytes.length >= 12 && String.fromCharCode(...bytes.subarray(0, 4)) === "RIFF" && String.fromCharCode(...bytes.subarray(8, 12)) === "WEBP";
|
|
227
|
+
}
|
|
228
|
+
return bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
|
|
229
|
+
}
|
|
230
|
+
function normalizeBackground(background) {
|
|
231
|
+
if (background === "transparent") {
|
|
232
|
+
return background;
|
|
233
|
+
}
|
|
234
|
+
if (!/^#[0-9a-f]{6}$/i.test(background)) {
|
|
235
|
+
throw new BackgroundRemoverError(
|
|
236
|
+
"invalid-background",
|
|
237
|
+
'Background must be "transparent" or a six-digit hex color.'
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return background.toUpperCase();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// src/image.ts
|
|
244
|
+
async function removeBackground(input, options = {}) {
|
|
245
|
+
await validateInputImage(input);
|
|
246
|
+
const background = normalizeBackground(
|
|
247
|
+
options.background ?? backgroundRemoverConfig.output.defaultBackground
|
|
248
|
+
);
|
|
249
|
+
const { output, backend } = await runBackgroundRemovalModel(input, options);
|
|
250
|
+
let transparentBlob;
|
|
251
|
+
try {
|
|
252
|
+
transparentBlob = await output.toBlob(backgroundRemoverConfig.output.mimeType);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
throw new BackgroundRemoverError(
|
|
255
|
+
"processing-failed",
|
|
256
|
+
"The background-removed image could not be created.",
|
|
257
|
+
{ cause: error }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const blob = background === "transparent" ? transparentBlob : await composeBackground(transparentBlob, background);
|
|
261
|
+
return {
|
|
262
|
+
blob,
|
|
263
|
+
width: output.width,
|
|
264
|
+
height: output.height,
|
|
265
|
+
mimeType: backgroundRemoverConfig.output.mimeType,
|
|
266
|
+
backend
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
async function composeBackground(input, color) {
|
|
270
|
+
let bitmap;
|
|
271
|
+
try {
|
|
272
|
+
bitmap = await createImageBitmap(input);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new BackgroundRemoverError(
|
|
275
|
+
"decode-failed",
|
|
276
|
+
"The background-removed image could not be decoded.",
|
|
277
|
+
{ cause: error }
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
if (typeof OffscreenCanvas !== "undefined") {
|
|
282
|
+
const canvas2 = new OffscreenCanvas(bitmap.width, bitmap.height);
|
|
283
|
+
const context2 = canvas2.getContext("2d", { alpha: false });
|
|
284
|
+
if (!context2) {
|
|
285
|
+
throw new BackgroundRemoverError("canvas-unavailable", "Canvas 2D is unavailable.");
|
|
286
|
+
}
|
|
287
|
+
context2.fillStyle = color;
|
|
288
|
+
context2.fillRect(0, 0, bitmap.width, bitmap.height);
|
|
289
|
+
context2.drawImage(bitmap, 0, 0);
|
|
290
|
+
try {
|
|
291
|
+
return await canvas2.convertToBlob({ type: backgroundRemoverConfig.output.mimeType });
|
|
292
|
+
} catch (error) {
|
|
293
|
+
throw new BackgroundRemoverError("encode-failed", "PNG encoding failed.", {
|
|
294
|
+
cause: error
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (typeof document === "undefined") {
|
|
299
|
+
throw new BackgroundRemoverError(
|
|
300
|
+
"canvas-unavailable",
|
|
301
|
+
"A browser canvas is required for a solid background."
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const canvas = document.createElement("canvas");
|
|
305
|
+
canvas.width = bitmap.width;
|
|
306
|
+
canvas.height = bitmap.height;
|
|
307
|
+
const context = canvas.getContext("2d", { alpha: false });
|
|
308
|
+
if (!context) {
|
|
309
|
+
throw new BackgroundRemoverError("canvas-unavailable", "Canvas 2D is unavailable.");
|
|
310
|
+
}
|
|
311
|
+
context.fillStyle = color;
|
|
312
|
+
context.fillRect(0, 0, bitmap.width, bitmap.height);
|
|
313
|
+
context.drawImage(bitmap, 0, 0);
|
|
314
|
+
return await new Promise((resolve, reject) => {
|
|
315
|
+
canvas.toBlob((blob) => {
|
|
316
|
+
if (blob) {
|
|
317
|
+
resolve(blob);
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
reject(new BackgroundRemoverError("encode-failed", "PNG encoding failed."));
|
|
321
|
+
}, backgroundRemoverConfig.output.mimeType);
|
|
322
|
+
});
|
|
323
|
+
} finally {
|
|
324
|
+
bitmap.close();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
export {
|
|
328
|
+
BackgroundRemoverError,
|
|
329
|
+
backgroundRemoverConfig,
|
|
330
|
+
matchesImageSignature,
|
|
331
|
+
normalizeBackground,
|
|
332
|
+
preloadBackgroundRemovalModel,
|
|
333
|
+
removeBackground,
|
|
334
|
+
resetBackgroundRemovalModel,
|
|
335
|
+
validateInputImage
|
|
336
|
+
};
|
|
337
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config.ts","../src/errors.ts","../src/model.ts","../src/validation.ts","../src/image.ts"],"sourcesContent":["import type { BackgroundRemoverDevice } from './types';\n\nexport const backgroundRemoverConfig = {\n model: {\n id: 'onnx-community/ormbg-ONNX',\n dtype: 'q8',\n defaultDevice: 'auto' as BackgroundRemoverDevice,\n },\n input: {\n maxFileSizeBytes: 10 * 1024 * 1024,\n maxMegapixels: 36,\n supportedMimeTypes: ['image/jpeg', 'image/png', 'image/webp'] as const,\n },\n output: {\n mimeType: 'image/png' as const,\n defaultBackground: 'transparent' as const,\n },\n} as const;\n","export type BackgroundRemoverErrorCode =\n | 'unsupported-input'\n | 'file-too-large'\n | 'invalid-signature'\n | 'decode-failed'\n | 'too-many-pixels'\n | 'invalid-background'\n | 'model-load-failed'\n | 'processing-failed'\n | 'canvas-unavailable'\n | 'encode-failed';\n\nexport class BackgroundRemoverError extends Error {\n constructor(\n public readonly code: BackgroundRemoverErrorCode,\n message: string,\n options?: ErrorOptions\n ) {\n super(message, options);\n this.name = 'BackgroundRemoverError';\n }\n}\n","import { backgroundRemoverConfig } from './config';\nimport { BackgroundRemoverError } from './errors';\nimport type {\n BackgroundRemovalProgress,\n BackgroundRemoverBackend,\n BackgroundRemoverDevice,\n ModelLoadOptions,\n} from './types';\n\ntype ProgressCallback = ModelLoadOptions['onProgress'];\n\ntype ModelOutput = {\n width: number;\n height: number;\n toBlob: (type?: string) => Promise<Blob>;\n};\n\ntype ModelRunner = (input: Blob) => Promise<ModelOutput>;\n\ntype LoadedModel = {\n backend: BackgroundRemoverBackend;\n runner: ModelRunner;\n};\n\ntype TransformersModule = typeof import('@huggingface/transformers');\n\nconst modelPromises = new Map<BackgroundRemoverBackend, Promise<ModelRunner>>();\nconst progressListeners = new Map<BackgroundRemoverBackend, Set<NonNullable<ProgressCallback>>>();\nlet transformersPromise: Promise<TransformersModule> | null = null;\n\nexport async function preloadBackgroundRemovalModel(\n options: ModelLoadOptions = {}\n): Promise<BackgroundRemoverBackend> {\n const loaded = await resolveModel(options.device, options.onProgress);\n return loaded.backend;\n}\n\nexport function resetBackgroundRemovalModel(): void {\n modelPromises.clear();\n}\n\nexport async function runBackgroundRemovalModel(\n input: Blob,\n options: ModelLoadOptions = {}\n): Promise<{ output: ModelOutput; backend: BackgroundRemoverBackend }> {\n const device = options.device ?? backgroundRemoverConfig.model.defaultDevice;\n const loaded = await resolveModel(device, options.onProgress);\n options.onProgress?.({ phase: 'processing', backend: loaded.backend });\n\n try {\n return { output: await loaded.runner(input), backend: loaded.backend };\n } catch (error) {\n if (device === 'auto' && loaded.backend === 'webgpu') {\n modelPromises.delete('webgpu');\n options.onProgress?.({ phase: 'fallback', backend: 'wasm' });\n const fallback = await resolveModel('wasm', options.onProgress);\n options.onProgress?.({ phase: 'processing', backend: 'wasm' });\n try {\n return { output: await fallback.runner(input), backend: 'wasm' };\n } catch (fallbackError) {\n throw new BackgroundRemoverError(\n 'processing-failed',\n 'Background removal failed.',\n { cause: fallbackError }\n );\n }\n }\n\n throw new BackgroundRemoverError('processing-failed', 'Background removal failed.', {\n cause: error,\n });\n }\n}\n\nasync function resolveModel(\n device: BackgroundRemoverDevice | undefined,\n onProgress?: ProgressCallback\n): Promise<LoadedModel> {\n const requested = device ?? backgroundRemoverConfig.model.defaultDevice;\n if (requested === 'auto') {\n if (supportsWebGpu()) {\n try {\n return { backend: 'webgpu', runner: await getModel('webgpu', onProgress) };\n } catch {\n onProgress?.({ phase: 'fallback', backend: 'wasm' });\n }\n }\n return { backend: 'wasm', runner: await getModel('wasm', onProgress) };\n }\n\n return { backend: requested, runner: await getModel(requested, onProgress) };\n}\n\nasync function getModel(\n backend: BackgroundRemoverBackend,\n onProgress?: ProgressCallback\n): Promise<ModelRunner> {\n const listener = onProgress ?? undefined;\n if (listener) {\n getProgressListeners(backend).add(listener);\n }\n\n try {\n if (!modelPromises.has(backend)) {\n modelPromises.set(backend, createModel(backend));\n }\n return await modelPromises.get(backend)!;\n } catch (error) {\n modelPromises.delete(backend);\n if (error instanceof BackgroundRemoverError) {\n throw error;\n }\n throw new BackgroundRemoverError('model-load-failed', 'The AI model could not be loaded.', {\n cause: error,\n });\n } finally {\n if (listener) {\n getProgressListeners(backend).delete(listener);\n }\n }\n}\n\nasync function createModel(backend: BackgroundRemoverBackend): Promise<ModelRunner> {\n const transformers = await getTransformers();\n emitProgress(backend, { phase: 'loading-model', backend });\n\n try {\n const model = await transformers.pipeline(\n 'background-removal',\n backgroundRemoverConfig.model.id,\n {\n dtype: backgroundRemoverConfig.model.dtype,\n device: backend,\n progress_callback: (progress: unknown) => handleModelProgress(backend, progress),\n }\n );\n emitProgress(backend, { phase: 'ready', backend, progress: 100 });\n return model as unknown as ModelRunner;\n } catch (error) {\n throw new BackgroundRemoverError('model-load-failed', 'The AI model could not be loaded.', {\n cause: error,\n });\n }\n}\n\nasync function getTransformers(): Promise<TransformersModule> {\n if (!transformersPromise) {\n transformersPromise = import('@huggingface/transformers').then((transformers) => {\n transformers.env.allowLocalModels = false;\n transformers.env.allowRemoteModels = true;\n if ('useBrowserCache' in transformers.env) {\n transformers.env.useBrowserCache = true;\n }\n return transformers;\n });\n }\n return transformersPromise;\n}\n\nfunction handleModelProgress(backend: BackgroundRemoverBackend, value: unknown): void {\n if (!value || typeof value !== 'object') {\n return;\n }\n\n const progress = value as { status?: unknown; progress?: unknown };\n if (progress.status === 'progress' && Number.isFinite(Number(progress.progress))) {\n emitProgress(backend, {\n phase: 'downloading-model',\n backend,\n progress: Math.max(0, Math.min(100, Math.round(Number(progress.progress)))),\n });\n return;\n }\n\n if (progress.status === 'ready' || progress.status === 'done') {\n emitProgress(backend, { phase: 'initializing', backend });\n }\n}\n\nfunction supportsWebGpu(): boolean {\n if (typeof navigator === 'undefined') {\n return false;\n }\n return Boolean((navigator as Navigator & { gpu?: unknown }).gpu);\n}\n\nfunction getProgressListeners(\n backend: BackgroundRemoverBackend\n): Set<NonNullable<ProgressCallback>> {\n let listeners = progressListeners.get(backend);\n if (!listeners) {\n listeners = new Set();\n progressListeners.set(backend, listeners);\n }\n return listeners;\n}\n\nfunction emitProgress(\n backend: BackgroundRemoverBackend,\n progress: BackgroundRemovalProgress\n): void {\n for (const listener of getProgressListeners(backend)) {\n listener(progress);\n }\n}\n","import { backgroundRemoverConfig } from './config';\nimport { BackgroundRemoverError } from './errors';\n\nexport async function validateInputImage(input: Blob): Promise<{ width: number; height: number }> {\n const mimeType = String(input.type || '').toLowerCase();\n if (!backgroundRemoverConfig.input.supportedMimeTypes.includes(\n mimeType as (typeof backgroundRemoverConfig.input.supportedMimeTypes)[number]\n )) {\n throw new BackgroundRemoverError(\n 'unsupported-input',\n 'Only JPEG, PNG, and WebP images are supported.'\n );\n }\n\n if (input.size > backgroundRemoverConfig.input.maxFileSizeBytes) {\n throw new BackgroundRemoverError(\n 'file-too-large',\n 'The image exceeds the configured file size limit.'\n );\n }\n\n const signature = new Uint8Array(await input.slice(0, 12).arrayBuffer());\n if (!matchesImageSignature(signature, mimeType)) {\n throw new BackgroundRemoverError(\n 'invalid-signature',\n 'The image file signature does not match its MIME type.'\n );\n }\n\n let bitmap: ImageBitmap;\n try {\n bitmap = await createImageBitmap(input);\n } catch (error) {\n throw new BackgroundRemoverError('decode-failed', 'The image could not be decoded.', {\n cause: error,\n });\n }\n\n try {\n const megapixels = (bitmap.width * bitmap.height) / 1_000_000;\n if (megapixels > backgroundRemoverConfig.input.maxMegapixels) {\n throw new BackgroundRemoverError(\n 'too-many-pixels',\n 'The image dimensions exceed the configured pixel limit.'\n );\n }\n return { width: bitmap.width, height: bitmap.height };\n } finally {\n bitmap.close();\n }\n}\n\nexport function matchesImageSignature(bytes: Uint8Array, mimeType: string): boolean {\n if (mimeType === 'image/png') {\n return bytes.length >= 8\n && bytes[0] === 0x89\n && bytes[1] === 0x50\n && bytes[2] === 0x4e\n && bytes[3] === 0x47\n && bytes[4] === 0x0d\n && bytes[5] === 0x0a\n && bytes[6] === 0x1a\n && bytes[7] === 0x0a;\n }\n\n if (mimeType === 'image/webp') {\n return bytes.length >= 12\n && String.fromCharCode(...bytes.subarray(0, 4)) === 'RIFF'\n && String.fromCharCode(...bytes.subarray(8, 12)) === 'WEBP';\n }\n\n return bytes.length >= 3\n && bytes[0] === 0xff\n && bytes[1] === 0xd8\n && bytes[2] === 0xff;\n}\n\nexport function normalizeBackground(background: string): 'transparent' | string {\n if (background === 'transparent') {\n return background;\n }\n if (!/^#[0-9a-f]{6}$/i.test(background)) {\n throw new BackgroundRemoverError(\n 'invalid-background',\n 'Background must be \"transparent\" or a six-digit hex color.'\n );\n }\n return background.toUpperCase();\n}\n","import { backgroundRemoverConfig } from './config';\nimport { BackgroundRemoverError } from './errors';\nimport { runBackgroundRemovalModel } from './model';\nimport type { RemoveBackgroundOptions, RemoveBackgroundResult } from './types';\nimport { normalizeBackground, validateInputImage } from './validation';\n\nexport async function removeBackground(\n input: Blob,\n options: RemoveBackgroundOptions = {}\n): Promise<RemoveBackgroundResult> {\n await validateInputImage(input);\n\n const background = normalizeBackground(\n options.background ?? backgroundRemoverConfig.output.defaultBackground\n );\n\n const { output, backend } = await runBackgroundRemovalModel(input, options);\n\n let transparentBlob: Blob;\n try {\n transparentBlob = await output.toBlob(backgroundRemoverConfig.output.mimeType);\n } catch (error) {\n throw new BackgroundRemoverError(\n 'processing-failed',\n 'The background-removed image could not be created.',\n { cause: error }\n );\n }\n\n const blob = background === 'transparent'\n ? transparentBlob\n : await composeBackground(transparentBlob, background);\n\n return {\n blob,\n width: output.width,\n height: output.height,\n mimeType: backgroundRemoverConfig.output.mimeType,\n backend,\n };\n}\n\nasync function composeBackground(input: Blob, color: string): Promise<Blob> {\n let bitmap: ImageBitmap;\n try {\n bitmap = await createImageBitmap(input);\n } catch (error) {\n throw new BackgroundRemoverError(\n 'decode-failed',\n 'The background-removed image could not be decoded.',\n { cause: error }\n );\n }\n\n try {\n if (typeof OffscreenCanvas !== 'undefined') {\n const canvas = new OffscreenCanvas(bitmap.width, bitmap.height);\n const context = canvas.getContext('2d', { alpha: false });\n if (!context) {\n throw new BackgroundRemoverError('canvas-unavailable', 'Canvas 2D is unavailable.');\n }\n context.fillStyle = color;\n context.fillRect(0, 0, bitmap.width, bitmap.height);\n context.drawImage(bitmap, 0, 0);\n try {\n return await canvas.convertToBlob({ type: backgroundRemoverConfig.output.mimeType });\n } catch (error) {\n throw new BackgroundRemoverError('encode-failed', 'PNG encoding failed.', {\n cause: error,\n });\n }\n }\n\n if (typeof document === 'undefined') {\n throw new BackgroundRemoverError(\n 'canvas-unavailable',\n 'A browser canvas is required for a solid background.'\n );\n }\n\n const canvas = document.createElement('canvas');\n canvas.width = bitmap.width;\n canvas.height = bitmap.height;\n const context = canvas.getContext('2d', { alpha: false });\n if (!context) {\n throw new BackgroundRemoverError('canvas-unavailable', 'Canvas 2D is unavailable.');\n }\n\n context.fillStyle = color;\n context.fillRect(0, 0, bitmap.width, bitmap.height);\n context.drawImage(bitmap, 0, 0);\n\n return await new Promise<Blob>((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob) {\n resolve(blob);\n return;\n }\n reject(new BackgroundRemoverError('encode-failed', 'PNG encoding failed.'));\n }, backgroundRemoverConfig.output.mimeType);\n });\n } finally {\n bitmap.close();\n }\n}\n"],"mappings":"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","names":["canvas","context"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@resizebox/background-remover",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Browser-first AI background removal for JavaScript and TypeScript.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"homepage": "https://resizebox.com/remove-background",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"keywords": [
|
|
25
|
+
"background remover",
|
|
26
|
+
"remove background",
|
|
27
|
+
"image background removal",
|
|
28
|
+
"transparent background",
|
|
29
|
+
"onnx",
|
|
30
|
+
"webgpu",
|
|
31
|
+
"wasm",
|
|
32
|
+
"browser",
|
|
33
|
+
"typescript",
|
|
34
|
+
"resizebox"
|
|
35
|
+
],
|
|
36
|
+
"publishConfig": {
|
|
37
|
+
"access": "public"
|
|
38
|
+
},
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean --sourcemap --target es2022",
|
|
41
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
42
|
+
"test": "vitest run --config vitest.config.ts",
|
|
43
|
+
"verify": "npm run typecheck && npm run test && npm run build",
|
|
44
|
+
"pack:check": "npm pack --dry-run",
|
|
45
|
+
"prepublishOnly": "npm run verify"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@huggingface/transformers": "4.2.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"esbuild": "0.28.2",
|
|
52
|
+
"tsup": "^8.5.0",
|
|
53
|
+
"typescript": "^5.9.0",
|
|
54
|
+
"vitest": "^3.2.4"
|
|
55
|
+
},
|
|
56
|
+
"overrides": {
|
|
57
|
+
"adm-zip": "0.6.0",
|
|
58
|
+
"esbuild": "$esbuild",
|
|
59
|
+
"sharp": "0.35.4"
|
|
60
|
+
}
|
|
61
|
+
}
|