"""Render a project Markdown report (headings, tables, bullets, quotes) to an A4 PDF with Korean fonts. usage: python md_to_pdf.py [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"\1", text) text = re.sub(r"`(.+?)`", r'\1', text) text = re.sub(r"~~(.+?)~~", r"\1", 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)