Skill: franklu0819-lang/md2pdf-weasyprint (version 2.0.0)

--- name: md2pdf description: Markdown 转 PDF 技能。将 Markdown 文件转换为精美的 PDF 文档，完美支持中文、代码高亮、自定义样式。 metadata: { "openclaw": { "requires": { "bins": ["python3"], "python": ["markdown", "weasyprint"] } } } --- # Markdown 转 PDF 技能 将 Markdown 文件转换为精美的 PDF 文档，支持中文、代码高亮、自定义样式。 ## ⭐ 推荐方案：WeasyPrint **完美支持中文，无需 LaTeX，自动处理依赖！** ### 快速开始 ```bash # 转换 Markdown 为 PDF（推荐） bash /root/.openclaw/workspace/skills/md2pdf/scripts/convert-weasyprint.sh input.md # 指定输出文件名 bash /root/.openclaw/workspace/skills/md2pdf/scripts/convert-weasyprint.sh input.md output.pdf ``` ### 优点 - ✅ **完美中文支持**（使用 Noto Sans CJK 字体） - ✅ **自动安装依赖**（首次运行时自动安装） - ✅ **专业排版样式**（代码高亮、表格美化） - ✅ **轻量级**（无需 LaTeX，仅 Python） - ✅ **跨平台**（支持 Linux、macOS、Windows） ## 功能特性 - ✅ Markdown 转 PDF - ✅ **完美中文支持**（无乱码） - ✅ 代码语法高亮（深色主题） - ✅ 专业表格样式（斑马纹、圆角） - ✅ 多级标题样式 - ✅ 引用块美化 - ✅ 列表、链接、图片支持 - ✅ 自定义 CSS 样式 ## 使用方法 ### 方法 1：WeasyPrint（推荐）⭐ **适合场景：** 需要完美中文显示、专业排版 ```bash # 使用 Bash 脚本（自动处理依赖） bash scripts/convert-weasyprint.sh document.md # 指定输出文件 bash scripts/convert-weasyprint.sh document.md output.pdf # 直接使用 Python 脚本 python3 scripts/convert-weasyprint.py input.md output.pdf ``` ### 方法 2：Pandoc（备选） **适合场景：** 已安装 LaTeX 环境 ```bash bash scripts/convert.sh input.md output.pdf ``` ### 方法 3：简化 HTML **适合场景：** 需要在浏览器中打印 ```bash bash scripts/convert-simple.sh input.md output.pdf ``` ## 脚本说明 ### convert-weasyprint.sh ⭐ **推荐使用** - WeasyPrint 方案，完美支持中文。 **特性：** - 自动检测并安装 Python 依赖（markdown、weasyprint） - 自动检测并安装中文字体（google-noto-sans-cjk-fonts） - 专业的 CSS 样式（代码高亮、表格美化） - 完整的错误处理 **用法：** ```bash bash scripts/convert-weasyprint.sh <输入.md> [输出.pdf] ``` **参数：** - `输入.md` (必需): Markdown 文件路径 - `输出.pdf` (可选): PDF 输出路径（默认：输入文件名.pdf） **示例：** ```bash # 转换单个文件 bash scripts/convert-weasyprint.sh README.md # 指定输出路径 bash scripts/convert-weasyprint.sh README.md /tmp/readup.pdf # 批量转换 for md in *.md; do bash scripts/convert-weasyprint.sh "$md" done ``` ### convert-weasyprint.py Python 转换脚本，被 `convert-weasyprint.sh` 调用，也可直接使用。 **用法：** ```bash python3 scripts/convert-weasyprint.py <输入.md> [输出.pdf] ``` **功能：** - Markdown 解析（支持表格、代码块、列表） - HTML 生成 - CSS 样式应用 - PDF 输出 ### convert.sh Pandoc 传统方案（需要 LaTeX）。 **用法：** ```bash bash scripts/convert.sh <输入.md> [输出.pdf] ``` **前置要求：** - `pandoc` - `xelatex` (LaTeX) - 中文字体 ### convert-simple.sh 简化版，生成 HTML 供浏览器打印。 **用法：** ```bash bash scripts/convert-simple.sh <输入.md> [输出.pdf] ``` ## 技术实现 ### WeasyPrint 方案 使用 Python 的 WeasyPrint 库进行转换： ```python # 1. Markdown → HTML html_content = markdown.markdown( md_content, extensions=['tables', 'fenced_code', 'nl2br', 'sane_lists'] ) # 2. HTML + CSS → PDF HTML(string=html_content).write_pdf( output_file, stylesheets=[CSS(string='...')] ) ``` **CSS 样式特性：** - 中文字体：Noto Sans CJK SC - 代码块：深色主题（#2c3e50 背景） - 表格：蓝色表头 + 斑马纹 - 引用块：左边框 + 浅灰背景 - 响应式设计 ### Pandoc 方案 使用 pandoc 的 LaTeX 引擎： ```bash pandoc input.md -o output.pdf \ --pdf-engine=xelatex \ -V CJKmainfont="Noto Sans CJK SC" \ -V geometry:margin=2cm ``` ## 支持的 Markdown 特性 - ✅ 标题（h1-h6） - ✅ 段落和换行 - ✅ 列表（有序、无序） - ✅ 代码块（支持语法高亮） - ✅ 表格 - ✅ 链接和图片 - ✅ 引用块 - ✅ 水平线 - ✅ 粗体和斜体 - ✅ 自动链接 ## 依赖管理 ### Python 依赖 ```bash # 自动安装（首次运行时） python3 -m pip install markdown weasyprint ``` ### 中文字体 ```bash # 自动安装（首次运行时） yum install -y google-noto-sans-cjk-fonts ``` ### Pandoc（可选） ```bash # CentOS/RHEL yum install -y pandoc # Ubuntu/Debian apt-get install -y pandoc ``` ## 故障排查 ### 问题 1：中文显示为方块或乱码 **解决方案（WeasyPrint）：** ```bash # 安装中文字体 yum install -y google-noto-sans-cjk-fonts # 验证字体 fc-list | grep "Noto Sans CJK" ``` **解决方案（Pandoc）：** ```bash # 安装中文字体 yum install -y google-noto-sans-cjk-sc-fonts # 使用 xelatex pandoc input.md -o output.pdf --pdf-engine=xelatex -V CJKmainfont="Noto Sans CJK SC" ``` ### 问题 2：Python 模块未找到 **解决方案：** ```bash # 安装 Python 依赖 python3 -m pip install markdown weasyprint # 验证安装 python3 -c "import markdown, weasyprint; print('OK')" ``` ### 问题 3：字体渲染问题 **解决方案：** ```bash # 清除字体缓存 fc-cache -fv # 重新安装字体 yum reinstall -y google-noto-sans-cjk-fonts ``` ### 问题 4：PDF 文件过大 **原因：** WeasyPrint 会嵌入完整字体 **解决方案：** - 使用子集化字体（需要额外工具） - 或使用在线字体（需要网络连接） - 或接受文件大小（通常 500KB-2MB） ## 完整示例 ### 示例 1：基本转换 ```bash # 创建测试文件 cat > test.md << 'EOF' # 测试文档 这是中文测试文档。 ## 功能列表 - 支持 Markdown - 代码高亮 - 表格样式 | 列1 | 列2 | |-----|-----| | A | B | \`\`\`python print("Hello, World!") \`\`\` EOF # 转换为 PDF bash scripts/convert-weasyprint.sh test.md ``` ### 示例 2：批量转换 ```bash # 转换目录下所有 Markdown 文件 for file in *.md; do echo "转换: $file" bash scripts/convert-weasyprint.sh "$file" done ``` ### 示例 3：集成到脚本 ```bash #!/bin/bash # 自动转换脚本 INPUT_DIR="/path/to/markdown/files" OUTPUT_DIR="/path/to/pdf/output" mkdir -p "$OUTPUT_DIR" for md in "$INPUT_DIR"/*.md; do output="$OUTPUT_DIR/$(basename "$md" .md).pdf" bash /root/.openclaw/workspace/skills/md2pdf/scripts/convert-weasyprint.sh "$md" "$output" done ``` ## 样式自定义 ### 修改 CSS 样式 编辑 `scripts/convert-weasyprint.py` 中的 CSS 字符串： ```python css = CSS(string=''' /* 修改字体大小 */ body { font-size: 12pt; /* 默认 11pt */ } /* 修改主色调 */ h1, h2 { color: #e74c3c; /* 改为红色 */ } /* 修改代码块背景 */ pre { background-color: #34495e; /* 深色主题 */ } ''') ``` ### 自定义模板 修改 HTML 模板以添加页眉、页脚等： ```python full_html = f""" <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>{title}</title> </head> <body> <header> <h1>我的公司</h1> </header> {html_content} <footer> <p>机密文档</p> </footer> </body> </html> """ ``` ## 性能优化 ### 加速转换 1. **使用缓存：** 保存字体缓存 2. **批量处理：** 合并多个 Markdown 为一个 PDF 3. **并行处理：** 使用 GNU parallel ```bash # 并行转换 ls *.md | parallel -j 4 bash scripts/convert-weasyprint.sh {} ``` ## 相关技能 - `feishu-doc`: 飞书文档操作 - `feishu-drive`: 飞书云文件管理 - `md2pdf`: Markdown 转 PDF（本技能） ## 常见问题 **Q: WeasyPrint 和 Pandoc 哪个更好？** A: 推荐使用 WeasyPrint： - ✅ 完美中文支持（无需配置） - ✅ 自动安装依赖 - ✅ 轻量级（无需 LaTeX） - ✅ 跨平台 Pandoc 适合已有 LaTeX 环境的场景。 **Q: 可以在 Docker 中使用吗？** A: 可以，但需要安装字体： ```dockerfile FROM python:3.11 RUN pip install markdown weasyprint RUN apt-get update && apt-get install -y fonts-noto-cjk ``` **Q: 支持数学公式吗？** A: WeasyPrint 不直接支持，可以： 1. 使用 MathJax（需要 JavaScript） 2. 或使用 Pandoc + LaTeX 方案 ## 更新日志 ### v2.0 (2026-02-16) - ✨ 新增 WeasyPrint 方案（推荐） - ✨ 完美中文支持 - ✨ 自动依赖安装 - ✨ 专业 CSS 样式 - ✨ 代码高亮深色主题 - 🐛 修复中文乱码问题 ### v1.0 - ⚠️ 初始版本（Pandoc 方案） - ⚠️ 需要手动安装 LaTeX - ⚠️ 中文支持有限 ## 许可证 MIT License --- **作者**: 小美 ⭐ **最后更新**: 2026-02-16 **推荐方案**: WeasyPrint (convert-weasyprint.sh)

