Overview
with the single weasyprint enginerenders all input as PDF. Regardless of the input form,
입력 → HTML → weasyprint → PDF it is unified into one path, faithfully handling both of the following scenarios.
- Styled HTML reports (예:
moai-content:html-report·moai-content:html-slideoutput, or user-created design HTML) → renders its own CSS as-is to preserve the screen design. Modern CSS layouts such as two-column card grids, custom colors, and web fonts are not broken. - Markdown · Structured JSON · Plain Text → rendered by applying default document formatting (A4, tables, heading styles).
CJK (Korean, Chinese, Japanese) glyphs are blocked from breaking by embedding the bundled Noto Sans CJK font with @font-faceand if Noto Sans CJK KRis installed on the system, fontconfig automatically falls back.
Why the single weasyprint engine: weasyprint faithfully renders full CSS (layout, colors, web fonts, tables). "Turn this HTML report into a PDF exactly as designed" is the most common request, and this path satisfies that requirement precisely. Coordinate-based low-level rendering (e.g., PyMuPDF insert_text) cannot reproduce modern CSS layouts, so it is not used.
Workflow
0. [폰트 확인] → assets/fonts/ 에 Noto Sans CJK OTF 4종 존재 확인, 누락 시 download_fonts.py 자동 호출
1. [입력 감지] → HTML(완성 문서) / Markdown / JSON / Text 자동 감지
2. [HTML 빌드] → 완성 HTML은 그대로, 그 외는 HTML로 변환 (+ 기본 문서 CSS)
3. [폰트 주입] → Noto Sans CJK @font-face CSS 결합 (CJK 깨짐 방지)
4. [렌더] → weasyprint HTML.write_pdf() 로 A4 PDF 생성
5. [자체 검수] → 산출 PDF 재확인 (페이지 수·CJK 표시·플레이스홀더 잔존)
The core execution is done with a single line of the bundled script:
# HTML 리포트를 디자인 그대로 PDF로 (가장 흔한 케이스)
python3 moai-office/skills/pdf-writer/scripts/render_pdf.py --in report.html --out report.pdf
# Markdown / JSON / Text → PDF
python3 moai-office/skills/pdf-writer/scripts/render_pdf.py --in doc.md --out doc.pdf
python3 moai-office/skills/pdf-writer/scripts/render_pdf.py --in data.json --out doc.pdf
scripts/render_pdf.pyautomatically detects the input type, renders finished HTML while preserving its own CSS,
and applies default formatting + Noto Sans CJK to Markdown/JSON/Text.
Step 0: Automatic font assurance (skill entry point)
import subprocess, sys
from pathlib import Path
SKILL_ROOT = Path(__file__).parent
DOWNLOADER = SKILL_ROOT / "scripts" / "download_fonts.py"
def ensure_fonts():
"""Noto Sans CJK OTF 4종 존재 검증, 누락 시 자동 다운로드."""
if subprocess.run([sys.executable, str(DOWNLOADER), "--check"]).returncode != 0:
if subprocess.run([sys.executable, str(DOWNLOADER)]).returncode != 0:
raise RuntimeError("Noto Sans CJK 폰트 다운로드 실패. 네트워크/GitHub 접근 확인 필요.")
ensure_fonts() # PDF 생성 직전 호출
The download occurs only the first time (about 64MB), and afterward --checkpasses immediately. If
Noto Sans CJK KRis already on the system, weasyprint automatically falls back even without the font, so CJK is displayed safely.
Steps 1–4: weasyprint rendering core code
scripts/render_pdf.pyThe core of this is as follows (see the script for the full implementation):
from weasyprint import HTML, CSS
from pathlib import Path
FONT_DIR = Path("moai-office/skills/pdf-writer/assets/fonts")
# 번들 Noto Sans CJK를 @font-face로 등록 (weight별)
FONT_FACE = "".join(
f"@font-face{{font-family:'Noto Sans CJK';font-weight:{w};"
f"src:url('file://{FONT_DIR}/NotoSansCJK-{n}.otf');}}"
for w, n in {300: "Light", 400: "Regular", 500: "Medium", 700: "Bold"}.items()
if (FONT_DIR / f"NotoSansCJK-{n}.otf").exists()
)
# (A) 완성 스타일 HTML → 자체 CSS 보존, CJK 폰트만 보강
HTML(string=styled_html, base_url=".").write_pdf("out.pdf", stylesheets=[CSS(string=FONT_FACE)])
# (B) Markdown/JSON/Text에서 만든 HTML → 기본 문서 CSS + 폰트
HTML(string=plain_html, base_url=".").write_pdf("out.pdf", stylesheets=[CSS(string=FONT_FACE + BASE_CSS)])
BASE_CSS는 @page { size:A4; margin:20mm } + body/heading/table styles, and by specifying font-family에
'Noto Sans CJK'CJK is always rendered with the embedded font.
Dependencies
pip install weasyprint # 핵심 렌더링 엔진 (풀 CSS → PDF)
pip install markdown # Markdown → HTML 변환 (선택, 없으면 최소 폴백)
weasyprint uses the system library
cairo/pango/gdk-pixbuf. It is included by default in most Cowork sandboxes and Linux/macOS environments, so it works with justpip install weasyprint.
Input format specification
Finished HTML (design preserved)
<html>·<body>·<!doctype html> If there is a signal, it is regarded as a finished documentand its own <style>·CSS are
rendered as-is. moai-content:html-report·moai-content:html-slide If you pass the output as-is,
you get a PDF with the same design as on screen.
Structured JSON spec
{
"title": "문서 제목",
"subtitle": "부제목 (선택)",
"author": "작성자 (선택)",
"date": "2026-01-01 (선택)",
"sections": [
{
"heading": "섹션 제목",
"level": 2,
"body": "본문 텍스트 (마크다운 인라인 지원)",
"table": { "headers": ["열1", "열2"], "rows": [["a", "b"]] },
"image": { "path": "./chart.png", "caption": "그림 1" }
}
]
}
Markdown input example
# 프로젝트 보고서
## 1. 개요
본 보고서는 ...
## 2. 결과
| 항목 | 수치 |
|------|------|
| 완료율 | 95% |
Usage example
- "Create this HTML report as a PDF exactly as designed" → Completed HTML path (CSS preserved)
- "Weekly report Markdown as a PDF without Korean text corruption" → Markdown path (default formatting + CJK)
- "Generate a quarterly report PDF with the JSON structure below" → JSON path
- "Output a Korean·Japanese·Chinese·English mixed manual as PDF" → Multilingual embedding
Output format
- File format:
.pdf(ISO 32000 PDF standard) - Page size: A4 (210 × 297mm, default margin 20mm) — for completed HTML, its own
@pagesettings take precedence - Font: Noto Sans CJK (single family covering Korean·Japanese·Simplified Chinese·Traditional Chinese·Latin, subset embedding)
- Encoding: Full UTF-8 support
Self-verification
After PDF generation, produce the output .pdfand reopen it to check for remaining placeholders, page count shortfalls, CJK encoding corruption, and design
omissionsare self-checked, and if there are issues, they are automatically fixed and regenerated, with a final PASS/FAIL reported.
Troubleshooting
| Symptom | Cause | Solution |
|---|---|---|
| Korean/Hanja appear as □□□ or blank | CJK font unresolved | download_fonts.py Secure bundled OTFs by running, or install the system Noto Sans CJK KR installation |
ModuleNotFoundError: weasyprint |
weasyprint not installed | pip install weasyprint Run |
cannot load library 'libpango...' |
Missing system libraries | (Debian/Ubuntu) apt install libpango-1.0-0 libpangocairo-1.0-0, (macOS) brew install pango |
| HTML design breaks in PDF | Completed HTML mistaken for a fragment | Input includes <html>/<body> wrapper, or --in *.html pass the file |
| Web fonts (CDN) not applied | Offline/CDN blocked | Inline fonts in the HTML or substitute system fonts (for offline PDFs, avoiding CDN dependency is recommended) |
| Tables split at page boundaries | CSS unspecified | tr { page-break-inside: avoid; } Add |
Related skills / handoff
| Skill | Relationship | When to use |
|---|---|---|
moai-content:html-report |
before (generate HTML report → convert to PDF with this skill) | "Make a report and also as PDF" flow |
moai-content:html-slide |
before (generate HTML slides → convert to PDF) | Slide deck as a PDF distribution copy |
moai-office:docx-generator |
alternative (editable Word deliverable) | When the recipient needs an editable file |
moai-core:ai-slop-reviewer |
after (AI pattern review of text deliverables) | Review the manuscript before generating the text PDF |
Technical references
- weasyprint official documentation: https://doc.courtbouillon.org/weasyprint/stable/
- Noto Sans CJK GitHub: https://github.com/notofonts/noto-cjk
- Noto Sans CJK releases (otf download): https://github.com/notofonts/noto-cjk/releases
- SIL OFL 1.1 license: https://openfontlicense.org/open-font-license-official-text/
- PDF ISO 32000 standard: https://opensource.adobe.com/dc-acrobat-sdk-docs/