# ============================================================ # 演示手卡 Markdown → PDF(A4,CJK 字体,reportlab 轻量渲染) # 用法: python demo-data/md_handout_to_pdf.py 输入.md 输出.pdf # ============================================================ from __future__ import annotations import re import sys from pathlib import Path from reportlab.lib import colors from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import ParagraphStyle from reportlab.lib.units import mm from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.platypus import ( Paragraph, SimpleDocTemplate, Spacer, Table, TableStyle, ) FONT_DIR = Path(sys.executable).parent.parent.parent / "fonts" pdfmetrics.registerFont(TTFont("NotoSC", str(FONT_DIR / "NotoSansSC-Regular.ttf"))) pdfmetrics.registerFont(TTFont("NotoSC-B", str(FONT_DIR / "NotoSansSC-Bold.ttf"))) pdfmetrics.registerFontFamily("NotoSC", normal="NotoSC", bold="NotoSC-B", italic="NotoSC", boldItalic="NotoSC-B") H1 = ParagraphStyle("h1", fontName="NotoSC-B", fontSize=15, leading=20, spaceAfter=4, textColor=colors.HexColor("#1a3a6b")) H2 = ParagraphStyle("h2", fontName="NotoSC-B", fontSize=11.5, leading=16, spaceBefore=7, spaceAfter=3, textColor=colors.HexColor("#1a3a6b")) BODY = ParagraphStyle("body", fontName="NotoSC", fontSize=8.8, leading=13.2, spaceAfter=2) QUOTE = ParagraphStyle("quote", parent=BODY, leftIndent=8, textColor=colors.HexColor("#666666"), fontSize=8.2, leading=12) CELL = ParagraphStyle("cell", fontName="NotoSC", fontSize=8.2, leading=11.5) CELL_B = ParagraphStyle("cellb", parent=CELL, fontName="NotoSC-B", textColor=colors.white) def inline(text: str) -> str: text = text.replace("&", "&").replace("<", "<").replace(">", ">") text = re.sub(r"\*\*(.+?)\*\*", r'\1', text) text = re.sub(r"`(.+?)`", r'\1', text) return text def render(md: str) -> list: flow = [] lines = md.splitlines() i = 0 while i < len(lines): line = lines[i].rstrip() if not line.strip(): i += 1 continue if line.startswith("# "): flow.append(Paragraph(inline(line[2:]), H1)) elif line.startswith("## "): flow.append(Paragraph(inline(line[3:]), H2)) elif line.startswith("> "): flow.append(Paragraph(inline(line[2:]), QUOTE)) elif line.startswith("- "): flow.append(Paragraph("· " + inline(line[2:]), BODY)) elif line.startswith("|"): rows = [] while i < len(lines) and lines[i].strip().startswith("|"): cells = [c.strip() for c in lines[i].strip().strip("|").split("|")] if not all(re.fullmatch(r":?-{2,}:?", c or "---") for c in cells): rows.append(cells) i += 1 i -= 1 if rows: ncol = max(len(r) for r in rows) data = [] for ri, r in enumerate(rows): st = CELL_B if ri == 0 else CELL data.append([Paragraph(inline(c), st) for c in r] + [""] * (ncol - len(r))) page_w = A4[0] - 24 * mm weights = [max((len(str(c)) for c in col), default=4) for col in zip(*[list(r) + [""] * (ncol - len(r)) for r in rows])] total = sum(weights) or 1 widths = [max(page_w * w / total, 40) for w in weights] # 窄列保底 40pt scale = page_w / sum(widths) tbl = Table(data, colWidths=[w * scale for w in widths]) tbl.setStyle(TableStyle([ ("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#1a3a6b")), ("ROWBACKGROUNDS", (0, 1), (-1, -1), [colors.white, colors.HexColor("#f2f5fa")]), ("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#c9d3e0")), ("VALIGN", (0, 0), (-1, -1), "TOP"), ("TOPPADDING", (0, 0), (-1, -1), 2.5), ("BOTTOMPADDING", (0, 0), (-1, -1), 2.5), ("LEFTPADDING", (0, 0), (-1, -1), 4), ("RIGHTPADDING", (0, 0), (-1, -1), 4), ])) flow.append(tbl) flow.append(Spacer(1, 3)) else: flow.append(Paragraph(inline(line), BODY)) i += 1 return flow def main() -> None: src, dst = Path(sys.argv[1]), Path(sys.argv[2]) doc = SimpleDocTemplate(str(dst), pagesize=A4, leftMargin=12 * mm, rightMargin=12 * mm, topMargin=11 * mm, bottomMargin=11 * mm, title="锐扬 APS 智能体 · 演示手卡") doc.build(render(src.read_text(encoding="utf-8"))) print(f"PDF 已生成: {dst}") if __name__ == "__main__": main()