Install any skill in seconds. Free to start, no credit card required.
Get Started Free →어떤 콘텐츠든 PDF 파일로 만들어 드립니다 — 스타일이 입혀진 HTML 리포트는 디자인을 그대로 보존해 변환하고, Markdown·구조화 JSON·일반 텍스트는 자동 서식으로 렌더합니다. weasyprint 단일 엔진으로 풀 CSS를 충실히 렌더하며, 번들 Noto Sans CJK 폰트를 임베딩해 한국어·일본어·중국어 글리프 깨짐을 근본 차단합니다. 다음과 같은 요청 시 반드시 이 스킬을 사용하세요: - "PDF로 만들어줘", "PDF로 생성해줘", "PDF로도 생성해줘", "PDF로 저장해줘", "PDF로 출력해줘", "PDF로 뽑아줘" - "이거 PDF로", "이 리포트 PDF로", "이 보고서 PDF로 변환해줘", "HTML을 PDF로", "HTML 리포트를 PDF로" - "PDF로 변환해줘", "문서 PDF 파일 생성", "보고서 PDF로 저장해줘", "계획서 PDF 파일" - "한글 PDF 만들어줘", "한국어 PDF", "일본어 PDF", "중국어 PDF", "다국어
.claude/skills/modu-ai-pdf-writer/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 95% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 79% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 115% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 90% | 0% |
weasyprint 단일 엔진으로 모든 입력을 PDF로 렌더합니다. 입력 형태와 무관하게 입력 → HTML → weasyprint → PDF 한 경로로 통일되어, 다음 두 시나리오를 모두 충실히 처리합니다.
moai-content:html-report·moai-content:html-slide 산출물,또는 사용자가 만든 디자인 HTML) → 자체 CSS를 그대로 렌더해 화면 디자인을 보존합니다. 2열 카드 그리드·커스텀 색·웹폰트 등 모던 CSS 레이아웃이 깨지지 않습니다.
CJK(한·중·일) 글리프는 번들 Noto Sans CJK 폰트를 @font-face로 임베딩해 깨짐을 차단하며, 시스템에 Noto Sans CJK KR이 설치돼 있으면 fontconfig가 자동 폴백합니다.
> 왜 weasyprint 단일 엔진인가: weasyprint는 풀 CSS(레이아웃·색·웹폰트·표)를 충실히 렌더합니다. > "이 HTML 리포트를 디자인 그대로 PDF로"가 가장 흔한 요청이며, 이 경로가 그 요구를 정확히 만족합니다. > 좌표 기반 저수준 렌더링(예: PyMuPDF insert_text)은 모던 CSS 레이아웃을 재현하지 못하므로 사용하지 않습니다.
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 표시·플레이스홀더 잔존)핵심 실행은 번들 스크립트 한 줄로 끝납니다:
bash# 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.py가 입력 종류를 자동 감지하고, 완성 HTML은 자체 CSS를 보존한 채 렌더하며, Markdown/JSON/Text는 기본 서식 + Noto Sans CJK를 적용합니다.
pythonimport 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 생성 직전 호출
다운로드는 최초 1회(약 64MB)만 발생하며, 이후 --check가 즉시 통과합니다. 시스템에 Noto Sans CJK KR이 이미 있으면 폰트가 없어도 weasyprint가 자동 폴백하므로 CJK는 안전하게 표시됩니다.
scripts/render_pdf.py의 핵심은 다음과 같습니다 (전체 구현은 스크립트 참조):
pythonfrom 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 } + 본문/제목/표 스타일로, font-family에 'Noto Sans CJK'를 지정해 CJK가 항상 임베딩 폰트로 렌더되게 합니다.
bashpip install weasyprint # 핵심 렌더링 엔진 (풀 CSS → PDF) pip install markdown # Markdown → HTML 변환 (선택, 없으면 최소 폴백)
> weasyprint는 시스템 라이브러리 cairo / pango / gdk-pixbuf를 사용합니다. 대부분의 > Cowork 샌드박스 및 Linux/macOS 환경에 기본 포함돼 있어 pip install weasyprint만으로 동작합니다.
<html>·<body>·<!doctype html> 신호가 있으면 완성 문서로 간주하여 자체 <style>·CSS를 그대로 렌더합니다. moai-content:html-report·moai-content:html-slide 산출물을 그대로 넘기면 화면과 동일한 디자인의 PDF가 나옵니다.
json{ "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# 프로젝트 보고서 ## 1. 개요 본 보고서는 ... ## 2. 결과 | 항목 | 수치 | |------|------| | 완료율 | 95% |
.pdf (ISO 32000 PDF 표준)@page 설정을 우선PDF 생성 후 산출 .pdf를 다시 열어 플레이스홀더 잔존·페이지 수 미달·CJK 인코딩 깨짐·디자인 누락을 자체 점검하고, 문제가 있으면 자동 수정 후 재생성하며 최종 PASS/FAIL을 보고합니다.
| 증상 | 원인 | 해결 방법 | |------|------|-----------| | 한글/한자가 □□□ 또는 공백 | CJK 폰트 미해결 | download_fonts.py 실행으로 번들 OTF 확보, 또는 시스템 Noto Sans CJK KR 설치 | | ModuleNotFoundError: weasyprint | weasyprint 미설치 | pip install weasyprint 실행 | | cannot load library 'libpango...' | 시스템 라이브러리 부재 | (Debian/Ubuntu) apt install libpango-1.0-0 libpangocairo-1.0-0, (macOS) brew install pango | | HTML 디자인이 PDF에서 깨짐 | 완성 HTML이 조각으로 오인됨 | 입력에 <html>/<body> 래퍼 포함, 또는 --in *.html 파일로 전달 | | 웹폰트(CDN) 미적용 | 오프라인/CDN 차단 | HTML에 폰트를 인라인하거나 시스템 폰트로 대체 (오프라인 PDF는 CDN 의존 회피 권장) | | 표가 페이지 경계에서 분리 | CSS 미지정 | tr { page-break-inside: avoid; } 추가 |
| 스킬 | 관계 | 사용 시점 | |------|------|-----------| | moai-content:html-report | before (HTML 리포트 생성 → 이 스킬로 PDF 변환) | "리포트 만들고 PDF로도" 흐름 | | moai-content:html-slide | before (HTML 슬라이드 생성 → PDF 변환) | 슬라이드 덱을 PDF 배포본으로 | | moai-office:docx-generator | alternative (편집 가능한 Word 산출물) | 수신자가 편집 가능 파일 필요 시 | | moai-core:ai-slop-reviewer | after (텍스트 산출물 AI 패턴 검수) | 텍스트 PDF 생성 전 원고 검수 |
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-06 | pass→pass | 14,632 | 10,160 | -31% | 1 | 1 | 0% | 2,601 | 4,613 | +77% | 0 | 0 | — |
case-05 | fail→pass | 12,397 | 5,432 | -56% | 1 | 1 | 0% | 1,992 | 3,683 | +85% | 0 | 0 | — |
case-07 | pass→pass | 15,666 | 8,549 | -45% | 1 | 1 | 0% | 2,649 | 4,246 | +60% | 0 | 0 | — |
case-08 | fail→pass | 10,370 | 4,451 | -57% | 1 | 1 | 0% | 1,788 | 3,489 | +95% | 0 | 0 | — |
case-01 | fail→fail | 10,570 | 5,126 | -52% | 1 | 1 | 0% | 2,115 | 3,556 | +68% | 0 | 0 | — |
case-02 | fail→fail | 29,592 | 5,375 | -82% | 1 | 1 | 0% | 6,182 | 3,633 | -41% | 0 | 0 | — |
case-03 | fail→fail | 31,314 | 5,677 | -82% | 1 | 1 | 0% | 6,198 | 3,045 | -51% | 0 | 0 | — |
case-04 | fail→fail | 13,875 | 10,701 | -23% | 1 | 1 | 0% | 2,433 | 4,724 | +94% | 0 | 0 | — |
case-09 | fail→pass | 14,039 | 6,389 | -54% | 1 | 1 | 0% | 2,221 | 3,966 | +79% | 0 | 0 | — |
case-10 | pass→pass | 12,876 | 12,939 | +0% | 1 | 1 | 0% | 2,095 | 5,096 | +143% | 0 | 0 | — |
case-11 | fail→pass | 12,018 | 14,439 | +20% | 1 | 1 | 0% | 2,280 | 4,897 | +115% | 0 | 0 | — |
case-12 | pass→pass | 13,897 | 10,771 | -22% | 1 | 1 | 0% | 2,173 | 4,557 | +110% | 0 | 0 | — |
case-13 | fail→pass | 12,611 | 7,736 | -39% | 1 | 1 | 0% | 2,108 | 4,012 | +90% | 0 | 0 | — |
case-14 | fail→pass | 12,722 | 6,421 | -50% | 1 | 1 | 0% | 2,254 | 3,842 | +70% | 0 | 0 | — |
case-15 | pass→pass | 8,373 | 9,069 | +8% | 1 | 1 | 0% | 1,486 | 4,449 | +199% | 0 | 0 | — |
case-16 | pass→pass | 11,019 | 6,236 | -43% | 1 | 1 | 0% | 1,825 | 3,869 | +112% | 0 | 0 | — |
case-17 | pass→pass | 13,043 | 8,552 | -34% | 1 | 1 | 0% | 2,028 | 4,185 | +106% | 0 | 0 | — |
case-18 | fail→pass | 8,813 | 3,320 | -62% | 1 | 1 | 0% | 1,384 | 3,290 | +138% | 0 | 0 | — |
case-19 | fail→pass | 19,310 | 10,703 | -45% | 1 | 1 | 0% | 3,486 | 4,954 | +42% | 0 | 0 | — |
case-20 | pass→pass | 12,616 | 15,508 | +23% | 1 | 1 | 0% | 2,193 | 5,529 | +152% | 0 | 0 | — |
case-21 | pass→pass | 15,227 | 11,127 | -27% | 1 | 1 | 0% | 2,510 | 4,747 | +89% | 0 | 0 | — |
case-22 | fail→pass | 15,110 | 2,154 | -86% | 1 | 1 | 0% | 2,511 | 3,127 | +25% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +41 percentage points is the difference between those two pass rates over the 21 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.