PHP

Last updated: 2026-06-29

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 + A-4/4e/4f), 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).

If the auto-download is unavailable for your version (the prebuilt native-library release for that exact version hasn't been published yet), the reliable path is to build or download libpdf_ffi once and point RUSTPDF_LIB at it. Step 1 always wins.

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 (0 → 1.4, 1 → 1.5, 2 → 1.7, 3 → 2.0).
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.
Validity. A document must have at least one page. Serializing an empty document throws an error. Color components (RGB/Gray/CMYK) are clamped to the valid 0–1 range.

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, provided the embedded font covers those characters). 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');
Font coverage. Characters outside the embedded font's coverage are silently dropped: they render as the missing-glyph box and won't extract or copy. The bundled Roboto fallback covers Latin, Greek and Cyrillic but not CJK, Arabic, Hebrew or emoji. Embed a font that covers every script you write.
No NUL bytes in strings. Text passed across the API must not contain a NUL (\x00) character. It truncates the string at the FFI boundary, silently dropping everything after the NUL. This applies to shown text, metadata and field names.

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, and A-4 (ISO 19005-4) is based on PDF 2.0.

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
A document title is recommended for valid PDF/A metadata, and required for the accessible “a” levels: 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');
Figures need a tagged document. figure() only produces an accessible, alt-texted figure inside a tagged/accessible document; on a plain document the alt text has no effect.

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');
PDF/A-4f needs an attachment. The A4f profile requires at least one embedded file (ISO 19005-4); the library now rejects A4f output that has no attachment, so call attachFile before serializing.
The licence gate is on PDF/A, not on attachFile itself. The licensed badge above reflects the PDF/A-3 workflow shown here. Calling attachFile on a plain (non-PDF/A) document does not require a licence: it succeeds and produces a valid PDF with an /EmbeddedFile. The licence is enforced only when you also request a PDF/A level, which is what makes the attachment archival.

ZUGFeRD / Factur-X e-invoices licensed

Turn the document into a ZUGFeRD / Factur-X electronic invoice: the embedded XML (the Cross-Industry Invoice) is attached as factur-x.xml, the file is marked PDF/A-3, and the Factur-X identification is written into the XMP metadata. The visible PDF is the human-readable invoice; the embedded XML is its machine-readable twin. Validates as PDF/A-3 + Factur-X under veraPDF.

facturx(xml: string, profile = FacturxProfile::EN16931): self
php
use RustPdf\{Document, FacturxProfile};

$xml = file_get_contents('factur-x.xml');         // your Cross-Industry Invoice XML
$doc = new Document();
$doc->setInfo(title: 'Invoice INV-2026-001');
$f = $doc->addFontFile('Roboto-Regular.ttf');
$doc->addPage()
    ->showText($f, 18, 72, 760, 'Invoice INV-2026-001');
$doc->facturx($xml, FacturxProfile::EN16931);
$doc->save('einvoice.pdf');        // PDF/A-3 + Factur-X; needs a PDF/A license

See the FacturxProfile enum for the conformance levels (Minimum … Extended).

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 and flatten fields later with EditableDoc.

Page index and rectangle are validated. A field (or internal link) whose page does not exist, or whose rectangle is degenerate (x1 < x0 or zero area), is rejected when the document is serialized: toBytes()/save() throw PdfException ("targets page index N…" / "degenerate rectangle…") instead of producing an invisible, never-appearing widget. Pass a 0-based page index that exists and a rectangle with x1 > x0 and y1 > y0.

Add clickable link rectangles to the current page: a web link opens a URL; an internal link jumps to another page (optionally scrolling so a given top y-coordinate sits at the top of the view).

linkUri(rect, uri): self   linkToPage(rect, pageIndex, ?top = null): self
php
$doc = new Document();
$f   = $doc->addFontFile('Roboto-Regular.ttf');
$doc->addPage()
    ->showText($f, 14, 72, 760, 'Visit rustpdf.dev (see page 2)')
    ->linkUri([72, 756, 320, 776], 'https://rustpdf.dev/')   // web link
    ->linkToPage([330, 756, 430, 776], 1, 800)               // jump to page 2
    ->addPage()
    ->save('links.pdf');

Rectangles are [x0, y0, x1, y1] arrays in points; pageIndex is 0-based.

Bookmarks / outline free

