Md to pdf
Convert Markdown files to well-formatted PDF with Chinese support, bookmarks, and page numbers. Uses PyMuPDF Story HTML rendering with DroidSansFallbackFull font for full CJK + Latin + special character coverage. Trigger on phrases like "markdown to pdf", "md转pdf", "生成pdf", "导出pdf", "markdown导出", or any request to convert .md files to PDF with proper Chinese rendering and bookmarks. Not for the reverse direction: to turn PDF/DOCX into Markdown, use doc-to-markdown instead.From its SKILL.md
npx -y skills add TbusOS/sky-skills --skill md-to-pdfAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 3 stars3 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
4.6 KB, as published. Nobody here has run it
Markdown to PDF Converter
Convert Markdown files to professional PDFs with full Chinese support, clickable bookmarks, and page numbers.
Features
- Full CJK + Latin + special character support (├── └── │ etc.)
- Auto-generated bookmarks from h1/h2/h3 headings with accurate page jumps
- Code blocks with background shading
- Tables, blockquotes, lists properly formatted
- Page numbers at bottom
- Auto font discovery (DroidSansFallbackFull, NotoSansCJK, WQY, etc.)
Workflow
1. Ensure dependencies
python3 -c "import fitz" 2>/dev/null || pip3 install pymupdf
2. Find a CJK font
The script auto-searches for fonts in these locations:
- User-specified
--fontpath ~/.fonts//usr/share/fonts/- Android SDK fonts (DroidSansFallbackFull.ttf)
If no font is found automatically, locate one:
find / -name "DroidSansFallbackFull.ttf" -o -name "NotoSansCJK*" -o -name "wqy-zenhei*" 2>/dev/null | head -5
3. Run the converter
python3 <skill-dir>/scripts/md2pdf.py input.md output.pdf
With explicit font:
python3 <skill-dir>/scripts/md2pdf.py input.md output.pdf --font /path/to/DroidSansFallbackFull.ttf
4. Verify output
After generating, verify the PDF:
import fitz
doc = fitz.open("output.pdf")
print(f"Pages: {len(doc)}")
print(f"Bookmarks: {len(doc.get_toc())}")
# Check first page text renders correctly
print(doc[0].get_text()[:300])
Supported Markdown Elements
| Element | Syntax | PDF Rendering |
|---|---|---|
| Headings | # ## ### | Sized text + bookmark entry |
| Code blocks | ``` | Gray background, monospace-style |
| Inline code | `code` | Red text with pink background |
| Tables | | col | | Bordered table with header shading |
| Blockquotes | > text | Gray text with left border |
| Lists | - item / 1. item | Bulleted / numbered |
| Bold | **text** | Bold text |
| Links | [text](url) | Text preserved, URL stripped |
| HR | --- | Spacer |
Font Recommendation
DroidSansFallbackFull.ttf is the recommended font. It produces ~7.7MB PDFs and covers:
- Latin characters (A-Z, a-z, 0-9)
- CJK Chinese characters
- Box-drawing characters (├── └── │)
- All common symbols
This font is bundled in Android SDK at frameworks/base/data/fonts/DroidSansFallbackFull.ttf.
Bookmark accuracy
Bookmarks are located by finding each heading's text in the rendered pages, with three safeguards (added 2026-07-23 after real breakage in a CJK document):
- Fallback beyond
search_for()— PyMuPDF'ssearch_for()silently fails on some headings (observed with、()/and enclosed numerals like②④). It used to fall back to page 1, so those bookmarks jumped to the cover. Now a normalizedget_text()line scan catches them. - Monotonic search — each heading is searched from the previous heading's page onward, so a later heading can't match an earlier page that repeats the same words.
- HTML entities un-escaped — a heading like
路径 <version>is<version>in the intermediate HTML but renders as<version>; without un-escaping, the lookup could never match.
Verify after generating:
import fitz, unicodedata
def norm(s): return unicodedata.normalize("NFC", s).strip()
d = fitz.open("output.pdf")
lines = [[norm(l) for l in d[i].get_text().split("\n")] for i in range(len(d))]
for lvl, title, pg in d.get_toc():
key = norm(title)
real = next((i+1 for i, ls in enumerate(lines) if any(key == l or key in l for l in ls)), None)
if real != pg:
print(f"BAD: {title} -> bookmark p{pg}, actually p{real}")
Regression baseline: two CJK documents (8 pages / 19 bookmarks and 11 pages / 28 bookmarks) both report 0 mismatched bookmarks.
Edge Cases
- No CJK font available: Script exits with error message. User must provide
--font. - Very long documents: PyMuPDF handles multi-page layout automatically.
- Complex nested lists: Only single-level lists are supported; nested items render as flat.
- Images in markdown: Not supported; only text content is converted.