Skip to content
Open
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
62 changes: 62 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,67 @@ Dokumentebene: `WC` (WillClose), `WS` (WillSave), `DS` (DidSave), `WP`

---

## Ein bestehendes Formular ausfüllen (AcroForm)

Das Obige erstellt ein neues Formular. Um ein von jemand anderem erzeugtes
PDF zu öffnen — eine hochgeladene Vorlage, ein Behördenformular — und die
Werte seiner bereits definierten Felder namentlich auszufüllen, verwenden
Sie stattdessen `ExistingFormFiller`:

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

ExistingFormFiller::fromFile('template.pdf')
->setValues([
'full_name' => 'Jane Roe',
'agree' => 'yes',
'employer.name' => 'Acme Corp', // über /Parent-Vererbung aufgelöst
])
->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 seinen vollständig
qualifizierten, durch Punkte getrennten Namen, Typ, aktuellen Wert, seinen
`/TU`-Tooltip (falls vorhanden) und (bei Checkbox/Radio) seine
Ein-Zustand-Optionsnamen, sodass ein Aufrufer die Form einer Vorlage vor
dem Ausfüllen ermitteln kann:

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

Unterstützte Feldtypen: `text`, `text-multiline`, `checkbox`, `radio`. Eine
Checkbox akzeptiert `on`/`yes`/`true`/`1` (ohne Groß-/Kleinschreibung) oder
ihren exakten Ein-Zustand-Exportnamen; alles andere hakt sie ab. Der Wert
einer Radiogruppe muss einem ihrer Options-Exportnamen entsprechen (ohne
Groß-/Kleinschreibung).

`flatten(?array $fieldNames = null)` bäckt den aktuellen Wert in den
Seiteninhalt ein und entfernt das interaktive Widget — übergeben Sie eine
Liste von Namen, um eine Teilmenge zu flatten und den Rest interaktiv zu
lassen, oder lassen Sie es weg, um alles zu flatten (was auch `/AcroForm`
entfernt, sobald nichts mehr darin übrig ist). Nur `text`/`text-multiline`-
Werte werden gezeichnet; andere Feldtypen verlieren nur ihr Widget. Das
Platzhalter-Erscheinungsbild eines nicht ausgefüllten geflatteten Feldes
(z. B. ein grauer Hintergrund) wird nicht eingebacken — nur Felder mit
einem Wert erhalten an ihrer Stelle etwas Gezeichnetes. Lassen Sie Felder
ungeflatteten (Standard), um das Ergebnis ein gewöhnliches interaktives PDF
bleiben zu lassen.

`stampImage()`/`stampImageBytes()` platziert ein PNG (mit Alphakanal) oder
JPEG an einer gegebenen Seite/x/y/Breite/Höhe, unabhängig von jedem Feld —
nützlich für eine Signatur oder ein Foto, das selbst kein Formularfeld ist.
Koordinaten sind Punkte mit Ursprung oben links.

---

## Annotationen

```php
Expand Down
56 changes: 56 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,61 @@ 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')
->setValues([
'full_name' => 'Jane Roe',
'agree' => 'yes',
'employer.name' => 'Acme Corp', // resolved via /Parent inheritance
])
->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, its `/TU`
tooltip (if any) and (for checkbox/radio) its on-state option names, 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} = {$field->value}\n";
}
```

Supported field types: `text`, `text-multiline`, `checkbox`, `radio`. A
checkbox accepts `on`/`yes`/`true`/`1` (case insensitive) or its exact
on-state export name; anything else unchecks it. A radio group's value must
match one of its option export names (case insensitive).

`flatten(?array $fieldNames = null)` bakes the current value into the page
content and removes the interactive widget — pass a list of names to
flatten a subset and leave the rest interactive, or omit it to flatten
everything (which also removes `/AcroForm` once nothing is left in it).
Only `text`/`text-multiline` values are drawn; other field types just lose
their widget. An unfilled flattened field's placeholder appearance (e.g. a
grey background) is not baked in — only fields with a value get anything
drawn in their place. Leave fields unflattened (the default) to keep the
result an ordinary interactive PDF instead.

`stampImage()`/`stampImageBytes()` place a PNG (with alpha) or JPEG at a
given page/x/y/width/height, independent of any field — useful for a
signature or photo that isn't itself a form field. Coordinates are
top-left-origin points.

---

## Annotations

```php
Expand Down
61 changes: 61 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,66 @@ JavaScript-хуки на уровне поля: `keystrokeScript`, `validateScri

