Skip to content

Latest commit

 

History

5 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

HTML2PDF 服务

基于 WeasyPrint 的 HTML 转 PDF 服务,使用 Docker 容器化部署。

📖 简介

本项目是 4teamwork/weasyprint 的功能超集,在保留原有功能的基础上增加了更多实用特性:

核心优势

  • JSON 接口 (/convert)(适合服务端 API 调用,无需构造 multipart)
  • Markdown 转 PDF(直接传入 Markdown 文本,一步到位)
  • API 参数设置页面配置(无需在 HTML 中定义 @page CSS)
  • HTML 片段自动补全(自动添加完整 HTML 结构)
  • CSS 参数支持(HTML 和 CSS 分离)
  • WeasyPrint 高级选项(zoom、元数据、PDF/A 等)
  • 内置中文字体(微软雅黑、华文黑体等 6 种字体)

🆚 功能对比

特性 4teamwork/weasyprint 本服务
基础 HTML 转 PDF
multipart 文件上传
JSON 接口 (/convert)
Markdown 转 PDF
API 参数设置页面
HTML 片段自动补全
CSS 参数支持
options 高级选项
中文字体预配置

⚠️ 适用场景与限制

✅ 适用场景

本服务适合转换静态 HTML/CSS 内容为 PDF,例如:

  • 📄 Markdown 文档、技术文档、API 文档
  • 📊 数据报告、财务报表、统计图表(纯 HTML/CSS 实现)
  • 🧾 发票、合同、证书等商务文档
  • 📰 文章、博客内容、新闻稿
  • 📋 表单、清单、检查表

❌ 不适用场景

本服务基于 WeasyPrint,它是一个服务端渲染引擎,不支持 JavaScript 执行。因此以下场景无法正常转换

  • Canvas 绘图<canvas> 元素不会被渲染
  • JavaScript 动态内容:所有 <script> 标签会被忽略
  • D3.js / ECharts / Chart.js:依赖 JavaScript 的图表库无法渲染
  • React / Vue / Angular:前端框架的动态渲染不会执行
  • Tailwind CSS CDN:使用 JIT 编译的 Tailwind CDN 版本无法工作(需使用预编译的静态 CSS)
  • Web Components:自定义元素的 JavaScript 逻辑不会执行
  • 动画和交互:CSS animations、transitions、:hover 等交互状态不会呈现

解决方案

方案 1:前端提取渲染后的 DOM 和样式(推荐)

对于 React / Vue / Angular 等框架,可以在前端 JavaScript 执行完成后,直接提取渲染后的 HTML 和 CSS:

// 提取目标 DOM 的 HTML(已经过 JavaScript 渲染)
const targetElement = document.querySelector('#app'); // 或其他容器
const html = targetElement.innerHTML; // 或使用 outerHTML

// 提取所有样式表的 CSS 规则(包括 <style> 和 <link> 引入的)
function extractAllCSS() {
  const cssTexts = [];
  
  for (let i = 0; i < document.styleSheets.length; i++) {
    const sheet = document.styleSheets[i];
    try {
      // 遍历样式表中的所有规则
      const rules = sheet.cssRules || sheet.rules;
      const cssText = Array.from(rules).map(rule => rule.cssText).join('\n');
      cssTexts.push(cssText);
    } catch (e) {
      // 跨域样式表无法访问(CORS 限制)
      console.warn('无法访问样式表:', sheet.href, e);
    }
  }
  
  return cssTexts.join('\n');
}

const allCSS = extractAllCSS();

// 调用转换接口
fetch('http://localhost:3000/convert', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    html: html,
    css: allCSS,
    page_size: 'A4',
    margin: '2cm'
  })
})
.then(res => res.blob())
.then(blob => {
  // 下载 PDF
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'output.pdf';
  a.click();
});

适用于:React、Vue、Angular、ECharts、D3.js 等所有客户端渲染的内容

ECharts 图表转 PDF 示例(✅ 推荐):

// ECharts 使用 SVG 渲染器(关键!)
const chart = echarts.init(document.getElementById('chart'), null, {
  renderer: 'svg'  // 使用 SVG 渲染,而非默认的 Canvas
});

// 配置图表
chart.setOption({
  title: { text: '销售数据' },
  xAxis: { data: ['1月', '2月', '3月', '4月', '5月'] },
  yAxis: {},
  series: [{
    type: 'bar',
    data: [120, 200, 150, 80, 70]
  }]
});

