ABDALLALSWAITI commited on
Commit
f6f6973
·
verified ·
1 Parent(s): b6d416c

Create api_service.py

Browse files
Files changed (1) hide show
  1. api_service.py +200 -0
api_service.py ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Request
2
+ from fastapi.responses import Response
3
+ from pydantic import BaseModel
4
+ import subprocess
5
+ import os
6
+ import tempfile
7
+ import shutil
8
+ import re
9
+ from typing import Optional
10
+
11
+ app = FastAPI(
12
+ title="HTML to PDF API",
13
+ description="Convert HTML to PDF using Puppeteer",
14
+ version="1.0.0"
15
+ )
16
+
17
+ class HTMLRequest(BaseModel):
18
+ html: str
19
+ aspect_ratio: Optional[str] = "auto" # "16:9", "1:1", "9:16", or "auto"
20
+
21
+ def detect_aspect_ratio(html_content):
22
+ """
23
+ Detect aspect ratio from HTML content
24
+ Returns: "16:9", "1:1", or "9:16"
25
+ """
26
+ # Check for viewport meta tag
27
+ viewport_match = re.search(r'<meta[^>]*viewport[^>]*content=["\']([^"\']*)["\']', html_content, re.IGNORECASE)
28
+ if viewport_match:
29
+ viewport = viewport_match.group(1).lower()
30
+ if 'width=device-width' in viewport or 'width=100%' in viewport:
31
+ if 'orientation=portrait' in viewport:
32
+ return "9:16"
33
+ elif 'orientation=landscape' in viewport:
34
+ return "16:9"
35
+
36
+ # Check for CSS aspect-ratio property
37
+ aspect_match = re.search(r'aspect-ratio\s*:\s*(\d+)\s*/\s*(\d+)', html_content, re.IGNORECASE)
38
+ if aspect_match:
39
+ width = int(aspect_match.group(1))
40
+ height = int(aspect_match.group(2))
41
+ ratio = width / height
42
+ if ratio > 1.5:
43
+ return "16:9"
44
+ elif ratio < 0.7:
45
+ return "9:16"
46
+ else:
47
+ return "1:1"
48
+
49
+ # Check for common presentation frameworks
50
+ if any(keyword in html_content.lower() for keyword in ['reveal.js', 'impress.js', 'slide', 'presentation']):
51
+ return "16:9"
52
+
53
+ # Default to A4 portrait
54
+ return "9:16"
55
+
56
+ def convert_html_to_pdf(html_content, aspect_ratio):
57
+ """
58
+ Convert HTML content to PDF using Puppeteer
59
+
60
+ Args:
61
+ html_content: String containing HTML content
62
+ aspect_ratio: One of "16:9", "1:1", or "9:16"
63
+
64
+ Returns:
65
+ Tuple of (pdf_bytes, error_message)
66
+ """
67
+ temp_dir = None
68
+ try:
69
+ # Create temporary directory
70
+ temp_dir = tempfile.mkdtemp()
71
+
72
+ # Inject CSS to preserve styles
73
+ style_injection = """
74
+ <style>
75
+ @page {
76
+ margin: 0;
77
+ }
78
+ * {
79
+ -webkit-print-color-adjust: exact !important;
80
+ print-color-adjust: exact !important;
81
+ color-adjust: exact !important;
82
+ }
83
+ body {
84
+ -webkit-print-color-adjust: exact !important;
85
+ print-color-adjust: exact !important;
86
+ }
87
+ </style>
88
+ """
89
+
90
+ # Insert style injection
91
+ if '</head>' in html_content:
92
+ html_content = html_content.replace('</head>', style_injection + '</head>')
93
+ elif '<body' in html_content:
94
+ html_content = html_content.replace('<body', style_injection + '<body', 1)
95
+ else:
96
+ html_content = style_injection + html_content
97
+
98
+ # Save HTML to temporary file
99
+ html_file = os.path.join(temp_dir, "input.html")
100
+ with open(html_file, 'w', encoding='utf-8') as f:
101
+ f.write(html_content)
102
+
103
+ # Get puppeteer script path
104
+ script_dir = os.path.dirname(os.path.abspath(__file__))
105
+ puppeteer_script = os.path.join(script_dir, 'puppeteer_pdf.js')
106
+
107
+ # Run Node.js script
108
+ result = subprocess.run(
109
+ ['node', puppeteer_script, html_file, aspect_ratio],
110
+ capture_output=True,
111
+ text=True,
112
+ timeout=60
113
+ )
114
+
115
+ if result.returncode != 0:
116
+ return None, f"PDF conversion failed: {result.stderr}"
117
+
118
+ # Read generated PDF
119
+ pdf_file = html_file.replace('.html', '.pdf')
120
+
121
+ if not os.path.exists(pdf_file):
122
+ return None, "PDF file was not generated"
123
+
124
+ with open(pdf_file, 'rb') as f:
125
+ pdf_bytes = f.read()
126
+
127
+ # Cleanup
128
+ shutil.rmtree(temp_dir, ignore_errors=True)
129
+
130
+ return pdf_bytes, None
131
+
132
+ except subprocess.TimeoutExpired:
133
+ if temp_dir:
134
+ shutil.rmtree(temp_dir, ignore_errors=True)
135
+ return None, "PDF conversion timed out (60 seconds)"
136
+ except Exception as e:
137
+ if temp_dir:
138
+ shutil.rmtree(temp_dir, ignore_errors=True)
139
+ return None, f"Error: {str(e)}"
140
+
141
+ @app.get("/")
142
+ async def root():
143
+ return {
144
+ "message": "HTML to PDF API",
145
+ "endpoints": {
146
+ "POST /convert": "Convert HTML to PDF",
147
+ "GET /health": "Health check"
148
+ }
149
+ }
150
+
151
+ @app.get("/health")
152
+ async def health():
153
+ return {"status": "healthy"}
154
+
155
+ @app.post("/convert")
156
+ async def convert_html(request: HTMLRequest):
157
+ """
158
+ Convert HTML to PDF
159
+
160
+ Request body:
161
+ {
162
+ "html": "<html>...</html>",
163
+ "aspect_ratio": "auto" // Optional: "auto", "16:9", "1:1", or "9:16"
164
+ }
165
+
166
+ Returns: PDF file as binary response
167
+ """
168
+ if not request.html or not request.html.strip():
169
+ raise HTTPException(status_code=400, detail="HTML content is required")
170
+
171
+ # Determine aspect ratio
172
+ if request.aspect_ratio == "auto":
173
+ aspect_ratio = detect_aspect_ratio(request.html)
174
+ elif request.aspect_ratio in ["16:9", "1:1", "9:16"]:
175
+ aspect_ratio = request.aspect_ratio
176
+ else:
177
+ raise HTTPException(
178
+ status_code=400,
179
+ detail="Invalid aspect_ratio. Must be 'auto', '16:9', '1:1', or '9:16'"
180
+ )
181
+
182
+ # Convert to PDF
183
+ pdf_bytes, error = convert_html_to_pdf(request.html, aspect_ratio)
184
+
185
+ if error:
186
+ raise HTTPException(status_code=500, detail=error)
187
+
188
+ # Return PDF as binary response
189
+ return Response(
190
+ content=pdf_bytes,
191
+ media_type="application/pdf",
192
+ headers={
193
+ "Content-Disposition": "attachment; filename=converted.pdf",
194
+ "X-Aspect-Ratio": aspect_ratio
195
+ }
196
+ )
197
+
198
+ if __name__ == "__main__":
199
+ import uvicorn
200
+ uvicorn.run(app, host="0.0.0.0", port=8000)