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()
|