// 等待图表渲染完成
setTimeout(() => {
  // 提取包含 SVG 的 HTML
  const chartContainer = document.getElementById('chart');
  const html = chartContainer.outerHTML;
  
  // 提取 CSS
  const css = extractAllCSS();
  
  // 发送到转换接口
  fetch('http://localhost:3000/convert', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
      html: html,
      css: css,
      page_size: 'A4',
      margin: '2cm'
    })
  })
  .then(res => res.blob())
  .then(blob => {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'chart.pdf';
    a.click();
  });
}, 500);  // 等待图表动画完成

为什么 ECharts SVG 模式有效?

  • ECharts 默认使用 Canvas 渲染(无法转换)
  • 指定 renderer: 'svg' 后,ECharts 生成纯 SVG 元素
  • SVG 是静态 XML 标记,WeasyPrint 可以直接渲染
  • 无需任何图片转换,效果完美!

注意事项

  • 确保在 DOM 渲染完成后(如 React 的 useEffect、Vue 的 onMounted)再提取 HTML
  • document.styleSheets同步 API,性能优于 fetch 方式
  • 跨域样式表(CDN 的 CSS)会因 CORS 限制无法访问 cssRules,需确保样式表支持 CORS 或使用同源部署
  • ECharts 必须使用 SVG 渲染器,Canvas 渲染器需要转为图片(见下方示例)
  • D3.js 默认生成 SVG,可直接使用此方案

Canvas 转图片示例

// 将 canvas 转为 img 标签
const canvas = document.querySelector('canvas');
const imgDataUrl = canvas.toDataURL('image/png');
const imgTag = `<img src="${imgDataUrl}" style="width: 100%;">`;

// 替换 canvas 后再提取 HTML
canvas.parentElement.innerHTML = imgTag;

方案 2:服务端预渲染

对于图表需求,使用服务端图表生成库(如 Python 的 matplotlib、plotly),将图表导出为 SVG 或 PNG 后嵌入 HTML

方案 3:使用预编译静态资源

对于 Tailwind CSS,使用预编译的静态 CSS 文件,而非 CDN 的 JIT 版本


🔄 需要 JavaScript 动态内容?考虑 Puppeteer/Playwright 方案

如果你的场景必须执行 JavaScript(如 Canvas 绘图、完整的 React/Vue 应用、复杂交互),可以考虑基于 Headless Chrome 的方案:

方案对比

维度 WeasyPrint (本项目) Puppeteer/Playwright
JavaScript 支持 ❌ 不支持 ✅ 完整支持
适用场景 静态 HTML/CSS、Markdown 文档 SPA 应用、Canvas 图表、动态内容
渲染引擎 自研 CSS 渲染器 Chromium 浏览器
资源占用 🟢 低(~50MB 内存) 🔴 高(~300-500MB 内存)
启动速度 🟢 快(毫秒级) 🟡 慢(需启动浏览器,秒级)
并发能力 🟢 高(Python 异步) 🟡 受限(浏览器实例数)
CSS 支持 🟡 部分 CSS3 🟢 完整现代 CSS
字体渲染 🟢 精确印刷级 🟢 浏览器级
PDF 质量 🟢 专业印刷品质 🟢 高质量
容器化体积 🟢 小(~200MB) 🔴 大(~1GB+)

Puppeteer 示例代码

const puppeteer = require('puppeteer');

async function htmlToPdf(htmlContent) {
  const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--disable-setuid-sandbox']
  });
  const page = await browser.newPage();
  await page.setContent(htmlContent, { waitUntil: 'networkidle0' });
  const pdf = await page.pdf({
    format: 'A4',
    margin: { top: '2cm', right: '2cm', bottom: '2cm', left: '2cm' },
    printBackground: true
  });
  await browser.close();
  return pdf;
}

为什么本项目选择 WeasyPrint?

  1. 目标场景明确:主要服务于文档、报告、Markdown 等静态内容转换,不需要 JavaScript 执行
  2. 性能优先:WeasyPrint 的资源占用和启动速度远优于 Headless Chrome,适合高并发 API 服务
  3. 容器化友好:镜像体积小(~200MB vs 1GB+),部署成本低
  4. 印刷品质:WeasyPrint 专为 PDF 生成优化,对 CSS Paged Media 规范支持更好,适合专业文档排版
  5. 依赖简单:纯 Python 生态,无需管理浏览器进程池

如何选择?

  • 选择 WeasyPrint(本项目):Markdown 文档、技术文档、数据报表、静态网页、发票合同等
  • 选择 Puppeteer/Playwright:需要截图整个 SPA 应用、Canvas 图表、复杂 JavaScript 动态内容

