ABDALLALSWAITI commited on
Commit
4c51b8c
·
verified ·
1 Parent(s): 703de2f

Update api.py

Browse files
Files changed (1) hide show
  1. api.py +223 -152
api.py CHANGED
@@ -1,9 +1,9 @@
1
  """
2
- FastAPI HTML to PDF Converter for Hugging Face Spaces
3
- File: app.py
4
  """
5
- from fastapi import FastAPI, File, UploadFile, Form, HTTPException
6
- from fastapi.responses import Response, HTMLResponse
7
  from fastapi.middleware.cors import CORSMiddleware
8
  import subprocess
9
  import os
@@ -13,13 +13,15 @@ import base64
13
  import re
14
  import mimetypes
15
  from typing import List, Optional
 
16
 
17
  app = FastAPI(
18
- title="HTML to PDF Converter API",
19
- description="Convert HTML to PDF with page breaks and embedded images",
20
- version="3.0"
21
  )
22
 
 
23
  app.add_middleware(
24
  CORSMiddleware,
25
  allow_origins=["*"],
@@ -76,83 +78,133 @@ def image_to_base64(image_bytes, filename):
76
  data_url = f"data:{mime_type};base64,{b64_data}"
77
  return data_url
78
  except Exception as e:
79
- print(f"Error converting {filename} to base64: {str(e)}")
80
- return None
81
 
82
- def embed_images_as_base64(html_content, images: List[UploadFile]):
83
  """Embed all images directly as base64 data URLs in the HTML"""
84
- if not images:
85
- return html_content, {}
86
-
87
- image_data_urls = {}
88
- for img in images:
89
- img.file.seek(0)
90
- image_bytes = img.file.read()
91
- data_url = image_to_base64(image_bytes, img.filename)
92
- if data_url:
93
- image_data_urls[img.filename] = data_url
94
- print(f"✓ Converted {img.filename} to base64")
95
-
96
- if not image_data_urls:
97
  return html_content, {}
98
 
99
  replacements = {}
100
 
101
- for filename, data_url in image_data_urls.items():
102
  escaped_name = re.escape(filename)
103
 
104
- # Pattern 1: img src
105
  pattern1 = rf'(<img[^>]*\s+src\s*=\s*)(["\'])(?:[^"\']*?/)?{escaped_name}\2'
106
  matches1 = list(re.finditer(pattern1, html_content, flags=re.IGNORECASE | re.DOTALL))
 
107
  if matches1:
108
  html_content = re.sub(pattern1, rf'\1\2{data_url}\2', html_content, flags=re.IGNORECASE | re.DOTALL)
109
- replacements[f"{filename} (img)"] = len(matches1)
110
 
111
  # Pattern 2: background-image
112
  pattern2 = rf'(background-image\s*:\s*url\s*\()(["\']?)(?:[^)"\']*/)?{escaped_name}\2(\))'
113
  matches2 = list(re.finditer(pattern2, html_content, flags=re.IGNORECASE))
 
114
  if matches2:
115
  html_content = re.sub(pattern2, rf'\1"{data_url}"\3', html_content, flags=re.IGNORECASE)
116
- replacements[f"{filename} (bg)"] = len(matches2)
117
 
118
- # Pattern 3: url()
119
  pattern3 = rf'(url\s*\()(["\']?)(?:[^)"\']*/)?{escaped_name}\2(\))'
120
  matches3 = list(re.finditer(pattern3, html_content, flags=re.IGNORECASE))
 
121
  if matches3:
122
  html_content = re.sub(pattern3, rf'\1"{data_url}"\3', html_content, flags=re.IGNORECASE)
123
- replacements[f"{filename} (url)"] = len(matches3)
124
 
125
  return html_content, replacements
126
 
127
  def inject_page_breaks(html_content: str, aspect_ratio: str):
128
- """Inject page break CSS"""
129
- page_size = "A4 landscape" if aspect_ratio == "16:9" else ("210mm 210mm" if aspect_ratio == "1:1" else "A4 portrait")
 
 
 
 
 
 
130
 
131
  page_css = f"""
132
  <style id="auto-page-breaks">
133
- @page {{ size: {page_size}; margin: 0; }}
134
- html, body {{ margin: 0 !important; padding: 0 !important; width: 100% !important; height: 100% !important; }}
 
 
 
 
 
 
 
 
 
 
135
  .page, .slide, section.page, article.page, div[class*="page"], div[class*="slide"] {{
136
- width: 100% !important; min-height: 100vh !important; height: 100vh !important;
137
- page-break-after: always !important; break-after: page !important;
138
- page-break-inside: avoid !important; break-inside: avoid !important;
139
- position: relative !important; box-sizing: border-box !important; overflow: hidden !important;
 
 
 
 
 
 
140
  }}
141
- .page:last-child, .slide:last-child, section.page:last-child, article.page:last-child {{
142
- page-break-after: auto !important; break-after: auto !important;
 
 
 
143
  }}
144
- body > section:not(.no-page-break), body > article:not(.no-page-break), body > div:not(.no-page-break) {{
145
- page-break-after: always !important; break-after: page !important; min-height: 100vh;
 
 
 
 
 
146
  }}
147
- body > section:last-child, body > article:last-child, body > div:last-child {{
 
 
 
148
  page-break-after: auto !important;
149
  }}
150
- .page-break, .page-break-after {{ page-break-after: always !important; break-after: page !important; }}
151
- .page-break-before {{ page-break-before: always !important; break-before: page !important; }}
152
- .no-page-break, .keep-together {{ page-break-inside: avoid !important; break-inside: avoid !important; }}
153
- h1, h2, h3, h4, h5, h6 {{ page-break-after: avoid !important; break-after: avoid !important; }}
154
- img, figure, table, pre, blockquote {{ page-break-inside: avoid !important; break-inside: avoid !important; }}
155
- * {{ -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; color-adjust: exact !important; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
156
  </style>
157
  """
158
 
@@ -165,8 +217,8 @@ def inject_page_breaks(html_content: str, aspect_ratio: str):
165
 
166
  return html_content
167
 
168
- def convert_html_to_pdf(html_content: str, aspect_ratio: str, temp_dir: str):
169
- """Convert HTML to PDF using Puppeteer"""
170
  try:
171
  html_content = inject_page_breaks(html_content, aspect_ratio)
172
 
@@ -174,10 +226,21 @@ def convert_html_to_pdf(html_content: str, aspect_ratio: str, temp_dir: str):
174
  with open(html_file, 'w', encoding='utf-8') as f:
175
  f.write(html_content)
176
 
177
- puppeteer_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'puppeteer_pdf.js')
 
 
 
 
 
