MiniPdf Office 转 PDF 支持 Rust 原生实现:3 MB 单文件部署、低内存

MiniPdf Office 转 PDF 支持 Rust 原生实现:3 MB 单文件部署、低内存

github link : https://github.com/mini-software/MiniPdf

有时候只想在 API、定时任务或 Container 里,把一份 Excel、Word 转成 PDF,最后却先安装了一整套 Office、LibreOffice,甚至还要处理 COM 权限、后台进程与 Runtime 版本。

这类方案不是不能用,而是部署一多,维护成本就会慢慢浮现:镜像文件变大、启动时间拉长、内存难以预估,转换进程卡住时还得处理残留的外部 Process。

MiniPdf 现在支持 Rust 原生实现,提供 minipdf crate 与 minipdf-cli。可以直接在 Rust 进程内解析 Office Open XML、排版并生成 PDF。

1. 先用 Rust 把 DOCX 转成 PDF

先添加 minipdf

bash 复制代码
cargo add minipdf

接着只需要一个方法:

rust 复制代码
fn main() -> minipdf::Result<()> {
	minipdf::convert_to_pdf("report.docx", "report.pdf")?;
	Ok(())
}

如果需要直接把 PDF 返回给 Web API,不想先保存为临时文件,也可以取得 Vec<u8>

rust 复制代码
let pdf_bytes = minipdf::convert_to_pdf_bytes("report.docx")?;

输入原本就在内存中时,可以使用 convert_bytes_to_pdf,省去自行创建临时 Office 文件的流程:

rust 复制代码
let office_bytes = std::fs::read("data.xlsx")?;
let pdf_bytes = minipdf::convert_bytes_to_pdf(&office_bytes)?;

CLI 的使用方式:

bash 复制代码
cargo install minipdf-cli
minipdf report.docx -o report.pdf

目前 Rust 版本支持 XLSXDOCX,输出为 PDF 1.4;PPTX 尚未支持,而且官方仍将它标记为 experimental。如果项目需要 PowerPoint 或更广泛的 Office 兼容性,现阶段仍应使用 MiniPdf 的 .NET 实现。

2. Rust 底层原理

MiniPdf 的 Rust workspace 很简单,公共 API 位于 crates/minipdf/src/lib.rs。从 convert_to_pdf 往下追,可以整理成以下流程:

  1. convert_to_pdf 创建默认的 ConversionOptions
  2. convert_to_pdf_with_options 调用 convert_to_pdf_bytes_with_options
  3. 使用 fs::read 将 Office 文件读入 Vec<u8>
  4. 优先根据扩展名分派到 convert_docx_bytesconvert_xlsx_bytes
  5. 没有可识别的扩展名时,再打开 ZIP package,从 word/xl/ 路径判断格式。
  6. 解析 XML、样式、图片与页面设置,创建内部文档结构。
  7. 将文字、线条、矩形与图片转换成 PdfOp,最后由 PdfDocument::to_bytes 写出 PDF objects、xref 与 trailer。
  8. 返回最外层后,才使用 fs::write 写入输出文件。

源代码重点部分可以简化成:

rust 复制代码
pub fn convert_to_pdf_with_options(
	input_path: impl AsRef<Path>,
	output_path: impl AsRef<Path>,
	options: &ConversionOptions,
) -> Result<()> {
	let pdf = convert_to_pdf_bytes_with_options(input_path, options)?;
	fs::write(output_path, pdf)?;
	Ok(())
}

pub fn convert_bytes_to_pdf_with_options(
	input: &[u8],
	options: &ConversionOptions,
) -> Result<Vec<u8>> {
	match detect_office_format(input)? {
		OfficeFormat::Docx => docx::convert_docx_bytes(input, options),
		OfficeFormat::Xlsx => xlsx::convert_xlsx_bytes(input, options),
		OfficeFormat::Unknown => Err(MiniPdfError::UnsupportedFormat),
	}
}

接着来看 XLSX。Office Open XML 本身就是 ZIP package,Rust 版本使用 zip 打开封装,再通过 roxmltree 读取 shared strings、styles、worksheet、print area、图片与分页信息。相关逻辑集中在 xlsx.rs

DOCX 的处理路径也很接近。它会解析 word/document.xmlword/styles.xml 与 relationships,转换成 paragraph、table、image、page break 等 DocxBlock,主要逻辑位于 docx.rs

