Rust is powerful, it can run on any platform and interoperate with other languages, like Tauri for cross-platform apps or in Python.
For a project I'm on I needed a way to generate PDFs locally and offline on a mobile phone, and to generate them in a Python web app. To solve this, I set out to create a library that works with both Tauri and Python.
The PDF Lib
The first step is to make a crate that will generate our PDFs. I'm using the printpdf
crate under the hood for creating the PDFs. The goal is to provide a simple API, passing a Vec<PdfBlock>
to generate the document with. PdfBlock
is an enum that can be a TextSection
or an Image
. Each can be positioned on the document, font size can be set, etc. You can find the repo here.
Use in Tauri
Using it in Tauri is like using any crate in Rust. First, we'll add it to our Cargo.toml
pdf-ing = { git = "https://github.com/ChristianPavilonis/pdf-ing" }
Then we'll create a function to use our crate with our application-specific data.
pub fn gen_pdf<P: AsRef<Path>>( my_data: MyStruct, my_image: P, out: P,) -> Result<()> { let doc = Doc::new(215.9, 279.4); let blocks = vec![ Image { path: my_image, dpi: 75.0, pos: (200.0, 14.0), }, TextSection { nodes: vec![ TextNode { content: my_data.my_property, font_size: 30.0, line_height: 18.0, }, // more stuff pdf_ing::generate_pdf(doc, blocks, &out)?; Ok(())}
Then, I'll create a command that will delegate to this function.
#[tauri::command]pub async fn make_pdf(app: AppHandle) -> Result<(), String> { let path_resolver = app.path(); let my_data = // get/build the data or maybe pass it from js // get Documents directory then append the filename. let mut output = path_resolver.document_dir().unwrap(); path.push("my_pdf.pdf"); // resolve image from application directory let my_image = path_resolver .resolve("my_image.jpg", BaseDirectory::AppData) .map_err(|e| e.to_string())?; gen_pdf(my_data, &my_image, &output) .map_err(|e| e.to_string())?; Ok(())}
The pyo3 Lib
To make a library that can be used in python we'll have to familiarize ourselves with a few tools.
- pyo3 is a Rust crate that makes it possible to call our Rust code from Python.
-
maturin is a build system that will help us build and publish
whl
files that we can later install and use in our Python code. - uv works pretty well for managing Python versions and virtual environments. I'll be using it for my examples.
To start a new pyo3 project, run Maturin's new command and pick pyo3.
uvx maturin new pydfingcd pydfinguv venv # to init python virtual env
pyo3 lets us create types that can be converted from Python objects using a derive macro, and lets us expose functions to a Python module using the #[pymodule]
macro.
First, let's add our crate to Cargo.toml
. I'll use my Git URL.
pdf-ing = { git = "https://github.com/ChristianPavilonis/pdf-ing" }
Then we can use pyo3 to wrap the crate.
Here's what my code looks like for wrapping my crate.
use std::{ path::{Path, PathBuf}, str::FromStr,}; use pdf_ing::*;use pyo3::{exceptions::{PyRuntimeError, PyValueError}, prelude::*}; #[derive(FromPyObject, Clone)]pub enum PyPdfBlock { Image { path: String, dpi: f32, pos: (f32, f32), }, TextSection { nodes: Vec<PyTextNode>, pos: (f32, f32), },} impl From<PyPdfBlock> for PdfBlock<String> { fn from(val: PyPdfBlock) -> Self { match val { PyPdfBlock::Image { path, dpi, pos } => PdfBlock::Image { path, dpi, pos }, PyPdfBlock::TextSection { nodes, pos } => PdfBlock::TextSection { nodes: nodes.iter().map(|n| TextNode { content: n.content.clone(), font_size: n.font_size, line_height: n.line_height, }).collect(), pos, }, } }} #[derive(FromPyObject, Clone)]pub struct PyTextNode { pub content: String, pub font_size: f32, pub line_height: f32,} #[pyfunction]pub fn gen_pdf(path: &str, width: f32, height: f32, blocks: Vec<PyPdfBlock>) -> PyResult<()> { let doc = Doc::new(width, height); let blocks: Vec<PdfBlock<String>> = blocks.iter().map(|b| PdfBlock::from(b.clone())).collect(); generate_pdf(doc, blocks, path).map_err(|e| PyRuntimeError::new_err(e.to_string()))?; Ok(())} /// A Python module implemented in Rust.#[pymodule]fn pydfing(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(gen_pdf, m)?)?; Ok(())}
You should be able to build now using Maturin.
uvx maturin build# install using pipuv pip install target/wheels/<whl-file>
Now, we should be able to call our Rust code from Python.
Use in Python
Now, to use in Python, we just have to import the module and call our function. I also added some classes to create the blocks.
import pydfing; class ImageBlock: def __init__(self, path, dpi, pos): self.path = path self.dpi = dpi self.pos = pos class TextNode: def __init__(self, content, font_size, line_height): self.content = content self.font_size = font_size self.line_height = line_height class TextBlock: def __init__(self, nodes, pos): self.nodes = nodes self.pos = pos # Artifacts cast with placeholder contentblocks = [ ImageBlock(path='myimage.jpg', dpi=75.0, pos=(200.0, 14.0)), TextBlock(nodes=[ TextNode(content="Hello, world!", font_size=30.0, line_height=18.0), TextNode(content="This is a PDF generated in Rust in Python. Crazy!", font_size=14.0, line_height=18.0), ], pos=(20.0, 24.0)),] pydfing.gen_pdf( path="output.pdf", width=215.9, height=279.4, blocks=blocks)
If we run using uv:
uv run test.py
We should now have a PDF file called output.pdf
.
Conclusion
The portability of Rust is one of its biggest advantages. Not only can we use it across devices with Tauri, but across languages also.