from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import Response, JSONResponse, HTMLResponse
from fastapi.middleware.cors import CORSMiddleware
import subprocess
import os
import tempfile
import shutil
import base64
import logging
from typing import List, Optional
import asyncio # Required for async subprocess
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
# Create FastAPI app
app = FastAPI(
title="HTML to PDF Converter API",
description="Convert HTML to PDF with page breaks and image support",
version="1.0.0"
)
# --- START: Corrected Startup Logic ---
@app.on_event("startup")
async def startup_event():
"""
Run startup checks. This code runs only once when the server starts.
"""
logger.info("============================================================")
logger.info("HTML to PDF Converter API Starting...")
logger.info("============================================================")
logger.info("Checking dependencies...")
try:
# Check Node.js version asynchronously
proc = await asyncio.create_subprocess_shell(
"node --version",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
if proc.returncode == 0:
logger.info(f"Node.js version: {stdout.decode().strip()}")
else:
logger.error(f"Could not get Node.js version: {stderr.decode().strip()}")
# Check for Chromium executable
chromium_path = os.getenv("PUPPETEER_EXECUTABLE_PATH", "/usr/bin/chromium")
if os.path.exists(chromium_path):
logger.info(f"Chromium found at: {chromium_path}")
else:
logger.error(f"Chromium executable not found at {chromium_path}!")
# Check for Puppeteer script
if os.path.exists("/app/puppeteer_pdf.js"):
logger.info("Puppeteer script found")
else:
logger.error("puppeteer_pdf.js not found in /app directory!")
logger.info("============================================================")
logger.info("API is ready!")
logger.info("============================================================")
except Exception as e:
logger.critical(f"A critical error occurred during startup: {e}")
# --- END: Corrected Startup Logic ---
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def root():
"""Root endpoint - returns immediately for health check"""
return {
"status": "online",
"service": "HTML to PDF Converter API",
"version": "1.0.0",
"docs": "/docs",
"ui": "/ui"
}
@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "healthy"}
@app.get("/ui", response_class=HTMLResponse)
async def ui():
"""UI documentation page"""
return """
HTML to PDF API
📄 HTML to PDF Converter API
✓ Online
A powerful API to convert HTML documents to PDF with support for
page breaks, image embedding, and multiple aspect ratios.
📡 Endpoints
POST /convert
Upload HTML file and convert to PDF
Parameters:
html_file - HTML file (required)
aspect_ratio - "16:9", "1:1", or "9:16" (optional)
images - Image files to embed (optional)
POST /convert-html
Convert HTML string to PDF
POST /convert-base64
Convert HTML to base64-encoded PDF (returns JSON)
📐 Aspect Ratios
16:9 - Landscape - Presentations
1:1 - Square - Social media
9:16 - Portrait - Documents (default)
"""
def detect_aspect_ratio(html: str) -> str:
"""Detect aspect ratio from HTML content"""
html_lower = html.lower()
if 'orientation=portrait' in html_lower:
return "9:16"
if 'orientation=landscape' in html_lower:
return "16:9"
if any(k in html_lower for k in ['slide', 'presentation', 'reveal']):
return "16:9"
return "9:16"
def inject_page_css(html: str, ratio: str) -> str:
"""Inject page break CSS into HTML"""
size = {
"16:9": "A4 landscape",
"1:1": "210mm 210mm",
"9:16": "A4 portrait"
}.get(ratio, "A4 portrait")
css = f""""""
if '' in html:
return html.replace('', css + '')
elif ' bytes:
"""Convert HTML to PDF using Puppeteer"""
temp_dir = tempfile.mkdtemp()
try:
html = inject_page_css(html, ratio)
html_file = os.path.join(temp_dir, "input.html")
with open(html_file, 'w', encoding='utf-8') as f:
f.write(html)
logger.info(f"Converting to PDF (aspect ratio: {ratio})")
result = subprocess.run(
['node', '/app/puppeteer_pdf.js', html_file, ratio],
capture_output=True,
text=True,
timeout=60,
cwd='/app'
)
if result.returncode != 0:
logger.error(f"Conversion failed: {result.stderr}")
raise Exception(f"PDF conversion failed: {result.stderr}")
pdf_file = html_file.replace('.html', '.pdf')
if not os.path.exists(pdf_file):
raise Exception("PDF file was not generated")
with open(pdf_file, 'rb') as f:
pdf_bytes = f.read()
logger.info(f"PDF generated: {len(pdf_bytes)} bytes")
return pdf_bytes
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
@app.post("/convert")
async def convert(
html_file: UploadFile = File(...),
aspect_ratio: Optional[str] = Form(None),
auto_detect: bool = Form(True),
images: Optional[List[UploadFile]] = File(None)
):
"""Convert uploaded HTML file to PDF"""
try:
html = (await html_file.read()).decode('utf-8')
if auto_detect:
aspect_ratio = detect_aspect_ratio(html)
elif not aspect_ratio:
aspect_ratio = "9:16"
if aspect_ratio not in ["16:9", "1:1", "9:16"]:
raise HTTPException(400, "Invalid aspect ratio. Must be 16:9, 1:1, or 9:16")
# Embed images if provided
if images:
for img in images:
img_bytes = await img.read()
b64 = base64.b64encode(img_bytes).decode()
ext = os.path.splitext(img.filename)[1].lower()
mime = "image/jpeg" if ext in ['.jpg', '.jpeg'] else "image/png"
data_url = f"data:{mime};base64,{b64}"
html = html.replace(img.filename, data_url)
pdf = convert_to_pdf(html, aspect_ratio)
return Response(
content=pdf,
media_type="application/pdf",
headers={
"Content-Disposition": "attachment; filename=output.pdf",
"X-Aspect-Ratio": aspect_ratio
}
)
except Exception as e:
logger.error(f"Error: {str(e)}")
raise HTTPException(500, str(e))
@app.post("/convert-html")
async def convert_html(
html_content: str = Form(...),
aspect_ratio: Optional[str] = Form(None),
auto_detect: bool = Form(True)
):
"""Convert HTML string to PDF"""
try:
if auto_detect:
aspect_ratio = detect_aspect_ratio(html_content)
elif not aspect_ratio:
aspect_ratio = "9:16"
if aspect_ratio not in ["16:9", "1:1", "9:16"]:
raise HTTPException(400, "Invalid aspect ratio")
pdf = convert_to_pdf(html_content, aspect_ratio)
return Response(
content=pdf,
media_type="application/pdf",
headers={
"Content-Disposition": "attachment; filename=output.pdf",
"X-Aspect-Ratio": aspect_ratio
}
)
except Exception as e:
logger.error(f"Error: {str(e)}")
raise HTTPException(500, str(e))
@app.post("/convert-base64")
async def convert_base64(
html_content: str = Form(...),
aspect_ratio: Optional[str] = Form(None),
auto_detect: bool = Form(True)
):
"""Convert HTML to base64-encoded PDF"""
try:
if auto_detect:
aspect_ratio = detect_aspect_ratio(html_content)
elif not aspect_ratio:
aspect_ratio = "9:16"
if aspect_ratio not in ["16:9", "1:1", "9:16"]:
raise HTTPException(400, "Invalid aspect ratio")
pdf = convert_to_pdf(html_content, aspect_ratio)
pdf_b64 = base64.b64encode(pdf).decode()
return JSONResponse({
"success": True,
"pdf_base64": pdf_b64,
"aspect_ratio": aspect_ratio,
"size_bytes": len(pdf)
})
except Exception as e:
logger.error(f"Error: {str(e)}")
raise HTTPException(500, str(e))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=7860)