path: README.md
content: # Markdown to PDF 技能

将 Markdown 文件转换为精美的 PDF 文档，完美支持中文、代码高亮、自定义样式。

## 特性

- ✅ **完美中文支持**（使用 Noto Sans CJK 字体）
- ✅ **自动安装依赖**（首次运行时自动安装）
- ✅ **专业排版样式**（代码高亮、表格美化）
- ✅ **轻量级**（无需 LaTeX，仅 Python）
- ✅ **跨平台**（支持 Linux、macOS、Windows）

## 快速开始

```bash
# 转换 Markdown 为 PDF
bash /root/.openclaw/workspace/skills/md2pdf/scripts/convert-weasyprint.sh input.md

# 指定输出文件名
bash /root/.openclaw/workspace/skills/md2pdf/scripts/convert-weasyprint.sh input.md output.pdf
```

## 依赖

- Python 3.x
- markdown
- weasyprint

依赖会在首次运行时自动安装。

## 作者

franklu0819-lang

## 许可证

MIT


path: scripts/convert-weasyprint.py
content: #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Markdown 转 PDF (WeasyPrint 方案)
支持完美中文显示、代码高亮、表格样式
修复：数字间距问题
"""

import sys
import re
import markdown
from weasyprint import HTML, CSS
from pathlib import Path

def convert_markdown_to_pdf(input_file, output_file):
    """将 Markdown 文件转换为 PDF"""

    # 读取 Markdown 文件
    md_path = Path(input_file)
    if not md_path.exists():
        raise FileNotFoundError(f"输入文件不存在: {input_file}")

    md_content = md_path.read_text(encoding='utf-8')

    # 预处理：在列表项中的 emoji 后添加空格
    md_content = re.sub(
        r'^(\s*[-*+]\s+)([\U0001F300-\U0001F9FF])([^\s])',
        r'\1\2 \3',
        md_content,
        flags=re.MULTILINE
    )

    # 转换为 HTML
    html_content = markdown.markdown(
        md_content,
        extensions=[
            'tables',
            'fenced_code',
            'nl2br',
            'sane_lists',
            'codehilite',
            'toc'
        ]
    )

    # 创建完整的 HTML 文档
    full_html = f"""
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{md_path.stem}</title>
    <style>
        @page {{
            size: A4;
            margin: 2cm;
        }}
        body {{
            font-family: "Noto Sans CJK SC", "Microsoft YaHei", "PingFang SC", sans-serif;
            font-size: 11pt;
            line-height: 1.6;
            color: #333;
            font-variant-numeric: tabular-nums;
            letter-spacing: normal;
        }}
        h1 {{
            color: #2c3e50;
            border-bottom: 3px solid #3498db;
            padding-bottom: 10px;
            font-size: 24pt;
            margin-top: 0;
            page-break-after: avoid;
            letter-spacing: normal;
        }}
        h2 {{
            color: #2c3e50;
            border-bottom: 2px solid #ecf0f1;
            padding-bottom: 8px;
            font-size: 18pt;
            margin-top: 30pt;
            page-break-after: avoid;
            letter-spacing: normal;
        }}
        h3 {{
            color: #34495e;
            font-size: 14pt;
            margin-top: 20pt;
            page-break-after: avoid;
            letter-spacing: normal;
        }}
        h4, h5, h6 {{
            color: #34495e;
            font-size: 12pt;
            margin-top: 15pt;
            page-break-after: avoid;
            letter-spacing: normal;
        }}
        table {{
            border-collapse: collapse;
            width: 100%;
            margin: 20px 0;
            page-break-inside: avoid;
            font-variant-numeric: tabular-nums;
            letter-spacing: normal;
        }}
        th {{
            background-color: #3498db;
            color: white;
            padding: 12px;
            text-align: left;
            font-weight: bold;
            border: 1px solid #2980b9;
            letter-spacing: normal;
        }}
        td {{
            padding: 12px;
            border: 1px solid #ddd;
            font-variant-numeric: tabular-nums;
            letter-spacing: normal;
        }}
        tr:nth-child(even) {{
            background-color: #f8f9fa;
        }}
        code {{
            background-color: #f8f9fa;
            padding: 2px 6px;
            border-radius: 3px;
            font-family: "Courier New", monospace;
            font-size: 0.9em;
            color: #e74c3c;
            font-variant-numeric: tabular-nums;
            letter-spacing: normal;
        }}
        pre {{
            background-color: #2c3e50;
            color: #ecf0f1;
            padding: 15px;
            border-radius: 5px;
            overflow-x: auto;
            border: 1px solid #34495e;
            page-break-inside: avoid;
        }}
        pre code {{
            background: none;
            padding: 0;
            color: inherit;
        }}
        blockquote {{
            border-left: 4px solid #3498db;
            margin: 20px 0;
            padding-left: 20px;
            color: #7f8c8d;
            background-color: #f8f9fa;
            padding: 15px 20px;
            border-radius: 0 5px 5px 0;
            page-break-inside: avoid;
        }}
        ul, ol {{
            margin: 10px 0;
            padding-left: 30px;
            list-style-position: outside;
        }}
        li {{
            margin: 5px 0;
            line-height: 1.6;
        }}
        ul li {{
            list-style-type: disc;
        }}
        ul ul li {{
            list-style-type: circle;
        }}
        ul ul ul li {{
            list-style-type: square;
        }}
        ol li {{
            list-style-type: decimal;
        }}
        hr {{
            border: none;
            border-top: 2px solid #ecf0f1;
            margin: 30px 0;
        }}
        strong {{
            color: #2c3e50;
            font-weight: bold;
        }}
        em {{
            font-style: italic;
        }}
        a {{
            color: #3498db;
            text-decoration: none;
        }}
        a:hover {{
            text-decoration: underline;
        }}
        img {{
            max-width: 100%;
            height: auto;
            display: block;
            margin: 20px auto;
        }}
    </style>