Build a navigable document outline. A Bookmark has a title, a target page and an optional top; nest children with ->child(...). A document with bookmarks opens with the outline pane shown.

new Bookmark(title, page, ?top = null)   ->child(bookmark)   addBookmark(bookmark): self
php
use RustPdf\{Document, Bookmark};

$doc = new Document();
$f   = $doc->addFontFile('Roboto-Regular.ttf');
for ($i = 0; $i < 3; $i++) {
    $doc->addPage();
}
$doc->addBookmark(
    (new Bookmark('Chapter 1', 0, 820))
        ->child(new Bookmark('Section 1.1', 1))
        ->child(new Bookmark('Section 1.2', 2)));
$doc->addBookmark(new Bookmark('Chapter 2', 2));
$doc->save('outline.pdf');

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');
Corrupt or truncated input. A badly damaged file (truncated mid-stream, for example) may still load via the recovery scan but recover zero pages, and load does not throw in that case. Serializing a zero-page document throws PdfException ("document has no pages") instead of writing an invalid file, but check pageCount() > 0 after loading untrusted input before relying on it.

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');
Page indices are 0-based. An out-of-range index to rotate/delete is silently ignored, and extract skips out-of-range indices. reorderPages requires a true permutation of every page index (each used exactly once); an invalid argument (wrong length, a repeated index, or out-of-range) is rejected and leaves the page order unchanged. The call is a silent no-op that raises no error, so an invalid reorder cannot be detected from a return value.

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');

Form fill & flatten free

Fill the fields of an existing AcroForm and (optionally) flatten them: filling generates a fresh appearance stream (no NeedAppearances), and flattening bakes every widget's appearance into the page content and removes the interactive form entirely.

MethodDescription
fieldNames(): arrayFully-qualified names of every terminal field.
fillTextField(name, value): boolSet a text (or text-style choice) field; returns whether it matched.
setCheckbox(name, checked = true): boolCheck/uncheck a checkbox.
setRadio(name, exportValue): boolSelect a radio button by its export value.
setChoice(name, value): boolSet a dropdown / list-box value.
flattenForms(): selfBake all fields into static content and drop the /AcroForm.
php
use RustPdf\EditableDoc;

$ed = EditableDoc::loadFile('form.pdf');
print_r($ed->fieldNames());          // ['applicant.name', 'agree', 'plan', ...]
$ed->fillTextField('applicant.name', 'Jane Doe');
$ed->setCheckbox('agree', true);
$ed->setRadio('billing', 'annual');
$ed->setChoice('plan', 'Pro');
$ed->flattenForms();                 // optional: make it non-editable
$ed->save('filled.pdf');

Watermarks free

Stamp a diagonal text watermark or a centered image watermark across every page, drawn semi-transparently over the existing content. Text uses the standard Helvetica font, so keep it to WinAnsi (Latin-1) for stamps like "CONFIDENTIAL".

watermarkText(text, size = 64.0, color = [0.5, 0.5, 0.5], opacity = 0.30, rotationDeg = 45.0, opaqueBackground = false): self
watermarkImageFile(path, width, height, opacity = 0.30, rotationDeg = 0.0): self

Set opaqueBackground: true to white out the area behind the text stamp (an opaque label rather than a translucent overlay). watermarkImageFile takes a rotationDeg angle to rotate the image stamp.

php
$ed = EditableDoc::loadFile('report.pdf');
$ed->watermarkText('CONFIDENTIAL', opacity: 0.25, rotationDeg: 45.0, opaqueBackground: true)
   ->watermarkImageFile('stamp.png', 200, 120, opacity: 0.30, rotationDeg: 30.0)
   ->save('stamped.pdf');

Positioned drawing free

Paint a filled rectangle or a line of positioned text at an exact spot on an existing page. Coordinates are in the page's visible space (origin lower-left, y up), so content lands where the viewer sees it regardless of any /Rotate. placeText uses the standard Helvetica font (WinAnsi / Latin-1) and rotationDeg rotates the text counter-clockwise about the anchor; align (an Align case) shifts the start point along the baseline so the text is left/right/center aligned about the anchor. drawImage takes raw JPEG/PNG image bytes (a binary string) and places the image with its lower-left corner at (x, y) scaled to width × height points. They all return false if the page index does not exist.