相关开源项目推荐(Puppeteer/Playwright 方案):

🚀 快速开始

本地开发

# 使用 Docker 运行
docker run -d -p 3000:3000 --name html2pdf icheerme/html2pdf:latest

# 查看日志
docker logs -f html2pdf

服务将在 http://localhost:3000 启动。

基础使用示例

# 1. 最简单的用法 - API 参数设置页面
curl -F "[email protected]" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     http://localhost:3000/convert -o output.pdf

# 2. 单独设置四边边距
curl -F "[email protected]" \
     -F "page_size=A4" \
     -F "margin_top=3cm" \
     -F "margin_right=2cm" \
     -F "margin_bottom=2.5cm" \
     -F "margin_left=2cm" \
     http://localhost:3000/convert -o output.pdf

# 3. 横向页面
curl -F "[email protected]" \
     -F "page_size=A4" \
     -F "page_orientation=landscape" \
     -F "margin=2cm" \
     http://localhost:3000/convert -o output.pdf

# 4. 包含图片资源
curl -F "[email protected]" \
     -F "asset.logo.png=@images/logo.png" \
     -F "asset.banner.jpg=@images/banner.jpg" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     http://localhost:3000/convert -o output.pdf

📡 API 接口

POST /convert

统一的 HTML 转 PDF 接口,根据 Content-Type 自动识别请求格式:

  • multipart/form-data - 文件上传模式(支持资源文件、附件)
  • application/json - JSON 字符串模式(适合服务端 API 调用)

模式 1: multipart/form-data(文件上传)

适用场景:需要上传 HTML 文件、图片、CSS 等资源文件。

请求格式multipart/form-data

请求参数

字段 类型 必需 说明 示例
html File 主 HTML 文件 @document.html
页面配置
page_size String 页面尺寸 A4, A3, A5, letter, legal
page_orientation String 页面方向 portrait(默认), landscape
margin String 页边距(统一) 2cm, 1.5cm 2cm, 1cm,1.5cm,2cm,1.5cm
margin_top String 上边距 3cm
margin_right String 右边距 2cm
margin_bottom String 下边距 2.5cm
margin_left String 左边距 2cm
像素单位转换
base_width_px Number 基准页面宽度(像素),启用 px → pt 自动转换 1920, 1440, 750
PDF 选项
optimize_images Boolean 是否优化图像 true, false
jpeg_quality Integer JPEG 质量(0-95) 85
pdf_variant String PDF 变体 pdf/a-3b, pdf/ua-1
options String (JSON) WeasyPrint 高级选项(JSON 字符串) 见高级选项章节
资源和附件
asset.* File 资源文件(图片、CSS 等) [email protected]
attachment.* File PDF 附件 [email protected]

使用示例

# 基础用法
curl -F "[email protected]" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     http://localhost:3000/convert -o output.pdf

# 包含图片资源
curl -F "[email protected]" \
     -F "asset.logo.png=@images/logo.png" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     http://localhost:3000/convert -o output.pdf

模式 2: application/json(JSON 字符串)

适用场景:服务端已经拼合好 HTML 字符串,无需上传文件。

请求格式application/json

请求参数

字段 类型 必需 说明 示例
html String ⚠️ HTML 内容字符串(与 markdown 二选一) "<!DOCTYPE html>..." 或 HTML 片段
markdown String ⚠️ Markdown 内容字符串(优先级高于 html) "# Title\n\nContent..."
css String 自定义 CSS 样式 "body { color: red; }"
page_size String 页面尺寸 "A4", "A3", "letter"
page_orientation String 页面方向 "portrait", "landscape"
margin String 页边距(统一) "2cm", "1.5cm 2cm"
margin_top String 上边距 "3cm"
margin_right String 右边距 "2cm"
margin_bottom String 下边距 "2.5cm"
margin_left String 左边距 "2cm"
像素单位转换
base_width_px Number 基准页面宽度(像素),启用 px → pt 自动转换 1920, 1440, 750
optimize_images Boolean 是否优化图像 true, false
jpeg_quality Integer JPEG 质量(0-95) 85
pdf_variant String PDF 变体 "pdf/a-3b"
options Object WeasyPrint 高级选项(对象) 见高级选项章节