</head>
<body>
    {html_content}
</body>
</html>
"""

    # 生成 PDF
    output_path = Path(output_file)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    HTML(string=full_html).write_pdf(str(output_path))

    return str(output_path)

def main():
    if len(sys.argv) < 2:
        print("用法: python3 convert-weasyprint.py <输入.md> [输出.pdf]")
        sys.exit(1)

    input_file = sys.argv[1]
    output_file = sys.argv[2] if len(sys.argv) > 2 else str(Path(input_file).with_suffix('.pdf'))

    try:
        result = convert_markdown_to_pdf(input_file, output_file)
        print(f"✅ PDF 生成成功: {result}")
    except Exception as e:
        print(f"❌ 转换失败: {e}", file=sys.stderr)
        sys.exit(1)

if __name__ == '__main__':
    main()


path: scripts/convert-weasyprint.sh
content: #!/bin/bash
# Markdown 转 PDF (WeasyPrint 方案)
# 支持完美中文显示、代码高亮、表格样式

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON_SCRIPT="$SCRIPT_DIR/convert-weasyprint.py"

# 检查参数
if [ $# -lt 1 ]; then
    echo "用法: $0 <输入.md> [输出.pdf]"
    echo ""
    echo "示例:"
    echo "  $0 document.md"
    echo "  $0 document.md output.pdf"
    exit 1
fi

INPUT_FILE="$1"
OUTPUT_FILE="${2:-${INPUT_FILE%.md}.pdf}"

# 检查输入文件
if [ ! -f "$INPUT_FILE" ]; then
    echo "❌ 错误: 输入文件不存在: $INPUT_FILE"
    exit 1
fi

# 检查 Python 依赖
if ! python3 -c "import markdown, weasyprint" 2>/dev/null; then
    echo "⚠️  Python 依赖未安装，正在安装..."
    python3 -m pip install -q markdown weasyprint
fi

# 检查中文字体
if ! fc-list | grep -q "Noto Sans CJK"; then
    echo "⚠️  中文字体未安装，正在安装..."
    yum install -y -q google-noto-sans-cjk-fonts
fi

# 检查 emoji 字体
if ! fc-list | grep -q "Noto.*Emoji"; then
    echo "⚠️  Emoji 字体未安装，正在安装..."
    yum install -y -q google-noto-emoji-color-fonts google-noto-emoji-fonts
fi

# 执行转换
echo "📄 开始转换..."
echo "   输入: $INPUT_FILE"
echo "   输出: $OUTPUT_FILE"
echo ""

python3 "$PYTHON_SCRIPT" "$INPUT_FILE" "$OUTPUT_FILE"

if [ $? -eq 0 ]; then
    echo ""
    echo "✅ 转换成功！"
    echo "📁 文件位置: $OUTPUT_FILE"
    echo "📊 文件大小: $(du -h "$OUTPUT_FILE" | cut -f1)"
else
    echo ""
    echo "❌ 转换失败"
    exit 1
fi


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)