fillRect(pageIndex, x, y, width, height, color = [1.0, 1.0, 1.0], opacity = 1.0): bool
placeText(pageIndex, x, y, text, size = 12.0, color = [0.0, 0.0, 0.0], rotationDeg = 0.0, align = Align::Left, fontId = -1, anchor = VerticalAnchor::Baseline): bool
drawImage(pageIndex, image, x, y, width, height, rotationDeg = 0.0, anchor = ImageAnchor::Corner): bool
php
use RustPdf\Pdf;
use RustPdf\EditableDoc;

$ed = EditableDoc::loadFile('invoice.pdf');
$geo = Pdf::measurePage(file_get_contents('invoice.pdf'), 0);

// Mask an old value with an opaque white rect, then stamp the new one over it.
$ed->fillRect(0, 100.0, $geo->height - 120.0, 180.0, 16.0, [1.0, 1.0, 1.0], 1.0);
$ed->placeText(0, 104.0, $geo->height - 116.0, 'PAID', 12.0, [0.0, 0.4, 0.0], 0.0);
$ed->drawImage(0, file_get_contents('logo.png'), 104.0, $geo->height - 180.0, 120.0, 48.0);
$ed->save('invoice-paid.pdf');

maskedText bundles the mask-then-stamp pattern into one call: it fills the box [x, y, x+width, y+height] in bgColor (default white), then writes text (standard Helvetica) horizontally aligned per align and vertically centered within the box. The classic use is masking a placeholder and stamping the real value over it without hand-computing the baseline. Returns false if the page index does not exist.

maskedText(pageIndex, x, y, width, height, text, size = 12.0, textColor = [0.0, 0.0, 0.0], bgColor = [1.0, 1.0, 1.0], align = Align::Left, fontId = -1, valign = VerticalAlign::Middle, padding = null): bool
php
use RustPdf\Align;
use RustPdf\EditableDoc;

$ed = EditableDoc::loadFile('contract.pdf');
// Replace a {{name}} placeholder box, centered.
$ed->maskedText(0, 200.0, 540.0, 220.0, 24.0, 'Ada Lovelace', 14.0, [0.0, 0.0, 0.0], [1.0, 1.0, 1.0], Align::Center);
$ed->save('contract-filled.pdf');

Embedded stamping fonts

By default the stamping primitives draw with the built-in Helvetica (WinAnsi / Latin-1). Register a TrueType/OpenType font on the EditableDoc and pass the returned id as fontId to placeText / maskedText / placeParagraph: the font is embedded as a subset, so stamped text renders with the real font's glyphs and metrics (full Unicode), exactly like Document::addFontFile() + showText(). fontId = -1 keeps the built-in Helvetica.

addFontFile(path): int
addFont(data): int  (raw TrueType/OpenType bytes as a binary string)
php
$ed = EditableDoc::loadFile('invoice.pdf');
$times = $ed->addFontFile('Times New Roman.ttf');
$ed->placeText(0, 72.0, 700.0, 'Fatura n.º 2026-001 (ação)', 14.0, fontId: $times);
$ed->save('stamped.pdf');

Vertical anchors

placeText's anchor (a VerticalAnchor case) says what y means. Baseline (the default) keeps the historical behavior: y is the text baseline. Top hangs the text from y: the baseline lands ascent × size below it, matching legacy fixed-position layout. Bottom rests the descender line on y. LineTop/LineBottom anchor the layout line box (OS/2 win metrics plus a fixed half-leading), matching the legacy engine's line placement exactly; use these when porting legacy layout engines code whose boxes must land pixel-perfect. Ascent/descent come from the selected font's metrics.

php
use RustPdf\VerticalAnchor;

// y = 760 is the TOP of the text (legacy fixed-position layout semantics):
$ed->placeText(0, 72.0, 760.0, 'Header', 18.0, anchor: VerticalAnchor::Top);
// y = 40 is the bottom of the layout line box:
$ed->placeText(0, 72.0, 40.0, 'Footer', 9.0, anchor: VerticalAnchor::LineBottom);

maskedText vertical alignment & padding

valign (a VerticalAlign case) controls where the line sits inside the box: Middle (default) keeps the historical cap-height centering, Top hangs the line from the top edge (baseline at y + height − ascent × size, top line-alignment in rectangle-based text APIs), Bottom rests the descender line on the bottom edge. padding is the horizontal edge inset in points for Left/Right alignment: text starts at x + padding (or ends at x + width − padding). null keeps the historical default min(0.15 × size, width / 4); pass 0.0 to start flush with the box edge like rectangle-based DrawString APIs.

