""" PDF 生成服务 - 基于 WeasyPrint + 系统字体 """ import markdown from weasyprint import HTML, CSS from weasyprint.text.fonts import FontConfiguration import io class PDFService: def __init__(self): self.font_config = FontConfiguration() def get_css(self): return """ @page { margin: 2cm; @bottom-right { content: counter(page); font-size: 9pt; } } /* 全局:macOS 用 Hiragino Sans GB / Heiti SC,Docker 用 WenQuanYi Micro Hei */ * { font-family: "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", "Arial Unicode MS", sans-serif !important; } body { font-size: 11pt; line-height: 1.6; color: #333; background-color: #fff; } h1, h2, h3, h4, h5, h6 { border-bottom: 1px solid #eee; padding-bottom: 0.3em; margin-top: 1.5em; margin-bottom: 1em; font-weight: bold; } /* 代码块:等宽字体优先,中文回退到对应平台的 CJK 字体 */ pre, code, pre *, code * { font-family: "Menlo", "DejaVu Sans Mono", "Courier New", "Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", monospace !important; background-color: transparent; } pre { background-color: #f6f8fa !important; padding: 16px; border-radius: 6px; white-space: pre-wrap; word-break: break-all; display: block; } code { background-color: rgba(175, 184, 193, 0.2); padding: 0.2em 0.4em; border-radius: 6px; } table { border-collapse: collapse; width: 100%; margin-bottom: 1em; } th, td { border: 1px solid #dfe2e5; padding: 6px 13px; } tr:nth-child(2n) { background-color: #f6f8fa; } img { max-width: 100%; } blockquote { border-left: 0.25em solid #dfe2e5; color: #6a737d; padding: 0 1em; margin-left: 0; } """ async def md_to_pdf(self, md_content: str, title: str = "Document", base_url: str = None) -> io.BytesIO: """将 Markdown 转换为 PDF 字节流""" html_content = markdown.markdown( md_content, extensions=['extra', 'codehilite', 'toc', 'tables'] ) full_html = f""" {title} {html_content} """ pdf_buffer = io.BytesIO() css = CSS(string=self.get_css(), font_config=self.font_config) HTML(string=full_html, base_url=base_url).write_pdf( pdf_buffer, stylesheets=[css], font_config=self.font_config ) pdf_buffer.seek(0) return pdf_buffer pdf_service = PDFService()