178
 
179
- if not os.path.exists(puppeteer_script):
180
- return None, f"Error: puppeteer_pdf.js not found at {puppeteer_script}"
 
 
 
 
 
 
181
 
182
  result = subprocess.run(
183
  ['node', puppeteer_script, html_file, aspect_ratio],
@@ -188,138 +251,99 @@ def convert_html_to_pdf(html_content: str, aspect_ratio: str, temp_dir: str):
188
  )
189
 
190
  if result.returncode != 0:
191
- return None, f"PDF conversion failed: {result.stderr}"
192
 
193
  pdf_file = html_file.replace('.html', '.pdf')
194
  if not os.path.exists(pdf_file):
195
- return None, "PDF file was not generated"
196
 
197
  with open(pdf_file, 'rb') as f:
198
  pdf_bytes = f.read()
199
 
200
- return pdf_bytes, None
201
 
202
  except subprocess.TimeoutExpired:
203
- return None, "Error: PDF conversion timed out"
204
  except Exception as e:
205
- return None, f"Error: {str(e)}"
206
 
207
- @app.get("/", response_class=HTMLResponse)
208
  async def root():
209
- """Root endpoint with documentation"""
210
- html = """
211
- <!DOCTYPE html>
212
- <html>
213
- <head>
214
- <title>HTML to PDF Converter API</title>
215
- <style>
216
- body { font-family: Arial; max-width: 800px; margin: 50px auto; padding: 20px; }
217
- h1 { color: #667eea; }
218
- code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; }
219
- pre { background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }
220
- </style>
221
- </head>
222
- <body>
223
- <h1>📄 HTML to PDF Converter API</h1>
224
- <p>Convert HTML to PDF with proper page breaks and embedded images.</p>
225
-
226
- <h2>🚀 Quick Start</h2>
227
- <pre>curl -X POST https://abdallalswaiti-htmlpdf.hf.space/convert \\
228
- -F 'html_content=&lt;html&gt;&lt;body&gt;&lt;div class="page"&gt;Hello&lt;/div&gt;&lt;/body&gt;&lt;/html&gt;' \\
229
- --output output.pdf</pre>
230
-
231
- <h2>📚 Endpoints</h2>
232
- <ul>
233
- <li><code>GET /</code> - This page</li>
234
- <li><code>GET /health</code> - Health check</li>
235
- <li><code>POST /convert</code> - Convert HTML to PDF</li>
236
- <li><code>GET /docs</code> - Interactive API documentation</li>
237
- </ul>
238
-
239
- <h2>💡 Features</h2>
240
- <ul>
241
- <li>✅ Automatic page break detection</li>
242
- <li>✅ Base64 image embedding</li>
243
- <li>✅ Multiple aspect ratios (16:9, 1:1, 9:16)</li>
244
- <li>✅ CSS @page support</li>
245
- </ul>
246
-
247
- <p><a href="/docs">📖 View Full API Documentation</a></p>
248
- </body>
249
- </html>
250
- """
251
- return html
252
 