特色功能

  1. Markdown 转 PDF:直接传入 Markdown 文本,自动转换为精美的 PDF

    • 支持标准 Markdown 语法
    • 支持表格、代码块、脚注等扩展语法
    • 自动应用专业的排版样式
    • 内置中文字体支持
  2. HTML 片段自动补全:自动检测并补全 HTML 结构

    • 添加 <!DOCTYPE html> 声明
    • 添加 <html> 标签(自动检测中文并设置 lang="zh-CN"
    • 添加 <head><body> 标签
    • 注入默认的中文字体样式
  3. CSS 参数支持:HTML 和 CSS 完全分离,无需在 HTML 中嵌入 <style> 标签

注意事项

  • JSON 模式不支持上传资源文件(asset.*)和附件(attachment.*
  • 如需使用图片,可在 HTML 中使用 base64 编码的 data URLs
  • 如需使用外部 CSS/图片/字体文件,请使用 multipart 模式
  • 最大请求体大小:50MB

使用示例

# Markdown 转 PDF
curl -X POST http://localhost:3000/convert \
  -H "Content-Type: application/json" \
  -d '{
    "markdown": "# 项目文档\n\n## 简介\n\n这是一个 **Markdown** 转 PDF 的示例。\n\n### 支持的功能\n\n- 列表项 1\n- 列表项 2\n\n### 代码示例\n\n```python\nprint(\"Hello World\")\n```\n\n| 列名1 | 列名2 |\n|-------|-------|\n| 数据1 | 数据2 |",
    "page_size": "A4",
    "margin": "2cm"
  }' \
  -o document.pdf

# HTML 片段 + CSS 样式
curl -X POST http://localhost:3000/convert \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<div><h1>标题</h1><p>内容</p></div>",
    "css": "h1 { color: #2c3e50; font-size: 24pt; } p { line-height: 1.8; }",
    "page_size": "A4",
    "margin": "2cm"
  }' \
  -o output.pdf

# 完整 HTML 文档
curl -X POST http://localhost:3000/convert \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<!DOCTYPE html><html><body><h1>Hello World</h1></body></html>",
    "page_size": "A4",
    "margin": "2cm"
  }' \
  -o output.pdf

Python 示例

import requests

# 示例 1: Markdown 转 PDF
markdown_content = """
# 技术文档

## 概述

这是一个使用 **Markdown** 编写的技术文档。

## 功能特性

- 支持标准 Markdown 语法
- 支持表格、代码块
- 自动应用专业排版

### 代码示例

\`\`\`python
def hello():
    print("Hello, World!")
\`\`\`

### 数据表格

| 指标 | 数值 |
|------|------|
| 用户数 | 10,234 |
| 日活跃 | 3,456 |
"""

response = requests.post(
    'http://localhost:3000/convert',
    json={
        'markdown': markdown_content,
        'page_size': 'A4',
        'margin': '2cm'
    }
)

with open('document.pdf', 'wb') as f:
    f.write(response.content)

# 示例 2: HTML + CSS
from jinja2 import Template

html_template = Template("""
<div class="report">
  <h1>{{ title }}</h1>
  <table>
    {% for item in items %}
    <tr><td>{{ item.name }}</td><td>{{ item.value }}</td></tr>
    {% endfor %}
  </table>
</div>
""")

css_styles = """
.report { padding: 20px; }
h1 { color: #2c3e50; font-size: 20pt; }
table { width: 100%; border-collapse: collapse; }
td { border: 1px solid #ddd; padding: 8pt; }
"""

html_content = html_template.render(
    title="月度报告",
    items=[{'name': '用户数', 'value': '10,234'}]
)

response = requests.post(
    'http://localhost:3000/convert',
    json={
        'html': html_content,
        'css': css_styles,
        'page_size': 'A4',
        'margin': '2cm'
    }
)

with open('report.pdf', 'wb') as f:
    f.write(response.content)

Node.js 示例

const axios = require('axios');
const fs = require('fs');

const htmlContent = `
<article>
  <h1>文章标题</h1>
  <p>这是内容...</p>
</article>
`;

const cssStyles = `
article { font-family: "Microsoft YaHei", sans-serif; }
h1 { font-size: 22pt; color: #333; }
p { line-height: 1.6; }
`;

axios.post('http://localhost:3000/convert', {
    html: htmlContent,
    css: cssStyles,
    page_size: 'A4',
    margin: '2cm'
}, {
    responseType: 'arraybuffer'
}).then(response => {
    fs.writeFileSync('article.pdf', response.data);
});

响应(两种模式相同):

  • Content-Type: application/pdf
  • Body: PDF 文件二进制数据

GET /health

健康检查接口。

