- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
126 lines
5.8 KiB
Python
126 lines
5.8 KiB
Python
"""Render a project Markdown report (headings, tables, bullets, quotes) to an A4 PDF with Korean fonts.
|
||
|
||
usage: python md_to_pdf.py <input.md> <output.pdf> [header text]
|
||
The .md file stays the canonical document; the PDF is a rendition of it.
|
||
"""
|
||
import re
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
from reportlab.lib import colors
|
||
from reportlab.lib.enums import TA_LEFT
|
||
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 (HRFlowable, KeepTogether, Paragraph, SimpleDocTemplate, Spacer, Table,
|
||
TableStyle)
|
||
|
||
pdfmetrics.registerFont(TTFont("Malgun", r"C:\Windows\Fonts\malgun.ttf"))
|
||
pdfmetrics.registerFont(TTFont("MalgunBd", r"C:\Windows\Fonts\malgunbd.ttf"))
|
||
pdfmetrics.registerFontFamily("Malgun", normal="Malgun", bold="MalgunBd", italic="Malgun", boldItalic="MalgunBd")
|
||
|
||
BASE = ParagraphStyle("base", fontName="Malgun", fontSize=9, leading=13, alignment=TA_LEFT)
|
||
ST = {
|
||
"h1": ParagraphStyle("h1", parent=BASE, fontName="MalgunBd", fontSize=15, leading=20, spaceAfter=6),
|
||
"h2": ParagraphStyle("h2", parent=BASE, fontName="MalgunBd", fontSize=12, leading=16, spaceBefore=10, spaceAfter=4),
|
||
"h3": ParagraphStyle("h3", parent=BASE, fontName="MalgunBd", fontSize=10, leading=14, spaceBefore=6, spaceAfter=3),
|
||
"p": BASE,
|
||
"quote": ParagraphStyle("q", parent=BASE, leftIndent=8, textColor=colors.HexColor("#444444"),
|
||
backColor=colors.HexColor("#F2F2F2"), borderPadding=4, spaceBefore=4, spaceAfter=4),
|
||
"li": ParagraphStyle("li", parent=BASE, leftIndent=12, bulletIndent=4),
|
||
"li2": ParagraphStyle("li2", parent=BASE, leftIndent=24, bulletIndent=16),
|
||
"cell": ParagraphStyle("cell", parent=BASE, fontSize=7.5, leading=10),
|
||
"cellh": ParagraphStyle("cellh", parent=BASE, fontName="MalgunBd", fontSize=7.5, leading=10),
|
||
}
|
||
WIDTH = A4[0] - 30 * mm
|
||
|
||
|
||
def inline(text):
|
||
text = text.replace("−", "-") # Malgun Gothic has no U+2212 minus glyph (renders blank)
|
||
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||
text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
|
||
text = re.sub(r"`(.+?)`", r'<font color="#7A3E00">\1</font>', text)
|
||
text = re.sub(r"~~(.+?)~~", r"<strike>\1</strike>", text)
|
||
return text
|
||
|
||
|
||
def table(lines):
|
||
rows = [[c.strip() for c in l.strip().strip("|").split("|")] for l in lines]
|
||
rows = [r for r in rows if not all(re.fullmatch(r":?-{2,}:?", c) for c in r if c)]
|
||
n = max(len(r) for r in rows)
|
||
rows = [r + [""] * (n - len(r)) for r in rows]
|
||
# minimum width = longest unbreakable token (so short codes like 'E-01' or '오류' never wrap per glyph),
|
||
# the remaining width is shared in proportion to text length
|
||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||
plain = lambda c: re.sub(r"[*`~]", "", c)
|
||
mins = [max(stringWidth(tok, "MalgunBd", 7.5) for r in rows for tok in (plain(r[i]).split() or [""])) + 8
|
||
for i in range(n)]
|
||
mins = [min(m, WIDTH * 0.3) for m in mins]
|
||
weights = [max(min(len(r[i]), 60) for r in rows) + 2 for i in range(n)]
|
||
spare = max(WIDTH - sum(mins), 0)
|
||
widths = [m + spare * w / sum(weights) for m, w in zip(mins, weights)]
|
||
if sum(widths) > WIDTH:
|
||
widths = [w * WIDTH / sum(widths) for w in widths]
|
||
data = [[Paragraph(inline(c), ST["cellh"] if k == 0 else ST["cell"]) for c in r] for k, r in enumerate(rows)]
|
||
t = Table(data, colWidths=widths, repeatRows=1)
|
||
t.setStyle(TableStyle([
|
||
("GRID", (0, 0), (-1, -1), 0.4, colors.HexColor("#999999")),
|
||
("BACKGROUND", (0, 0), (-1, 0), colors.HexColor("#E6E6E6")),
|
||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||
("LEFTPADDING", (0, 0), (-1, -1), 3), ("RIGHTPADDING", (0, 0), (-1, -1), 3),
|
||
]))
|
||
return t
|
||
|
||
|
||
def build(md, out, header):
|
||
lines = md.splitlines()
|
||
story, i = [], 0
|
||
while i < len(lines):
|
||
l = lines[i]
|
||
if l.startswith("|"):
|
||
block = []
|
||
while i < len(lines) and lines[i].startswith("|"):
|
||
block.append(lines[i])
|
||
i += 1
|
||
story += [table(block), Spacer(1, 4)]
|
||
continue
|
||
if l.startswith("### "):
|
||
story.append(Paragraph(inline(l[4:]), ST["h3"]))
|
||
elif l.startswith("## "):
|
||
story.append(Paragraph(inline(l[3:]), ST["h2"]))
|
||
elif l.startswith("# "):
|
||
story.append(Paragraph(inline(l[2:]), ST["h1"]))
|
||
elif l.startswith(">"):
|
||
story.append(Paragraph(inline(l.lstrip("> ")), ST["quote"]))
|
||
elif re.match(r"^\s{2,}- ", l):
|
||
story.append(Paragraph(inline(l.strip()[2:]), ST["li2"], bulletText="–"))
|
||
elif l.startswith("- "):
|
||
story.append(Paragraph(inline(l[2:]), ST["li"], bulletText="•"))
|
||
elif l.strip() == "---":
|
||
story.append(HRFlowable(width="100%", thickness=0.5, color=colors.HexColor("#BBBBBB"), spaceBefore=4,
|
||
spaceAfter=4))
|
||
elif l.strip():
|
||
story.append(Paragraph(inline(l), ST["p"]))
|
||
i += 1
|
||
|
||
def deco(c, d):
|
||
c.saveState()
|
||
c.setFont("Malgun", 7.5)
|
||
c.setFillColor(colors.HexColor("#666666"))
|
||
c.drawString(15 * mm, A4[1] - 10 * mm, header)
|
||
c.drawRightString(A4[0] - 15 * mm, 10 * mm, f"{d.page}")
|
||
c.restoreState()
|
||
|
||
doc = SimpleDocTemplate(str(out), pagesize=A4, leftMargin=15 * mm, rightMargin=15 * mm, topMargin=16 * mm,
|
||
bottomMargin=16 * mm, title=Path(out).stem, author="심영표")
|
||
doc.build(story, onFirstPage=deco, onLaterPages=deco)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
src, dst = Path(sys.argv[1]), Path(sys.argv[2])
|
||
hdr = sys.argv[3] if len(sys.argv) > 3 else src.stem
|
||
build(src.read_text(encoding="utf-8"), dst, hdr)
|
||
print(dst, dst.stat().st_size)
|