PHP

The rust-pdf/rustpdf package wraps the rust-pdf C core with idiomatic, chainable classes over PHP's built-in FFI extension (PHP 8.1+, no compilation). 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. Each holds a native handle freed automatically on destruction; call close() to free it promptly.

Installation

Install from Packagist with Composer:

shell
composer require rust-pdf/rustpdf

Requires PHP 8.1+ with the FFI extension. The package itself is pure PHP; the native library (libpdf_ffi) for your OS/architecture is fetched automatically on first use (see Native library). Verify it loaded:

php
<?php
require 'vendor/autoload.php';
echo RustPdf\Pdf::version(), "\n";   // native library version
Enable FFI. The extension ships with PHP 8.1+ but is often disabled for the web SAPI. In CLI it is on by default; for FPM/web set in php.ini:
extension=ffi
ffi.enable=true        ; or, hardened: ffi.enable=preload + ffi.preload=...
Without it the install succeeds but the first call throws.

Quick start

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

php
<?php
require 'vendor/autoload.php';

use RustPdf\Document;

$doc = new Document();            // A4 by default
$doc->addPage()
    ->setFillRgb(0.86, 0.20, 0.18)
    ->rect(72, 640, 200, 120)     // x, y, width, height (points)
    ->fill()
    ->save('out.pdf');

Most methods return the document, so calls chain. toBytes() returns the PDF as a binary string instead of writing a file:

php
$doc  = new Document();
$font = $doc->addFontFile('Roboto-Regular.ttf');
$doc->addPage()
    ->setFillRgb(0.1, 0.1, 0.12)
    ->rect(0, 800, 595, 42)->fill()
    ->showText($font, 24, 72, 740, 'Olá, açúcar — café');
$data = $doc->toBytes();          // binary string
file_put_contents('out.pdf', $data);

Native library

The binding loads a prebuilt libpdf_ffi matching your platform. It is resolved in this order:

  1. RUSTPDF_LIB: an absolute path to a library you provide (always wins);
  2. the copy bundled into the package under lib/<os>-<arch>/;
  3. a development build under target/{debug,release} (monorepo checkout);
  4. lazy download of the matching prebuilt library on first use.

Because Composer runs install scripts only for the root project (never a dependency), the download cannot fire automatically on composer require. The lazy fetch (step 4) covers most setups with no configuration. If your production filesystem is read-only, fetch the library once at deploy/CI time instead:

shell
php vendor/rust-pdf/rustpdf/bin/rustpdf-install-lib

Or wire it into your root composer.json:

json
"scripts": {
  "post-install-cmd": "@php vendor/rust-pdf/rustpdf/bin/rustpdf-install-lib",
  "post-update-cmd":  "@php vendor/rust-pdf/rustpdf/bin/rustpdf-install-lib"
}
Set RUSTPDF_NO_DOWNLOAD=1 to forbid the network fetch; you must then supply the library through RUSTPDF_LIB. Prebuilt platforms: macOS (arm64, x86_64), Linux (x86_64, aarch64) and Windows (x86_64).

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 throw PdfException 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:

php
RustPdf\Pdf::activateLicense($token);   // throws PdfException 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

Handles & lifetime

Each Document/EditableDoc owns a native handle. It is freed automatically when the object is garbage-collected (__destruct), so explicit cleanup is optional. For long-running workers or tight loops, call close() to release it immediately.

Authoring: create & save

new Document() free

Creates an empty document (A4 default page size). The handle is freed on destruction or via close().

MethodDescription
addPage(?w = null, ?h = null)Append a page, optionally sized in points.
setDefaultSize(w, h)Default size for subsequently added pages.
setVersion(v)Set the PDF header version (e.g. 14 → PDF 1.4, 17 → 1.7).
pageCount()Number of pages so far.
toBytes()Render the document to a binary string.
save(path)Render and write to a file.
close()Free the native handle.

Pages & vector graphics

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

MethodDescription
setFillRgb(r, g, b)Fill color.
setStrokeRgb(r, g, b)Stroke color.
setLineWidth(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.
php
$doc = new Document();
$doc->addPage()
    ->setStrokeRgb(0.10, 0.45, 0.90)->setLineWidth(3)
    ->rect(72, 600, 300, 160)->stroke()
    ->setFillRgb(0.95, 0.77, 0.06)
    ->rect(120, 640, 120, 80)->fill()
    ->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.

addFontFile(path): int   addFont(data: string): int
showText(font, size, x, y, text, headingLevel = 0): self

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

php
$doc     = new Document();
$regular = $doc->addFontFile('Roboto-Regular.ttf');
// …or from bytes you already have in memory:
// $regular = $doc->addFont(file_get_contents('Roboto-Regular.ttf'));

$doc->addPage()
    ->showText($regular, 28, 72, 760, 'Invoice #1024')
    ->showText($regular, 12, 72, 720, '日本語 · Ελληνικά · العربية')
    ->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): self