响应示例

{
  "status": "healthy",
  "service": "html2pdf",
  "version": "1.0.0"
}

⚙️ 页面配置详解

支持的纸张尺寸

尺寸 说明 尺寸(mm)
A4 A4 标准 210 × 297
A3 A3 标准 297 × 420
A5 A5 标准 148 × 210
letter 美国信纸 8.5in × 11in
legal 美国法律文件 8.5in × 14in

也可以自定义尺寸(需要在 HTML 的 @page 中定义):

@page { size: 210mm 297mm; }

边距设置方式

1. 统一边距(四边相同):

-F "margin=2cm"

2. 上下、左右边距

-F "margin=2.5cm 2cm"  # 上下 2.5cm,左右 2cm

3. 单独设置四边(推荐,最灵活):

-F "margin_top=3cm" \
-F "margin_right=2cm" \
-F "margin_bottom=2.5cm" \
-F "margin_left=2cm"

页面方向

# 纵向(默认)
-F "page_orientation=portrait"

# 横向
-F "page_orientation=landscape"

🔢 像素单位自动转换 (base_width_px)

问题背景

前端开发中通常使用 px(像素) 作为尺寸单位,但 WeasyPrint 在生成 PDF 时按照 96 DPI 标准渲染 px 单位,这会导致:

  • 字体过大font-size: 18px 在 PDF 中显示异常大
  • 布局错乱:元素尺寸不符合预期,内容溢出页面
  • 排版拥挤:行高、间距等比例失调

解决方案

通过 base_width_px 参数,服务会自动将 HTML 和 CSS 中的 所有 px 单位转换为 pt 单位,按照目标纸张尺寸进行比例缩放。

工作原理

缩放比例 = A4 可用宽度(481pt)/ base_width_px
转换后尺寸 = 原始 px 值 × 缩放比例

例如:base_width_px=1920(常见桌面设计稿宽度)

  • A4 Portrait 可用宽度 ≈ 481pt(减去默认 2cm 边距)
  • 缩放比例 = 481 / 1920 ≈ 0.25
  • font-size: 18pxfont-size: 4.5pt
  • width: 800pxwidth: 200pt

转换的 CSS 属性

服务会自动转换以下 CSS 属性中的 px 值:

  • 尺寸width, height, min-width, max-width, min-height, max-height
  • 字体font-size, line-height, letter-spacing, word-spacing
  • 间距margin, padding(及各方向属性)
  • 边框border-width, border-radius(及各方向属性)
  • 定位top, right, bottom, left
  • 其他text-indent, gap, row-gap, column-gap

使用示例

multipart 接口

curl -F "[email protected]" \
     -F "base_width_px=1920" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     http://localhost:3000/convert -o output.pdf

JSON 接口

curl -X POST http://localhost:3000/convert \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<div style=\"font-size: 18px; width: 800px; padding: 20px;\">内容</div>",
    "base_width_px": 1920,
    "page_size": "A4",
    "margin": "2cm"
  }' \
  -o output.pdf

Python 示例(前端动态内容提取 + 像素转换):

import requests

# 前端提取的 HTML(已经过 JavaScript 渲染,使用 px 单位)
html_content = """
<div class="container" style="width: 1200px; padding: 40px;">
  <h1 style="font-size: 32px; margin-bottom: 20px;">报告标题</h1>
  <p style="font-size: 16px; line-height: 24px;">正文内容...</p>
  <div style="width: 800px; height: 400px;">图表区域</div>
</div>
"""

# 提取的 CSS(也使用 px 单位)
css_styles = """
.container { font-family: "Microsoft YaHei"; }
h1 { color: #2c3e50; }
p { color: #555; }
"""

response = requests.post(
    'http://localhost:3000/convert',
    json={
        'html': html_content,
        'css': css_styles,
        'base_width_px': 1920,  # 原始设计宽度
        'page_size': 'A4',
        'margin': '2cm'
    }
)

with open('report.pdf', 'wb') as f:
    f.write(response.content)

# 服务会自动转换:
# - font-size: 32px → 8pt
# - font-size: 16px → 4pt
# - width: 800px → 200pt
# 等等...

JavaScript 示例(配合前端提取方案):

// 步骤 1:前端提取渲染后的 HTML 和 CSS
const targetElement = document.querySelector('#app');
const html = targetElement.outerHTML;

