Background removal sounds like a tiny feature.
Upload image. Remove background. Return transparent PNG. Done.
Then your product gets real users.
Someone uploads a 12MB phone photo. Someone uploads a WebP. Someone uploads a logo with a white background. Someone uploads a blurry product image. Someone uploads a portrait where the hair blends into the wall. Someone uploads a file named final_final_REAL.png, and somehow it is actually a PDF.
So yes, background removal can be a simple AI feature.
But if you want to ship it inside an app, you need a proper API around it: file validation, secure upload handling, provider calls, error handling, response normalization, and maybe storage if your frontend needs a downloadable result.
In this guide, we’ll build a Background Removal API with FastAPI and Eden AI, so your app can remove image backgrounds without the manual editing circus.
What are we building?
We’ll build a small FastAPI backend that accepts an image upload, sends it to Eden AI’s background removal endpoint, and returns a clean result to the client.
The workflow looks like this:
frontend upload
→ FastAPI backend
→ Eden AI background removal
→ processed image URL or file
→ frontend displays/downloads result
Eden AI’s background removal documentation lists a universal feature path for image background removal and shows providers behind one standardized API layer, which is useful when you want one integration pattern instead of wiring every image provider separately.
We’ll build:
- A
/remove-backgroundendpoint. - File type and file size checks.
- Eden AI request handling.
- Normalized JSON response.
- Download/proxy helper for returning the processed image.
- Cleaner production notes for security, storage, and frontend use.
Why FastAPI?
FastAPI is a good fit for this kind of image API because it makes upload endpoints clean and developer-friendly.
FastAPI supports file uploads with UploadFile, and its docs explain that UploadFile exposes a file-like object, which is useful when you need to pass uploaded files to libraries or HTTP clients. FastAPI’s file upload tutorial also notes that you need python-multipart to receive uploaded files from forms.
In normal language:
FastAPI makes image upload endpoints less annoying.
That is exactly what we need.
Why Eden AI?
Eden AI is useful here because it gives you one API layer for AI features, including background removal.
Instead of directly integrating separate providers one by one, you can call Eden AI’s background removal feature and choose a provider through the payload. Eden AI’s Python background removal tutorial shows the endpoint pattern:
https://api.edenai.run/v2/image/background_removal
and demonstrates sending an image file with an authorization bearer token.
That makes Eden AI a practical choice for:
- Fast prototypes.
- Multi-provider AI apps.
- No-code or low-code workflows.
- Apps that may switch providers later.
- Teams that want one billing/API layer for several AI features.
What does background removal return?
A background removal API usually returns either:
- A URL to the processed image.
- A base64 image.
- A provider-specific result object.
- A status/error object.
The exact response shape can vary by Eden AI provider, so our backend should normalize the response before returning it to the frontend.
A frontend-friendly response might look like this:
{
"status": "success",
"provider": "api4ai",
"image_url": "https://...",
"original_filename": "product-photo.jpg"
}
If something fails, return a clean error:
{
"status": "error",
"message": "Background removal failed.",
"details": "Provider did not return a processed image."
}
That way your frontend does not need to understand every provider’s raw response format.
Step 1: Create the project
Create a new folder:
mkdir background-removal-api
cd background-removal-api
Create a virtual environment:
python -m venv .venv
Activate it.
On macOS/Linux:
source .venv/bin/activate
On Windows PowerShell:
.venv\Scripts\Activate.ps1
Install dependencies:
pip install fastapi uvicorn python-multipart requests python-dotenv pillow
We’ll use:
| Package | Why |
|---|---|
fastapi | Build the API |
uvicorn | Run the FastAPI app |
python-multipart | Handle file uploads |
requests | Call Eden AI |
python-dotenv | Load environment variables |
pillow | Validate image dimensions and format |
Step 2: Add environment variables
Create a .env file:
EDENAI_API_KEY=your_edenai_api_key_here
EDENAI_BACKGROUND_REMOVAL_URL=https://api.edenai.run/v2/image/background_removal
EDENAI_PROVIDER=api4ai
MAX_UPLOAD_MB=10
Do not hardcode your Eden AI API key in the Python file.
That small decision saves you from accidentally pushing secrets to GitHub and ruining your afternoon.
Step 3: Create the FastAPI app
Create main.py:
import os
from fastapi import FastAPI
from dotenv import load_dotenv
load_dotenv()
app = FastAPI(
title="Background Removal API",
description="Remove image backgrounds with FastAPI and Eden AI.",
version="1.0.0"
)
@app.get("/health")
def health_check():
return {
"status": "ok"
}
Run the app:
uvicorn main:app --reload
Open:
http://127.0.0.1:8000/health
You should see:
{
"status": "ok"
}
FastAPI also gives you interactive docs at:
http://127.0.0.1:8000/docs
That is helpful for testing uploads without building a frontend first.
Step 4: Add upload validation
Before calling Eden AI, we should reject bad uploads.
We’ll check:
- File exists.
- MIME type is allowed.
- File size is under the limit.
- Image can be opened by Pillow.
Add this to main.py:
import os
import io
from PIL import Image
from fastapi import UploadFile, HTTPException
ALLOWED_MIME_TYPES = {
"image/jpeg",
"image/png",
"image/webp"
}
def get_max_upload_bytes() -> int:
max_mb = int(os.getenv("MAX_UPLOAD_MB", "10"))
return max_mb * 1024 * 1024
async def validate_image_upload(file: UploadFile) -> bytes:
if file.content_type not in ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=400,
detail=f"Unsupported file type: {file.content_type}"
)
content = await file.read()
if len(content) > get_max_upload_bytes():
raise HTTPException(
status_code=413,
detail="Image is too large."
)
try:
image = Image.open(io.BytesIO(content))
image.verify()
except Exception as exc:
raise HTTPException(
status_code=400,
detail="Uploaded file is not a valid image."
) from exc
return content
This gives us a clean validation layer.
One important detail: after reading UploadFile, the file pointer is consumed. Since we return the bytes, we can send those bytes directly to Eden AI instead of trying to read the file again.
Step 5: Call Eden AI’s background removal API
Now create a helper function.
Add this to main.py:
import requests
def call_edenai_background_removal(
image_bytes: bytes,
filename: str,
content_type: str
) -> dict:
api_key = os.getenv("EDENAI_API_KEY")
endpoint = os.getenv(
"EDENAI_BACKGROUND_REMOVAL_URL",
"https://api.edenai.run/v2/image/background_removal"
)
provider = os.getenv("EDENAI_PROVIDER", "api4ai")
if not api_key:
raise HTTPException(
status_code=500,
detail="EDENAI_API_KEY is not configured."
)
headers = {
"Authorization": f"Bearer {api_key}"
}
data = {
"providers": provider
}
files = {
"file": (filename, image_bytes, content_type)
}
try:
response = requests.post(
endpoint,
data=data,
files=files,
headers=headers,
timeout=60
)
except requests.RequestException as exc:
raise HTTPException(
status_code=502,
detail="Could not reach Eden AI."
) from exc
if response.status_code >= 400:
raise HTTPException(
status_code=502,
detail={
"message": "Eden AI returned an error.",
"status_code": response.status_code,
"response": response.text
}
)
return response.json()
This function sends the image to Eden AI and returns the raw JSON response.
The important parts:
- API key goes in the
Authorizationheader. - Image is sent as multipart form data.
- Provider is passed in the form payload.
- Timeout prevents the request from hanging forever.
- Errors are translated into API-friendly responses.
Step 6: Normalize Eden AI’s response
Provider responses can vary, so let’s create a normalizer.
Add:
def normalize_edenai_response(raw_response: dict, provider: str) -> dict:
provider_result = raw_response.get(provider)
if not provider_result:
return {
"status": "error",
"provider": provider,
"message": "Provider result was not found in Eden AI response.",
"raw": raw_response
}
if provider_result.get("status") == "fail":
return {
"status": "error",
"provider": provider,
"message": provider_result.get("error", "Provider failed."),
"raw": provider_result
}
image_url = (
provider_result.get("image_resource_url")
or provider_result.get("image_url")
or provider_result.get("url")
)
if not image_url:
return {
"status": "error",
"provider": provider,
"message": "No processed image URL found in provider response.",
"raw": provider_result
}
return {
"status": "success",
"provider": provider,
"image_url": image_url,
"raw": provider_result
}
This keeps your frontend from dealing with raw provider quirks.
If Eden AI or the provider changes response fields, you only update this normalizer.
Step 7: Create the /remove-background endpoint
Now add the actual endpoint.
from fastapi import File
@app.post("/remove-background")
async def remove_background(file: UploadFile = File(...)):
image_bytes = await validate_image_upload(file)
provider = os.getenv("EDENAI_PROVIDER", "api4ai")
raw_response = call_edenai_background_removal(
image_bytes=image_bytes,
filename=file.filename or "image.png",
content_type=file.content_type or "image/png"
)
normalized = normalize_edenai_response(raw_response, provider)
if normalized["status"] == "error":
raise HTTPException(
status_code=502,
detail=normalized
)
return {
"status": "success",
"original_filename": file.filename,
"provider": normalized["provider"],
"image_url": normalized["image_url"]
}
Run the app again:
uvicorn main:app --reload
Then test from FastAPI docs:
http://127.0.0.1:8000/docs
Upload an image to /remove-background.
You should get a JSON response with the processed image URL.
Step 8: Test with cURL
You can also test from the terminal:
curl -X POST "http://127.0.0.1:8000/remove-background" \
-F "[email protected]"
Example response:
{
"status": "success",
"original_filename": "product-photo.jpg",
"provider": "api4ai",
"image_url": "https://..."
}
That is the basic API working.
Step 9: Return the processed image through your API
Sometimes you do not want the frontend to use the provider URL directly.
Maybe you want to proxy the image through your backend. Maybe you want to hide provider URLs. Maybe you want to control caching or downloads.
Add a helper endpoint that downloads an image from a URL and streams it back.
from fastapi.responses import StreamingResponse
from urllib.parse import urlparse
def validate_result_url(url: str) -> None:
parsed = urlparse(url)
if parsed.scheme not in {"http", "https"}:
raise HTTPException(
status_code=400,
detail="Invalid image URL."
)
@app.get("/download-result")
def download_result(url: str):
validate_result_url(url)
try:
response = requests.get(url, stream=True, timeout=60)
except requests.RequestException as exc:
raise HTTPException(
status_code=502,
detail="Could not download processed image."
) from exc
if response.status_code >= 400:
raise HTTPException(
status_code=502,
detail="Processed image could not be downloaded."
)
return StreamingResponse(
response.iter_content(chunk_size=8192),
media_type=response.headers.get("content-type", "image/png"),
headers={
"Content-Disposition": "attachment; filename=background-removed.png"
}
)
Now the frontend can call:
GET /download-result?url=<processed-image-url>
For production, be more strict with URL validation. Open proxy endpoints can become a security problem if you let users pass any random URL.
Step 10: Save the processed result locally
For some apps, returning a URL is enough.
For others, you may want to save the processed image to your own storage.
Example local save:
from pathlib import Path
from uuid import uuid4
RESULTS_DIR = Path("results")
RESULTS_DIR.mkdir(exist_ok=True)
def save_processed_image(image_url: str) -> str:
validate_result_url(image_url)
response = requests.get(image_url, timeout=60)
if response.status_code >= 400:
raise HTTPException(
status_code=502,
detail="Could not download processed image."
)
output_filename = f"{uuid4()}.png"
output_path = RESULTS_DIR / output_filename
output_path.write_bytes(response.content)
return str(output_path)
Then update the endpoint:
@app.post("/remove-background-and-save")
async def remove_background_and_save(file: UploadFile = File(...)):
image_bytes = await validate_image_upload(file)
provider = os.getenv("EDENAI_PROVIDER", "api4ai")
raw_response = call_edenai_background_removal(
image_bytes=image_bytes,
filename=file.filename or "image.png",
content_type=file.content_type or "image/png"
)
normalized = normalize_edenai_response(raw_response, provider)
if normalized["status"] == "error":
raise HTTPException(
status_code=502,
detail=normalized
)
saved_path = save_processed_image(normalized["image_url"])
return {
"status": "success",
"original_filename": file.filename,
"provider": normalized["provider"],
"image_url": normalized["image_url"],
"saved_path": saved_path
}
For production, save to object storage like S3, Google Cloud Storage, Azure Blob Storage, or another secure storage layer instead of local disk.
Step 11: Add CORS for frontend apps
If a browser frontend calls this API from another origin, add CORS.
pip install fastapi[standard]
Then in main.py:
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:5173"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Keep CORS strict in production.
Avoid this in a real deployed app:
allow_origins=["*"]
That may be fine for quick local testing, but it is too loose for production.
Step 12: Create a tiny frontend test
Here is a simple HTML file you can use for testing.
Create test.html:
<!doctype html>
<html>
<head>
<title>Background Removal Test</title>
</head>
<body>
<h1>Remove background</h1>
<input id="fileInput" type="file" accept="image/*" />
<button id="uploadButton">Upload</button>
<pre id="output"></pre>
<img id="resultImage" style="max-width: 400px;" />
<script>
const fileInput = document.getElementById("fileInput");
const uploadButton = document.getElementById("uploadButton");
const output = document.getElementById("output");
const resultImage = document.getElementById("resultImage");
uploadButton.addEventListener("click", async () => {
const file = fileInput.files[0];
if (!file) {
output.textContent = "Please choose an image first.";
return;
}
const formData = new FormData();
formData.append("file", file);
const response = await fetch("http://127.0.0.1:8000/remove-background", {
method: "POST",
body: formData
});
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
if (data.image_url) {
resultImage.src = data.image_url;
}
});
</script>
</body>
</html>
Open the file in your browser and test.
This is not fancy. It just proves the backend works.
Full FastAPI example
Here is the complete main.py version.
import os
import io
from pathlib import Path
from uuid import uuid4
from urllib.parse import urlparse
import requests
from dotenv import load_dotenv
from PIL import Image
from fastapi import FastAPI, File, UploadFile, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
load_dotenv()
app = FastAPI(
title="Background Removal API",
description="Remove image backgrounds with FastAPI and Eden AI.",
version="1.0.0"
)
app.add_middleware(
CORSMiddleware,
allow_origins=[
"http://localhost:3000",
"http://localhost:5173"
],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
ALLOWED_MIME_TYPES = {
"image/jpeg",
"image/png",
"image/webp"
}
RESULTS_DIR = Path("results")
RESULTS_DIR.mkdir(exist_ok=True)
def get_max_upload_bytes() -> int:
max_mb = int(os.getenv("MAX_UPLOAD_MB", "10"))
return max_mb * 1024 * 1024
async def validate_image_upload(file: UploadFile) -> bytes:
if file.content_type not in ALLOWED_MIME_TYPES:
raise HTTPException(
status_code=400,
detail=f"Unsupported file type: {file.content_type}"
)
content = await file.read()
if len(content) > get_max_upload_bytes():
raise HTTPException(
status_code=413,
detail="Image is too large."
)
try:
image = Image.open(io.BytesIO(content))
image.verify()
except Exception as exc:
raise HTTPException(
status_code=400,
detail="Uploaded file is not a valid image."
) from exc
return content
def call_edenai_background_removal(
image_bytes: bytes,
filename: str,
content_type: str
) -> dict:
api_key = os.getenv("EDENAI_API_KEY")
endpoint = os.getenv(
"EDENAI_BACKGROUND_REMOVAL_URL",
"https://api.edenai.run/v2/image/background_removal"
)
provider = os.getenv("EDENAI_PROVIDER", "api4ai")
if not api_key:
raise HTTPException(
status_code=500,
detail="EDENAI_API_KEY is not configured."
)
headers = {
"Authorization": f"Bearer {api_key}"
}
data = {
"providers": provider
}
files = {
"file": (filename, image_bytes, content_type)
}
try:
response = requests.post(
endpoint,
data=data,
files=files,
headers=headers,
timeout=60
)
except requests.RequestException as exc:
raise HTTPException(
status_code=502,
detail="Could not reach Eden AI."
) from exc
if response.status_code >= 400:
raise HTTPException(
status_code=502,
detail={
"message": "Eden AI returned an error.",
"status_code": response.status_code,
"response": response.text
}
)
return response.json()
def normalize_edenai_response(raw_response: dict, provider: str) -> dict:
provider_result = raw_response.get(provider)
if not provider_result:
return {
"status": "error",
"provider": provider,
"message": "Provider result was not found in Eden AI response.",
"raw": raw_response
}
if provider_result.get("status") == "fail":
return {
"status": "error",
"provider": provider,
"message": provider_result.get("error", "Provider failed."),
"raw": provider_result
}
image_url = (
provider_result.get("image_resource_url")
or provider_result.get("image_url")
or provider_result.get("url")
)
if not image_url:
return {
"status": "error",
"provider": provider,
"message": "No processed image URL found in provider response.",
"raw": provider_result
}
return {
"status": "success",
"provider": provider,
"image_url": image_url,
"raw": provider_result
}
def validate_result_url(url: str) -> None:
parsed = urlparse(url)
if parsed.scheme not in {"http", "https"}:
raise HTTPException(
status_code=400,
detail="Invalid image URL."
)
def save_processed_image(image_url: str) -> str:
validate_result_url(image_url)
response = requests.get(image_url, timeout=60)
if response.status_code >= 400:
raise HTTPException(
status_code=502,
detail="Could not download processed image."
)
output_filename = f"{uuid4()}.png"
output_path = RESULTS_DIR / output_filename
output_path.write_bytes(response.content)
return str(output_path)
@app.get("/health")
def health_check():
return {
"status": "ok"
}
@app.post("/remove-background")
async def remove_background(file: UploadFile = File(...)):
image_bytes = await validate_image_upload(file)
provider = os.getenv("EDENAI_PROVIDER", "api4ai")
raw_response = call_edenai_background_removal(
image_bytes=image_bytes,
filename=file.filename or "image.png",
content_type=file.content_type or "image/png"
)
normalized = normalize_edenai_response(raw_response, provider)
if normalized["status"] == "error":
raise HTTPException(
status_code=502,
detail=normalized
)
return {
"status": "success",
"original_filename": file.filename,
"provider": normalized["provider"],
"image_url": normalized["image_url"]
}
@app.post("/remove-background-and-save")
async def remove_background_and_save(file: UploadFile = File(...)):
image_bytes = await validate_image_upload(file)
provider = os.getenv("EDENAI_PROVIDER", "api4ai")
raw_response = call_edenai_background_removal(
image_bytes=image_bytes,
filename=file.filename or "image.png",
content_type=file.content_type or "image/png"
)
normalized = normalize_edenai_response(raw_response, provider)
if normalized["status"] == "error":
raise HTTPException(
status_code=502,
detail=normalized
)
saved_path = save_processed_image(normalized["image_url"])
return {
"status": "success",
"original_filename": file.filename,
"provider": normalized["provider"],
"image_url": normalized["image_url"],
"saved_path": saved_path
}
@app.get("/download-result")
def download_result(url: str):
validate_result_url(url)
try:
response = requests.get(url, stream=True, timeout=60)
except requests.RequestException as exc:
raise HTTPException(
status_code=502,
detail="Could not download processed image."
) from exc
if response.status_code >= 400:
raise HTTPException(
status_code=502,
detail="Processed image could not be downloaded."
)
return StreamingResponse(
response.iter_content(chunk_size=8192),
media_type=response.headers.get("content-type", "image/png"),
headers={
"Content-Disposition": "attachment; filename=background-removed.png"
}
)
Run it:
uvicorn main:app --reload
How to choose the Eden AI provider
Eden AI’s background removal docs list available provider routes and pricing per 1,000 files. That gives you a way to compare cost and behavior without changing your entire app.
Provider choice may depend on:
- Price.
- Image quality.
- Latency.
- Maximum file size.
- Output format.
- Subject type: people, products, animals, logos.
- Edge quality around hair/fur/glass.
- Background complexity.
- Batch volume.
- Commercial usage needs.
Test with your own image set.
Use:
- Product photos.
- Portraits.
- Hair/fur edges.
- Transparent objects.
- White objects on white backgrounds.
- Shadows.
- Busy backgrounds.
- Low-light images.
- Mobile photos.
- Logos and graphics.
Background removal looks perfect in demos and gets spicy in production.
Production tips
A production background removal API needs more than one endpoint.
Add authentication
Do not expose the endpoint publicly with no controls.
Use:
- API keys.
- JWT auth.
- Session auth.
- Rate limits.
- Per-user quotas.
Add rate limiting
Image APIs can get expensive fast.
Add rate limits by:
- User ID.
- IP address.
- Workspace.
- API key.
- Subscription tier.
Store only what you need
Images can contain personal or sensitive content.
Decide:
- Do you store original uploads?
- Do you store processed images?
- How long do you keep them?
- Can users delete them?
- Are images encrypted at rest?
- Are logs redacted?
Track usage
Log:
| Field | Why it matters |
|---|---|
| User ID | Billing and abuse checks |
| File size | Cost and performance |
| Provider | Quality and cost tracking |
| Latency | User experience |
| Status | Debugging |
| Error type | Reliability |
| Output URL | Result tracking |
| Timestamp | Audit and analytics |
Add review for risky uploads
If your app processes marketplace images, ads, profile photos, or identity documents, add safety checks.
A background removal feature should not become a way to launder problematic images through your app.
Common errors and fixes
Here are the issues you’ll probably hit.
| Error | Likely cause | Fix |
|---|---|---|
RuntimeError: Form data requires python-multipart | Missing upload dependency | Install python-multipart |
Unsupported file type | User uploaded PDF/GIF/etc. | Allow only image formats or add conversion |
Image is too large | File exceeds limit | Compress image or raise limit |
EDENAI_API_KEY is not configured | Missing .env variable | Add key and reload app |
Could not reach Eden AI | Network/provider issue | Retry, fallback, or show friendly error |
No processed image URL found | Response shape changed/provider failed | Inspect raw response and update normalizer |
| CORS error in browser | Frontend origin blocked | Add frontend URL to CORS config |
| Poor cutout quality | Hard image/background | Try another provider or ask for better image |
The normalizer is the first place to check when the Eden AI response does not look how your code expects.
Where LLMAPI fits
LLMAPI can fit around this background removal workflow when your app needs text generation, routing, metadata, or automation after the image is processed.
Background removal gives you an edited image.
LLMAPI can help with what happens next.
| Task | Example |
|---|---|
| Product listing text | Generate marketplace title and description |
| Image QA summary | Explain whether image may need manual editing |
| Workflow routing | Send failed cutouts to human design review |
| Alt text | Generate accessible image descriptions |
| Ad copy | Create ad variants for processed product images |
| Metadata generation | Suggest tags and categories |
| Support message | Explain to users why an upload failed |
| Batch report | Summarize background removal jobs by status |
Example workflow:
image upload
→ FastAPI
→ Eden AI background removal
→ processed image
→ LLMAPI generates product copy / alt text / review note
→ app displays final asset
This works especially well for e-commerce tools, creator apps, marketplace uploads, social media tools, ad platforms, and internal content operations.
Example: background removal plus product copy
Imagine a user uploads a product photo.
Your app removes the background, then uses LLMAPI to create a listing draft.
Processed image is ready.
Now generate:
1. Product title
2. Short description
3. 5 tags
4. Alt text
That gives the user more than a cutout.
It gives them a finished asset workflow.
A simple production workflow
Here is the version we would ship first:
user uploads image
→ FastAPI validates file
→ FastAPI calls Eden AI
→ app receives processed image URL
→ app saves result to storage
→ frontend displays result
→ optional LLMAPI metadata/copy generation
Then add:
- Auth.
- Rate limits.
- Usage logs.
- Storage cleanup.
- Provider fallback.
- Better error messages.
- Frontend image preview.
- Batch processing.
- Optional manual review queue.
- Billing or quota tracking.
That gives you a real product feature, not just a demo script.
The practical takeaway
You can build a background removal API with FastAPI by accepting image uploads, validating them, sending them to Eden AI’s background removal endpoint, normalizing the response, and returning a processed image URL to your frontend.
The basic flow is:
FastAPI upload → Eden AI background removal → normalized response → frontend result
Use FastAPI for the backend wrapper. Use Eden AI for background removal. Add validation so your API does not accept messy files blindly. Normalize provider responses so your frontend stays clean. Add storage, authentication, rate limits, and logs before production. Use LLMAPI after background removal when your product needs alt text, product copy, metadata, support messages, or workflow routing.
That is how background removal becomes a product feature instead of a one-off image trick.