Skip to content

Modifying Existing PDFs

Dragon edited this page Jun 15, 2026 · 8 revisions

Modifying Existing PDFs

PdfEditor::open() (or PdfEditor::fromBytes()) opens an existing PDF and lets you update its metadata, append new pages, delete and reorder pages, fill its AcroForm fields, and sign it - all without touching the original bytes. Every change is written as an appended incremental revision, so the original content (including any signatures already present) remains byte-for-byte intact.

Both classic xref tables and cross-reference streams (PDF 1.5+) are supported.

use DragonOfMercy\PhpPdf\PdfEditor;

$pdf = PdfEditor::open('invoice.pdf');            // open from file path (unencrypted)
$pdf = PdfEditor::open('protected.pdf', 'pass');  // open an encrypted PDF
// or from bytes:
$pdf = PdfEditor::fromBytes($pdfBytes);
$pdf = PdfEditor::fromBytes($pdfBytes, 'pass');

Editing encrypted PDFs

PdfEditor now supports editing encrypted PDFs. Pass the document password as the second argument to open() or fromBytes(). The password is optional: PDFs protected with only a permissions password (empty user password) open without a password argument; a supplied password is tried as the user password first, then as the owner password.

The appended incremental revision is automatically re-encrypted using the source document's scheme and recovered file key. The original /Encrypt dictionary and /ID pair are forwarded intact so the edited file remains a valid encrypted PDF that opens with the same password.

Supported encryption schemes: RC4 40-bit, RC4 128-bit, AES-128, AES-256.

use DragonOfMercy\PhpPdf\PdfEditor;

// Permissions-only encryption (empty user password) - no password argument needed:
$pdf = PdfEditor::open('readonly.pdf');
$pdf->setTitle('Updated title');
$pdf->save('readonly-updated.pdf');

// User- or owner-password protected:
$pdf = PdfEditor::open('locked.pdf', 'secret');
$pdf->setTitle('Updated title');
$page = $pdf->appendPage();
$page->cell(0, 10, 'New page added to an encrypted document');
$pdf->save('locked-updated.pdf');

Signing limitation. Signing, adding document timestamps, and enabling LTV on an encrypted PDF is not yet supported. PdfEditor throws a PdfException with a clear message if a signature, timestamp, or LTV request is made on an encrypted source. Metadata edits, appended pages, and AcroForm field fills are fully supported.

Updating metadata

Use the fluent setters to update the document's information dictionary. When the file already contains an XMP metadata packet, it is refreshed to stay consistent.

$pdf = PdfEditor::open('report.pdf');

$pdf->setTitle('Annual Report 2025')
    ->setAuthor('Acme Corp')
    ->setSubject('Finance')
    ->setKeywords('annual report finance 2025')
    ->setCreator('ReportGenerator 3.0');

$pdf->save('report-updated.pdf');
// or get the bytes directly:
$bytes = $pdf->output();

All five setters are optional - set only what you need. save() writes to a file; output() returns the PDF bytes as a string.

Appending pages

appendPage() appends a blank page and returns a fully functional Page object. You can draw on it with the complete page API: cells, images, tables, barcodes, SVG, Markdown, and so on.

use DragonOfMercy\PhpPdf\PdfEditor;
use DragonOfMercy\PhpPdf\Font;

$pdf = PdfEditor::open('contract.pdf');

$page = $pdf->appendPage();
$page->setFont(Font::helvetica(), 11);
$page->cell(0, 10, 'Addendum - see attachment A');

$pdf->save('contract-with-addendum.pdf');

The appended page is part of the same incremental revision as any metadata edits you make, so a single save() / output() call writes everything at once.

Deleting and reordering pages

deletePages() removes pages and reorderPages() changes their order. Both take 1-based page numbers (page 1 is the first page), matching PdfReader::page().

use DragonOfMercy\PhpPdf\PdfEditor;

$pdf = PdfEditor::open('report.pdf');

$pdf->deletePages(2, 5);          // remove pages 2 and 5
$pdf->reorderPages([3, 1, 2]);    // new order of the surviving pages, by original number

$pdf->save('report-edited.pdf');

When you delete a page, any reference that pointed at it would otherwise dangle, so the editor cleans them up automatically:

  • Bookmarks (outline items) targeting a deleted page are removed; their surviving children are kept and promoted to the removed item's place.
  • Named destinations targeting a deleted page are removed.
  • Internal links (GoTo link annotations) targeting a deleted page are removed from the pages that remain. External links (URLs / email) are untouched.

Reordering never breaks references: a page keeps its identity, only its position changes, so bookmarks and links keep resolving with no cleanup needed.

You can combine the two in one call - deletions apply first and define the surviving set, then reorderPages() permutes those survivors (by their original numbers); any pages you appended with appendPage() come last. Both methods are written as a single appended incremental revision, so the original bytes stay intact, and both work on encrypted PDFs.

Validation throws a PdfException with a clear message for an out-of-range or duplicate page number, an attempt to delete every page, or a reorderPages() argument that is not an exact permutation of the surviving pages.

// Combined: drop page 2, then reorder the rest.
PdfEditor::open('doc.pdf')
    ->deletePages(2)
    ->reorderPages([3, 1])
    ->save('doc-edited.pdf');