function extractAllCSS() {
  const cssTexts = [];
  for (let i = 0; i < document.styleSheets.length; i++) {
    const sheet = document.styleSheets[i];
    try {
      const rules = sheet.cssRules || sheet.rules;
      const cssText = Array.from(rules).map(rule => rule.cssText).join('\n');
      cssTexts.push(cssText);
    } catch (e) {
      console.warn('无法访问样式表:', sheet.href, e);
    }
  }
  return cssTexts.join('\n');
}

const allCSS = extractAllCSS();

// 步骤 2:发送到转换接口,启用 px 转换
fetch('http://localhost:3000/convert', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({
    html: html,
    css: allCSS,
    base_width_px: 1920,  // 指定原始设计宽度
    page_size: 'A4',
    margin: '2cm'
  })
})
.then(res => res.blob())
.then(blob => {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'output.pdf';
  a.click();
});

常见 base_width_px 取值

设计场景 推荐 base_width_px 说明
桌面端网页 1920 标准桌面显示器宽度
桌面端网页 1440 MacBook Pro 等高分屏
移动端网页 750 iPhone 设计稿常用宽度
移动端网页 375 iPhone 逻辑像素宽度
平板端网页 1024 iPad 横屏宽度
自定义 <容器宽度> 使用实际容器的 offsetWidth

注意事项

  1. 仅转换内联样式和 <style> 标签中的样式

    • 外部 CSS 文件(<link>)中的样式也会被转换
    • Data URL 内嵌的 CSS 也会被转换
  2. 不影响已经使用 pt、cm、mm 等单位的样式

    • 只转换明确标注为 px 的值
    • 已使用印刷单位的样式保持不变
  3. 适用于所有 CSS 属性

    • 包括简写属性(如 margin: 10px 20px
    • 包括 calc() 表达式中的 px 值
  4. 与 zoom 选项的区别

    • base_width_px:智能单位转换,保持布局比例
    • zoom:整体缩放,但不改变单位(WeasyPrint 不支持 CSS zoom 属性

🔧 高级选项 (options 参数)

options 参数支持 WeasyPrint 的所有高级特性,让您可以精细控制 PDF 生成过程。

支持的选项

选项 类型 说明 示例值
zoom Float 缩放比例 1.0 (默认), 1.5, 0.8
pdf_version String PDF 版本 "1.4", "1.7", "2.0"
pdf_identifier Boolean 生成唯一 PDF 标识符 true, false
pdf_variant String PDF 变体/标准 "pdf/a-3b", "pdf/ua-1"
pdf_forms Boolean 包含 PDF 表单 true, false
uncompressed_pdf Boolean 生成未压缩的 PDF true, false
custom_metadata Object 自定义 PDF 元数据 {"author": "...", "title": "..."}

使用方式

multipart 接口(JSON 字符串格式):

curl -F "[email protected]" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     -F 'options={"zoom": 1.2, "pdf_version": "1.7", "custom_metadata": {"author": "IDG Capital", "title": "Q3 Report"}}' \
     http://localhost:3000/convert -o output.pdf

JSON 接口(对象格式):

curl -X POST http://localhost:3000/convert \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<div><h1>季度报告</h1></div>",
    "page_size": "A4",
    "margin": "2cm",
    "options": {
      "zoom": 1.2,
      "pdf_version": "1.7",
      "custom_metadata": {
        "author": "IDG Capital",
        "title": "Q3 Financial Report",
        "subject": "Quarterly Report",
        "keywords": "finance, quarterly, report"
      }
    }
  }' \
  -o report.pdf

常见使用场景

1. 设置 PDF 元数据(文档属性)

import requests

response = requests.post(
    'http://localhost:3000/convert',
    json={
        'html': html_content,
        'page_size': 'A4',
        'margin': '2cm',
        'options': {
            'custom_metadata': {
                'author': '张三',
                'title': '2026年度财务报告',
                'subject': '年度报告',
                'keywords': '财务,年度,审计',
                'creator': 'IDG Capital PDF Service'
            }
        }
    }
)

2. 生成 PDF/A 归档格式

// PDF/A-3b 适合长期归档
axios.post('http://localhost:3000/convert', {
    html: htmlContent,
    page_size: 'A4',
    margin: '2cm',
    options: {
        pdf_variant: 'pdf/a-3b',  // 归档标准
        pdf_version: '1.7'
    }
});

3. 缩放 PDF 内容

# 放大 20% 后生成 PDF(适合高分辨率打印)
curl -X POST http://localhost:3000/convert \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<div>...</div>",
    "page_size": "A4",
    "options": {"zoom": 1.2}
  }' \
  -o output.pdf

