Spaces:
Running
Running
| """Convert a markdown file to PDF using Playwright (Chromium headless). | |
| Chromium has full CSS3 support including proper page-break handling, | |
| which xhtml2pdf (the previous converter) did not. The trade-off is a | |
| ~150 MB Chromium binary dependency, installed once via: | |
| pip install playwright | |
| playwright install chromium | |
| """ | |
| from __future__ import annotations | |
| import sys | |
| from pathlib import Path | |
| import markdown | |
| from playwright.sync_api import sync_playwright | |
| CSS = """ | |
| @page { | |
| size: letter; | |
| margin: 0.5in; | |
| } | |
| body { | |
| font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | |
| font-size: 9pt; | |
| line-height: 1.35; | |
| color: #222; | |
| } | |
| h1 { font-size: 17pt; color: #1a1a1a; margin: 0.3em 0 0.2em; | |
| border-bottom: 2px solid #333; padding-bottom: 3px; | |
| page-break-after: avoid; break-after: avoid; } | |
| h2 { font-size: 13pt; color: #1a1a1a; margin: 0.8em 0 0.2em; | |
| border-bottom: 1px solid #999; padding-bottom: 2px; | |
| page-break-after: avoid; break-after: avoid; } | |
| h3 { font-size: 11pt; color: #1a1a1a; margin: 0.6em 0 0.15em; | |
| page-break-after: avoid; break-after: avoid; } | |
| p, li { font-size: 9pt; margin: 0.25em 0; } | |
| ul, ol { margin: 0.2em 0; padding-left: 1.3em; } | |
| table { | |
| border-collapse: collapse; width: 100%; | |
| margin: 0.4em 0; font-size: 8.5pt; | |
| page-break-inside: avoid; | |
| break-inside: avoid; | |
| } | |
| th, td { | |
| border: 1px solid #aaa; padding: 3px 5px; | |
| text-align: left; vertical-align: top; | |
| } | |
| th { background: #eee; font-weight: bold; } | |
| thead { display: table-header-group; } | |
| tr { page-break-inside: avoid; break-inside: avoid; } | |
| code { | |
| background: #f0f0f0; | |
| padding: 1px 3px; | |
| border-radius: 2px; | |
| font-family: Consolas, "Courier New", monospace; | |
| font-size: 8.5pt; | |
| } | |
| pre { | |
| background: #f5f5f5; | |
| border: 1px solid #ddd; | |
| padding: 6px; | |
| border-radius: 3px; | |
| font-family: Consolas, "Courier New", monospace; | |
| font-size: 8pt; | |
| line-height: 1.25; | |
| white-space: pre-wrap; | |
| page-break-inside: avoid; | |
| break-inside: avoid; | |
| } | |
| blockquote { | |
| border-left: 3px solid #888; | |
| margin-left: 0; | |
| padding-left: 10px; | |
| color: #555; | |
| font-style: italic; | |
| } | |
| hr { border: 0; border-top: 1px solid #ccc; margin: 0.6em 0; } | |
| div { break-inside: avoid; page-break-inside: avoid; } | |
| """ | |
| def convert(md_path: Path, pdf_path: Path) -> None: | |
| md_text = md_path.read_text(encoding="utf-8") | |
| html_body = markdown.markdown( | |
| md_text, | |
| extensions=["tables", "fenced_code", "sane_lists", "md_in_html"], | |
| ) | |
| html = f"""<!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset="utf-8"> | |
| <style>{CSS}</style> | |
| </head> | |
| <body> | |
| {html_body} | |
| </body> | |
| </html>""" | |
| with sync_playwright() as p: | |
| browser = p.chromium.launch() | |
| page = browser.new_page() | |
| page.set_content(html, wait_until="networkidle") | |
| page.pdf( | |
| path=str(pdf_path), | |
| format="Letter", | |
| margin={"top": "0.5in", "bottom": "0.5in", "left": "0.5in", "right": "0.5in"}, | |
| print_background=True, | |
| ) | |
| browser.close() | |
| print(f"Wrote {pdf_path}") | |
| if __name__ == "__main__": | |
| if len(sys.argv) != 3: | |
| print("Usage: md_to_pdf.py <input.md> <output.pdf>") | |
| sys.exit(1) | |
| convert(Path(sys.argv[1]), Path(sys.argv[2])) | |