1. 概述
office2pdf 是一个库和命令行工具,使用纯 Rust 将 DOCX、XLSX 和 PPTX 文件转换为 PDF。
它无需外部运行时(LibreOffice、Chromium、Docker)即可独立运行,使用 Typst 引擎作为布局/PDF 后端。
核心价值
-
零依赖:无需外部二进制文件/服务,作为单个可执行文件运行
-
高质量输出:以 95% 的保真度再现原始文档的布局/样式
-
库优先:可嵌入到其他 Rust 项目中,CLI 只是一个轻量封装
2. 目标用户
| 用户 |
使用场景 |
| 后端开发人员 |
服务端文档 → PDF 转换(报告、发票、打印输出) |
| DevOps/基础设施 |
无需 LibreOffice/Docker 的轻量级转换流水线 |
| CLI 用户 |
从终端快速批量转换 |
| Rust 开发人员 |
作为 crate 嵌入到项目中 |
3. 功能需求
3.1 输入格式
DOCX (Word)
| 优先级 |
功能 |
描述 |
| P0 |
文本 |
段落、换行、分页 |
| P0 |
行内格式 |
粗体、斜体、下划线、删除线、字体、字号、颜色 |
| P0 |
段落格式 |
对齐(左/右/居中/两端对齐)、缩进、行距 |
| P0 |
表格 |
基本表格、单元格合并、边框、背景色 |
| P0 |
图片 |
内嵌图片、基本尺寸调整 |
| P1 |
列表 |
编号列表、项目符号列表(多级) |
| P1 |
页眉/页脚 |
文本、页码 |
| P1 |
页面设置 |
纸张大小、页边距、方向(纵向/横向) |
| P1 |
样式 |
应用文档样式表(标题 1~6 等) |
| P2 |
目录 |
目录渲染 |
| P2 |
超链接 |
PDF 中的可点击链接 |
| P2 |
脚注/尾注 |
脚注、尾注渲染 |
| P3 |
文字环绕 |
图片周围的文字流动 |
| P3 |
公式 |
OMML 数学公式 |
| P3 |
图表 |
内嵌图表渲染 |
PPTX (PowerPoint)
| 优先级 |
功能 |
描述 |
| P0 |
幻灯片 → 页面 |
1 张幻灯片 = 1 页的映射 |
| P0 |
文本框 |
位置、大小、文本内容、格式 |
| P0 |
基本形状 |
矩形、圆形、线条等 |
| P0 |
图片 |
幻灯片内的图片放置 |
| P1 |
背景 |
纯色、渐变、图片背景 |
| P1 |
母版/版式 |
幻灯片母版 → 版式 → 幻灯片继承链 |
| P1 |
主题 |
主题颜色、主题字体解析 |
| P1 |
表格 |
幻灯片内的表格渲染 |
| P2 |
组合形状 |
组合形状的处理 |
| P2 |
形状样式 |
阴影、反射、旋转、透明度 |
| P3 |
SmartArt |
SmartArt 图形 |
| P3 |
图表 |
内嵌图表 |
XLSX (Excel)
| 优先级 |
功能 |
描述 |
| P0 |
单元格数据 |
文本、数字、日期值的输出 |
| P0 |
基本表格布局 |
行/列 → PDF 表格转换 |
| P0 |
单元格合并 |
合并单元格的处理 |
| P1 |
单元格格式 |
字体、颜色、背景色、边框 |
| P1 |
列宽/行高 |
反映原始尺寸 |
| P1 |
数字格式 |
货币、百分比、日期格式字符串 |
| P1 |
工作表选择 |
转换特定工作表或所有工作表 |
| P2 |
打印区域 |
反映配置的打印区域 |
| P2 |
页眉/页脚 |
工作表的页眉/页脚 |
| P2 |
分页符 |
手动分页符的处理 |
| P3 |
条件格式 |
条件格式渲染 |
| P3 |
图表 |
内嵌图表渲染 |
3.2 输出
| 功能 |
描述 |
| PDF 输出 |
生成有效的 PDF 文件 |
| PDF 版本 |
默认 PDF 1.7,可选 PDF/A |
| 字体嵌入 |
在 PDF 中嵌入使用的字体 |
| 元数据 |
标题、作者、创建日期等 |
4. 非功能需求
| 类别 |
需求 |
| 性能 |
10 页文档 < 1 秒,100 页 < 5 秒 |
| 内存 |
处理 100 页文档时 < 500MB |
| 二进制大小 |
CLI < 50MB(包含字体) |
| 平台 |
Windows、macOS、Linux |
| 错误处理 |
跳过无法解析的元素并发出警告,继续整体转换 |
| 字体回退 |
系统字体发现 + 内置默认字体 |
5. 架构
5.1 转换流水线
输入文件 (.docx/.pptx/.xlsx)
│
▼
[1. 解析器] ─── docx-rs / ppt-rs / umya-spreadsheet
│
▼
[2. IR (中间表示)] ← 格式无关的文档模型
│
▼
[3. Typst 代码生成] ─── IR → Typst 标记生成
│
▼
[4. Typst 编译] ─── typst-as-lib / typst crate
│
▼
[5. PDF 导出] ─── typst-pdf
│
▼
output.pdf
5.2 IR (中间表示) 设计
所有输入格式在输出到 Typst 之前都转换为统一的 IR:
pub struct Document {
pub metadata: Metadata,
pub pages: Vec<Page>,
pub styles: StyleSheet,
}
pub enum Page {
Flow(FlowPage),
Fixed(FixedPage),
Table(TablePage),
}
pub struct FlowPage {
pub size: PageSize,
pub margins: Margins,
pub header: Option<HeaderFooter>,
pub footer: Option<HeaderFooter>,
pub content: Vec<Block>,
}
pub enum Block {
Paragraph(Paragraph),
Table(Table),
Image(Image),
PageBreak,
List(List),
}
5.3 项目结构
office2pdf/
├── Cargo.toml # 工作区根目录
├── crates/
│ ├── office2pdf/ # 库 crate
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── lib.rs # 公共 API
│ │ ├── error.rs # thiserror 错误类型
│ │ ├── ir/ # 中间表示
│ │ │ ├── mod.rs
│ │ │ ├── document.rs # Document, Page, Block 等
│ │ │ ├── style.rs # 样式/格式模型
│ │ │ └── elements.rs # Paragraph, Table, Image 等
│ │ ├── parser/ # 输入格式解析器
│ │ │ ├── mod.rs # Parser trait
│ │ │ ├── docx.rs # DOCX → IR
│ │ │ ├── pptx.rs # PPTX → IR
│ │ │ └── xlsx.rs # XLSX → IR
│ │ ├── render/ # IR → PDF 渲染
│ │ │ ├── mod.rs
│ │ │ ├── typst_gen.rs # IR → Typst 标记生成
│ │ │ └── pdf.rs # Typst 编译 + PDF 输出
│ │ └── config.rs # 转换选项
│ └── office2pdf-cli/ # CLI crate
│ ├── Cargo.toml
│ └── src/
│ └── main.rs # 基于 clap 的 CLI
├── tests/ # 集成测试
│ ├── fixtures/ # 测试文档文件
│ └── integration_tests.rs
└── fonts/ # 内置默认字体
5.4 关键依赖
| Crate |
用途 |
备注 |
typst |
布局引擎 |
或 typst-as-lib
|
typst-pdf |
PDF 输出 |
typst-kit |
字体发现 |
docx-rs |
DOCX 解析 |
v0.4.19,活跃维护中 |
umya-spreadsheet |
XLSX 解析(含格式) |
支持样式/格式提取 |
ppt-rs |
PPTX 解析 |
python-pptx 的 Rust 移植版 |
clap |
CLI 参数解析 |
v4 derive |
thiserror |
库错误 |
anyhow |
CLI 错误 |
6. 公共 API(库)
use office2pdf::{Document, ConvertOptions, Format};
let pdf_bytes = office2pdf::convert("input.docx")?;
std::fs::write("output.pdf", pdf_bytes)?;
let options = ConvertOptions::builder()
.paper_size(PaperSize::A4)
.font_paths(vec!["./fonts"])
.pdf_standard(PdfStandard::PdfA2b)
.build();
let pdf_bytes = office2pdf::convert_with_options("input.xlsx", &options)?;
let docx_bytes = std::fs::read("input.docx")?;
let pdf_bytes = office2pdf::convert_bytes(&docx_bytes, Format::Docx, &options)?;
let options = ConvertOptions::builder()
.sheet_names(vec!["Sheet1"])
.slide_range(1..=5)
.build();
7. CLI 接口
office2pdf input.docx
office2pdf input.pptx -o output.pdf
office2pdf input.xlsx --sheets "Sheet1,Sheet2"
office2pdf input.pptx --slides 1-5
office2pdf input.docx --paper a4 --landscape
office2pdf input.docx --font-path ./fonts
office2pdf *.docx --outdir ./pdfs/
office2pdf --version
office2pdf --help
8. 实施阶段
阶段 1:MVP(基础文本 + 图片)
- 项目结构搭建(工作区、CI)
- IR 定义
- DOCX P0 功能 → IR → Typst → PDF
- PPTX P0 功能 → IR → Typst → PDF
- XLSX P0 功能 → IR → Typst → PDF
- 基础 CLI 功能
阶段 2:格式 + 样式
- 所有 P1 功能实现
- 字体嵌入/回退
- 增强错误处理
阶段 3:高级功能
- P2 功能实现
- PDF/A 支持
- 性能优化
- 批量转换
阶段 4:完善
- P3 功能(在可行范围内)
- 边缘情况处理
- 文档、示例、crates.io 发布
9. 验证方法
| 方法 |
描述 |
| Golden 测试 |
测试文档 → PDF → 截图对比 |
| 往返测试 |
转换已知文档并通过文本提取验证 |
| 手动对比 |
MS Office PDF 输出与 office2pdf 输出对比 |
| CI |
每次提交时自动转换测试文档集 |
10. 风险
| 风险 |
影响 |
缓解措施 |
| OOXML 规范复杂性 |
解析 crate 可能不支持所有元素 |
跳过不支持的元素 + 警告日志 |
| Typst IR 转换限制 |
某些布局可能无法在 Typst 中表达 |
在 Typst 能力范围内进行最佳近似 |
| 字体兼容性 |
原始字体可能不可用 |
系统字体发现 + 回退字体映射 |
| 解析 crate 维护 |
依赖 crate 可能被弃用 |
准备分支,考虑为核心解析器自行实现 |