4. 无障碍 PDF (PDF/UA)

# 生成符合无障碍标准的 PDF
response = requests.post(
    'http://localhost:3000/convert',
    json={
        'html': accessible_html,
        'options': {'pdf_variant': 'pdf/ua-1'}
    }
)

5. 调试模式(未压缩 PDF)

# 生成未压缩的 PDF,方便调试和检查内部结构
curl -X POST http://localhost:3000/convert \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<div>...</div>",
    "options": {"uncompressed_pdf": true}
  }' \
  -o debug.pdf

🎨 中文字体支持

项目已预配置 6 种中文字体,内置在 Docker 镜像中。

可用字体列表

字体文件 字体名称(CSS) 说明 大小
msyh.ttc Microsoft YaHei 微软雅黑 19MB
msyhbd.ttc Microsoft YaHei Bold 微软雅黑粗体 17MB
msyhl.ttc Microsoft YaHei Light 微软雅黑细体 12MB
STHeiti Medium.ttc STHeiti, STHeiti Medium 华文黑体中等 54MB
STHeiti Light.ttc STHeiti Light 华文黑体细体 54MB
Menlo.ttc Menlo 等宽编程字体 2.1MB

使用方法

在 HTML 或 CSS 中指定字体族:

body {
  font-family: "Microsoft YaHei", "STHeiti", sans-serif;
}

/* 标题使用粗体 */
h1, h2, h3 {
  font-family: "Microsoft YaHei Bold", "STHeiti Medium", sans-serif;
}

/* 代码块使用等宽字体 */
code, pre {
  font-family: "Menlo", "Consolas", monospace;
}

🔒 安全配置

外部资源访问控制

为了安全考虑,默认情况下服务禁止访问外部 URL 资源。如需允许特定域名的资源,需配置环境变量 WEASYPRINT_ALLOWED_URLS_PATTERN

当前允许的域名

  • https://unpkg.com/* - CDN 资源
  • https://fonts.example.com/* - 字体资源
  • https://logos.example.com/* - Logo 资源

修改配置请编辑 docker-compose.ymldocker-compose-dev.yml 中的环境变量:

environment:
  - WEASYPRINT_ALLOWED_URLS_PATTERN=^https://unpkg\.com.*|^https://fonts\.googleapis\.com.*

📄 最佳实践

