Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,30 @@ All notable changes to `dskripchenko/php-pdf` are documented in this file.
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- **`ExistingFormFiller` — fill, flatten and stamp a PDF form produced
elsewhere** (contributed by Christiaan Baartse, #1). Reads the AcroForm's
fields by fully-qualified name (`fields()`), sets text, check box, radio,
combo and list box values (`setValue()`), bakes fields into the page
(`flatten()`), and places an image on a page (`stampImage()`).
- Filled fields get a generated appearance stream — the `/DA` font, size
(auto size included), colour and alignment, the `/MK` background and
border, comb cells, multi-line wrapping — so values show in every viewer.
- Values the form's fonts cannot show are drawn with an embedded TrueType
font: `useFont()`, `useFontProvider()`, or Liberation picked up
automatically when `dskripchenko/php-pdf-fonts-liberation` is installed.
- `flatten()` draws each widget's own appearance, as Acrobat does, so check
marks, backgrounds and borders survive.
- Values are validated: `/MaxLen`, radio and choice options; signature and
push-button fields refuse a value.
- Inherited page resources, the page's graphics state, `/Info`, `/ID`, the
CropBox and `/Rotate` are respected; an XFA definition is dropped once
values change.
- `ObjectImporter::withSource()` — import objects from a second document into
a graph without losing the current source's deduplication.

## [1.9.4] — 2026-08-17

### Changed
Expand Down
84 changes: 84 additions & 0 deletions docs/de/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ einen Rundgang oder springen Sie direkt zum benötigten Feature.
- [SVG](#svg)
- [Hyperlinks und Lesezeichen](#hyperlinks-und-lesezeichen)
- [Formulare (AcroForm)](#formulare-acroform)
- [Ein bestehendes Formular ausfüllen (AcroForm)](#ein-bestehendes-formular-ausfüllen-acroform)
- [Annotationen](#annotationen)
- [Verschlüsselung](#verschlüsselung)
- [Digitale Signatur](#digitale-signatur)
Expand Down Expand Up @@ -519,6 +520,89 @@ Dokumentebene: `WC` (WillClose), `WS` (WillSave), `DS` (DidSave), `WP`

---

## Ein bestehendes Formular ausfüllen (AcroForm)

Oben wird ein neues Formular erstellt. Um ein PDF zu öffnen, das jemand
anderes erzeugt hat — eine hochgeladene Vorlage, ein Behördenformular — und
die Werte seiner bereits definierten Felder per Name auszufüllen, verwenden
Sie stattdessen `ExistingFormFiller`:

```php
use Dskripchenko\PhpPdf\Pdf\Forms\ExistingFormFiller;

ExistingFormFiller::fromFile('template.pdf')
->useFont('fonts/LiberationSans-Regular.ttf') // für nicht-lateinische Werte
->setValues([
'full_name' => 'Erika Mustermann',
'agree' => true,
'employer.name' => 'Acme GmbH', // über /Parent-Vererbung aufgelöst
'country' => 'Deutschland', // Combo-Box-Option (Exportwert oder Beschriftung)
])
->stampImage(0, 'signature.png', x: 100, y: 600, width: 120, height: 40)
->flatten()
->toFile('filled.pdf');
```

Das Quelldokument wird nie verändert — jeder Aufruf arbeitet auf einer
tiefen Kopie des *gesamten* Objektgraphen, sodass alles Unberührte
(Lesezeichen, Metadaten, andere Annotationen) unverändert in die Ausgabe
übernommen wird. `fields()` liefert für jedes Feld den vollqualifizierten
Punktnamen, Typ, aktuellen Wert, zulässige Optionen, `/MaxLen` und den
`/TU`-Tooltip, sodass man den Aufbau einer Vorlage vor dem Ausfüllen
erkunden kann:

```php
foreach (ExistingFormFiller::fromFile('template.pdf')->fields() as $name => $field) {
echo "{$name}: {$field->type} = ".json_encode($field->value)."\n";
}
```

Werte je Feldtyp:

| Typ | Akzeptierter Wert |
|---|---|
| `text`, `text-multiline` | ein String; `/MaxLen` wird durchgesetzt |
| `checkbox` | `true`/`false`, `on`/`yes`/`true`/`1`, `off`/`no`/`false`/`0` oder sein Exportwert |
| `radio` | eine der `options` (ohne Groß-/Kleinschreibung) oder `Off` |
| `combo`, `list` | Exportwert oder Beschriftung einer Option; eine Liste davon bei Mehrfachauswahl |
| `signature`, `push` | nicht setzbar — `LogicException` |

Eine unbekannte Option wird mit `InvalidArgumentException` abgelehnt, statt
stillschweigend ignoriert zu werden.

Ausgefüllte Text- und Auswahlfelder erhalten einen neu erzeugten
Appearance-Stream — Schrift, Größe (inklusive Auto-Größe), Farbe und
Ausrichtung aus `/DA`, Hintergrund und Rahmen aus `/MK`, Comb-Zellen,
mehrzeiliger Umbruch —, sodass der Wert in jedem Viewer erscheint, nicht nur
in solchen, die `/NeedAppearances` beachten. Kann die formulareigene Schrift
einen Wert nicht darstellen (Kyrillisch, Griechisch, CJK, …), wird er mit
einer eingebetteten TrueType-Schrift gezeichnet: Übergeben Sie eine an
`useFont()` (oder einen `FontProvider` an `useFontProvider()`), oder
installieren Sie `dskripchenko/php-pdf-fonts-liberation`, das dann
automatisch verwendet wird. Ohne Schrift löst ein solcher Wert eine
`RuntimeException` mit dem Feldnamen aus.

`flatten(?array $fieldNames = null)` macht Felder so zu statischem
Seiteninhalt, wie Acrobat es tut: Das eigene Erscheinungsbild jedes Widgets
— Häkchen, Radio-Punkte, Hintergründe, Rahmen — wird auf die Seite gezeichnet
und das Widget entfernt. Übergeben Sie eine Namensliste, um nur einen Teil
zu glätten und den Rest interaktiv zu lassen, oder lassen Sie sie weg, um
alle Widgets zu glätten und `/AcroForm` zu entfernen.

`stampImage()`/`stampImageBytes()` platzieren ein PNG (mit Alpha) oder JPEG
unabhängig von Feldern — nützlich für eine Unterschrift oder ein Foto, das
selbst kein Formularfeld ist. `x`/`y` ist die linke obere Ecke des Bildes in
Punkt, gemessen von der linken oberen Ecke der Seite *so, wie ein Viewer sie
zeigt* (CropBox, nach `/Rotate`); `pageSize($index)` liefert diese Größe.

Die Ausgabe ist eine vollständige, unverschlüsselte Neuschreibung des
Dokuments. Bestehende digitale Signaturen sind danach nicht mehr gültig, und
sobald sich etwas ändert, wird eine XFA-Formulardefinition entfernt
(XFA-fähige Viewer würden sonst die XFA-Ebene statt der ausgefüllten Werte
anzeigen).

---

## Annotationen

```php
Expand Down
79 changes: 79 additions & 0 deletions docs/en/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ the feature you need.
- [SVG](#svg)
- [Hyperlinks and bookmarks](#hyperlinks-and-bookmarks)
- [Forms (AcroForm)](#forms-acroform)
- [Fill an existing form (AcroForm)](#fill-an-existing-form-acroform)
- [Annotations](#annotations)
- [Encryption](#encryption)
- [Digital signing](#digital-signing)
Expand Down Expand Up @@ -507,6 +508,84 @@ events: `WC` (WillClose), `WS` (WillSave), `DS` (DidSave), `WP`

---

## Fill an existing form (AcroForm)

The above authors a brand-new form. To open a PDF someone else produced —
an uploaded template, a government form — and fill in the values of its
already-defined fields by name, use `ExistingFormFiller` instead:

```php
use Dskripchenko\PhpPdf\Pdf\Forms\ExistingFormFiller;

ExistingFormFiller::fromFile('template.pdf')
->useFont('fonts/LiberationSans-Regular.ttf') // for non-Latin values
->setValues([
'full_name' => 'Jane Roe',
'agree' => true,
'employer.name' => 'Acme Corp', // resolved via /Parent inheritance
'country' => 'Germany', // combo box option (export value or label)
])
->stampImage(0, 'signature.png', x: 100, y: 600, width: 120, height: 40)
->flatten()
->toFile('filled.pdf');
```

The source document is never mutated — every call works on a deep copy of
the *entire* object graph, so anything not touched (outlines, metadata,
other annotations) survives untouched into the output. `fields()` returns
each field's fully-qualified dotted name, type, current value, accepted
options, `/MaxLen` and `/TU` tooltip, so a caller can discover a template's
shape before filling it:

```php
foreach (ExistingFormFiller::fromFile('template.pdf')->fields() as $name => $field) {
echo "{$name}: {$field->type} = ".json_encode($field->value)."\n";
}
```

Values by field type:

| Type | Accepted value |
|---|---|
| `text`, `text-multiline` | a string; `/MaxLen` is enforced |
| `checkbox` | `true`/`false`, `on`/`yes`/`true`/`1`, `off`/`no`/`false`/`0`, or its export value |
| `radio` | one of `options` (case insensitive), or `Off` |
| `combo`, `list` | an option's export value or its label; a list of them for a multi-select list box |
| `signature`, `push` | cannot be set — `LogicException` |

An unknown option is rejected with an `InvalidArgumentException` rather than
silently ignored.

Filled text and choice fields get a freshly generated appearance stream —
the field's `/DA` font, size (auto size included), colour and alignment, its
`/MK` background and border, comb cells, multi-line wrapping — so the value
shows in every viewer, not only those that honour `/NeedAppearances`. When
the form's own font cannot show a value (Cyrillic, Greek, CJK, …), the
value is drawn with an embedded TrueType font: pass one to `useFont()` (or a
`FontProvider` to `useFontProvider()`), or install
`dskripchenko/php-pdf-fonts-liberation`, which is then picked up
automatically. Without one, such a value throws a `RuntimeException` naming
the field.

`flatten(?array $fieldNames = null)` turns fields into static page content
the way Acrobat does: each widget's own appearance — check marks, radio
dots, backgrounds, borders — is drawn on the page and the widget is
removed. Pass a list of names to flatten a subset and leave the rest
interactive, or omit it to flatten every widget and drop `/AcroForm`.

`stampImage()`/`stampImageBytes()` place a PNG (with alpha) or JPEG,
independent of any field — useful for a signature or photo that isn't
itself a form field. `x`/`y` is the image's top-left corner in points from
the top-left of the page *as a viewer shows it* (the CropBox, after
`/Rotate`); `pageSize($index)` returns that size.

The output is a full rewrite of the document, unencrypted. Existing digital
signatures no longer verify, and once anything changes an XFA form
definition is removed (XFA-aware viewers would otherwise show the XFA layer
instead of the filled values).

---

## Annotations

```php
Expand Down
81 changes: 81 additions & 0 deletions docs/ru/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
- [SVG](#svg)
- [Гиперссылки и закладки](#гиперссылки-и-закладки)
- [Формы (AcroForm)](#формы-acroform)
- [Заполнение существующей формы (AcroForm)](#заполнение-существующей-формы-acroform)
- [Аннотации](#аннотации)
- [Шифрование](#шифрование)
- [Цифровая подпись](#цифровая-подпись)
Expand Down Expand Up @@ -519,6 +520,86 @@ JavaScript-хуки на уровне поля: `keystrokeScript`, `validateScri

---

## Заполнение существующей формы (AcroForm)

Выше описано создание новой формы. Чтобы открыть PDF, созданный кем-то
другим — загруженный шаблон, государственную форму — и заполнить значения
уже определённых в нём полей по имени, используйте вместо этого
`ExistingFormFiller`:

```php
use Dskripchenko\PhpPdf\Pdf\Forms\ExistingFormFiller;

ExistingFormFiller::fromFile('template.pdf')
->useFont('fonts/LiberationSans-Regular.ttf') // для кириллицы и прочего не-Latin
->setValues([
'full_name' => 'Иван Петров',
'agree' => true,
'employer.name' => 'ООО «Ромашка»', // разрешается через наследование /Parent
'country' => 'Россия', // вариант combo box (экспортное значение или подпись)
])
->stampImage(0, 'signature.png', x: 100, y: 600, width: 120, height: 40)
->flatten()
->toFile('filled.pdf');
```

Исходный документ никогда не изменяется — каждый вызов работает с глубокой
копией *всего* графа объектов, поэтому всё, что не затронуто (оглавление,
метаданные, прочие аннотации), переходит в результат без изменений.
`fields()` возвращает для каждого поля полное имя через точку, тип, текущее
значение, допустимые варианты, `/MaxLen` и подсказку `/TU` — так можно
изучить устройство шаблона до заполнения:

```php
foreach (ExistingFormFiller::fromFile('template.pdf')->fields() as $name => $field) {
echo "{$name}: {$field->type} = ".json_encode($field->value, JSON_UNESCAPED_UNICODE)."\n";
}
```

Значения по типам полей:

| Тип | Что принимает |
|---|---|
| `text`, `text-multiline` | строку; `/MaxLen` соблюдается |
| `checkbox` | `true`/`false`, `on`/`yes`/`true`/`1`, `off`/`no`/`false`/`0` или своё экспортное значение |
| `radio` | один из `options` (без учёта регистра) или `Off` |
| `combo`, `list` | экспортное значение варианта или его подпись; для list box с множественным выбором — список |
| `signature`, `push` | задать нельзя — `LogicException` |

Неизвестный вариант отклоняется с `InvalidArgumentException`, а не
игнорируется молча.

Заполненные текстовые поля и поля выбора получают заново сгенерированный
внешний вид — шрифт, размер (включая авторазмер), цвет и выравнивание из
`/DA`, фон и рамку из `/MK`, comb-ячейки, перенос строк — поэтому значение
видно в любом просмотрщике, а не только в тех, что учитывают
`/NeedAppearances`. Если собственный шрифт формы не может показать значение
(кириллица, греческий, CJK…), оно рисуется встроенным TrueType-шрифтом:
передайте его в `useFont()` (или `FontProvider` в `useFontProvider()`), либо
установите `dskripchenko/php-pdf-fonts-liberation` — тогда он подхватится
автоматически. Без шрифта такое значение приводит к `RuntimeException` с
именем поля.

`flatten(?array $fieldNames = null)` превращает поля в статичное содержимое
страницы так же, как это делает Acrobat: собственный внешний вид каждого
виджета — галочки, точки радиокнопок, фон, рамки — рисуется на странице, а
виджет удаляется. Передайте список имён, чтобы «запечь» только часть полей и
оставить остальные интерактивными, или не передавайте ничего — тогда
обрабатываются все виджеты и `/AcroForm` удаляется.

`stampImage()`/`stampImageBytes()` ставят PNG (с альфа-каналом) или JPEG
независимо от полей — например, подпись или фото, которые сами полем формы
не являются. `x`/`y` — левый верхний угол картинки в пунктах от левого
верхнего угла страницы *в том виде, как её показывает просмотрщик* (CropBox,
с учётом `/Rotate`); этот размер возвращает `pageSize($index)`.

Результат — полностью пересобранный документ без шифрования. Существующие
цифровые подписи перестают проходить проверку, а при любом изменении
удаляется XFA-описание формы (иначе просмотрщики с поддержкой XFA показали бы
XFA-слой вместо заполненных значений).

---

## Аннотации

```php
Expand Down
Loading
Loading