php
use RustPdf\{Align, VerticalAlign};

// Flush top-left inside the box, no inset (rectangle-based DrawString semantics).
$ed->maskedText(0, 80.0, 300.0, 200.0, 40.0, 'Ada Lovelace', 12.0,
    align: Align::Left, valign: VerticalAlign::Top, padding: 0.0);

Paragraph wrapping

placeParagraph stamps a paragraph with automatic word wrapping: the text is broken into lines that fit width points (greedy, by word; "\n" forces a break) and drawn downward from (x, y). With the default anchor = VerticalAnchor::Top, y is the top of the block and the first baseline lands ascent × size below it (legacy fixed-position layout); Baseline makes y the first line's baseline; Bottom/LineBottom bottom-pin the block: its bottom rests on y and it grows upward by its real content height. maxHeight is a ceiling: lines whose descender would cross the limit are cut (bottom-pinned blocks are cut from the top, so the last lines stay pinned); null = unlimited. lineHeight scales the default 1.2 × size leading, align includes Align::Justify, and rotationDeg rotates the laid-out block about the anchor. Pass a fontId to wrap and draw with an embedded font (its real metrics drive the break points).

placeParagraph(pageIndex, x, y, width, text, size = 12.0, color = [0.0, 0.0, 0.0], align = Align::Left, fontId = -1, maxHeight = null, lineHeight = 1.0, anchor = VerticalAnchor::Top, rotationDeg = 0.0): bool
placeParagraphMeasured(...same arguments...): array{lines: int, height: float}
php
use RustPdf\{Align, VerticalAnchor};

$terms = 'These terms and conditions apply to every order placed through this document ...';

// Wrap into a 300 pt column from the top-left corner (72, 500), max 120 pt tall.
$ed->placeParagraph(0, 72.0, 500.0, 300.0, $terms, 10.0,
    align: Align::Justify, maxHeight: 120.0);

// Bottom-pinned fine print: the block's BOTTOM rests on y = 60 and grows upward.
$ed->placeParagraph(0, 72.0, 60.0, 450.0, $terms, 8.0, anchor: VerticalAnchor::Bottom);

// Measure while stamping to stack the next block without re-measuring.
$m = $ed->placeParagraphMeasured(0, 72.0, 700.0, 300.0, $terms, 10.0);
$nextY = 700.0 - $m['height'] - 12.0;   // ['lines' => 5, 'height' => 68.4]

Coordinate space: Visible vs Media

setStampSpace chooses the coordinate space of all positioned stamping primitives (fillRect, placeText, maskedText, placeParagraph, drawImage) for subsequent calls. StampSpace::Visible (default) keeps the historical behavior: coordinates in the page's displayed space, compensating /Rotate so a rotationDeg = 0 stamp reads upright on screen. StampSpace::Media interprets coordinates and rotationDeg in the raw PDF user space (the raw-coordinate semantics of legacy layout engines), never composing with the page's /Rotate or crop offset. Use it to reproduce legacy-engine placement on rotated/scanned pages, or when porting coordinates captured from a PDF tool that reports media-space values. Watermarks and redaction are unaffected. See the interactive positioning guide to explore both spaces and every anchor visually.

setStampSpace(space): self
php
use RustPdf\StampSpace;

$ed->setStampSpace(StampSpace::Media);       // raw user space, legacy layout semantics
$ed->placeText(0, 100.0, 100.0, 'media-space stamp');
$ed->setStampSpace(StampSpace::Visible);     // back to the default

Rotated image anchor

drawImage's anchor (an ImageAnchor case) controls how a rotated image is anchored at (x, y). Corner (default) rotates the image about its own lower-left corner, so the image sweeps around the point. BoundingBox lands the rotated image's bounding box with its lower-left at (x, y) (bounding-box layout semantics: the drawn pixels always sit at/above/right of the anchor; e.g. a 90° image occupies [x, x+height] × [y, y+width]).

php
use RustPdf\ImageAnchor;

$stamp = file_get_contents('approved.png');
$ed->drawImage(0, $stamp, 300.0, 60.0, 120.0, 60.0, 30.0, ImageAnchor::BoundingBox);