1. 推荐的 HTML 结构(纯净模式)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>报告标题</title>
  <style>
    /* 只定义内容样式,不定义 @page */
    body {
      font-family: "Microsoft YaHei", "STHeiti", sans-serif;
      font-size: 12pt;
      line-height: 1.6;
    }
    h1 { color: #2c3e50; }
  </style>
</head>
<body>
  <h1>标题</h1>
  <p>内容</p>
</body>
</html>

调用方式

curl -F "[email protected]" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     http://localhost:3000 -o report.pdf

2. 分页控制

强制分页

在 HTML 的 <style> 中添加:

/* 在元素前强制分页 */
.chapter {
  page-break-before: always;
}

/* 在元素后强制分页 */
.section {
  page-break-after: always;
}

避免元素内部被分页切断

为了防止表格、图片、代码块等元素在分页时被切断,使用以下 CSS 属性:

/* 避免在元素内部分页(保持元素完整) */
.keep-together {
  page-break-inside: avoid;
  break-inside: avoid;  /* 新标准 */
}

/* 避免标题与后续内容分离 */
h1, h2, h3, h4, h5, h6 {
  page-break-after: avoid;
  break-after: avoid;
}

/* 防止表格被切断 */
table {
  page-break-inside: avoid;
  break-inside: avoid;
}

/* 防止图片被切断 */
img {
  page-break-inside: avoid;
  break-inside: avoid;
}

/* 防止代码块被切断 */
pre, code {
  page-break-inside: avoid;
  break-inside: avoid;
}

实际应用示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <style>
    body {
      font-family: "Microsoft YaHei", sans-serif;
      font-size: 12pt;
      line-height: 1.6;
    }
    
    /* 每个章节从新页开始 */
    .chapter {
      page-break-before: always;
    }
    
    /* 卡片内容保持完整,不被分页切断 */
    .card {
      border: 1px solid #ddd;
      padding: 15pt;
      margin-bottom: 10pt;
      page-break-inside: avoid;
    }
    
    /* 表格不被切断 */
    table {
      width: 100%;
      border-collapse: collapse;
      page-break-inside: avoid;
    }
    
    /* 标题与后续内容保持在同一页 */
    h2 {
      page-break-after: avoid;
    }
  </style>
</head>
<body>
  <div class="chapter">
    <h1>第一章</h1>
    <p>章节内容...</p>
  </div>
  
  <div class="card">
    <h3>重要信息卡片</h3>
    <p>这个卡片不会被分页切断</p>
  </div>
  
  <table>
    <thead>
      <tr><th>列1</th><th>列2</th></tr>
    </thead>
    <tbody>
      <tr><td>数据1</td><td>数据2</td></tr>
    </tbody>
  </table>
</body>
</html>

注意事项

  • page-break-inside: avoid 只对不超过一页高度的元素有效
  • 如果元素本身超过一页,WeasyPrint 会强制分页
  • 同时使用旧标准 page-break-* 和新标准 break-* 可以提高兼容性

3. 页眉页脚

关闭页眉页脚(企业内部文档推荐)

不在 HTML 中添加任何 @page 规则,通过 API 参数控制页面:

curl -F "[email protected]" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     http://localhost:3000/convert -o output.pdf

启用页眉页脚

在 HTML 的 <style> 中定义:

@page {
  /* 页眉 - 居中显示 */
  @top-center {
    content: "文档标题";
    font-size: 10pt;
    color: #666;
  }
  
  /* 页脚 - 右侧显示页码 */
  @bottom-right {
    content: "第 " counter(page) " 页 / 共 " counter(pages) " 页";
    font-size: 10pt;
    color: #666;
  }
}

注意:如果在 HTML 中定义了 @page,它会覆盖 API 参数中的页面配置。

4. 资源引用

推荐方式:使用 asset.* 上传资源

curl -F "[email protected]" \
     -F "[email protected]" \
     -F "[email protected]" \
     -F "page_size=A4" \
     -F "margin=2cm" \
     http://localhost:3000/convert -o output.pdf

HTML 中使用相对路径引用:

<img src="logo.png" alt="Logo">
<link rel="stylesheet" href="style.css">

📦 部署

Docker 一键部署(推荐)

使用预构建的 Docker 镜像快速部署:

# 拉取并运行最新版本
docker run -d \
  -p 3000:3000 \
  --name html2pdf \
  --restart unless-stopped \
  icheerme/html2pdf:latest

# 查看日志
docker logs -f html2pdf

服务启动后访问:http://localhost:3000

自定义配置

如需自定义配置(如允许外部资源、环境变量等),可创建 docker-compose.yml

services:
  html2pdf:
    image: icheerme/html2pdf:latest
    container_name: html2pdf
    restart: unless-stopped
    ports:
      - "3000:3000"
    environment:
      - PORT=3000
      # 可选:允许特定外部资源域名
      - WEASYPRINT_ALLOWED_URLS_PATTERN=^https://unpkg\.com.*|^https://cdn\.jsdelivr\.net.*

启动服务:

docker compose up -d

从源码构建

如需从源码构建自定义版本:

# 克隆仓库
git clone https://github.com/icheer/html2pdf.git
cd html2pdf

# 构建镜像
docker build -t html2pdf:custom .

# 运行
docker run -d -p 3000:3000 --name html2pdf html2pdf:custom

🛠️ 技术栈

  • WeasyPrint 69.0:HTML/CSS 到 PDF 的渲染引擎
  • aiohttp 3.9.5:异步 HTTP 服务器
  • Python 3.12:编程语言
  • Docker:容器化部署
  • GitHub Actions:CI/CD 自动化

🐛 故障排查

中文显示为方块

确保 HTML 中指定了中文字体:

body { font-family: "Microsoft YaHei", "STHeiti", sans-serif; }

外部资源加载失败

检查 URL 是否在 WEASYPRINT_ALLOWED_URLS_PATTERN 白名单中。

PDF 文件过大

启用图像优化:

-F "optimize_images=true" \
-F "jpeg_quality=75"

页面配置不生效

检查 HTML 中是否定义了 @page CSS 规则。如果定义了,它会覆盖 API 参数。建议移除 HTML 中的 @page 规则,完全使用 API 参数控制。

📚 参考文档

🤝 贡献

欢迎贡献代码、报告问题或提出建议!

📄 开源协议

本项目采用 MIT License 开源协议。

⭐ Star History

如果这个项目对你有帮助,欢迎点个 Star ⭐️!

📞 联系方式


感谢使用 HTML2PDF! 🎉

About

基于 WeasyPrint 的 HTML 转 PDF 服务,使用 Docker 容器化部署。

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Contributors

Languages