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.

📚 API Documentation 💚 Health Check

📡 Endpoints

POST /convert

Upload HTML file and convert to PDF

Parameters:

POST /convert-html

Convert HTML string to PDF

POST /convert-base64

Convert HTML to base64-encoded PDF (returns JSON)

📐 Aspect Ratios

""" 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)