"""
FastAPI Backend for HTML to PDF Conversion
Runs alongside Streamlit on port 7860
"""
from fastapi import FastAPI, UploadFile, File, Form, HTTPException
from fastapi.responses import Response, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
import subprocess
import os
import tempfile
import shutil
import re
import mimetypes
from typing import List, Optional
from pathlib import Path
app = FastAPI(
title="HTML to PDF API",
description="Convert HTML to PDF with image support and page breaks",
version="1.0.0"
)
# Add CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def detect_aspect_ratio(html_content):
"""Detect aspect ratio from HTML content"""
viewport_match = re.search(r']*viewport[^>]*content=["\']([^"\']*)["\']', html_content, re.IGNORECASE)
if viewport_match:
viewport = viewport_match.group(1).lower()
if 'orientation=portrait' in viewport:
return "9:16"
elif 'orientation=landscape' in viewport:
return "16:9"
aspect_match = re.search(r'aspect-ratio\s*:\s*(\d+)\s*/\s*(\d+)', html_content, re.IGNORECASE)
if aspect_match:
width = int(aspect_match.group(1))
height = int(aspect_match.group(2))
ratio = width / height
if ratio > 1.5:
return "16:9"
elif ratio < 0.7:
return "9:16"
else:
return "1:1"
if any(keyword in html_content.lower() for keyword in ['reveal.js', 'impress.js', 'slide', 'presentation']):
return "16:9"
return "9:16"
def normalize_image_paths(html_content):
"""Replace complex image paths with just filenames"""
replacements = {}
# Pattern for img src - capture full path and extract filename
def replace_img_src(match):
prefix = match.group(1)
quote = match.group(2)
full_path = match.group(3)
# Extract just the filename from the full path
filename = os.path.basename(full_path)
replacements[f"img: {full_path}"] = filename
return f'{prefix}{quote}{filename}{quote}'
html_content = re.sub(
r'(]*\s+src\s*=\s*)(["\'])([^"\']+\.(jpg|jpeg|png|gif|svg|webp|bmp|JPG|JPEG|PNG|GIF|SVG|WEBP|BMP))(\2)',
replace_img_src,
html_content,
flags=re.IGNORECASE
)
# Pattern for background-image
def replace_bg_image(match):
prefix = match.group(1)
quote = match.group(2)
full_path = match.group(3)
suffix = match.group(5)
# Extract just the filename from the full path
filename = os.path.basename(full_path)
replacements[f"bg: {full_path}"] = filename
return f'{prefix}{quote}{filename}{quote}{suffix}'
html_content = re.sub(
r'(background-image\s*:\s*url\s*\()(["\']?)([^)"\'/]+\.(jpg|jpeg|png|gif|svg|webp|bmp|JPG|JPEG|PNG|GIF|SVG|WEBP|BMP))(\2)(\))',
replace_bg_image,
html_content,
flags=re.IGNORECASE
)
# Pattern for CSS url()
def replace_url(match):
prefix = match.group(1)
quote = match.group(2)
full_path = match.group(3)
suffix = match.group(5)
# Extract just the filename from the full path
filename = os.path.basename(full_path)
replacements[f"url: {full_path}"] = filename
return f'{prefix}{quote}{filename}{quote}{suffix}'
html_content = re.sub(
r'(url\s*\()(["\']?)([^)"\'/]+\.(jpg|jpeg|png|gif|svg|webp|bmp|JPG|JPEG|PNG|GIF|SVG|WEBP|BMP))(\2)(\))',
replace_url,
html_content,
flags=re.IGNORECASE
)
return html_content, replacements
def inject_page_breaks(html_content: str, aspect_ratio: str):
"""Automatically inject page breaks and page sizing CSS"""
if aspect_ratio == "16:9":
page_size = "288mm 162mm"
elif aspect_ratio == "1:1":
page_size = "210mm 210mm"
else:
page_size = "162mm 288mm"
page_css = f"""
"""
if '' in html_content:
html_content = html_content.replace('', page_css + '')
elif '