pdfpecker for Rust
Fast HTML and Markdown to PDF, and PDF to images, text and Markdown, for Rust. No browser, no Chromium download, no .NET to install, and no runtime dependencies.
cargo add pdfpecker
use ;
let pdf = html_to_pdf?;
write?;
// Images and CSS resolve next to the file.
let invoice = html_file_to_pdf?;
let readme = markdown_file_to_pdf?;
let pngs = pdf_to_images?; // one PNG per page
let text = pdf_to_text?; // pages separated by \f
let both = merge_pdfs?;
This is the PdfPecker .NET library, compiled ahead of time into a
native library: the same library the Python and
npm packages ship. PdfPecker has its own HTML parser, CSS engine, layout
engine, PDF writer and PDF renderer, so a one-page invoice takes under a millisecond and no browser ever starts. It
runs no JavaScript in the documents and never goes online: images, stylesheets and fonts load from base_dir. The
crate version is the library version it holds. Documentation of what PdfPecker renders, its CSS support and its
limitations: pdfpecker.net/docs.
Platforms and deployment
Rust 1.85 or later on Linux (glibc 2.28+ and musl, x64 and arm64), macOS 12+ (Apple Silicon and Intel) and Windows (x64 and arm64).
The build script downloads the target's native library from the pdfpecker wheel on PyPI (about 9 MB, once; it is kept
in your cache folder) with curl or wget, and checks the wheel and the library against SHA-256 values that the crate
carries. cargo run and cargo test then find it on their own. To ship a program, copy the library next to the
executable:
| Platform | File | From |
|---|---|---|
| Linux | pdfpecker_native.so |
target/<profile>/build/pdfpecker-*/out/ |
| macOS | pdfpecker_native.dylib |
the same |
| Windows | pdfpecker_native.dll |
the same |
pdfpecker::library_path() returns the file that was loaded. The library is looked for in PDFPECKER_NATIVE_LIB
(a file path), then next to the executable, then where the build put it.
| Build-time variable | Effect |
|---|---|
PDFPECKER_LIB_DIR |
A folder that already holds the library; nothing is downloaded (offline and air-gapped builds). |
PDFPECKER_CACHE_DIR |
Where downloaded libraries are kept, instead of the user's cache folder. |
On musl (Alpine), the program must be linked dynamically to load the library: build with
RUSTFLAGS="-C target-feature=-crt-static". The build stops with that advice otherwise.
Speed
From Rust on a 4-core Linux cloud VM, median time per document after warm-up; the documents are the benchmark pages of the PdfPecker repository:
| Invoice, 1 page | Feature tour, 5 pages | Report, 29 pages | New process, first PDF | Peak memory, one PDF | |
|---|---|---|---|---|---|
| pdfpecker | 0.72 ms | 2.9 ms | 28 ms | 22 ms | 26 MB |
| Chromium 153 via chromiumoxide 0.9 | 18 ms | 32 ms | 231 ms | 115 ms | 396 MB |
| IronPress 1.7 | 41 ms | 168 ms | 1,873 ms | 185 ms | 40 MB |
Conversions run in parallel from several threads: 6,600 invoices a second on the 4 cores.
API
Every function returns pdfpecker::Result. PDFs are given as &[u8]; options are built with chained methods from
::new() or Default::default().
| Function | Returns |
|---|---|
html_to_pdf(html, &PdfOptions) |
the PDF bytes |
html_file_to_pdf(path, &PdfOptions) |
the same for a file; base_dir is the file's folder |
markdown_to_pdf(markdown, &PdfOptions) |
a PDF from CommonMark with GitHub tables, task lists, strikethrough and footnotes |
markdown_file_to_pdf(path, &PdfOptions) |
as html_file_to_pdf |
pdf_to_images(pdf, &ImageOptions) |
encoded images, one per page: dpi, format (PNG, JPEG, WebP), pages, width, height, transparent, quality, password |
pdf_to_text(pdf, &TextOptions) |
the text in reading order, pages separated by \f: pages, password, order (TextOrder::Reading or TopToBottom) |
pdf_to_text_pages(pdf, &TextOptions) |
the text of each page |
pdf_to_markdown(pdf, &MarkdownOptions) |
GitHub Flavored Markdown of any PDF, for search indexes and language models: pages, include_links, include_annotations, include_images, page_comments, max_pages, password |
pdf_page_count(pdf, &PageCountOptions) |
the number of pages: password |
merge_pdfs(&[pdfs], &MergeOptions) |
one PDF from several: title, author, bookmarks, creation_date |
pdf_to_pdfa(pdf, &PdfAOptions) |
any PDF converted to PDF/A-2b (default), -2u, -3b or -3u, as PdfAResult { pdf, changes, problems } |
redact_pdf(pdf, &RedactOptions) |
the PDF with text, images and drawings removed for good: text, pattern, area, ignore_case, fill_color |
read_barcodes(data, &BarcodeOptions) |
the QR, Data Matrix, Code 128 and EAN-13 codes in an image or a PDF, as Barcode { format, text, data, page, x, y, width, height, corners, .. }: formats, try_inverted, pages, password, dpi |
library_version(), library_path() |
the PdfPecker version inside, and the library file loaded |
pages takes a page number (3), an array or Vec ([1, 3]), a range (2..=4) or a string such as "1-3,5" or
"last", numbered from 1. Every call that reads or writes a document also takes a timeout.
Options of the HTML and Markdown functions (PdfOptions):
| Option | Meaning |
|---|---|
base_dir |
The folder relative image, stylesheet and font paths resolve against. Without it, a document from a string loads no local files. |
allow_file_access |
Let the document read files outside base_dir too. Only for HTML you trust. |
page_size, landscape |
PageSize::A3, A4 (default), A5, Letter, Legal or Millimeters { width, height }. A CSS @page { size } rule wins. |
margin |
Margin::all(points) or Margin::new(top, right, bottom, left), 72 points per inch. Default 36. CSS @page { margin } wins. |
scale, shrink_to_fit |
Zoom the content; shrink content wider than the page (on by default). |
header_text, footer_text |
A line of text in the top or bottom margin; {page} and {pages} are replaced. CSS margin boxes (@top-center, ...) do more. |
font, use_system_fonts |
A TrueType/OpenType file or folder that CSS font-family can name (call once per file or folder); or look families up among the installed fonts. Without fonts, text uses the standard PDF fonts, which cover Western European characters only. |
title, author, subject, keywords, language |
Document properties. |
user_style_sheet |
CSS applied after the document's own. |
compress, bookmarks |
Compress the streams; make bookmarks from the headings (both on by default). |
max_pages |
Fail with ErrorKind::Limit beyond this many pages (default 100,000). |
creation_date |
A fixed SystemTime: the output is then byte-identical from run to run. |
conformance |
Conformance::PdfA2b, PdfA3b, PdfA2u, PdfA3u, PdfA4 or PdfA4f. |
accessibility |
Accessibility::Tagged, PdfUa1 or PdfUa2. |
Errors carry an ErrorKind: InvalidArgument (a bad option value, a page past the end), Unreadable (not a readable
PDF, image or font), Password, Limit, Timeout, Failed, Io (a font or image file that could not be read),
Library (the native library is missing or does not fit) and Internal.
Every function is safe to call from several threads at once. The calls block, so async code runs them with
tokio::task::spawn_blocking or its runtime's equivalent.
Building from this repository
The crate uses the native libraries in the Python package's wheels on PyPI, which are built from the released PdfPecker NuGet package; nothing is compiled from .NET here.
cargo test # downloads this machine's library on the first build
ci/test.sh # also packages the crate and tests it as a dependency
python3 scripts/update-wheels.py # after changing the version in Cargo.toml: writes wheels.txt
Releasing
After a PdfPecker release reaches PyPI (the Python package follows each NuGet release), a pull request here changes
version in Cargo.toml, runs scripts/update-wheels.py and adds functions or options for what the release added.
The owner then publishes a GitHub release tagged v<version>; the CI workflow tests every platform and publishes the
crate to crates.io.
License
MIT. The native library also contains the .NET runtime, zlib-ng and the fonts and data PdfPecker bundles, each under
its own license: see THIRD-PARTY-NOTICES.md.