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:
pubstructDocument{pubmetadata:Metadata,pubpages:Vec<Page>,pubstyles:StyleSheet,}pubenumPage{Flow(FlowPage),// DOCX:流式文本页面Fixed(FixedPage),// PPTX:固定坐标页面Table(TablePage),// XLSX:基于表格的页面}pubstructFlowPage{pubsize:PageSize,pubmargins:Margins,pubheader:Option<HeaderFooter>,pubfooter:Option<HeaderFooter>,pubcontent:Vec<Block>,}pubenumBlock{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(库)
useoffice2pdf::{Document,ConvertOptions,Format};// 简单用法letpdf_bytes=office2pdf::convert("input.docx")?;std::fs::write("output.pdf",pdf_bytes)?;// 带选项letoptions=ConvertOptions::builder().paper_size(PaperSize::A4).font_paths(vec!["./fonts"]).pdf_standard(PdfStandard::PdfA2b).build();letpdf_bytes=office2pdf::convert_with_options("input.xlsx",&options)?;// 从字节转换letdocx_bytes=std::fs::read("input.docx")?;letpdf_bytes=office2pdf::convert_bytes(&docx_bytes,Format::Docx,&options)?;// 仅转换特定工作表/幻灯片letoptions=ConvertOptions::builder().sheet_names(vec!["Sheet1"])// XLSX:仅特定工作表.slide_range(1..=5)// PPTX:仅幻灯片 1-5.build();
7. CLI 接口
# 基本转换office2pdf input.docx# → input.pdfoffice2pdf input.pptx-ooutput.pdf# 指定输出路径# 选项office2pdf input.xlsx--sheets"Sheet1,Sheet2"# 仅特定工作表office2pdf input.pptx--slides1-5# 仅特定幻灯片office2pdf input.docx--papera4--landscape# 纸张设置office2pdf input.docx --font-path ./fonts# 字体路径# 批量转换office2pdf *.docx--outdir./pdfs/# 批量转换多个文件# 信息office2pdf--versionoffice2pdf--help
8. 实施阶段
阶段 1:MVP(基础文本 + 图片)
- 项目结构搭建(工作区、CI)
- IR 定义
- DOCX P0 功能 → IR → Typst → PDF
- PPTX P0 功能 → IR → Typst → PDF
- XLSX P0 功能 → IR → Typst → PDF
- 基础 CLI 功能
阶段 2:格式 + 样式
阶段 3:高级功能
阶段 4:完善
- P3 功能(在可行范围内)
- 边缘情况处理
- 文档、示例、crates.io 发布
9. 验证方法
| 方法 | 描述 |
|---|
| Golden 测试 | 测试文档 → PDF → 截图对比 |
| 往返测试 | 转换已知文档并通过文本提取验证 |
| 手动对比 | MS Office PDF 输出与 office2pdf 输出对比 |
| CI | 每次提交时自动转换测试文档集 |
10. 风险
| 风险 | 影响 | 缓解措施 |
|---|
| OOXML 规范复杂性 | 解析 crate 可能不支持所有元素 | 跳过不支持的元素 + 警告日志 |
| Typst IR 转换限制 | 某些布局可能无法在 Typst 中表达 | 在 Typst 能力范围内进行最佳近似 |
| 字体兼容性 | 原始字体可能不可用 | 系统字体发现 + 回退字体映射 |
| 解析 crate 维护 | 依赖 crate 可能被弃用 | 准备分支,考虑为核心解析器自行实现 |