aps-agent/demo-data/md_handout_to_pdf.py

112 lines
4.8 KiB
Python
Raw Permalink Normal View History

# ============================================================
# 演示手卡 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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
text = re.sub(r"\*\*(.+?)\*\*", r'<font name="NotoSC-B">\1</font>', text)
text = re.sub(r"`(.+?)`", r'<font name="NotoSC" color="#8a4b08">\1</font>', 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()