253
  @app.get("/health")
254
  async def health():
255
  """Health check endpoint"""
256
- return {
257
- "status": "healthy",
258
- "version": "3.0",
259
- "api": "HTML to PDF Converter"
260
- }
261
 
262
  @app.post("/convert")
263
  async def convert_to_pdf(
264
- html_file: Optional[UploadFile] = File(None),
265
- html_content: Optional[str] = Form(None),
266
- aspect_ratio: Optional[str] = Form(None),
267
- auto_detect: bool = Form(True),
268
- images: Optional[List[UploadFile]] = File(None)
269
  ):
270
  """
271
- Convert HTML to PDF with page breaks and embedded images
272
 
273
- - **html_file**: HTML file upload (optional)
274
- - **html_content**: Raw HTML content (optional)
275
- - **aspect_ratio**: "16:9", "1:1", or "9:16"
276
- - **auto_detect**: Auto-detect aspect ratio
277
- - **images**: Image files to embed
278
  """
279
  temp_dir = None
280
-
281
  try:
282
- if not html_file and not html_content:
283
- raise HTTPException(status_code=400, detail="Either html_file or html_content must be provided")
284
-
285
- if html_file:
286
- content = await html_file.read()
287
- try:
288
- html = content.decode('utf-8')
289
- except UnicodeDecodeError:
290
- html = content.decode('latin-1')
291
- filename = html_file.filename
292
- else:
293
- html = html_content
294
- filename = "converted.pdf"
295
-
296
- temp_dir = tempfile.mkdtemp()
297
-
298
- if images:
299
- html, replacements = embed_images_as_base64(html, images)
300
 
301
- if auto_detect or not aspect_ratio:
302
- aspect_ratio = detect_aspect_ratio(html)
303
- else:
304
- if aspect_ratio not in ["16:9", "1:1", "9:16"]:
305
- raise HTTPException(status_code=400, detail="Invalid aspect_ratio")
 
306
 
307
- pdf_bytes, error = convert_html_to_pdf(html, aspect_ratio, temp_dir)
 
 
308
 
309
- if error:
310
- raise HTTPException(status_code=500, detail=error)
 
 
 
 
 
 
 
 
311
 
312
- output_filename = filename.replace('.html', '.pdf').replace('.htm', '.pdf')
313
- if not output_filename.endswith('.pdf'):
314
- output_filename = 'converted.pdf'
315
 
 
316
  return Response(
317
  content=pdf_bytes,
318
  media_type="application/pdf",
319
  headers={
320
- "Content-Disposition": f"attachment; filename={output_filename}",
321
  "X-Aspect-Ratio": aspect_ratio,
322
- "X-Images-Embedded": str(len(images)) if images else "0"
 
323
  }
324
  )
325
 
