Python

The rustpdf package wraps the rust-pdf C core with idiomatic, context-managed classes. It covers the whole product surface: vector graphics, embedded/subset fonts & Unicode text, paragraphs, images, PDF/A (1b–3a), tagged/accessible output, attachments, AcroForm fields, manipulation, text extraction, encryption and digital signatures.

Two classes do almost everything. Document authors a new PDF; EditableDoc loads and manipulates an existing one. Both are context managers: use them with with so native handles are always freed.

Installation

Install the package from PyPI. The wheel bundles the native shared library (libpdf_ffi) built from the Rust core for your platform — nothing else to build or configure.

shell
pip install rustpdf

Verify it loaded:

python
import rustpdf
print(rustpdf.version())          # native library version
print(rustpdf.library_path())     # which .dylib/.so/.dll was loaded

Quick start

A one-page document with a filled rectangle, saved to disk:

python
import rustpdf

with rustpdf.Document() as doc:          # A4 by default
    doc.add_page()
    doc.set_fill_rgb(0.86, 0.20, 0.18)
    doc.rect(72, 640, 200, 120)          # x, y, width, height (points)
    doc.fill()
    doc.save("out.pdf")

Most methods return the document, so calls chain:

python
with rustpdf.Document() as doc:
    font = doc.add_font_file("Roboto-Regular.ttf")
    (doc.add_page()
        .set_fill_rgb(0.1, 0.1, 0.12)
        .rect(0, 800, 595, 42).fill()
        .show_text(font, 24, 72, 740, "Olá, açúcar — café"))
    data = doc.to_bytes()                # in-memory bytes instead of a file

Licensing & activation

Basic generation (everything above) is always free. The corporate features (PDF/A, digital signatures/PAdES, encryption, accessibility) require an active license token. Without one, those calls raise PdfError and produce no output.

Activation needs no rebuild. Easiest is an environment variable, auto-activated the first time a corporate feature is used:

shell
export RUSTPDF_LICENSE="010f0000…"           # the token we email you
# or point at a file:
export RUSTPDF_LICENSE_FILE=/etc/rustpdf/license.txt

Or activate explicitly in code:

python
rustpdf.activate_license(token)   # raises PdfError if forged / expired / malformed
Verification is fully offline: signature + expiry checked against a public key embedded in the library. No network callback, no telemetry.

Coordinate system

Threading & concurrency

The core is Send but not Sync: you can build many documents in parallel, but a single handle must never be touched by two threads at once.

python
from concurrent.futures import ThreadPoolExecutor
import rustpdf

def render(i: int) -> bytes:
    with rustpdf.Document() as doc:          # one document per task
        doc.add_page()
        doc.set_fill_rgb(0.1, 0.1, 0.12)
        doc.rect(72, 700, 200, 80).fill()
        return doc.to_bytes()

with ThreadPoolExecutor(max_workers=8) as pool:
    pdfs = list(pool.map(render, range(100)))   # 100 PDFs built concurrently

Authoring: create & save

rustpdf.Document() free

Creates an empty document (A4 default page size). Use as a context manager; call close() manually only if you can't.

MethodDescription
add_page(size=None)Append a page. size is an optional (width, height) tuple in points.
set_default_size(w, h)Default size for subsequently added pages.
set_version(v)Set the PDF header version (e.g. 14 → PDF 1.4, 17 → 1.7).
page_countProperty: number of pages so far.
to_bytes()Render the document to bytes.
save(path)Render and write to a file.

Pages & vector graphics

Graphics state and path operators mirror PDF's content-stream model. Colors are RGB in 0.0–1.0.

MethodDescription
set_fill_rgb(r, g, b)Fill color.
set_stroke_rgb(r, g, b)Stroke color.
set_line_width(w)Stroke width in points.
rect(x, y, w, h)Add a rectangle subpath.
fill()Fill the current path with the fill color.
stroke()Stroke the current path with the stroke color.
python
with rustpdf.Document() as doc:
    doc.add_page()
    doc.set_stroke_rgb(0.10, 0.45, 0.90).set_line_width(3)
    doc.rect(72, 600, 300, 160).stroke()
    doc.set_fill_rgb(0.95, 0.77, 0.06)
    doc.rect(120, 640, 120, 80).fill()
    doc.save("shapes.pdf")

Fonts & text

Fonts are embedded and subsetted, with HarfBuzz-quality shaping, kerning and full Unicode (Type0/CIDFontType2 with ToUnicode, so text extracts and copies correctly). Register a font once, then reference it by its integer id.

add_font_file(path) → int   add_font(data: bytes) → int
show_text(font, size, x, y, text, heading_level=0)