Redaction licensed

True redaction: the text and graphics whose origin falls inside a rectangle are removed from the content stream (not just covered), so the data is gone from the file and is no longer extractable. Opaque black boxes are then painted over the regions.

redact(pageIndex, rects): bool
php
$ed = EditableDoc::loadFile('statement.pdf');
// rects = array of [x0, y0, x1, y1] on that page
$ed->redact(0, [[60, 590, 400, 620], [60, 540, 400, 570]]);
$ed->save('redacted.pdf');     // throws PdfException without a redaction license
Content under a rect is deleted before the file is written, so Pdf::extractText on the output no longer returns it. redact returns false if the page index does not exist.
redact() is not chainable. Unlike the other EditableDoc mutators (which return $this), redact returns a bool for whether the page existed. Call it as its own statement: $ed->redact(...); $ed->save(...);. Writing $ed->redact(...)->save(...) calls save() on a boolean and raises a PHP Error.

Convert to PDF/A licensed

Convert an existing PDF to archival PDF/A (a basic profile: A-1b, A-2b or A-3b). An sRGB output intent, PDF/A XMP metadata (synced with /Info) and a document /ID are added. Fails if any font is not embedded (PDF/A requires every font embedded) or a level-A profile is requested.

convertToPdfa(level = PdfaLevel::A2b): self
php
use RustPdf\{EditableDoc, PdfaLevel};

$ed = EditableDoc::loadFile('in.pdf');
$ed->convertToPdfa(PdfaLevel::A2b)   // throws PdfException if fonts aren't embedded
   ->save('archival.pdf');           // veraPDF: PDF/A-2b compliant

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.

Passwords protect, permissions only advise. A non-empty user password is real cryptographic protection: opening with the wrong password is rejected (cross-checked against qpdf for all three ciphers). The read-only permission flags, by contrast, are advisory: they are enforced only by the viewer, and a file with an empty user password opens with no prompt, so any tool can strip the restrictions. Treat read-only as a hint to well-behaved viewers, not as an access control.

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);

Normalize & downgrade free

Remove PDF/A conformance markers or downgrade the PDF version. setVersion changes the output header version, stripPdfa removes the OutputIntents, XMP pdfaid and /Version, and normalize does both (strip PDF/A then set the version) to produce a plain PDF.

MethodDescription
setVersion(version): selfSet the output version (0 → 1.4, 1 → 1.5, 2 → 1.7, 3 → 2.0).
stripPdfa(): selfStrip PDF/A conformance (OutputIntents, XMP pdfaid, /Version).
normalize(version = 2): selfStrip PDF/A and set the version in one call.
php
$ed = EditableDoc::loadFile('archival.pdf');
$ed->normalize(2)              // plain PDF 1.7, PDF/A markers removed
   ->save('plain.pdf');