Limits. The page tree is rewritten and the page objects of deleted pages become unreferenced (the incremental model never rewrites the original bytes, so file size is not reclaimed). Destinations expressed as a remote/explicit page number rather than a page reference, and the structure tree of a tagged PDF, are not adjusted. Signing in the same call as a page operation is not supported - apply the page operations first, save, then sign the result.

Filling form fields

Open a PDF that contains an AcroForm, inspect its fields, set values, and save. Each filled field gets a generated appearance stream (/AP) so the value is rendered without relying on viewer-side rendering, and /NeedAppearances false is written.

Listing fields

use DragonOfMercy\PhpPdf\PdfEditor;

$pdf = PdfEditor::open('form.pdf');

foreach ($pdf->formFields() as $field) {
    echo $field->name, ' [', $field->type, ']', PHP_EOL;
}

formFields() returns list<FormFieldInfo>. field($name) returns a single FormFieldInfo or throws if the name is not found.

Setting field values

$pdf = PdfEditor::open('application.pdf');

$pdf->setField('FullName', 'Alice Dupont');          // text (single-line or multiline)
$pdf->setField('Subscribed', true);                  // checkbox
$pdf->setField('Gender', 'female');                  // radio (export-name string)
$pdf->setField('Country', 'ch');                     // combobox (export key)
$pdf->setField('Languages', ['en', 'fr']);            // listbox (one or more export keys)

$pdf->save('application-filled.pdf');
Field type Value type Notes
Text (single-line) string Written as-is.
Text (multiline) string Newlines preserved.
Checkbox bool true checks the field.
Radio button string The export name of the button to select.
Combobox string The export key of the chosen option.
Listbox string|list<string> Pass an array to select multiple items.

Font support for appearances. Appearance generation supports three categories of /DR font:

  • Standard-14 fonts (Helvetica, Courier, Times) - always available.
  • Simple embedded fonts (Type1 / TrueType): the library reuses the font from the source PDF (referenced, not re-embedded) and uses its /Widths for measurement and its /Encoding (WinAnsiEncoding base, with /Differences) for encoding.
  • Composite embedded fonts (Type0, Identity-H, CIDFontType2 / FontFile2): the library reuses the TrueType composite font from the source PDF and generates a 2-byte GID hex appearance stream. Supported encoding: Identity-H with a CIDFontType2 descendant and an embedded /FontFile2 (TrueType) program.

Fail-fast limits that throw a PdfException naming the field: CFF (/FontFile3) composite fonts are not yet supported; non-Identity-H Type0 encodings are not yet supported; simple fonts with a base encoding other than WinAnsiEncoding are rejected; a font missing /Widths is rejected; and a character not representable in the font's encoding is rejected (no silent substitution).

Flattening fields

$editor = PdfEditor::open('form.pdf');
$editor->setField('name', 'Jane Doe');
$editor->setField('agree', true);
$editor->flattenFields();            // freeze every value-bearing field
// or: $editor->flattenFields(['name']); // freeze a subset

$editor->save('flattened.pdf');

Flattening burns each widget's appearance into the page content and removes the interactivity. Signature and push-button fields are preserved. It works on encrypted sources and is written as an incremental revision, so the original bytes stay intact.

Signing

PdfEditor::sign(), addSignature(), addDocumentTimestamp(), and enableLtv() work on an opened PDF with full parity to Document. Each signature, document timestamp, or /DSS is written as a stacked incremental revision that leaves the original bytes intact and cryptographically covers every prior byte - including any metadata edits, appended pages, and field fills, which are written as the first revision.

use DragonOfMercy\PhpPdf\PdfEditor;
use DragonOfMercy\PhpPdf\Signature\SigningCertificate;
use DragonOfMercy\PhpPdf\Signature\Tsa;

$pdf = PdfEditor::open('form.pdf');
$pdf->setField('FullName', 'Alice');

$pdf->sign(
    SigningCertificate::fromPkcs12('cert.p12', 'password'),
    field: 'Approval',
    reason: 'I approve this document',
    timestamp: Tsa::http('https://freetsa.org/tsr'),
)->save('signed.pdf');

See the Digital Signatures page for the full API reference: signature formats (adbe.pkcs7.detached / ETSI.CAdES.detached), multiple signers, document timestamps, long-term validation (LTV / PAdES-B-LT / B-LTA), and visible signature appearance.

Limitations

  • Signing, adding document timestamps, and enabling LTV on an encrypted source PDF are not yet supported (PdfEditor throws a PdfException at sign time; metadata edits, appended pages, and form field fills work normally on encrypted sources).
  • Appearance generation (form-fill) supports Standard-14 fonts, non-Standard-14 simple fonts (Type1 / TrueType) embedded in the source /DR, and Type0 composite fonts using Identity-H encoding with a CIDFontType2 / /FontFile2 TrueType descendant. CFF (/FontFile3) composite fonts and non-Identity-H Type0 encodings are not yet supported (the library throws a PdfException naming the field).
  • A source whose /AcroForm is a direct (inline) dictionary is rejected when signing - the /AcroForm must be an indirect reference.
  • Page deletion does not reclaim the deleted page objects' bytes (incremental model), does not adjust remote/explicit page-number destinations or a tagged PDF's structure tree, and cannot be combined with signing in the same call.
  • The visible signature caption (when using SignatureAppearance) is drawn with Standard-14 Helvetica only.

See also

Clone this wiki locally