heading_level (1–6) tags the run as H1H6 in an accessible document (see Accessibility); leave 0 for ordinary text.

python
with rustpdf.Document() as doc:
    regular = doc.add_font_file("Roboto-Regular.ttf")
    # …or from bytes you already have in memory:
    # regular = doc.add_font(open("Roboto-Regular.ttf", "rb").read())

    doc.add_page()
    doc.show_text(regular, 28, 72, 760, "Invoice #1024")
    doc.show_text(regular, 12, 72, 720, "日本語 · Ελληνικά · العربية")
    doc.save("text.pdf")

Paragraphs

The paragraph layer wraps, aligns and justifies text inside a fixed width (greedy line breaking using shaped glyph widths).

paragraph(font, size, x, y, width, text, align=Align.LEFT)
python
from rustpdf import Document, Align

intro = ("A long paragraph that wraps to the given width and is justified "
         "automatically; extra space is distributed between words.")

with Document() as doc:
    f = doc.add_font_file("Roboto-Regular.ttf")
    doc.add_page()
    doc.paragraph(f, 12, 72, 700, 451, intro, align=Align.JUSTIFY)
    doc.save("paragraph.pdf")

See the Align enum for the alignment options.

Images

JPEGs are embedded verbatim (DCTDecode, no re-encode). PNGs are decoded and re-encoded (FlateDecode); alpha becomes an /SMask, palette becomes an Indexed color space. Register an image once, draw it many times.

MethodDescription
add_image_file(path) → intLoad JPEG/PNG from a file; returns the image id.
add_image_png(data: bytes) → intRegister a PNG from memory.
add_image_jpeg(data: bytes) → intRegister a JPEG from memory.
draw_image(image, x, y, w, h)Draw at (x, y) scaled to w × h points.
figure(image, x, y, w, h, alt)Draw as a tagged /Figure with alt text (accessibility).
python
with rustpdf.Document() as doc:
    logo = doc.add_image_file("logo.png")
    doc.add_page()
    doc.draw_image(logo, 72, 680, 160, 90)
    doc.save("with_image.pdf")

PDF/A licensed

Produce archival-grade output. pdfa() defaults to A-2b; pass a PdfaLevel for a specific level. An embedded sRGB ICC profile, output intent, XMP metadata and document /ID are added automatically; A-1b also forces PDF 1.4 and emits a /CIDSet.

pdfa(level=None)
python
from rustpdf import Document, PdfaLevel

with Document() as doc:
    doc.pdfa(PdfaLevel.A2B).set_info(title="Q3 Report", author="Acme Inc.")
    f = doc.add_font_file("Roboto-Regular.ttf")
    doc.add_page()
    doc.show_text(f, 20, 72, 760, "Archival report")
    doc.save("report_pdfa.pdf")     # raises PdfError without a license granting PDF/A
PDF/A requires the title to be set for valid metadata: call set_info(title=…).

Accessibility (Tagged PDF / PDF/UA) licensed

tagged() builds a logical structure tree (PDF/UA-1). Combine with pdfa(PdfaLevel.A2A) for archival and accessible output. Use heading_level on show_text for H1H6, and figure(..., alt=…) for described images.

tagged()
python
from rustpdf import Document, PdfaLevel

with Document() as doc:
    doc.pdfa(PdfaLevel.A2A).tagged().set_info(title="Accessible report")
    f = doc.add_font_file("Roboto-Regular.ttf")
    doc.add_page()
    doc.show_text(f, 26, 72, 760, "Annual report", heading_level=1)
    doc.show_text(f, 14, 72, 720, "Overview", heading_level=2)
    doc.show_text(f, 11, 72, 690, "Body paragraph of the section…")
    chart = doc.add_image_file("chart.png")
    doc.figure(chart, 72, 520, 300, 150, alt="Revenue grew 18% year over year")
    doc.save("accessible.pdf")

Attachments (PDF/A-3) licensed

PDF/A-3 allows embedding arbitrary source files (e.g. the XML behind an e-invoice). Each attachment carries a MIME type and an AFRelationship.

attach_file(name, mime, data, relationship=AFRelationship.SOURCE, description="")
python
from rustpdf import Document, PdfaLevel, AFRelationship

xml = open("invoice.xml", "rb").read()
with Document() as doc:
    doc.pdfa(PdfaLevel.A3B).set_info(title="E-invoice 1024")
    f = doc.add_font_file("Roboto-Regular.ttf")
    doc.add_page()
    doc.show_text(f, 18, 72, 760, "Invoice 1024")
    doc.attach_file("invoice.xml", "text/xml", xml,
                    AFRelationship.SOURCE, "Structured invoice data")
    doc.save("einvoice.pdf")