// or step by step:
$ed->stripPdfa()->setVersion(1);   // PDF 1.5, no PDF/A conformance

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."
"Signature is Valid" vs. "Certificate isn't Trusted" are two separate checks. The cryptographic check (Pdf::verifySignatures returning is_valid, and pdfsig's "Signature Validation: Signature is Valid.") proves the bytes were not tampered with. The trust check is independent: a self-signed or test certificate makes pdfsig also print "Certificate issuer isn't Trusted." That is expected and does not mean the signature is invalid. To make the trust check pass, sign with a certificate chaining to a CA in the verifier's trust store.

Deferred / HSM signing licensed

When the private key lives in an HSM, a cloud KMS, a smartcard or a PKI authentication token, it must never enter the library. These flows keep the key where it is and work with any PKI (eIDAS, AATL and similar). The signer certificate and any intermediates are passed independently of the key.

Model A: bring your own signer

signWith builds the CMS signed attributes, then calls your callable with the bytes to sign and embeds the raw RSA signature it returns. The callable typically forwards the bytes to a cloud HSM and never sees the document.

Pdf::signWith(pdf, certDer, callable signHash, chain = [], ?options = null): string
php
use RustPdf\{Pdf, SigningOptions};

$pdf     = file_get_contents('contract.pdf');
$certDer = file_get_contents('signer-cert.der');     // X.509 certificate (DER)

$signed = Pdf::signWith(
    $pdf,
    $certDer,
    // receives the to-be-signed bytes, returns the raw RSA PKCS#1 v1.5
    // signature over their SHA-256; the key never leaves the HSM:
    fn (string $toSign): string => $hsm->signRsaSha256($toSign),
    chain: [$intermediateDer],                       // intermediates (DER)
    options: new SigningOptions(reason: 'Approved', pades: true),
);
file_put_contents('contract.signed.pdf', $signed);

Model B: two-phase signing

When the signer is asynchronous (a remote service or a user interaction), split the operation. beginSigning prepares the document and returns a SigningSession; hand its hash to the remote signer, build a DER CMS / PKCS#7 container, then embed it. The key never reaches the library.

Pdf::beginSigning(pdf, ?options = null): SigningSession
Pdf::completeSignature(document, container): string
SigningSession methodReturns
getDocument(): stringThe prepared PDF with a zero-filled /Contents placeholder.
getBytes(): stringThe exact bytes the signature covers (the two ByteRange segments).
getHash(): stringRaw 32-byte SHA-256 of getBytes(): the value the HSM signs.
complete(container): stringEmbed the finished DER CMS container, returning the signed PDF.
php
use RustPdf\{Pdf, SigningOptions};

$session   = Pdf::beginSigning($pdf, new SigningOptions(pades: true));
$container = $hsm->buildCmsContainer($session->getHash());   // raw 32-byte hash
$signed    = $session->complete($container);                 // or Pdf::completeSignature(...)
file_put_contents('contract.signed.pdf', $signed);

Inspect existing signatures

Detect prior signature fields before adding another. Each entry is a SignatureField with $name and a $signed flag; an empty list means no signature fields exist.

Pdf::listSignatures(pdf): SignatureField[]
php
foreach (Pdf::listSignatures($pdf) as $field) {
    printf("%s: %s\n", $field->name, $field->signed ? 'signed' : 'empty');
}

Signing options

SigningOptions tunes both deferred flows (and is accepted by signWith and beginSigning).

PropertyMeaning
reason, location, nameOptional signature metadata strings.
padestrue produces a PAdES-B-B signature (ETSI.CAdES.detached).
certifyA Certify DocMDP level applied by the first signature.
containerSizeReserved /Contents bytes; 0 means the default (8192). Raise it for large cloud-HSM CMS containers.
policyA SignaturePolicy for PAdES-EPES, or null for none.
visible, visiblePage, visibleRect, visibleTextDraw a visible signature appearance: enable visible, place it with visiblePage (0-based) and visibleRect ([x0,y0,x1,y1] points), and add caption lines via visibleText.
visibleImagePNG/JPEG bytes drawn aspect-fit behind any visibleText in the appearance rectangle, or null for none.

The visible appearance accepts an embedded image: set visibleImage to PNG or JPEG bytes and it is drawn aspect-fit behind the caption text.

php
$options = new SigningOptions(
    reason: 'Approved',
    visible: true,
    visiblePage: 0,
    visibleRect: [72, 72, 272, 152],
    visibleText: "Jane Doe\nApproved",
    visibleImage: file_get_contents('signature.png'),
);

Certify (DocMDP)

ValueMeaning
Certify::NoneNot a certifying signature (default).
Certify::Locked/P 1: no changes permitted after signing.
Certify::Forms/P 2: form-filling and signing permitted.
Certify::FormsAndAnnotations/P 3: form-filling, signing and annotations permitted.

SignaturePolicy (PAdES-EPES)

Constructor: new SignaturePolicy(oid, hash, ?hashAlgorithmOid = null, ?uri = null) where oid is the policy OID (dotted-decimal), hash is the raw policy-document hash, hashAlgorithmOid defaults to SHA-256 when null, and uri is an optional SPURI qualifier pointing at where the policy can be retrieved.

The library never holds the private key. In both models the signing key stays inside the HSM, KMS, smartcard or PKI token: Model A calls back to it for the raw RSA signature, and Model B exports only the hash to sign. Use certify (DocMDP) on the first signature only.

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);

Network timestamp (AD-RT)

