mtownsend/remove-bg
Remove backgrounds from images in PHP and Laravel with a simple wrapper around the remove.bg API. Pass a file path, URL, or binary image and get a transparent PNG back in one call, with clean error handling for size limits and failed requests.
Install
composer require mtownsend/remove-bg
Laravel 5.5+ auto-discovers Mtownsend\RemoveBg\RemoveBgServiceProvider and registers the RemoveBg facade alias. On older/non-Laravel setups add the provider and alias manually in config/app.php.
Configure credentials
php artisan vendor:publish --provider="Mtownsend\RemoveBg\RemoveBgServiceProvider" --tag="config"
Set REMOVE_BG_API_KEY in .env. The key is the only hard requirement; everything else has sane defaults. Create a key at https://www.remove.bg/api and note that calls are billed against your account's credits.
Where to look first
config/removebg.php — the published config; confirms the env var name and any default size/format settings.src/RemoveBg.php — the single class that matters; read file(), url(), base64(), save(), getContents(), getBase64() to learn the fluent surface.src/Exceptions/ — every failure mode (InvalidApiKey, InvalidImage, etc.) is a concrete exception class; these are what you catch.First use case
Background removal on a user-uploaded profile photo, saved to storage:
use Mtownsend\RemoveBg\RemoveBg;
$contents = RemoveBg::file($request->file('avatar')->getRealPath())
->getContents();
Storage::disk('public')->put("avatars/{$id}.png", $contents);
That is the 90% use case. Everything else is variations on "get bytes out, put bytes somewhere."
Source ingestion — three entry points
The package mirrors remove.bg's three inputs:
RemoveBg::file($path) — local path or an UploadedFile/SplFileInfo. Pass $request->file('photo') directly rather than calling getRealPath(); the wrapper handles Illuminate\Http\UploadedFile.RemoveBg::url($url) — remove.bg fetches the image server-side. Only works for publicly reachable URLs; do not hand it signed S3 URLs with tiny TTLs or localhost addresses.RemoveBg::base64($data) — raw base64 string, without the data:image/png;base64, prefix. Strip it before passing.Output — three sinks
->getContents() — raw binary. The most portable; pipe into Storage::put(), response()->stream(), or file_put_contents().->getBase64() — encoded string for JSON responses or <img src="data:..."> embedding.->save($path) — writes the result to disk in one call. Convenient for local dev; prefer getContents() + Storage in production so you control the disk, visibility, and path prefix.Controller pattern — synchronous
Fine for a single-image, admin-triggered operation or a prototype:
public function store(Request $request)
{
$request->validate(['photo' => 'required|image|max:12000']);
try {
$bytes = RemoveBg::file($request->file('photo'))->getContents();
} catch (\Mtownsend\RemoveBg\Exceptions\RemoveBgException $e) {
report($e);
return back()->withErrors('Background removal failed, try again.');
}
$path = "processed/".Str::uuid().".png";
Storage::disk('s3')->put($path, $bytes);
return response()->json(['path' => $path]);
}
Job pattern — asynchronous
remove.bg latency is typically 2–8 seconds; never block a web request on it in production. Wrap the call in a queued job:
class RemoveImageBackground implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 30;
public function __construct(public int $imageId) {}
public function handle(): void
{
$image = Image::findOrFail($this->imageId);
try {
$bytes = RemoveBg::file(Storage::disk('local')->path($image->original_path))
->getContents();
Storage::disk('s3')->put($image->clean_path, $bytes);
$image->update(['status' => 'clean']);
} catch (RemoveBgException $e) {
// Don't retry 402 (out of credits) or 400 (bad input)
if (in_array($e->getCode(), [400, 402])) {
$image->update(['status' => 'failed']);
return;
}
throw $e; // 5xx and network errors are worth retrying
}
}
}
The nuance: this package surfaces the upstream HTTP status in the exception code, so branch on getCode() to distinguish "retry" (5xx, timeouts) from "don't bother" (client errors, credit exhaustion).
Chaining / options
Where the wrapper exposes optional parameters it will be fluent:
RemoveBg::file($path)
->size('preview') // 'preview' | 'full' | 'auto'
->save($dst);
Check the README for the exact supported set — the wrapper's surface is a subset of remove.bg's API, and features added upstream after the last release won't be there (see Gotchas).
Testing
Never call the real API in tests — you'll burn credits and slow CI. Either fake the facade or swap the boundary:
RemoveBg::shouldReceive('file')
->once()
->andReturnSelf();
RemoveBg::shouldReceive('getContents')
->once()
->andReturn(file_get_contents(base_path('tests/fixtures/clean.png')));
The fixture-based approach is preferable: it forces you to keep the file() → getContents() boundary narrow in production code, which makes it trivially mockable.
Integration tips
Package health is the biggest gotcha. Last release was 2023-06-08, zero dependents, low overall score. The wrapper is thin, so the risk is not "it will break tomorrow" but "new remove.bg API parameters won't be surfaced." If you need type=car, channels=rgba, bg_color, or add_shadow, verify the fluent methods actually exist in src/RemoveBg.php before architecting around them — otherwise you'll be subclassing or forking. Since it's MIT and one class, a local fork or a thin hand-rolled Guzzle client are equally viable if the API surface is insufficient.
Every successful call costs a credit. The remove.bg API is credit-based, with the free tier limited to preview size (roughly 625×400). If you need full-resolution output you must be on a paid plan and request size=full — a "free" preview that looks fine in dev can be silently upscaled garbage in prod. Add a hard alert on 402 responses; running out of credits mid-deploy is a classic outage.
Input constraints are enforced upstream, not by the package. remove.bg caps at ~12 MB and ~25 megapixels. Validate with Laravel's max:12000 (KB) rule and check dimensions, otherwise you'll pay for the round trip just to get a 400 back.
Base64 prefix will break you. removebg's base64 input expects a bare string. Passing a full data URI yields a 400 that reads like a generic "invalid image." Strip with Str::after($data, ',') defensively.
Exceptions are your only error signal. The wrapper throws rather than returning nulls, but the exception classes and codes are the upstream ones. Log $e->getCode() and $e->getMessage() — they carry the remove.bg reason text (insufficient_credits, unknown_foreground, etc.), which is far more useful than the class name.
Timeouts default to whatever Guzzle uses (usually none/infinite). If you don't wrap the call in a queue with a timeout, a hung remove.bg request can tie up a PHP-FPM worker. Set timeout in the config if the package exposes it, or wrap the whole job in a try { ... } finally with set_time_limit().
save() semantics vary with your filesystem. If you pass a relative path it resolves relative to the PHP working directory, not storage_path(). Use absolute paths or Storage::path(). In practice, prefer getContents() + Storage and avoid save() entirely in app code — it keeps the package out of your storage abstraction.
Facade alias collisions. The alias RemoveBg is generic. In a large app with another vendor package or your own class named similarly, you'll get a silent resolution to the wrong class. Register your own alias or inject Mtownsend\RemoveBg\RemoveBg via constructor rather than relying on the global facade.
API key handling. The key lives in config, which is cached. If you rotate keys, you must run php artisan config:cache again — a stale cached key produces 401s that look like a permissions issue. Also: don't log the key, and be aware remove.bg processes your images on their servers — for user-submitted or sensitive imagery, disclose this in your privacy policy and make sure it's in your data processing agreement.
Debugging recipe. When a call fails opaquely: (1) dd($e->getCode(), $e->getMessage()); (2) hit remove.bg's /api/v1/account endpoint with the same key to confirm credits are non-zero; (3) try the same file via curl -F image_file=@... against the raw API — if that succeeds and the wrapper fails, it's a payload/multipart issue in the wrapper, and you're now justified in forking or bypassing it.
How can I help you explore Laravel packages today?