Skip to content

Commit 3d7e30d

Browse files
authored
Merge pull request #17 from Kundan-Krishna366/feature/heic-support
Add Native HEIC Support and Fix EXIF Orientation
2 parents d7220b2 + d12a41c commit 3d7e30d

3 files changed

Lines changed: 121 additions & 33 deletions

File tree

apps/web/backend/app/main.py

Lines changed: 40 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@
99
from fastapi.middleware.cors import CORSMiddleware
1010
from fastapi.responses import Response, JSONResponse, FileResponse
1111
from fastapi.staticfiles import StaticFiles
12-
from PIL import Image
12+
from PIL import Image, ImageOps
13+
import pillow_heif # Required for HEIF/HEIC decoding
1314
import uvicorn
1415

16+
1517
# Import withoutbg package (install via: uv sync or pip install -e ../../../packages/python)
1618
from withoutbg import WithoutBG, __version__
1719
from withoutbg.exceptions import WithoutBGError
1820
from withoutbg.api import ProAPI
1921

22+
# Enable HEIF support globally for Pillow
23+
pillow_heif.register_heif_opener()
24+
2025
app = FastAPI(
2126
title="withoutbg API",
2227
description="AI-powered background removal API",
@@ -56,7 +61,7 @@ async def health_check():
5661
"status": "healthy",
5762
"version": __version__,
5863
"service": "withoutbg-api",
59-
"models_loaded": _model is not None
64+
"models_loaded": _model is not None,
6065
}
6166

6267

@@ -70,24 +75,36 @@ async def remove_background_endpoint(
7075
"""
7176
Remove background from a single image.
7277
78+
Supports standard formats (PNG, JPG, WebP) and native Apple HEIC/HEIF files.
79+
Automatically handles EXIF orientation to ensure upright output.
80+
7381
Args:
7482
file: Image file to process
7583
format: Output format (png, jpg, webp)
7684
quality: Quality for JPEG output (1-100)
7785
api_key: Optional API key for cloud processing
78-
86+
7987
Returns:
8088
Processed image with background removed
8189
"""
8290
try:
83-
# Validate file type
84-
if not file.content_type or not file.content_type.startswith("image/"):
85-
raise HTTPException(status_code=400, detail="File must be an image")
86-
91+
# Support standard image types and native Apple HEIC/HEIF
92+
is_image = file.content_type and file.content_type.startswith("image/")
93+
is_heic = file.filename and file.filename.lower().endswith((".heic", ".heif"))
94+
95+
if not (is_image or is_heic):
96+
raise HTTPException(
97+
status_code=400, detail="File must be an image (JPEG, PNG, or HEIC)"
98+
)
99+
87100
# Read uploaded file
88101
contents = await file.read()
89-
input_image = Image.open(io.BytesIO(contents))
90-
102+
raw_image = Image.open(io.BytesIO(contents))
103+
104+
# 1. Apply EXIF orientation (prevents rotated mobile uploads)
105+
# 2. Force RGBA for consistency across inference models
106+
input_image = ImageOps.exif_transpose(raw_image).convert("RGBA")
107+
91108
# Process image using appropriate model
92109
if api_key:
93110
# Use API for this specific request
@@ -98,13 +115,13 @@ async def remove_background_endpoint(
98115
if _model is None:
99116
raise HTTPException(
100117
status_code=503,
101-
detail="Models not loaded. Server may still be starting up."
118+
detail="Models not loaded. Server may still be starting up.",
102119
)
103120
result = _model.remove_background(input_image)
104-
121+
105122
# Convert result to bytes
106123
output_buffer = io.BytesIO()
107-
124+
108125
# Handle format conversion
109126
if format.lower() in ["jpg", "jpeg"]:
110127
# Convert RGBA to RGB for JPEG
@@ -121,17 +138,15 @@ async def remove_background_endpoint(
121138
else: # PNG
122139
result.save(output_buffer, format="PNG")
123140
media_type = "image/png"
124-
141+
125142
output_buffer.seek(0)
126-
143+
127144
return Response(
128145
content=output_buffer.getvalue(),
129146
media_type=media_type,
130-
headers={
131-
"Content-Disposition": f"inline; filename=withoutbg.{format}"
132-
}
147+
headers={"Content-Disposition": f"inline; filename=withoutbg.{format}"},
133148
)
134-
149+
135150
except WithoutBGError as e:
136151
raise HTTPException(status_code=500, detail=str(e))
137152
except Exception as e:
@@ -142,10 +157,10 @@ async def remove_background_endpoint(
142157
async def get_usage_endpoint(api_key: str):
143158
"""
144159
Get API usage statistics.
145-
160+
146161
Args:
147162
api_key: API key for cloud service
148-
163+
149164
Returns:
150165
Usage statistics
151166
"""
@@ -161,7 +176,7 @@ async def get_usage_endpoint(api_key: str):
161176
if STATIC_DIR.exists():
162177
# Serve static assets (js, css, images, etc.)
163178
app.mount("/assets", StaticFiles(directory=STATIC_DIR / "assets"), name="assets")
164-
179+
165180
# Root route - serve index.html
166181
@app.get("/")
167182
async def root():
@@ -170,25 +185,25 @@ async def root():
170185
if index_path.exists():
171186
return FileResponse(index_path)
172187
raise HTTPException(status_code=404, detail="Frontend not found")
173-
188+
174189
# Catch-all route for React SPA - must be last
175190
@app.get("/{full_path:path}")
176191
async def serve_frontend(full_path: str):
177192
"""Serve the React frontend for all non-API routes."""
178193
# Don't serve frontend for API routes
179194
if full_path.startswith("api/"):
180195
raise HTTPException(status_code=404, detail="API endpoint not found")
181-
196+
182197
# Try to serve the requested file
183198
file_path = STATIC_DIR / full_path
184199
if file_path.is_file():
185200
return FileResponse(file_path)
186-
201+
187202
# Otherwise, serve index.html (SPA routing)
188203
index_path = STATIC_DIR / "index.html"
189204
if index_path.exists():
190205
return FileResponse(index_path)
191-
206+
192207
raise HTTPException(status_code=404, detail="Not found")
193208

194209

apps/web/backend/pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ dependencies = [
99
"uvicorn[standard]>=0.32.0",
1010
"python-multipart>=0.0.12",
1111
"pillow>=8.0.0",
12+
"pillow-heif>=1.3.0",
1213
]
1314

1415
[build-system]
@@ -17,3 +18,6 @@ build-backend = "hatchling.build"
1718

1819
[tool.hatch.build.targets.wheel]
1920
packages = ["app"]
21+
22+
[tool.uv.sources]
23+
withoutbg = { path = "../../../packages/python", editable = true }

apps/web/backend/uv.lock

Lines changed: 77 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)