To timestamp against a network RFC 3161 TSA instead of a local key, use the transport-agnostic helpers: beginTimestamp prepares the document and returns [document, tbs]; SHA-256 the tbs, build a request with timestampRequest, POST it to the TSA, extract the token with timestampTokenFromResponse, then embed it with completeSignature. You do the HTTP POST; free public TSAs work (FreeTSA https://freetsa.org/tsr, DigiCert http://timestamp.digicert.com).

Pdf::beginTimestamp(pdf): array
Pdf::timestampRequest(imprint, ?nonce = null, certReq = true): string
Pdf::timestampTokenFromResponse(response): string
php
use RustPdf\Pdf;

[$document, $tbs] = Pdf::beginTimestamp($signed);
$imprint = hash('sha256', $tbs, true);            // raw 32-byte SHA-256
$req     = Pdf::timestampRequest($imprint);       // RFC 3161 TimeStampReq (DER)

// POST the DER request to the TSA (any HTTP client):
$response = file_get_contents('https://freetsa.org/tsr', false, stream_context_create([
    'http' => ['method' => 'POST', 'header' => 'Content-Type: application/timestamp-query', 'content' => $req],
]));

$token = Pdf::timestampTokenFromResponse($response);   // TimeStampToken (CMS)
$lta   = Pdf::completeSignature($document, $token);
file_put_contents('contract.lta.pdf', $lta);

Validate signatures licensed

Validate every signature in a PDF: each report recomputes the /ByteRange digest, parses the CMS, and checks that the cryptographic signature is valid, that the messageDigest matches the covered bytes, and whether the signature covers the whole document.

Pdf::verifySignatures(data: string): array

Each entry is an associative array with keys field_name, sub_filter, signer, covers_whole_document, digest_valid, signature_valid, is_valid and byte_range, plus certificate detail (any may be null): issuer, serial_number, valid_from, valid_to, algorithm (e.g. SHA256withRSA), signing_time, cert_count and has_timestamp. The field_name and signer may be null when absent. An empty array means the document is unsigned. Signature validation is a licensed feature: this call requires an active license (signatures) even when the document is unsigned. It is not available on the free tier.

php
use RustPdf\Pdf;

$data = file_get_contents('contract.signed.pdf');
foreach (Pdf::verifySignatures($data) as $sig) {
    printf("%s valid: %s | covers whole doc: %s\n",
        $sig['signer'],
        $sig['is_valid'] ? 'yes' : 'no',
        $sig['covers_whole_document'] ? 'yes' : 'no');
    printf("  issuer: %s | serial: %s | %s\n",
        $sig['issuer'], $sig['serial_number'], $sig['algorithm']);
    printf("  valid %s … %s | signed: %s | timestamped: %s\n",
        $sig['valid_from'], $sig['valid_to'],
        $sig['signing_time'],
        $sig['has_timestamp'] ? 'yes' : 'no');
}

Text & image extraction

Extract a document's text, mapping shown glyph codes back to Unicode through each font's ToUnicode map, with space/line inference. Raster images can be pulled out too: JPEGs are written verbatim as .jpg, everything else as .png. The output directory is created automatically if it does not already exist.

Pdf::extractText(pdf: string): string free
Pdf::extractImagesToDir(pdf: string, dir: string): int free
php
use RustPdf\Pdf;

$data = file_get_contents('report.pdf');
echo Pdf::extractText($data);

$n = Pdf::extractImagesToDir($data, 'out_images/');   // returns how many were written
echo "wrote $n image(s)\n";

Find every occurrence of a query and get its position: findText returns a list of TextHit objects, each with a page and a bounding box (x, y, width, height) in PDF points, origin lower-left. The search is case-insensitive unless caseSensitive is set. See the concept page on searching PDF text.

Pdf::findText(pdf: string, query: string, caseSensitive = false): TextHit[]
php
use RustPdf\Pdf;

$data = file_get_contents('report.pdf');
foreach (Pdf::findText($data, 'Total') as $hit) {
    printf("page %d: \"%s\" at (%.1f, %.1f) %.1f×%.1f pt\n",
        $hit->page, $hit->text,
        $hit->x, $hit->y, $hit->width, $hit->height);
}

Page geometry free

Read each page's size, rotation and boxes without modifying the file. width/height ignore /Rotate; rotatedWidth/rotatedHeight are the dimensions a viewer actually shows. Sizes are in PDF points (72 per inch).

Pdf::measurePages(pdf: string): PageGeometry[]
Pdf::measurePage(pdf: string, pageIndex: int): PageGeometry

Each PageGeometry has page (0-based), width, height, rotation (0/90/180/270), rotatedWidth, rotatedHeight, and the mediaBox/cropBox as a PdfRect (x0, y0, x1, y1 with width()/height() helpers).

php
use RustPdf\Pdf;

$data = file_get_contents('report.pdf');
$g = Pdf::measurePage($data, 0);
printf("page %d: %.1f×%.1f pt, rotate %d\n", $g->page, $g->width, $g->height, $g->rotation);
printf("visible: %.1f×%.1f pt\n", $g->rotatedWidth, $g->rotatedHeight);

Document inspection free

Get a non-mutating overview of a PDF, its version, PDF/A level (if any) and encryption posture, without decrypting or rewriting it. Handy for triage before deciding how to process a file.

Pdf::inspect(pdf: string): PdfOverview

PdfOverview carries version (e.g. "1.7"), pdfaLevel (e.g. "2b", or null if not PDF/A), encrypted, encryption ("None", "RC4", "AES-128" or "AES-256"), requiresPassword, and pageCount.

php
use RustPdf\Pdf;

$o = Pdf::inspect(file_get_contents('report.pdf'));
printf("PDF %s, %d page(s)\n", $o->version, $o->pageCount);
printf("PDF/A: %s, encryption: %s\n", $o->pdfaLevel ?? 'no', $o->encryption);
if ($o->requiresPassword) echo "needs a password to open\n";

Render a page to an image licensed

Rasterize a page to a PNG image. A native Rust renderer (built on tiny-skia, with no headless browser) interprets the page content stream, painting real glyph outlines, vector graphics, images, color and transparency. Page rendering is a Pro feature; page_count is free.

Pdf::renderPageToPng(string $pdf, int $page = 0, float $dpi = 150.0): string licensed
Pdf::pageCount(string $pdf): int free
php
$data = file_get_contents("report.pdf");
echo Pdf::pageCount($data) . " page(s)\n";
file_put_contents("page1.png", Pdf::renderPageToPng($data, 0, 150.0));

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)
PdfaLevel::A4PDF/A-4 (ISO 19005-4, based on PDF 2.0)
PdfaLevel::A4ePDF/A-4e (engineering)
PdfaLevel::A4fPDF/A-4f (requires at least one embedded file)

