Use weasyprint instead of wkhtmltopdf.
Browse files
app.py
CHANGED
|
@@ -18,7 +18,7 @@ import requests
|
|
| 18 |
import gradio as gr
|
| 19 |
from openai import OpenAI
|
| 20 |
import markdown
|
| 21 |
-
import
|
| 22 |
|
| 23 |
from langchain_mcp_adapters.client import MultiServerMCPClient
|
| 24 |
|
|
@@ -195,21 +195,20 @@ def safe_json_dumps(obj, **kwargs):
|
|
| 195 |
return json.dumps({"error": f"Error serializing: {str(e)}", "data": str(obj)}, **kwargs)
|
| 196 |
|
| 197 |
def markdown_to_pdf(markdown_content: str, output_path: str) -> str:
|
| 198 |
-
"""Convert markdown to PDF using
|
| 199 |
try:
|
| 200 |
# Convert markdown to HTML and add exercise break classes
|
| 201 |
html_content = markdown.markdown(markdown_content, extensions=['tables', 'fenced_code'])
|
| 202 |
-
|
| 203 |
# Add CSS classes for better page breaks
|
| 204 |
import re
|
| 205 |
# Add exercise-break class to h2 elements (exercises)
|
| 206 |
html_content = re.sub(r'<h2>', r'<h2 class="exercise-break">', html_content)
|
| 207 |
-
|
| 208 |
# Get the logo path
|
| 209 |
logo_path = "https://huggingface.co/spaces/Agents-MCP-Hackathon/ipmentor-subnetting-exercises-generator/resolve/main/assets/logo.svg"
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
# Add CSS styling with IPMentor branding colors
|
| 213 |
styled_html = f"""
|
| 214 |
<!DOCTYPE html>
|
| 215 |
<html>
|
|
@@ -218,28 +217,31 @@ def markdown_to_pdf(markdown_content: str, output_path: str) -> str:
|
|
| 218 |
<style>
|
| 219 |
@page {{
|
| 220 |
margin: 1in 1in 100px 1in;
|
|
|
|
|
|
|
|
|
|
| 221 |
}}
|
| 222 |
-
|
| 223 |
-
body {{
|
| 224 |
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 225 |
-
line-height: 1.6;
|
| 226 |
margin: 0;
|
| 227 |
padding: 20px;
|
| 228 |
color: #333;
|
| 229 |
background: #fefefe;
|
| 230 |
}}
|
| 231 |
-
|
| 232 |
-
h1 {{
|
| 233 |
-
color: #FC8100;
|
| 234 |
border-bottom: 4px solid #FED200;
|
| 235 |
padding-bottom: 15px;
|
| 236 |
margin-bottom: 30px;
|
| 237 |
font-size: 2.2em;
|
| 238 |
font-weight: bold;
|
| 239 |
}}
|
| 240 |
-
|
| 241 |
-
h2 {{
|
| 242 |
-
color: #FC8100;
|
| 243 |
border-bottom: 3px solid #FFCB00;
|
| 244 |
padding-bottom: 8px;
|
| 245 |
margin-top: 40px;
|
|
@@ -247,49 +249,49 @@ def markdown_to_pdf(markdown_content: str, output_path: str) -> str:
|
|
| 247 |
font-size: 1.5em;
|
| 248 |
page-break-after: avoid;
|
| 249 |
}}
|
| 250 |
-
|
| 251 |
-
h3 {{
|
| 252 |
-
color: #FE8100;
|
| 253 |
margin-top: 25px;
|
| 254 |
margin-bottom: 15px;
|
| 255 |
font-size: 1.2em;
|
| 256 |
}}
|
| 257 |
-
|
| 258 |
p {{
|
| 259 |
margin-bottom: 15px;
|
| 260 |
text-align: justify;
|
| 261 |
}}
|
| 262 |
-
|
| 263 |
em {{
|
| 264 |
color: #F05600;
|
| 265 |
font-style: italic;
|
| 266 |
}}
|
| 267 |
-
|
| 268 |
strong {{
|
| 269 |
color: #FE8100;
|
| 270 |
}}
|
| 271 |
-
|
| 272 |
a {{
|
| 273 |
color: #F05600;
|
| 274 |
text-decoration: none;
|
| 275 |
border-bottom: 1px dotted #F05600;
|
| 276 |
}}
|
| 277 |
-
|
| 278 |
a:hover {{
|
| 279 |
border-bottom: 1px solid #F05600;
|
| 280 |
}}
|
| 281 |
-
|
| 282 |
-
img {{
|
| 283 |
-
max-width: 85%;
|
| 284 |
max-height: 450px;
|
| 285 |
-
height: auto;
|
| 286 |
display: block;
|
| 287 |
margin: 25px auto;
|
| 288 |
border: 2px solid #FED200;
|
| 289 |
border-radius: 8px;
|
| 290 |
box-shadow: 0 4px 8px rgba(254, 129, 0, 0.1);
|
| 291 |
}}
|
| 292 |
-
|
| 293 |
code {{
|
| 294 |
background: #FFF4E6;
|
| 295 |
color: #FC8100;
|
|
@@ -298,16 +300,16 @@ def markdown_to_pdf(markdown_content: str, output_path: str) -> str:
|
|
| 298 |
font-family: 'Courier New', monospace;
|
| 299 |
border: 1px solid #FED200;
|
| 300 |
}}
|
| 301 |
-
|
| 302 |
-
pre {{
|
| 303 |
-
background: #FFF8F0;
|
| 304 |
-
padding: 20px;
|
| 305 |
border-radius: 8px;
|
| 306 |
border-left: 6px solid #F05600;
|
| 307 |
margin: 20px 0;
|
| 308 |
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 309 |
}}
|
| 310 |
-
|
| 311 |
hr {{
|
| 312 |
border: none;
|
| 313 |
height: 3px;
|
|
@@ -315,83 +317,46 @@ def markdown_to_pdf(markdown_content: str, output_path: str) -> str:
|
|
| 315 |
margin: 30px 0;
|
| 316 |
border-radius: 2px;
|
| 317 |
}}
|
| 318 |
-
|
| 319 |
-
|
| 320 |
.exercise-break {{
|
| 321 |
page-break-before: always;
|
| 322 |
margin-top: 0;
|
| 323 |
}}
|
| 324 |
-
|
| 325 |
.exercise-break:first-of-type {{
|
| 326 |
page-break-before: avoid;
|
| 327 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
</style>
|
| 329 |
</head>
|
| 330 |
<body>
|
| 331 |
{html_content}
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
| 333 |
</body>
|
| 334 |
</html>
|
| 335 |
"""
|
| 336 |
-
|
| 337 |
-
#
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as footer_file:
|
| 341 |
-
footer_html_path = footer_file.name
|
| 342 |
-
footer_content = f"""
|
| 343 |
-
<!DOCTYPE html>
|
| 344 |
-
<html>
|
| 345 |
-
<head>
|
| 346 |
-
<meta charset="UTF-8">
|
| 347 |
-
<style>
|
| 348 |
-
body {{
|
| 349 |
-
margin: 0;
|
| 350 |
-
padding: 8px;
|
| 351 |
-
text-align: center;
|
| 352 |
-
}}
|
| 353 |
-
.logo {{
|
| 354 |
-
height: 38px;
|
| 355 |
-
width: auto;
|
| 356 |
-
}}
|
| 357 |
-
</style>
|
| 358 |
-
</head>
|
| 359 |
-
<body>
|
| 360 |
-
<img src="{logo_path}" alt="IPMentor" class="logo">
|
| 361 |
-
</body>
|
| 362 |
-
</html>
|
| 363 |
-
"""
|
| 364 |
-
footer_file.write(footer_content)
|
| 365 |
-
|
| 366 |
-
# Configure PDF options
|
| 367 |
-
options = {
|
| 368 |
-
'page-size': 'A4',
|
| 369 |
-
'margin-top': '1in',
|
| 370 |
-
'margin-right': '1in',
|
| 371 |
-
'margin-bottom': '1in',
|
| 372 |
-
'margin-left': '1in',
|
| 373 |
-
'encoding': "UTF-8",
|
| 374 |
-
'no-outline': None,
|
| 375 |
-
'enable-local-file-access': None,
|
| 376 |
-
'print-media-type': None,
|
| 377 |
-
'disable-smart-shrinking': None
|
| 378 |
-
}
|
| 379 |
-
|
| 380 |
-
# Add footer if logo exists
|
| 381 |
-
if footer_html_path:
|
| 382 |
-
options['footer-html'] = footer_html_path
|
| 383 |
-
options['footer-spacing'] = '5'
|
| 384 |
-
|
| 385 |
-
# Generate PDF
|
| 386 |
-
try:
|
| 387 |
-
pdfkit.from_string(styled_html, output_path, options=options)
|
| 388 |
-
finally:
|
| 389 |
-
# Clean up temporary footer file
|
| 390 |
-
if footer_html_path and os.path.exists(footer_html_path):
|
| 391 |
-
os.remove(footer_html_path)
|
| 392 |
-
|
| 393 |
return output_path
|
| 394 |
-
|
| 395 |
except Exception as e:
|
| 396 |
raise Exception(f"PDF generation failed: {str(e)}")
|
| 397 |
|
|
|
|
| 18 |
import gradio as gr
|
| 19 |
from openai import OpenAI
|
| 20 |
import markdown
|
| 21 |
+
from weasyprint import HTML
|
| 22 |
|
| 23 |
from langchain_mcp_adapters.client import MultiServerMCPClient
|
| 24 |
|
|
|
|
| 195 |
return json.dumps({"error": f"Error serializing: {str(e)}", "data": str(obj)}, **kwargs)
|
| 196 |
|
| 197 |
def markdown_to_pdf(markdown_content: str, output_path: str) -> str:
|
| 198 |
+
"""Convert markdown to PDF using weasyprint."""
|
| 199 |
try:
|
| 200 |
# Convert markdown to HTML and add exercise break classes
|
| 201 |
html_content = markdown.markdown(markdown_content, extensions=['tables', 'fenced_code'])
|
| 202 |
+
|
| 203 |
# Add CSS classes for better page breaks
|
| 204 |
import re
|
| 205 |
# Add exercise-break class to h2 elements (exercises)
|
| 206 |
html_content = re.sub(r'<h2>', r'<h2 class="exercise-break">', html_content)
|
| 207 |
+
|
| 208 |
# Get the logo path
|
| 209 |
logo_path = "https://huggingface.co/spaces/Agents-MCP-Hackathon/ipmentor-subnetting-exercises-generator/resolve/main/assets/logo.svg"
|
| 210 |
+
|
| 211 |
+
# Add CSS styling with IPMentor branding colors and footer with logo
|
|
|
|
| 212 |
styled_html = f"""
|
| 213 |
<!DOCTYPE html>
|
| 214 |
<html>
|
|
|
|
| 217 |
<style>
|
| 218 |
@page {{
|
| 219 |
margin: 1in 1in 100px 1in;
|
| 220 |
+
@bottom-center {{
|
| 221 |
+
content: element(footer);
|
| 222 |
+
}}
|
| 223 |
}}
|
| 224 |
+
|
| 225 |
+
body {{
|
| 226 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 227 |
+
line-height: 1.6;
|
| 228 |
margin: 0;
|
| 229 |
padding: 20px;
|
| 230 |
color: #333;
|
| 231 |
background: #fefefe;
|
| 232 |
}}
|
| 233 |
+
|
| 234 |
+
h1 {{
|
| 235 |
+
color: #FC8100;
|
| 236 |
border-bottom: 4px solid #FED200;
|
| 237 |
padding-bottom: 15px;
|
| 238 |
margin-bottom: 30px;
|
| 239 |
font-size: 2.2em;
|
| 240 |
font-weight: bold;
|
| 241 |
}}
|
| 242 |
+
|
| 243 |
+
h2 {{
|
| 244 |
+
color: #FC8100;
|
| 245 |
border-bottom: 3px solid #FFCB00;
|
| 246 |
padding-bottom: 8px;
|
| 247 |
margin-top: 40px;
|
|
|
|
| 249 |
font-size: 1.5em;
|
| 250 |
page-break-after: avoid;
|
| 251 |
}}
|
| 252 |
+
|
| 253 |
+
h3 {{
|
| 254 |
+
color: #FE8100;
|
| 255 |
margin-top: 25px;
|
| 256 |
margin-bottom: 15px;
|
| 257 |
font-size: 1.2em;
|
| 258 |
}}
|
| 259 |
+
|
| 260 |
p {{
|
| 261 |
margin-bottom: 15px;
|
| 262 |
text-align: justify;
|
| 263 |
}}
|
| 264 |
+
|
| 265 |
em {{
|
| 266 |
color: #F05600;
|
| 267 |
font-style: italic;
|
| 268 |
}}
|
| 269 |
+
|
| 270 |
strong {{
|
| 271 |
color: #FE8100;
|
| 272 |
}}
|
| 273 |
+
|
| 274 |
a {{
|
| 275 |
color: #F05600;
|
| 276 |
text-decoration: none;
|
| 277 |
border-bottom: 1px dotted #F05600;
|
| 278 |
}}
|
| 279 |
+
|
| 280 |
a:hover {{
|
| 281 |
border-bottom: 1px solid #F05600;
|
| 282 |
}}
|
| 283 |
+
|
| 284 |
+
img {{
|
| 285 |
+
max-width: 85%;
|
| 286 |
max-height: 450px;
|
| 287 |
+
height: auto;
|
| 288 |
display: block;
|
| 289 |
margin: 25px auto;
|
| 290 |
border: 2px solid #FED200;
|
| 291 |
border-radius: 8px;
|
| 292 |
box-shadow: 0 4px 8px rgba(254, 129, 0, 0.1);
|
| 293 |
}}
|
| 294 |
+
|
| 295 |
code {{
|
| 296 |
background: #FFF4E6;
|
| 297 |
color: #FC8100;
|
|
|
|
| 300 |
font-family: 'Courier New', monospace;
|
| 301 |
border: 1px solid #FED200;
|
| 302 |
}}
|
| 303 |
+
|
| 304 |
+
pre {{
|
| 305 |
+
background: #FFF8F0;
|
| 306 |
+
padding: 20px;
|
| 307 |
border-radius: 8px;
|
| 308 |
border-left: 6px solid #F05600;
|
| 309 |
margin: 20px 0;
|
| 310 |
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
| 311 |
}}
|
| 312 |
+
|
| 313 |
hr {{
|
| 314 |
border: none;
|
| 315 |
height: 3px;
|
|
|
|
| 317 |
margin: 30px 0;
|
| 318 |
border-radius: 2px;
|
| 319 |
}}
|
| 320 |
+
|
|
|
|
| 321 |
.exercise-break {{
|
| 322 |
page-break-before: always;
|
| 323 |
margin-top: 0;
|
| 324 |
}}
|
| 325 |
+
|
| 326 |
.exercise-break:first-of-type {{
|
| 327 |
page-break-before: avoid;
|
| 328 |
}}
|
| 329 |
+
|
| 330 |
+
.footer {{
|
| 331 |
+
position: running(footer);
|
| 332 |
+
text-align: center;
|
| 333 |
+
padding: 8px;
|
| 334 |
+
}}
|
| 335 |
+
|
| 336 |
+
.footer img {{
|
| 337 |
+
height: 38px;
|
| 338 |
+
width: auto;
|
| 339 |
+
margin: 0 auto;
|
| 340 |
+
border: none;
|
| 341 |
+
box-shadow: none;
|
| 342 |
+
}}
|
| 343 |
</style>
|
| 344 |
</head>
|
| 345 |
<body>
|
| 346 |
{html_content}
|
| 347 |
+
|
| 348 |
+
<div class="footer">
|
| 349 |
+
<img src="{logo_path}" alt="IPMentor">
|
| 350 |
+
</div>
|
| 351 |
</body>
|
| 352 |
</html>
|
| 353 |
"""
|
| 354 |
+
|
| 355 |
+
# Generate PDF using WeasyPrint
|
| 356 |
+
HTML(string=styled_html).write_pdf(output_path)
|
| 357 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 358 |
return output_path
|
| 359 |
+
|
| 360 |
except Exception as e:
|
| 361 |
raise Exception(f"PDF generation failed: {str(e)}")
|
| 362 |
|