PDF Creator
Convert markdown files to professional PDF documents with proper Chinese font support, theme system, and visual self-check. Use for Markdown → PDF, printable Markdown documents, and Markdown formatted for print or mobile reading. Prefer its CJK typography, header/footer suppression, and visual verification over manual pandoc/Chrome commands. Scope is Markdown → PDF only. Existing Word/WPS → PDF, including manuscript excerpts and layout repair, routes to daymade-docs:docx-creator; do not round-trip an authoritative Word manuscript through Markdown. Word output also routes to docx-creator.
- Skill ID
- daymade/claude-code-skills/pdf-creator
- Publisher
- daymade
- Repository
- claude-code-skills
- Installs
- 0
- Files
- 16
- Synced
- Sep 23, 2026
Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.
daymade/claude-code-skills/pdf-creatorInstalls these files- SKILL.md
- scripts/batch_convert.py
- scripts/check_table_borders.py
- scripts/md_to_pdf.py
- scripts/tests/test_backend_routing.py
- scripts/tests/test_cjk_code_blocks.py
- scripts/tests/test_cjk_tables.py
- scripts/tests/test_list_rendering.py
- scripts/tests/test_self_check_preview.py
- scripts/tests/test_table_border_check.py
- scripts/tests/test_typography_lint.py
- themes/cjk-auto.css
- themes/default.css
- themes/mobile.css
- themes/warm-terra-menu.css
- themes/warm-terra.css
What this skill tells the agent
PDF Creator
Create professional PDF documents from markdown with Chinese font support and theme system.
For an existing .docx source, use docx-creator/references/word-to-pdf.md instead. Choose by the authoritative input as well as the output suffix.
Quick Start
Every scripts/… path below is relative to this skill's own directory — run the commands from there, or substitute the absolute path. Running them from the directory holding your markdown fails with Failed to spawn: 'scripts/md_to_pdf.py'. Omitting the output path is allowed and derives it from the input's basename (input.md → input.pdf, in the input's directory).
# Default theme (formal: Songti SC + black/grey, A4 print)
uv run --with weasyprint scripts/md_to_pdf.py input.md output.pdf
# Warm theme (training: PingFang SC + terra cotta)
uv run --with weasyprint scripts/md_to_pdf.py input.md --theme warm-terra
# Mobile theme (narrow page, large font — for phone reading / WeChat sharing)
uv run --with weasyprint scripts/md_to_pdf.py input.md --theme mobile
# Batch convert all markdown files with a specific theme
uv run --with weasyprint scripts/batch_convert.py *.md --theme warm-terra --no-preview
# No weasyprint? Use Chrome backend (auto-detected if weasyprint unavailable)
python scripts/md_to_pdf.py input.md --theme warm-terra --backend chrome
# List available themes
python scripts/md_to_pdf.py --list-themes dummy.mdThemes
Stored in themes/*.css. Each theme is a standalone CSS file.
| Theme | Page Size | Font | Color | Best for |
|---|---|---|---|---|
default | A4 | Songti SC + Heiti SC | Black/grey | Legal docs, contracts, formal reports |
cjk-auto | A4 | Songti SC + Heiti SC | Black/grey | Tables with uneven column content (course schedules, itemized lists) |
warm-terra | A4 | PingFang SC | Terra cotta (#d97756) + warm neutrals | Course outlines, training materials, workshops |
warm-terra-menu | A4 | PingFang SC | Terra cotta (#d97756) + warm neutrals | warm-terra variant hardened for module menus/lists: 2-column long-text tables wrap without first-column overflow + Menlo unicode-range keeps CJK inline-code from rendering blank in Preview/Adobe |
mobile | 148mm × 210mm | PingFang SC | Terra cotta + warm neutrals | Phone reading, WeChat sharing, on-the-go reference |
To create a new theme: copy themes/default.css, modify, save as themes/your-theme.css.
Print vs Mobile: Choose the Right Theme
| Scenario | Recommended Theme | Why |
|---|---|---|
| Print on A4 paper, handouts, contracts | default | Standard page size, formal typography |
| Training materials, course outlines | warm-terra | Warm accent color, readable for workshop contexts |
| Send via WeChat, read on phone | mobile | Narrow page (148mm), 15px font, 1.9 line-height — comfortable on small screens |
| Both print AND mobile needed | Run twice with different themes | The skill is fast; generate both versions |
Decision rule: If the user does not specify, default to warm-terra for training/course content and default for formal documents — but pick cjk-auto over default when the document's tables have uneven column content (schedules, itemized lists), which is what that theme's table-layout: auto exists for. Ask "是否需要手机版?" only when the output channel is unclear.
Backends
The script auto-detects the best available backend from content × theme — the theme half matters because the reason CJK ever needed Chrome is a property of the theme's font stack, not of the text being Chinese:
- CJK + a Songti/Heiti theme (
default,cjk-auto) → weasyprint. These themes embed CID TrueType, which every reader renders, so Chrome buys nothing and costs the clip described below. - CJK + a PingFang theme (
warm-terra,mobile,warm-terra-menu, and any theme not on the safe list) → Chrome. weasyprint subset-embeds PingFang SC as CID Type 0C OpenType, which macOS Preview / Adobe Reader fail to render — garbled text on the recipient's device even though Chrome's own PDF viewer looks fine. - Non-CJK content → weasyprint (faster, no browser startup).
Routing is pinned by scripts/tests/test_backend_routing.py. A theme you add yourself is treated as PingFang-class until you add it to _WEASYPRINT_SAFE_CJK_THEMES in md_to_pdf.py — do that only after confirming its CJK faces are CID TrueType.
| Backend | Install | Pros | Cons |
|---|---|---|---|
weasyprint | pip install weasyprint | Precise CSS rendering, no browser needed, does not clip overflow | Subsets PingFang SC as CID Type 0C — unreadable in Preview/Adobe |
chrome | Google Chrome installed | Zero Python deps, renders PingFang correctly | Clips anything past the @page box (see below) |
Override with --backend chrome or --backend weasyprint; an explicit flag always wins over auto-detection.
The theme does not tell you which backend ran. When weasyprint is not importable, a default / cjk-auto render falls back to Chrome — with a warning on stderr, and exit 0. Read the backend= field of the Generated: line to find out which one you got; that field, not the theme name, is what decides whether the Chrome-only step below applies.
Chrome clips, it does not merely overflow
Chrome wraps each page in a re W* n clip path at the @page content box. Content past that box is still in the PDF's object layer but is never painted. Measured on A4 with margin: 2.5cm 2cm 2cm 2cm: the clip path ends at 538.90pt, while the table's right border sits at 545.18pt — so the border is silently amputated.
This is not a "wide table" problem — under `default` and `cjk-auto` it is every table. Those themes set table { table-layout: fixed; width: 100% }, so a two-column table spans the same full content width as a ten-column one and lands its right border at the same 545.18pt. Measured: a trivial | 周一 | 周二 | table clips identically to a six-column fee schedule. Content width is irrelevant; the theme's own width: 100% plus cell padding is what crosses the line.
The overflow itself is by design: the CJK typography layer sets overflow-wrap: normal precisely so content overflows rather than breaking mid-token (see "CJK Typography" below). That trade-off is safe under weasyprint and destructive under Chrome.
The reason this survived repeated delivery is not that the preview lies — it is that the symptom looks deliberate. The last column's text is complete and correctly spaced; only a hairline border is gone, which reads as a styling choice. Meanwhile the visual checklist below primes you to look for text cut off, which is exactly what does not happen here.
Rasterisers do honour the clip, so the preview PNGs this script already generates (pdftoppm at 130 dpi) do show the defect, as does a 400 dpi render — at 400 dpi, zero ink across the 25 pixel columns straddling the expected border. What does not show it is any check that reads coordinates instead of pixels — pdfplumber still reports a rect at 545.18pt, because the object is genuinely there.
Rather than rely on noticing a missing hairline, run the check. It takes two forms, and for a Chrome-rendered PDF only the second one is a verdict.
# Form 1 — one file. Compares ink against the PDF's own object layer.
uv run --with pdfplumber --with pillow --with numpy scripts/check_table_borders.py out.pdfForm 1 takes each detected table's column boundaries, counts the ones that actually have ink in a pdftoppm raster, and exits non-zero naming any boundary present in the PDF but absent on paper. It reports how many tables it measured, and says NOTHING CHECKED rather than PASS when it found none — a document whose table it could not detect has not been cleared, it has been skipped.
More from daymade
Use this skill in a project
Open any RiverX project, search the Skills panel for daymade/claude-code-skills/pdf-creator, and install it in one click.