AcroForm fields

Build interactive forms with generated appearance streams (no NeedAppearances). Rectangles are (x0, y0, x1, y1); page is a 0-based page index. Dotted names ("a.b.c") create hierarchical fields.

MethodDescription
text_field(name, page, rect, value="", size=0.0)Text input (size=0 → auto font size).
checkbox(name, page, rect, checked=False)Checkbox.
dropdown(name, page, rect, options, selected=None, size=0.0)Combo box from a list of strings.
radio_group(name, page, buttons, selected=None)buttons = list of (rect, export_value) tuples.
python
with rustpdf.Document() as doc:
    doc.add_page()
    doc.text_field("applicant.name", 0, (72, 700, 320, 720), value="")
    doc.checkbox("agree", 0, (72, 660, 88, 676), checked=False)
    doc.dropdown("plan", 0, (72, 620, 240, 640),
                 ["Starter", "Pro", "Enterprise"], selected=1)
    doc.radio_group("billing", 0, [
        ((72, 580, 88, 596), "monthly"),
        ((140, 580, 156, 596), "annual"),
    ], selected=1)
    doc.save("form.pdf")

Fill fields later with EditableDoc.fill_text_field.

Metadata

set_info(title=None, author=None, subject=None, keywords=None, creator=None)

Sets the document information dictionary (and, for PDF/A, the matching XMP). Pass only the fields you need.

python
doc.set_info(title="Q3 Report", author="Acme Inc.",
             subject="Quarterly results", keywords="finance, q3")

Manipulation: load an existing PDF

EditableDoc parses an existing document (classic & xref streams, object streams, all standard filters, RC4/AES decryption) into an editable model. Pages are a flat list; the page tree is rebuilt on output.

EditableDoc.load(data: bytes, password=None)
EditableDoc.load_file(path, password=None)
python
from rustpdf import EditableDoc

with EditableDoc.load_file("in.pdf") as ed:
    print(ed.page_count)

# encrypted input:
with EditableDoc.load_file("secured.pdf", password="user-or-owner-pw") as ed:
    ed.save("plain.pdf")

Pages: merge, split, reorder, rotate

MethodDescription
merge(other)Append all pages of another EditableDoc (objects renumbered & remapped).
rotate_page(index, degrees)Rotate one page (90 / 180 / 270).
delete_page(index)Remove a page.
reorder_pages(order)Reorder with a full permutation list of indices.
extract_pages(indices) → EditableDocNew document containing just those pages.
page_countProperty: current page count.
python
with EditableDoc.load_file("a.pdf") as a, EditableDoc.load_file("b.pdf") as b:
    a.merge(b)                       # a now has a's pages followed by b's
    a.rotate_page(0, 90)
    a.reorder_pages(list(reversed(range(a.page_count))))
    a.save("merged.pdf")

with EditableDoc.load_file("merged.pdf") as doc:
    with doc.extract_pages([0, 2]) as subset:   # pages 1 and 3
        subset.save("subset.pdf")

Metadata, overlay & form fill

MethodDescription
set_info(key, value)Set one info entry (e.g. "Title").
get_info(key) → strRead an info entry.
set_xmp(xml: bytes)Replace the XMP metadata stream.
overlay_page(index, content: bytes)Overlay a content-stream fragment onto a page (stamps/watermarks).
fill_text_field(name, value) → boolFill an AcroForm text field; returns whether it was found.
python
with EditableDoc.load_file("form.pdf") as ed:
    ed.set_info("Title", "Filled form")
    found = ed.fill_text_field("applicant.name", "Edivan Teixeira")
    print("filled:", found, "| title:", ed.get_info("Title"))
    ed.save("filled.pdf")

Optimize & compact

MethodDescription
optimize()Drop unreferenced objects, Flate-compress uncompressed streams, dedupe identical objects.
compact(on=True)Pack objects into object streams + emit a cross-reference stream.
python
with EditableDoc.load_file("big.pdf") as ed:
    ed.optimize().compact(True)
    ed.save("small.pdf")

Encryption licensed

Apply standard-handler encryption at output. AES-256 (V5/R6) uses OS-CSPRNG keys/IVs.

encrypt(user="", owner="", method=Encryption.AES256, read_only=False)
python
from rustpdf import EditableDoc, Encryption

with EditableDoc.load_file("in.pdf") as ed:
    ed.encrypt(user="", owner="owner-secret",
               method=Encryption.AES256, read_only=True)
    ed.save("secured.pdf")          # raises PdfError without an Encryption license

See the Encryption enum for RC4 / AES-128 / AES-256.