最后的 PDF 不是交给 Office 或浏览器打印,而是由 pdf.rs 自行创建 PDF 1.4 结构。PNG 图像数据使用 Flate 压缩,JPEG 可以保留 DCT 数据,实际用到的字体 glyph 也会通过 subsetter 创建 subset,避免把整个字体全部放进 PDF。

3. "无特殊依赖"不等于完全没有依赖

Rust MiniPdf 在运行转换时,不需要安装:

  1. Microsoft Office
  2. LibreOffice
  3. Adobe Acrobat
  4. .NET Runtime

也不需要启动上述应用程序的外部 Process。对于 Container、CI runner、Serverless Job 或权限受限的主机来说,这会让部署与故障排查简单很多。

但它并不是"零依赖"。编译阶段仍会使用 ziproxmltreeflate2imagerustybuzzttf-parsersubsetter 等 Rust crates,只是这些代码会一起编译进原生可执行文件,不需要在目标主机上另外安装对应的 Runtime。

有两个实际边界需要注意:

  1. 从 crate 自行构建需要 Rust 1.82 以上。
  2. 英文可以使用 PDF 内置的 Helvetica;中文、日文、韩文等 Unicode 内容仍需要可用字体。可以让 CLI 查找系统 fallback fonts,也可以用 --fonts 指定自己的字体目录。

例如在生产环境中,可以只携带项目需要使用的字体:

bash 复制代码
minipdf report.docx --fonts ./fonts -o report.pdf

4. 轻量化:Rust CLI 只有 3.35 MiB

先比较目前 repository 内已经构建完成的 Windows x64 release binary:

实现 可执行文件大小
Rust minipdf.exe 3,511,808 bytes(3.35 MiB)
.NET Native AOT MiniPdf.Cli.exe 10,674,688 bytes(10.18 MiB)

Rust CLI 大约是 .NET Native AOT CLI 的三分之一。

5. 实际环境测试

本次测试环境如下:

项目 环境
OS Windows 10 Pro 10.0.19045
CPU AMD Ryzen 5 5600X
Rust toolchain cargo 1.96.0rustc 1.96.0
.NET SDK 10.0.103
测试文件 Business expenses budget2.xlsx
输入大小 125,082 bytes

测试方式是让 Rust release CLI 与 .NET Native AOT CLI 转换同一份 XLSX,先预热一次,再分别启动五个独立 Process。每次验证 exit code、输出文件存在,而且 PDF header 是 %PDF-1.4;时间取五次中位数,Peak Working Set 则在 Process 存活期间采样。

第一组测试让两边都传入空的 --fonts 目录,避免系统字体扫描影响结果:

实现 时间中位数 Peak Working Set 中位数 PDF 大小
Rust 105.66 ms 15.72 MiB 464,432 bytes
.NET Native AOT 632.99 ms 972.99 MiB 641,314 bytes

在这个案例中,Rust 约为 6 倍快,峰值 Working Set 也低很多。可以发现,Rust 原生引擎在不处理额外字体加载时,进程启动与转换路径相当精简。

但空字体目录不适合直接作为生产环境建议,因为缺少字体时,Unicode 文字的显示结果可能不同。这组数据主要用于隔离字体扫描成本。

相关推荐
小灰灰搞电子1 小时前
Rust+Slint 实现圆形变色按钮源码分享
rust·slint·变色按钮
flyxl1 小时前
DataZen 架构设计(一):一个现代桌面数据库工具是如何构建的
rust
k4m7v2pz1 小时前
用 rust-verb-shell 重写进程管理:从 game.sh 到 .rvs 的迁移实录
开发语言·后端·rust
mldong13 小时前
同一份 15 个流程 JSON,第六种语言也跑通了:工作流引擎 Rust 移植实录
架构·rust
Naylor2 天前
只借不占:Rust 的引用与借用
后端·rust
对象存储与RustFS2 天前
RustFS 后台扫描器与自愈调参:五档速度、位腐检测周期与并发上限
后端·rust·开源
q平面人3 天前
【总工笔记】mindoc平板、桌面端笔记软件,发布到mindoc服务端
rust·tauri·华为平板·mindoc·总工笔记
MC皮蛋侠客3 天前
Tauri 2.x 系列(九):安全模型——Capabilities、Permissions、Scope 与 CSP
rust·tauri
Source.Liu3 天前
【Dioxus】Windows 环境下 Rust + Dioxus 安装配置笔记
windows·rust·dioxus