Align

ValueMeaning
Align::LeftLeft-aligned (default)
Align::RightRight-aligned
Align::CenterCentered
Align::JustifyJustified (space distributed between words)

VerticalAnchor

ValueMeaning of y (placeText / placeParagraph)
VerticalAnchor::BaselineThe (first line's) baseline: the historical default of placeText
VerticalAnchor::TopTop of the text/block (legacy fixed-position layout): default of placeParagraph
VerticalAnchor::BottomDescender line rests on y; paragraphs bottom-pin and grow upward
VerticalAnchor::LineTopTop of the layout line box (exact legacy layout engines line placement)
VerticalAnchor::LineBottomBottom of the layout line box

VerticalAlign

ValueLine position inside a maskedText box
VerticalAlign::TopLine hangs from the top edge (top line-alignment in rectangle-based text APIs)
VerticalAlign::MiddleCap-height centered (default)
VerticalAlign::BottomDescender line rests on the bottom edge

StampSpace

ValueCoordinate space for stamping
StampSpace::VisibleDisplayed space, compensating /Rotate (default)
StampSpace::MediaRaw PDF user space (legacy layout semantics), ignoring /Rotate

ImageAnchor

ValueRotated-image anchoring at (x, y)
ImageAnchor::CornerThe image's own lower-left corner (default)
ImageAnchor::BoundingBoxThe rotated bounding box's lower-left (bounding-box layout semantics)

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

FacturxProfile

ValueConformance level
FacturxProfile::MinimumMinimal header data only
FacturxProfile::BasicWLBasic, without line items
FacturxProfile::BasicBasic, with line items
FacturxProfile::EN16931EN 16931 (Comfort): the interoperable core, default
FacturxProfile::ExtendedEN 16931 plus extensions

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. Token activation failures carry the dedicated license status code (12); a gated build, sign or encrypt call instead reports that operation’s own status (for example Serialize = 4 or Sign = 10) with the same “requires a valid license” message.

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=4 (Serialize): 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 and Ruby: browse all docs. They share one core, so behavior is identical.