php
use RustPdf\{Document, Align};

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

$doc = new Document();
$f   = $doc->addFontFile('Roboto-Regular.ttf');
$doc->addPage()
    ->paragraph($f, 12, 72, 700, 451, $intro, Align::Justify)
    ->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
addImageFile(path): intLoad JPEG/PNG from a file; returns the image id.
addImagePng(data: string): intRegister a PNG from memory.
addImageJpeg(data: string): intRegister a JPEG from memory.
drawImage(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).
php
$doc  = new Document();
$logo = $doc->addImageFile('logo.png');
$doc->addPage()
    ->drawImage($logo, 72, 680, 160, 90)
    ->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(?PdfaLevel $level = null): self
php
use RustPdf\{Document, PdfaLevel};

$doc = new Document();
$doc->pdfa(PdfaLevel::A2b)->setInfo(title: 'Q3 Report', author: 'Acme Inc.');
$f = $doc->addFontFile('Roboto-Regular.ttf');
$doc->addPage()
    ->showText($f, 20, 72, 760, 'Archival report')
    ->save('report_pdfa.pdf');   // throws PdfException without a PDF/A license
PDF/A requires the title to be set for valid metadata: call setInfo(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 headingLevel on showText for H1H6, and figure(..., alt) for described images.

tagged(): self
php
use RustPdf\{Document, PdfaLevel};

$doc = new Document();
$doc->pdfa(PdfaLevel::A2a)->tagged()->setInfo(title: 'Accessible report');
$f = $doc->addFontFile('Roboto-Regular.ttf');
$doc->addPage()
    ->showText($f, 26, 72, 760, 'Annual report', 1)   // H1
    ->showText($f, 14, 72, 720, 'Overview', 2)         // H2
    ->showText($f, 11, 72, 690, 'Body paragraph of the section…');
$chart = $doc->addImageFile('chart.png');
$doc->figure($chart, 72, 520, 300, 150, 'Revenue grew 18% year over year')
    ->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.

attachFile(name, mime, data, rel = AFRelationship::Source, description = ''): self
php
use RustPdf\{Document, PdfaLevel, AFRelationship};

$xml = file_get_contents('invoice.xml');
$doc = new Document();
$doc->pdfa(PdfaLevel::A3b)->setInfo(title: 'E-invoice 1024');
$f = $doc->addFontFile('Roboto-Regular.ttf');
$doc->addPage()
    ->showText($f, 18, 72, 760, 'Invoice 1024')
    ->attachFile('invoice.xml', 'text/xml', $xml,
                 AFRelationship::Source, 'Structured invoice data')
    ->save('einvoice.pdf');

AcroForm fields

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

MethodDescription
textField(name, page, rect, value = '', size = 0)Text input (size=0 → auto font size).
checkbox(name, page, rect, checked)Checkbox.
dropdown(name, page, rect, options, ?selected, size = 0)Combo box from an array of strings.
radioGroup(name, page, buttons, ?selected)buttons = array of [rect, export] pairs.
php
$doc = new Document();
$doc->addPage()
    ->textField('applicant.name', 0, [72, 700, 320, 720], '')
    ->checkbox('agree', 0, [72, 660, 88, 676], false)
    ->dropdown('plan', 0, [72, 620, 240, 640],
               ['Starter', 'Pro', 'Enterprise'], 1)
    ->radioGroup('billing', 0, [
        [[72, 580, 88, 596], 'monthly'],
        [[140, 580, 156, 596], 'annual'],
    ], 1)
    ->save('form.pdf');

Fill fields later with EditableDoc::fillTextField.

Metadata

setInfo(?title, ?author, ?subject, ?keywords, ?creator): self

Sets the document information dictionary (and, for PDF/A, the matching XMP). Pass only the fields you need; named arguments keep it readable.

php
$doc->setInfo(
    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: string, ?password = null): self
EditableDoc::loadFile(path: string, ?password = null): self
php
use RustPdf\EditableDoc;

$ed = EditableDoc::loadFile('in.pdf');
echo $ed->pageCount(), "\n";
$ed->close();

// encrypted input:
$sec = EditableDoc::loadFile('secured.pdf', 'user-or-owner-pw');
$sec->save('plain.pdf');

Pages: merge, split, reorder, rotate

MethodDescription
merge(other)Append all pages of another EditableDoc (objects renumbered & remapped).
rotatePage(index, degrees)Rotate one page (90 / 180 / 270).
deletePage(index)Remove a page.
reorderPages(order)Reorder with a full permutation array of indices.
extractPages(indices): EditableDocNew document containing just those pages.
pageCount()Current page count.
php
$a = EditableDoc::loadFile('a.pdf');
$b = EditableDoc::loadFile('b.pdf');
$a->merge($b);                         // a now has a's pages followed by b's
$a->rotatePage(0, 90);
$a->reorderPages(array_reverse(range(0, $a->pageCount() - 1)));
$a->save('merged.pdf');

$subset = $a->extractPages([0, 2]);    // pages 1 and 3
$subset->save('subset.pdf');

Metadata, overlay & form fill

MethodDescription
setInfo(key, value)Set one info entry (e.g. "Title").
getInfo(key): stringRead an info entry.
setXmp(xml: string)Replace the XMP metadata stream.
overlayPage(index, content: string)Overlay a content-stream fragment onto a page (stamps/watermarks).
fillTextField(name, value): boolFill an AcroForm text field; returns whether it was found.
php
$ed = EditableDoc::loadFile('form.pdf');
$ed->setInfo('Title', 'Filled form');
$found = $ed->fillTextField('applicant.name', 'Jane Doe');
printf("filled: %s | title: %s\n", $found ? 'yes' : 'no', $ed->getInfo('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.
php
$ed = EditableDoc::loadFile('big.pdf');
$ed->optimize()->compact(true)->save('small.pdf');

Encryption licensed

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

encrypt(method = Encryption::Aes256, user = '', owner = '', readOnly = false): self
php
use RustPdf\{EditableDoc, Encryption};

$ed = EditableDoc::loadFile('in.pdf');
$ed->encrypt(Encryption::Aes256, owner: 'owner-secret', readOnly: true)
   ->save('secured.pdf');          // throws PdfException without an Encryption license

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

Output & incremental update

MethodDescription
toBytes(): stringSerialize the manipulated document.
save(path)Serialize to a file.
toBytesIncremental(original: string): stringAppend only changes to the original bytes (signature-safe, non-destructive).
php
$original = file_get_contents('in.pdf');
$ed = EditableDoc::load($original);
$ed->setInfo('Subject', 'reviewed');
$incremental = $ed->toBytesIncremental($original);   // original bytes preserved verbatim
file_put_contents('reviewed.pdf', $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 binary strings. pades: true switches to PAdES-B-B.

Pdf::sign(pdf, keyDer, certDer, ?reason, ?location, ?name, pades = false): string
php
use RustPdf\Pdf;

$pdf     = file_get_contents('contract.pdf');
$keyDer  = file_get_contents('signing-key.pkcs8.der');   // PKCS#8 private key (DER)
$certDer = file_get_contents('signing-cert.der');        // X.509 certificate (DER)

$signed = Pdf::sign($pdf, $keyDer, $certDer,
    reason: 'Approved', location: 'New York',
    name: 'Jane Doe', pades: true);
file_put_contents('contract.signed.pdf', $signed);
// Verify in a shell: pdfsig contract.signed.pdf  →  "Signature is Valid."

Timestamp & DSS (PAdES LTV) licensed

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

Pdf::addDss(pdf, certs = [], crls = []): string
Pdf::timestamp(pdf, tsaKeyDer, tsaCertDer, ?date = null): string
php
$signed = file_get_contents('contract.signed.pdf');

// B-LT: embed validation material (caller supplies DER certs/CRLs)
$lt = Pdf::addDss($signed, [$certDer], [$crlDer]);

// B-LTA: add a document timestamp signed by a TSA key/cert
$lta = Pdf::timestamp($lt, $tsaKeyDer, $tsaCertDer);
file_put_contents('contract.lta.pdf', $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.

Pdf::extractText(pdf: string): string free
php
$data = file_get_contents('report.pdf');
echo RustPdf\Pdf::extractText($data);

Enums

All enums are PHP 8.1 backed enums in the RustPdf namespace.

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 throws PdfException (a RuntimeException) carrying the status (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.

php
use RustPdf\{Document, PdfException};

try {
    $doc = new Document();
    $doc->pdfa()->setInfo(title: 'x')->addPage()->save('out.pdf');
} catch (PdfException $e) {
    fprintf(STDERR, "failed: %d %s\n", $e->status, $e->getMessage());
    // e.g. PdfStatus=7: feature 'pdfa' requires a valid license
}

Utilities

FunctionDescription
Pdf::version(): stringNative library version string.
Pdf::activateLicense(token)Activate a license token (throws on invalid/expired).
Looking for another language? The same API exists in Python, Node / TypeScript, Delphi / Free Pascal, Swift, C#, Go, Ruby and Java: browse all docs. They share one core, so behavior is identical.