99from fastapi .middleware .cors import CORSMiddleware
1010from fastapi .responses import Response , JSONResponse , FileResponse
1111from fastapi .staticfiles import StaticFiles
12- from PIL import Image
12+ from PIL import Image , ImageOps
13+ import pillow_heif # Required for HEIF/HEIC decoding
1314import uvicorn
1415
16+
1517# Import withoutbg package (install via: uv sync or pip install -e ../../../packages/python)
1618from withoutbg import WithoutBG , __version__
1719from withoutbg .exceptions import WithoutBGError
1820from withoutbg .api import ProAPI
1921
22+ # Enable HEIF support globally for Pillow
23+ pillow_heif .register_heif_opener ()
24+
2025app = 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(
142157async 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):
161176if 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
0 commit comments