112 lines
4.8 KiB
Python
112 lines
4.8 KiB
Python
# ============================================================
|
||
# 演示手卡 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'<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()
|