---

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

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

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

ExistingFormFiller::fromFile('template.pdf')
->setValues([
'full_name' => 'Jane Roe',
'agree' => 'yes',
'employer.name' => 'Acme Corp', // разрешается через наследование /Parent
])
->stampImage(0, 'signature.png', x: 100, y: 600, width: 120, height: 40)
->flatten()
->toFile('filled.pdf');
```

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

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

Поддерживаемые типы полей: `text`, `text-multiline`, `checkbox`, `radio`.
Чекбокс принимает `on`/`yes`/`true`/`1` (без учёта регистра) или точное имя
экспорта включённого состояния; любое другое значение снимает отметку.
Значение группы радиокнопок должно совпадать с одним из имён экспорта
опций (без учёта регистра).

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

`stampImage()`/`stampImageBytes()` размещает PNG (с альфа-каналом) или
JPEG по заданным странице/x/y/ширине/высоте, независимо от каких-либо
полей — полезно для подписи или фотографии, которая сама по себе не
является полем формы. Координаты заданы в точках с началом координат
в левом верхнем углу.

---

## Аннотации

```php
Expand Down
51 changes: 51 additions & 0 deletions docs/zh/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ HTML 转 PDF 一直深入到最底层的页面发射。每个章节都是自包
- [SVG](#svg)
- [超链接与书签](#超链接与书签)
- [表单(AcroForm)](#表单acroform)
- [填写已有表单(AcroForm)](#填写已有表单acroform)
- [标注](#标注)
- [加密](#加密)
- [数字签名](#数字签名)
Expand Down Expand Up @@ -498,6 +499,56 @@ DocumentBuilder::new()

---

## 填写已有表单(AcroForm)

以上是从零创建新表单。若要打开他人生成的 PDF——上传的模板、政府表格——
并按名称填写其中已定义字段的值,请改用 `ExistingFormFiller`:

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

ExistingFormFiller::fromFile('template.pdf')
->setValues([
'full_name' => 'Jane Roe',
'agree' => 'yes',
'employer.name' => 'Acme Corp', // 通过 /Parent 继承解析
])
->stampImage(0, 'signature.png', x: 100, y: 600, width: 120, height: 40)
->flatten()
->toFile('filled.pdf');
```

源文档永远不会被修改——每次调用都作用于*整个*对象图的深拷贝,因此未被
触及的内容(大纲、元数据、其他标注)会原样保留到输出中。`fields()` 会
返回每个字段的完整点分名称、类型、当前值、`/TU` 提示文字(如果有),以及
(对复选框/单选按钮而言)其选中状态的导出名称,方便调用方在填写前了解
模板的结构:

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

支持的字段类型:`text`、`text-multiline`、`checkbox`、`radio`。复选框接受
`on`/`yes`/`true`/`1`(不区分大小写)或其精确的选中状态导出名称;其他任何
值都会取消勾选。单选按钮组的值必须匹配其某个选项的导出名称(不区分大小
写)。

`flatten(?array $fieldNames = null)` 会将当前值绘制进页面内容并移除交互
式部件——传入字段名称列表可只压平其中一部分、其余字段保持交互,省略参数
则压平全部字段(这也会在 `/AcroForm` 中不再留有任何字段时将其整体移除)。
仅 `text`/`text-multiline` 的值会被绘制;其他字段类型只是失去其部件。未
填写字段被压平后,其占位外观(例如灰色背景)不会被绘制保留——只有有值的
字段才会在原位置绘制内容。保持默认(不压平)可使结果仍是一份普通的交互
式 PDF。

`stampImage()`/`stampImageBytes()` 可在指定的页面/x/y/宽/高处放置一张 PNG
(保留透明度)或 JPEG 图片,与任何字段无关——适用于签名或照片这类本身并
非表单字段的内容。坐标以左上角为原点。

---

## 标注

```php
Expand Down
26 changes: 26 additions & 0 deletions scripts/conformance/generate.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
use Dskripchenko\PhpPdf\Element\Table;
use Dskripchenko\PhpPdf\Font\Ttf\TtfFile;
use Dskripchenko\PhpPdf\Layout\Engine;
use Dskripchenko\PhpPdf\Pdf\Document as PdfDocument;
use Dskripchenko\PhpPdf\Pdf\Forms\ExistingFormFiller;
use Dskripchenko\PhpPdf\Pdf\PdfAConfig;
use Dskripchenko\PhpPdf\Pdf\PdfFont;
use Dskripchenko\PhpPdf\Pdf\PdfXConfig;
Expand Down Expand Up @@ -185,4 +187,28 @@
$failures += $ok ? 0 : 1;
}

// Visual fixture for ExistingFormFiller: a small template with a text field
// and a checkbox, filled, flattened, and stamped with a signature image —
// exercises the whole fill/flatten/stamp pipeline in one rendered page.
$template = PdfDocument::new(compressStreams: false);
$templatePage = $template->addPage();
$templatePage->addFormField('text', 'full_name', 72, 700, 250, 20, defaultValue: '');
$templatePage->addFormField('checkbox', 'agree', 72, 660, 14, 14);

$formFillOk = true;
try {
$bytes = ExistingFormFiller::fromBytes($template->toBytes())
->setValue('full_name', 'Jane Roe')
->setValue('agree', 'yes')
->flatten()
->stampImage(0, $root.'/tests/fixtures/1x1.png', 72, 600, 40, 40)
->toBytes();
file_put_contents($outDir.'/form-fill.pdf', $bytes);
echo "generated $outDir/form-fill.pdf\n";
} catch (\Throwable $e) {
fwrite(STDERR, "FAILED form-fill: {$e->getMessage()}\n");
$formFillOk = false;
}
$failures += $formFillOk ? 0 : 1;

exit($failures > 0 ? 1 : 0);
2 changes: 1 addition & 1 deletion scripts/conformance/visual-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ FUZZ="5%"
MAX_DIFF_PCT="0.5"

# First page of each document is the visual fixture.
DOCS=(pdfa-1b pdfx-4)
DOCS=(pdfa-1b pdfx-4 form-fill)

mkdir -p "$RENDER" "$GOLDEN"

Expand Down
63 changes: 63 additions & 0 deletions scripts/fixtures/generate-parent-inherited-form.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

declare(strict_types=1);

/**
* Hand-builds a minimal AcroForm PDF with a text field reachable only
* through /Parent-chain name inheritance ("employer.name") — a shape the
* library's own FormField authoring API cannot produce (it only emits flat
* fields, plus its own auto-generated radio-group parent/kids). Used as a
* static fixture by tests/Pdf/Forms/ExistingFormFillerTest.php.
*
* Run: php scripts/fixtures/generate-parent-inherited-form.php
*/

require __DIR__.'/../../vendor/autoload.php';

use Dskripchenko\PhpPdf\Pdf\Writer;

$writer = new Writer();

$pageId = $writer->reserveObject();
$pagesId = $writer->reserveObject();
$catalogId = $writer->reserveObject();
$acroFormId = $writer->reserveObject();
$parentId = $writer->reserveObject();
$childId = $writer->reserveObject();

$contentsId = $writer->addObject("<< /Length 0 >>\nstream\n\nendstream");

// Non-terminal field node: only /T + /Kids, no /FT/value of its own.
$writer->setObject($parentId, "<< /FT /Tx /T (employer) /Kids [{$childId} 0 R] >>");

// Terminal field, itself the sole widget annotation: partial /T "name" +
// /Parent -> fully-qualified name resolves to "employer.name".
$writer->setObject($childId, sprintf(
'<< /Type /Annot /Subtype /Widget /FT /Tx /Rect [100 700 300 720] '
.'/T (name) /Parent %d 0 R /P %d 0 R /F 4 /V () /DA (/Helv 10 Tf 0 g) >>',
$parentId,
$pageId,
));

$writer->setObject($acroFormId, sprintf(
'<< /Fields [%d 0 R] /DA (/Helv 10 Tf 0 g) /DR << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> >>',
$parentId,
));

$writer->setObject($pageId, sprintf(
'<< /Type /Page /Parent %d 0 R /MediaBox [0 0 612 792] /Contents %d 0 R '
.'/Resources << /Font << /Helv << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> '
.'/Annots [%d 0 R] >>',
$pagesId,
$contentsId,
$childId,
));

$writer->setObject($pagesId, sprintf('<< /Type /Pages /Kids [%d 0 R] /Count 1 >>', $pageId));
$writer->setObject($catalogId, sprintf('<< /Type /Catalog /Pages %d 0 R /AcroForm %d 0 R >>', $pagesId, $acroFormId));
$writer->setRoot($catalogId);

$outPath = __DIR__.'/../../tests/fixtures/forms/parent-inherited.pdf';
file_put_contents($outPath, $writer->toBytes());

echo "Wrote {$outPath}\n";
Loading