Output & incremental update

MethodDescription
to_bytes() → bytesSerialize the manipulated document.
save(path)Serialize to a file.
to_bytes_incremental(original: bytes) → bytesAppend only changes to the original bytes (signature-safe, non-destructive).
python
original = open("in.pdf", "rb").read()
with EditableDoc.load(original) as ed:
    ed.set_info("Subject", "reviewed")
    incremental = ed.to_bytes_incremental(original)   # original bytes preserved verbatim
open("reviewed.pdf", "wb").write(incremental)

Digital signatures licensed

Sign a PDF with a PKCS#7 detached signature via an incremental update (the original bytes are preserved). Keys and certificates are passed as DER bytes. pades=True switches to PAdES-B-B.

rustpdf.sign(pdf, key_der, cert_der, *, reason=None, location=None, name=None, pades=False) → bytes
python
import rustpdf

pdf_bytes = open("contract.pdf", "rb").read()
key_der   = open("signing-key.pkcs8.der", "rb").read()   # PKCS#8 private key (DER)
cert_der  = open("signing-cert.der", "rb").read()        # X.509 certificate (DER)

signed = rustpdf.sign(pdf_bytes, key_der, cert_der,
                      reason="Approved", location="São Paulo",
                      name="Edivan Teixeira", pades=True)
open("contract.signed.pdf", "wb").write(signed)
# Verify in a shell: pdfsig contract.signed.pdf  →  "Signature is Valid."

Timestamp & DSS (PAdES LTV) licensed

Build long-term-validation signatures offline. add_dss appends a Document Security Store (/DSS with certs/CRLs, PAdES-B-LT); timestamp appends an RFC 3161 document timestamp (/DocTimeStamp, PAdES-B-LTA).

rustpdf.add_dss(pdf, certs=(), crls=()) → bytes
rustpdf.timestamp(pdf, tsa_key_der, tsa_cert_der, *, date=None) → bytes
python
signed = open("contract.signed.pdf", "rb").read()

# B-LT: embed validation material (caller supplies DER certs/CRLs)
lt = rustpdf.add_dss(signed, certs=[cert_der], crls=[crl_der])

# B-LTA: add a document timestamp signed by a TSA key/cert
lta = rustpdf.timestamp(lt, tsa_key_der, tsa_cert_der)
open("contract.lta.pdf", "wb").write(lta)

Text extraction

Extract a document's text, mapping shown glyph codes back to Unicode through each font's ToUnicode map, with space/line inference.

rustpdf.extract_text(data: bytes) → str free
python
data = open("report.pdf", "rb").read()
print(rustpdf.extract_text(data))

Enums

PdfaLevel

ValueLevel
PdfaLevel.A1BPDF/A-1b (basic, PDF 1.4)
PdfaLevel.A2BPDF/A-2b (basic): default of pdfa()
PdfaLevel.A2APDF/A-2a (accessible: pair with tagged())
PdfaLevel.A3BPDF/A-3b (basic, allows attachments)
PdfaLevel.A3APDF/A-3a (accessible + attachments)

Align

ValueMeaning
Align.LEFTLeft-aligned (default)
Align.RIGHTRight-aligned
Align.CENTERCentered
Align.JUSTIFYJustified (space distributed between words)

AFRelationship

ValueMeaning
AFRelationship.SOURCESource data for the document (e.g. the invoice XML)
AFRelationship.DATAData used to derive the visual content
AFRelationship.ALTERNATIVEAlternative representation
AFRelationship.SUPPLEMENTSupplementary material
AFRelationship.UNSPECIFIEDUnspecified relationship

Encryption

ValueCipher
Encryption.RC4RC4 (legacy)
Encryption.AES128AES-128
Encryption.AES256AES-256 (V5/R6): recommended

Error handling

Every failing native call raises PdfError (a RuntimeError) carrying the PdfStatus code and the library's last-error message. License failures (missing/expired/forged token, or a feature the token doesn't grant) surface here too.

python
from rustpdf import Document, PdfError

try:
    with Document() as doc:
        doc.pdfa().set_info(title="x")
        doc.add_page()
        doc.save("out.pdf")
except PdfError as e:
    print("failed:", e)        # e.g. PdfStatus=7: feature 'pdfa' requires a valid license

Utilities

FunctionDescription
rustpdf.version() → strNative library version string.
rustpdf.library_path() → PathPath of the loaded shared library.
rustpdf.activate_license(token)Activate a license token (raises on invalid/expired).
Looking for another language? The same API exists in Delphi / Free Pascal, C#, Go, PHP, Ruby, Node and Java: browse all docs. They share one core, so behavior is identical.