@@ -331,7 +355,54 @@ async def convert_to_pdf(
331
  if temp_dir and os.path.exists(temp_dir):
332
  shutil.rmtree(temp_dir, ignore_errors=True)
333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  if __name__ == "__main__":
335
  import uvicorn
336
- port = int(os.environ.get("PORT", 7860))
337
- uvicorn.run(app, host="0.0.0.0", port=port)
 
1
  """
2
+ FastAPI Backend for HTML to PDF Conversion
3
+ Runs alongside Streamlit on port 7860
4
  """
5
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
6
+ from fastapi.responses import Response, JSONResponse
7
  from fastapi.middleware.cors import CORSMiddleware
8
  import subprocess
9
  import os
 
13
  import re
14
  import mimetypes
15
  from typing import List, Optional
16
+ from pathlib import Path
17
 
18
  app = FastAPI(
19
+ title="HTML to PDF API",
20
+ description="Convert HTML to PDF with image support and page breaks",
21
+ version="1.0.0"
22
  )
23
 
24
+ # Add CORS middleware
25
  app.add_middleware(
26
  CORSMiddleware,
27
  allow_origins=["*"],
 
78
  data_url = f"data:{mime_type};base64,{b64_data}"
79
  return data_url
80
  except Exception as e:
81
+ raise HTTPException(status_code=400, detail=f"Error converting {filename} to base64: {str(e)}")
 
82
 
83
+ def embed_images_as_base64(html_content, images_dict):
84
  """Embed all images directly as base64 data URLs in the HTML"""
85
+ if not images_dict:
 
 
 
 
 
 
 
 
 
 
 
 
86
  return html_content, {}
87
 
88
  replacements = {}
89
 
90
+ for filename, data_url in images_dict.items():
91
  escaped_name = re.escape(filename)
92
 
93
+ # Pattern 1: img src attribute
94
  pattern1 = rf'(<img[^>]*\s+src\s*=\s*)(["\'])(?:[^"\']*?/)?{escaped_name}\2'
95
  matches1 = list(re.finditer(pattern1, html_content, flags=re.IGNORECASE | re.DOTALL))
96
+ count1 = len(matches1)
97
  if matches1:
98
  html_content = re.sub(pattern1, rf'\1\2{data_url}\2', html_content, flags=re.IGNORECASE | re.DOTALL)
99
+ replacements[f"{filename} (img src)"] = count1
100
 
101
  # Pattern 2: background-image
102
  pattern2 = rf'(background-image\s*:\s*url\s*\()(["\']?)(?:[^)"\']*/)?{escaped_name}\2(\))'
103
  matches2 = list(re.finditer(pattern2, html_content, flags=re.IGNORECASE))
104
+ count2 = len(matches2)
105
  if matches2:
106
  html_content = re.sub(pattern2, rf'\1"{data_url}"\3', html_content, flags=re.IGNORECASE)
107
+ replacements[f"{filename} (bg-image)"] = count2
108
 
109
+ # Pattern 3: CSS url()
110
  pattern3 = rf'(url\s*\()(["\']?)(?:[^)"\']*/)?{escaped_name}\2(\))'
111
  matches3 = list(re.finditer(pattern3, html_content, flags=re.IGNORECASE))
112
+ count3 = len(matches3)
113
  if matches3:
114
  html_content = re.sub(pattern3, rf'\1"{data_url}"\3', html_content, flags=re.IGNORECASE)
115
+ replacements[f"{filename} (url)"] = count3
116
 
117
  return html_content, replacements
118
 
119
  def inject_page_breaks(html_content: str, aspect_ratio: str):
120
+ """Automatically inject page breaks and page sizing CSS"""
121
+
122
+ if aspect_ratio == "16:9":
123
+ page_size = "A4 landscape"
124
+ elif aspect_ratio == "1:1":
125
+ page_size = "210mm 210mm"
126
+ else:
127
+ page_size = "A4 portrait"
128
 
129
  page_css = f"""
130
  <style id="auto-page-breaks">
131
+ @page {{
132
+ size: {page_size};
133
+ margin: 0;
134
+ }}
135
+
136
+ html, body {{
137
+ margin: 0 !important;
138
+ padding: 0 !important;
139
+ width: 100% !important;
140
+ height: 100% !important;
141
+ }}
142
+
143
  .page, .slide, section.page, article.page, div[class*="page"], div[class*="slide"] {{
144
+ width: 100% !important;
145
+ min-height: 100vh !important;
146
+ height: 100vh !important;
147
+ page-break-after: always !important;
148
+ break-after: page !important;
149
+ page-break-inside: avoid !important;
150
+ break-inside: avoid !important;
151
+ position: relative !important;
152
+ box-sizing: border-box !important;
153
+ overflow: hidden !important;
154
  }}
155
+
156
+ .page:last-child, .slide:last-child,
157
+ section.page:last-child, article.page:last-child {{
158
+ page-break-after: auto !important;
159
+ break-after: auto !important;
160
  }}
161
+
162
+ body > section:not(.no-page-break),
163
+ body > article:not(.no-page-break),
164
+ body > div:not(.no-page-break) {{
165
+ page-break-after: always !important;
166
+ break-after: page !important;
167
+ min-height: 100vh;
168
  }}
169
+
170
+ body > section:last-child,
171
+ body > article:last-child,
172
+ body > div:last-child {{
173
  page-break-after: auto !important;
174
  }}
175
+
176
+ .page-break, .page-break-after {{
177
+ page-break-after: always !important;
178
+ break-after: page !important;
179
+ }}
180
+
181
+ .page-break-before {{
182
+ page-break-before: always !important;
183
+ break-before: page !important;
184
+ }}
185
+
186
+ .no-page-break, .keep-together {{
187
+ page-break-inside: avoid !important;
188
+ break-inside: avoid !important;
189
+ }}
190
+
191
+ h1, h2, h3, h4, h5, h6 {{
192
+ page-break-after: avoid !important;
193
+ break-after: avoid !important;
194
+ page-break-inside: avoid !important;
195
+ break-inside: avoid !important;
196
+ }}
197
+
198
+ img, figure, table, pre, blockquote {{
199
+ page-break-inside: avoid !important;
200
+ break-inside: avoid !important;
201
+ }}
202
+
203
+ * {{
204
+ -webkit-print-color-adjust: exact !important;
205
+ print-color-adjust: exact !important;
206
+ color-adjust: exact !important;
207
+ }}
208
  </style>
209
  """
210
 
 
217
 
218
  return html_content
219
 
220
+ def convert_html_to_pdf(html_content, aspect_ratio, temp_dir):
221
+ """Convert HTML content to PDF using Puppeteer"""
222
  try:
223
  html_content = inject_page_breaks(html_content, aspect_ratio)
224
 
 
226
  with open(html_file, 'w', encoding='utf-8') as f:
227
  f.write(html_content)
228
 
229
+ # Find puppeteer script
230
+ possible_paths = [
231
+ 'puppeteer_pdf.js',
232
+ '/app/puppeteer_pdf.js',
233
+ os.path.join(os.path.dirname(__file__), 'puppeteer_pdf.js'),
234
+ ]
235
 
236
+ puppeteer_script = None
237
+ for path in possible_paths:
238
+ if os.path.exists(path):
239
+ puppeteer_script = path
240
+ break
241
+
242
+ if not puppeteer_script:
243
+ raise Exception("puppeteer_pdf.js not found")
244
 
245
  result = subprocess.run(
246
  ['node', puppeteer_script, html_file, aspect_ratio],
 
251
  )
252
 
253
  if result.returncode != 0:
254
+ raise Exception(f"PDF conversion failed: {result.stderr}")
255
 
256
  pdf_file = html_file.replace('.html', '.pdf')
257
  if not os.path.exists(pdf_file):
258
+ raise Exception("PDF file was not generated")
259
 
260
  with open(pdf_file, 'rb') as f:
261
  pdf_bytes = f.read()
262
 
263
+ return pdf_bytes
264
 
265
  except subprocess.TimeoutExpired:
266
+ raise Exception("PDF conversion timed out (60 seconds)")
267
  except Exception as e:
268
+ raise Exception(f"Error: {str(e)}")
269
 
270
+ @app.get("/")
271
  async def root():
272
+ """API root endpoint"""
273
+ return {
274
+ "message": "HTML to PDF Converter API",
275
+ "version": "1.0.0",
276
+ "endpoints": {
277
+ "POST /convert": "Convert HTML to PDF",
278
+ "GET /health": "Health check",
279
+ "GET /docs": "API documentation"
280
+ }
281
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  @app.get("/health")
284
  async def health():
285
  """Health check endpoint"""
286
+ return {"status": "healthy"}
 
 
 
 
287
 
288
  @app.post("/convert")
289
  async def convert_to_pdf(
290
+ html_file: UploadFile = File(..., description="HTML file to convert"),
291
+ aspect_ratio: Optional[str] = Form(None, description="Aspect ratio: 16:9, 1:1, or 9:16"),
292
+ auto_detect: bool = Form(True, description="Auto-detect aspect ratio from HTML"),
293
+ images: Optional[List[UploadFile]] = File(None, description="Images to embed in HTML")
 
294
  ):
295
  """
296
+ Convert HTML to PDF with optional image embedding
297
 
298
+ - **html_file**: HTML file to convert (required)
299
+ - **aspect_ratio**: Page aspect ratio (optional if auto_detect=true)
300
+ - **auto_detect**: Auto-detect aspect ratio from HTML content
301
+ - **images**: Image files to embed as base64 in HTML
 
302
  """
303
  temp_dir = None
 
304
  try:
305
+ # Read HTML content
306
+ html_content = await html_file.read()
307
+ try:
308
+ html_content = html_content.decode('utf-8')
309
+ except UnicodeDecodeError:
310
+ html_content = html_content.decode('latin-1')
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
+ # Detect or use provided aspect ratio
313
+ if auto_detect:
314
+ detected_ratio = detect_aspect_ratio(html_content)
315
+ aspect_ratio = detected_ratio
316
+ elif not aspect_ratio:
317
+ aspect_ratio = "9:16"
318
 
319
+ # Validate aspect ratio
320
+ if aspect_ratio not in ["16:9", "1:1", "9:16"]:
321
+ raise HTTPException(status_code=400, detail="Invalid aspect ratio. Must be 16:9, 1:1, or 9:16")
322
 
323
+ # Process images if provided
324
+ image_replacements = {}
325
+ if images:
326
+ images_dict = {}
327
+ for img in images:
328
+ img_bytes = await img.read()
329
+ data_url = image_to_base64(img_bytes, img.filename)
330
+ images_dict[img.filename] = data_url
331
+
332
+ html_content, image_replacements = embed_images_as_base64(html_content, images_dict)
333
 
334
+ # Create temp directory and convert
335
+ temp_dir = tempfile.mkdtemp()
336
+ pdf_bytes = convert_html_to_pdf(html_content, aspect_ratio, temp_dir)
337
 
338
+ # Return PDF
339
  return Response(
340
  content=pdf_bytes,
341
  media_type="application/pdf",
342
  headers={
343
+ "Content-Disposition": f"attachment; filename=converted.pdf",
344
  "X-Aspect-Ratio": aspect_ratio,
345
+ "X-Image-Replacements": str(len(image_replacements)),
346
+ "X-PDF-Size": str(len(pdf_bytes))
347
  }
348
  )
349
 
 
355
  if temp_dir and os.path.exists(temp_dir):
356
  shutil.rmtree(temp_dir, ignore_errors=True)
357
 
358
+ @app.post("/convert-base64")
359
+ async def convert_to_pdf_base64(
360
+ html_content: str = Form(..., description="HTML content as string"),
361
+ aspect_ratio: Optional[str] = Form(None, description="Aspect ratio: 16:9, 1:1, or 9:16"),
362
+ auto_detect: bool = Form(True, description="Auto-detect aspect ratio from HTML")
363
+ ):
364
+ """
365
+ Convert HTML string to PDF and return as base64
366
+
367
+ - **html_content**: HTML content as string (required)
368
+ - **aspect_ratio**: Page aspect ratio (optional if auto_detect=true)
369
+ - **auto_detect**: Auto-detect aspect ratio from HTML content
370
+ """
371
+ temp_dir = None
372
+ try:
373
+ # Detect or use provided aspect ratio
374
+ if auto_detect:
375
+ detected_ratio = detect_aspect_ratio(html_content)
376
+ aspect_ratio = detected_ratio
377
+ elif not aspect_ratio:
378
+ aspect_ratio = "9:16"
379
+
380
+ # Validate aspect ratio
381
+ if aspect_ratio not in ["16:9", "1:1", "9:16"]:
382
+ raise HTTPException(status_code=400, detail="Invalid aspect ratio. Must be 16:9, 1:1, or 9:16")
383
+
384
+ # Create temp directory and convert
385
+ temp_dir = tempfile.mkdtemp()
386
+ pdf_bytes = convert_html_to_pdf(html_content, aspect_ratio, temp_dir)
387
+
388
+ # Convert to base64
389
+ pdf_base64 = base64.b64encode(pdf_bytes).decode('utf-8')
390
+
391
+ return JSONResponse({
392
+ "success": True,
393
+ "pdf_base64": pdf_base64,
394
+ "aspect_ratio": aspect_ratio,
395
+ "size_bytes": len(pdf_bytes)
396
+ })
397
+
398
+ except HTTPException:
399
+ raise
400
+ except Exception as e:
401
+ raise HTTPException(status_code=500, detail=str(e))
402
+ finally:
403
+ if temp_dir and os.path.exists(temp_dir):
404
+ shutil.rmtree(temp_dir, ignore_errors=True)
405
+
406
  if __name__ == "__main__":
407
  import uvicorn
408
+ uvicorn.run(app, host="0.0.0.0", port=7860)