printpdf 是一个用于创建、读取、写入和渲染 PDF 文档的 Rust 库。
Website | Crates.io | Documentation | Donate
!IMPORTANT
HTML 到 PDF 的渲染功能仍处于实验阶段(存根 API),并且正在开发中。
目前,您需要手动定位 PDF 元素。
特性
- 页面、书签、链接注释(读取 / 写入)
- 图层(读取 / 写入)
- 图形:线条、形状、贝塞尔曲线、SVG 内容(读取 / 写入)
- 图像编码 / 解码(读取支持:实验性 / 写入支持需使用
image库) - 嵌入式字体、Unicode 支持(读取支持:实验性 / 写入支持)
- 最小化文件大小(自动子集化字体)
- 高级图形 - 叠印、混合等
- 高级排版 - 字符 / 单词缩放和间距、上标、下标等
- 嵌入 SVG(内部使用
svg2pdf库)
实验性特性:
- 将 PDF 页面渲染为 SVG(每页一个独立的 SVG 文件)
- 您可以使用生成的 SVG,通过
resvg将 PDF 页面渲染为图像
- 您可以使用生成的 SVG,通过
- 从 PDF 页面提取文本(自动解码 Unicode、换行符和文本位置)
- 用于简单页面布局的最小化 XHTML 布局(使用
azul-layout+kuchikiHTML 解析器)- 目前这仅是一个存根 API:它可以编译,但不会产生可用的输出
- 当
azul-layout获得更好的 HTML 求解器时,HTML 到 PDF 的管道将得到改进
不支持的功能:
- 渐变
- 图案
- 文件附件
- 开放印前接口
- 半色调图像
- 各种 PDF 标准的符合性 / 错误检查
- 嵌入式 JavaScript
实时 WASM32 演示请参见:https://fschutt.github.io/printpdf
编写 PDF
基础示例
rust
use printpdf::*;
fn main() {
let mut doc = PdfDocument::new("My first PDF");
let page1_contents = vec![Op::Marker { id: "debugging-marker".to_string() }];
let page1 = PdfPage::new(Mm(10.0), Mm(250.0), page1_contents);
let pdf_bytes: Vec<u8> = doc
.with_pages(vec![page1])
.save(&PdfSaveOptions::default());
}
图形
rust
use printpdf::*;
fn main() {
let mut doc = PdfDocument::new("My first PDF");
let line = Line {
// 二次形状。"false" 决定下一个(后续)点是否是贝塞尔控制点(用于曲线)
// 如果您想要孔洞,只需将点的缠绕顺序从顺时针重新排序为逆时针。
points: vec![
(Point::new(Mm(100.0), Mm(100.0)), false),
(Point::new(Mm(100.0), Mm(200.0)), false),
(Point::new(Mm(300.0), Mm(200.0)), false),
(Point::new(Mm(300.0), Mm(100.0)), false),
],
is_closed: true,
};
// 三角形形状
let polygon = Polygon {
rings: vec![vec![
(Point::new(Mm(150.0), Mm(150.0)), false),
(Point::new(Mm(150.0), Mm(250.0)), false),
(Point::new(Mm(350.0), Mm(250.0)), false),
]],
mode: PaintMode::FillStroke,
winding_order: WindingOrder::NonZero,
};
// 图形配置
let fill_color = Color::Cmyk(Cmyk::new(0.0, 0.23, 0.0, 0.0, None));
let outline_color = Color::Rgb(Rgb::new(0.75, 1.0, 0.64, None));
let mut dash_pattern = LineDashPattern::default();
dash_pattern.dash_1 = Some(20);
let extgstate = ExtendedGraphicsStateBuilder::new()
.with_overprint_stroke(true)
.with_blend_mode(BlendMode::multiply())
.build();
let page1_contents = vec![
// 添加 line1(正方形)
Op::SetOutlineColor { col: Color::Rgb(Rgb::new(0.75, 1.0, 0.64, None)) },
Op::SetOutlineThickness { pt: Pt(10.0) },
Op::DrawLine { line: line },
// 添加 line2(三角形)
Op::SaveGraphicsState,
Op::LoadGraphicsState { gs: doc.add_graphics_state(extgstate) },
Op::SetLineDashPattern { dash: dash_pattern },
Op::SetLineJoinStyle { join: LineJoinStyle::Round },
Op::SetLineCapStyle { cap: LineCapStyle::Round },
Op::SetFillColor { col: fill_color },
Op::SetOutlineThickness { pt: Pt(15.0) },
Op::SetOutlineColor { col: outline_color },
Op::DrawPolygon { polygon: polygon },
Op::RestoreGraphicsState,
];
let page1 = PdfPage::new(Mm(10.0), Mm(250.0), page1_contents);
let pdf_bytes: Vec<u8> = doc
.with_pages(vec![page1])
.save(&PdfSaveOptions::default());
}
图像
- 图像仅在发布模式下被压缩。在调试模式下,您可能会得到巨大的 PDF(6 MB 或更多)。
- 为了使这个过程更快,请使用
BufReader而不是直接从文件读取。 - 图像的缩放是隐式完成的,以适应 300 dpi 下的一像素等于一点。
rust
use printpdf::*;
fn main() {
let mut doc = PdfDocument::new("My first PDF");
let image_bytes = include_bytes!("assets/img/BMP_test.bmp");
let image = RawImage::decode_from_bytes(image_bytes).unwrap(); // 需要 --feature bmp
// 在 PDF 中,图像是一个 `XObject`,由唯一的 `ImageId` 标识
let image_xobject_id = doc.add_image(image);
let page1_contents = vec![
Op::UseXobject {
id: image_xobject_id.clone(),
transform: XObjectTransform::default()
}
];
let page1 = PdfPage::new(Mm(10.0), Mm(250.0), page1_contents);
let pdf_bytes: Vec<u8> = doc
.with_pages(vec![page1])
.save(&PdfSaveOptions::default());
}
字体
rust
use printpdf::*;
fn main() {
let mut doc = PdfDocument::new("My first PDF");
let roboto_bytes = include_bytes!("assets/fonts/RobotoMedium.ttf").unwrap()
let font_index = 0;
let mut warnings = Vec::new();
let font = ParsedFont::from_bytes(&roboto_bytes, font_index, &mut warnings).unwrap();
// 如果您需要自定义文本整形(内部使用 `allsorts` 字体整形器)
// let glyphs = font.shape(text);
// printpdf 自动跟踪 PDF 中使用的字体
let font_id = doc.add_font(&font);
let text_pos = Point {
x: Mm(10.0).into(),
y: Mm(100.0).into(),
}; // 从左下角开始
let page1_contents = vec![
Op::SetLineHeight { lh: Pt(33.0) },
Op::SetWordSpacing { pt: Pt(33.0) },
Op::SetCharacterSpacing { multiplier: 10.0 },
Op::SetTextCursor { pos: text_pos },
// Op::WriteCodepoints { ... }
// Op::WriteCodepointsWithKerning { ... }
Op::WriteText {
items: vec![TextItem::Text("Lorem ipsum".to_string())],
font: font_id.clone(),
},
Op::AddLineBreak,
Op::WriteText {
items: vec![TextItem::Text("dolor sit amet".to_string())],
font: font_id.clone(),
},
Op::AddLineBreak,
];
let save_options = PdfSaveOptions {
subset_fonts: true, // 保存时自动子集化字体
..Default::default()
};
let page1 = PdfPage::new(Mm(10.0), Mm(250.0), page1_contents);
let mut warnings = Vec::new();
let pdf_bytes: Vec<u8> = doc
.with_pages(vec![page1])
.save(&save_options, &mut warnings);
}
表格、HTML
为了创建表格等,printpdf 使用了一个基本的布局系统,类似于 wkhtmltopdf(尽管在功能方面更有限)。它对于基本的页面布局、书籍渲染和报告 / 表单 / 等来说已经足够好。包括自动分页。
由于 printpdf 支持 WASM,因此有一个交互式演示在 https://fschutt.github.io/printpdf - 尝试使用 XML。
有关 XML 语法描述,请参见 SYNTAX.md。
rust
// 需要 --features="html"
use printpdf::*;
fn main() {
// 参见 https://fschutt.github.io/printpdf 获取交互式 WASM 演示!
let html = r#"
<html>
<!-- printpdf 自动将内容分页 -->
<body style="padding:10mm">
<p style="color: red; font-family: sans-serif;" data-chapter="1" data-subsection="First subsection">Hello!</p>
<div style="width:200px;height:200px;background:red;" data-chapter="1" data-subsection="Second subsection">
<p>World!</p>
</div>
</body>
<!-- 为每个页面配置页眉和页脚 -->
<head>
<header>
<h4 style="color: #2e2e2e;min-height: 8mm;">Chapter {attr:chapter} * {attr:subsection}</h4>
<p style="position: absolute;top:5mm;left:5mm;">{builtin:pagenum}</p>
</header>
<footer>
<hr/>
<footer/>
</head>
</html>
"#;
let options = XmlRenderOptions {
// 在 HTML 中使用的命名图像,例如 ["image1.png" => DecodedImage(image1_bytes)]
images: BTreeMap::new(),
// 在 HTML 中使用的命名字体,例如 ["Roboto" => DecodedImage(roboto_bytes)]
fonts: BTreeMap::new(),
// 默认页面宽度,printpdf 将自动分页
page_width: Mm(210.0),
// 默认页面高度
page_height: Mm(297.0),
};
let pdf_bytes = PdfDocument::new("My PDF")
.with_html(html, &options).unwrap()
.save(&PdfSaveOptions::default());
}
路线图
printpdf 的目标是成为一个通用 PDF 库,例如 libharu 或类似的库。由 printpdf 生成的 PDF 应始终遵循 PDF 标准,除非您将其关闭。目前,仅涵盖了标准 PDF/X-3:2002(即根据 Adobe Acrobat 的有效 PDF)。随着时间的推移,将支持更多标准。
printpdf 维基实时更新于:https://github.com/fschutt/printpdf/wiki
以下是我在开发这个库时找到的一些资源:
PDFXPlorer,显示 PDF 的 DOM 树,需要 .NET 2.0- 官方 PDF 1.7 参考
- [德语] 如何在 PDF 中嵌入 Unicode 字体
- PDF X/1-a 验证器
- PDF X/3 技术说明
许可证 / 支持
本库采用 MIT 许可证授权。
您可以在 https://github.com/sponsors/fschutt 进行捐赠(一次性或定期)。谢谢!