PHPackages                             kalle/pdf - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Templating &amp; Views](/categories/templating)
4. /
5. kalle/pdf

ActiveLibrary[Templating &amp; Views](/categories/templating)

kalle/pdf
=========

A lightweight, native PDF engine for PHP for creating structured PDF documents programmatically

v2.x-dev(2mo ago)10MITPHPPHP ^8.5CI failing

Since Apr 15Pushed 2mo agoCompare

[ Source](https://github.com/kalicki2k/pdf)[ Packagist](https://packagist.org/packages/kalle/pdf)[ Docs](https://github.com/kalicki2k/pdf)[ RSS](/packages/kalle-pdf/feed)WikiDiscussions main Synced today

READMEChangelogDependencies (7)Versions (4)Used By (0)

PDF
===

[](#pdf)

Projektueberblick
-----------------

[](#projektueberblick)

`pdf2` ist ein nativer PDF-Generator in PHP. Das Projekt erzeugt PDF-Dateien programmgesteuert ueber ein eigenes Dokumentmodell, einen eigenen Objektgraph-Build und einen eigenen Writer. Externe Werkzeuge wie `qpdf` und `veraPDF` werden fuer Pruefung und Regressionen verwendet, nicht als Kern des Renderpfads.

Der aktuelle Codebestand geht deutlich ueber "Text in PDF schreiben" hinaus. Im Repository sind unter anderem umgesetzt:

- fluente Document-Builder-API ueber `Pdf::document()` und `Document::make()`
- Textlayout inklusive Bidi- und Script-Shaping-Pfaden
- Embedded Fonts und Font-Subsetting
- Bildimporte fuer mehrere Formate
- Tabellen, Listen, Header/Footer und Seitendekorationen
- Links, Named Destinations, Outlines und Inhaltsverzeichnis
- AcroForm-Felder und mehrere allgemeine Annotationstypen
- Tagged PDF / PDF/UA-Pfade
- PDF/A-Profile mit explizitem Support-Matrix-Ansatz
- Verschluesselung, Signaturpfad und strukturierte Debug-/Performance-Events

Dokumentationsstruktur
----------------------

[](#dokumentationsstruktur)

Die Projekt-Doku ist jetzt auf mehrere Dateien verteilt, damit Maintainer und Nutzer nicht alles aus einer einzigen README herauslesen muessen:

- [docs/architecture.md](/home/skalicki/Projekte/kalle/pdf2/docs/architecture.md)Architektur, Schichten, Render-Pipeline, interne Builder und Writer
- [docs/public-api.md](/home/skalicki/Projekte/kalle/pdf2/docs/public-api.md)Einstiegspunkte, Public API, Feature-Ueberblick und typische Nutzung
- [docs/image-import.md](/home/skalicki/Projekte/kalle/pdf2/docs/image-import.md)Bildimport-Pipeline, Formatgrenzen und Decoder-Aufbau
- [docs/profiles-and-compliance.md](/home/skalicki/Projekte/kalle/pdf2/docs/profiles-and-compliance.md)Standardprofile, PDF/A-, PDF/UA-Scope, Signatur- und Verschluesselungsgrenzen
- [docs/development.md](/home/skalicki/Projekte/kalle/pdf2/docs/development.md)Setup, Docker, Tests, Regressionen, Diagnose und interne Werkzeuge

Schnellstart
------------

[](#schnellstart)

```
use Kalle\Pdf\Pdf;

$document = Pdf::document()
    ->title('Hello PDF')
    ->author('Example')
    ->text('Hallo Welt')
    ->build();

Pdf::writeToFile($document, __DIR__ . '/hello.pdf');
```

Die zwei wichtigsten API-Pfade sind:

- `Pdf` als kleine Fassade fuer Builder, Rendern, Signieren und Textmessung
- `Document::make()` bzw. `DefaultDocumentBuilder::make()` fuer direkte Arbeit mit der Builder-API

Architektur in Kurzform
-----------------------

[](#architektur-in-kurzform)

Der aktuelle Renderpfad besteht aus drei getrennten Stufen:

1. `DefaultDocumentBuilder` sammelt Dokumentzustand, Seiteninhalte und semantische Zusatzstrukturen.
2. `DocumentSerializationPlanBuilder` validiert das Dokument und baut einen PDF-Objektgraphen.
3. `Writer\Renderer` serialisiert den fertigen Plan in Datei, Stream oder String.

Diese Trennung ist fuer das Projekt zentral, weil Features wie PDF/A, Tagged PDF, Attachments, Formulare oder Verschluesselung nicht nur reine Surface-API sind, sondern den finalen Objektgraphen beeinflussen.

Aktueller Stand
---------------

[](#aktueller-stand)

Der aktuelle Stand ist funktional breit, aber bewusst nicht "alles aus der PDF-Spezifikation". Wiederkehrendes Muster im Code und in den Tests:

- lieber enger, explizit validierter Positivpfad
- klare Fehler bei nicht freigegebenen Kombinationen
- profilabhaengige Regeln fuer PDF/A und PDF/UA
- Regressionen fuer Low-level-PDF-Struktur und Standardscope

Die nachfolgenden README-Abschnitte beschreiben weiterhin konkrete Teilbereiche und Beispielpfade des aktuellen Funktionsumfangs.

PDF/A Scope
-----------

[](#pdfa-scope)

Der aktuelle PDF/A-Scope ist bewusst konservativ und folgt eher dem Prinzip "hart blocken statt halb erlauben":

- `PDF/A-1b`: stabil fuer den aktuell freigegebenen Scope mit eingebetteten Fonts, XMP/Info-Metadaten, OutputIntent, Annotation-APs und den geprueften Farbpfaden.
- `PDF/A-1a`: bewusst enger als das volle Normspektrum. Im Formularpfad sind nur `TextField`, `ComboBoxField` und `ListBoxField` freigegeben. Popup-Related-Objects sowie URI-Annotation-Actions sind im PDF/A-1-Pfad explizit verboten.
- `PDF/A-2b`: explizit freigegeben fuer den kleinen validierten Annotation- und Form-Scope. Erlaubt sind aktuell `Link`, `Text`, `Highlight` und `FreeText` sowie `TextField`, `Checkbox`, `RadioButtonGroup`, `ComboBox` und `ListBox`. Popup-Related-Objects, Seiten-Dateianhang-Annotationen, Push Buttons und Signaturfelder bleiben gesperrt.
- `PDF/A-2a`: freigegeben fuer den aktuellen Tagged-Pfad mit Strukturbaum, ParentTree, eingebetteten Unicode-Fonts, XMP/OutputIntent, getaggten `Link`/`Text`/`Highlight`/`FreeText`-Annotationen und dem getaggten Form-Subset aus `TextField`, `Checkbox`, `RadioButtonGroup`, `ComboBox` und `ListBox`. Andere Seitenannotationen, Push Buttons und Signaturfelder bleiben im aktuellen A-Scope gesperrt.
- `PDF/A-2u`: robuster Positivpfad fuer Unicode-Fonts, Metadaten, OutputIntent sowie denselben kleinen Annotation- und Form-Scope wie `PDF/A-2b`. Externe `URI`-Links sind in diesem Profil ausdruecklich erlaubt. Popup-Related-Objects, Seiten-Dateianhang-Annotationen, Push Buttons und Signaturfelder bleiben gesperrt.
- `PDF/A-3b`: dokumentweite Embedded Files und Associated Files am Catalog sind im aktuellen Scope abgedeckt. Zusaetzlich ist derselbe kleine Form-Subset wie bei `PDF/A-2b` freigegeben. Seitennahe Dateianhang-Annotationen, Popup-Related-Objects, Push Buttons und Signaturfelder bleiben gesperrt.
- `PDF/A-3a`: freigegeben fuer den aktuellen Tagged-Pfad mit Strukturbaum, ParentTree, eingebetteten Unicode-Fonts, XMP/OutputIntent, getaggten `Link`/`Text`/`Highlight`/`FreeText`-Annotationen, dem getaggten Form-Subset und dokumentweiten Associated Files am Catalog. Andere Seitenannotationen, Push Buttons und Signaturfelder bleiben im aktuellen A-Scope gesperrt.
- `PDF/A-3u`: erweitert den aktuellen `PDF/A-3b`-Scope um den extractable-Unicode-Font-Pfad. Dokumentweite Associated Files am Catalog sowie derselbe kleine Annotation- und Form-Scope bleiben freigegeben; seitennahe Dateianhang-Annotationen, Popup-Related-Objects, Push Buttons und Signaturfelder bleiben gesperrt.
- `PDF/A-4`: im aktuellen engen PDF-2.0-Basisscope freigegeben. Metadata, Revision Marker, der explizite `Link`/`Text`/`Highlight`/`FreeText`-Annotationscope und der kleine AcroForm-Subset aus `TextField`, `Checkbox`, `RadioButtonGroup`, `ComboBox` und `ListBox` sind abgesichert; Attachments, Push Buttons und Signaturfelder bleiben blockiert.
- `PDF/A-4e`: im aktuellen engen Engineering-Scope freigegeben. Zusaetzlich zum PDF/A-4-Basisscope sind ein kleiner optional-content-Subset (`OCG`, `OCMD`, `VE`, `Configs`), ein kleiner `RichMedia`-Subset und ein kleiner `3D`-Subset freigegeben. `SetOCGState`-Pushbuttons sowie breitere RichMedia-/3D-/Presentation-/Engineering-Wiring-Varianten bleiben blockiert.
- `PDF/A-4f`: im aktuellen engen Scope freigegeben. Zusaetzlich zum PDF/A-4-Basisscope sind dokumentweite Associated Files abgesichert; Engineering-Features, Push Buttons und Signaturfelder bleiben blockiert.

Die Engine validiert PDF/A-1 inzwischen nicht nur auf vorbereiteten Zwischenstrukturen, sondern auch gegen den finalen Objektgraphen vor dem Schreiben. Fuer PDF/A-2/3 laeuft derselbe finale Objektgraph-Check inzwischen fuer den gemeinsamen Catalog-, Metadata-, OutputIntent-, Attachment- und Seitenpfad. Fuer die PDF/A-4-Familie gibt es jetzt ebenfalls dedizierte Regressionen fuer `PDF/A-4`, `PDF/A-4e` und `PDF/A-4f`. Die PDF/A-Regressionsskripte pruefen die geschriebenen Dateien zusaetzlich mit `qpdf --check`, bevor veraPDF laeuft.

Struktur
--------

[](#struktur)

Der Quellcode ist jetzt grob nach Verantwortlichkeiten organisiert:

```
src/
├─ Color/
├─ Debug/
├─ Document/
├─ Drawing/
├─ Font/
├─ Image/
├─ Page/
├─ Text/
├─ Writer/
└─ Pdf.php

```

Observability
-------------

[](#observability)

Die Engine unterstuetzt ein fokussiertes Debug-/Observability-System fuer Lifecycle-, PDF-Struktur- und Performance-Events. Es erzeugt keine visuelle Debug-Ausgabe im PDF, sondern nur strukturierte Events ueber einen `DebugSink`.

```
use Kalle\Pdf\Debug\DebugConfig;
use Kalle\Pdf\Document\Document;

$document = Document::make()
    ->title('Rechnung 2026-001')
    ->debug(
        DebugConfig::json()
            ->toStdout()
    )
    ->build();
```

- Lifecycle-Logging protokolliert Engine-Schritte wie `document.created`, `page.added`, `write.started` und `write.finished`.
- PDF-Struktur-Debugging protokolliert PDF-nahe Ereignisse wie `object.created`, `object.serialized`, `stream.serialized`, `xref.written` und `trailer.written`.
- Performance-Logging misst Render- und Write-Schritte wie `document.render`, `page.render` und `file.write` inklusive Laufzeit- und Speichermetriken.

Optional kann statt eines formatbasierten Sinks auch direkt ein PSR-3-Logger angebunden werden:

```
use Kalle\Pdf\Debug\LogLevel;

$document = Document::make()
    ->debug(DebugConfig::make()->logLifecycle(LogLevel::Info))
    ->withLogger($logger)
    ->build();
```

Ein groesseres ausfuehrbares Beispiel mit zehn Seiten und JSON-Logger liegt in `examples/observability.php`.

Fuer Docker- oder Container-Setups kann direkt in `stdout` oder `stderr` geloggt werden:

```
$document = Document::make()
    ->debug(
        DebugConfig::json()
            ->toStdout()
    )
    ->build();
```

Fuer strukturierte JSON-Lines ohne PSR-3 steht `JsonDebugSink` als direkte Low-Level-Option zur Verfuegung, zum Beispiel fuer Dateien, `stdout` oder `stderr`:

```
$document = Document::make()
    ->debug(
        DebugConfig::json()
            ->toFile(__DIR__ . '/var/pdf-debug.log')
    )
    ->build();
```

Fuer lesbare Terminal- oder Datei-Ausgaben steht ausserdem `text()` zur Verfuegung:

```
$document = Document::make()
    ->debug(
        DebugConfig::text()
            ->toStderr()
    )
    ->build();
```

Fuer Tests oder lokale Inspektion kann `InMemoryDebugSink` alle Events im Speicher sammeln:

```
use Kalle\Pdf\Debug\InMemoryDebugSink;

$sink = new InMemoryDebugSink();
$document = Document::make()
    ->debug(DebugConfig::make()->sink($sink))
    ->build();

$records = $sink->records();
```

Bilder
------

[](#bilder)

Das Bildfundament ist über `ImageSource` und `ImagePlacement` angebunden. Die aktuelle API erwartet bereits vorbereitete Bilddaten, die als PDF-Image-XObject eingebettet werden.

```
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Image\ImageColorSpace;
use Kalle\Pdf\Image\ImagePlacement;
use Kalle\Pdf\Image\ImageSource;

$document = DefaultDocumentBuilder::make()
    ->image(
        ImageSource::jpeg($jpegBytes, 600, 300, ImageColorSpace::RGB),
        ImagePlacement::absolute(left: 40, bottom: 500, width: 180),
    )
    ->build();
```

Fuer vorbereitete Rasterdaten stehen neben `ImageSource::jpeg(...)` jetzt auch explizite PDF-Filterfabriken wie `ImageSource::flate(...)`, `ImageSource::lzw(...)`, `ImageSource::runLength(...)` und `ImageSource::ccittFax(...)` bereit. Fuer rohe Rasterdaten kann `ImageSource::compressed(...)` eine kompakte PDF-Kompression automatisch auswaehlen. Dieser Auswahlpfad wird inzwischen auch von dekodierten Rasterimporten fuer nicht-indizierte Pixelbilder genutzt, damit `fromPath(...)` und manuell erzeugte Raster dieselbe Kompressionsstrategie teilen. `ImageSource::monochrome(...)` packt rohe 1-Bit-Zeilen in ein bilevel PDF-Bild und beruecksichtigt dabei jetzt auch CCITT-Fax-Kompression; `ImageSource::monochromeCcitt(...)` erzwingt diesen Pfad explizit.

Aktueller Bild-Scope von `ImageSource::fromPath(...)`:

FormatUnterstuetztBewusst nicht unterstuetztJPEGGray, RGB, CMYK als Direct-Pass-Throughexotische JPEG-Varianten ausserhalb der erkannten KanalzahlenPNG8-Bit Gray/RGB/Indexed, Gray+Alpha, RGBA, `tRNS`, nicht interlacedandere Bit-Tiefen, Adam7-InterlacingGIFstatisch, ein Full-Canvas-Frame, Palette, transparenter IndexAnimation, Interlacing, partielle FramesBMPunkomprimiert 24-Bit RGB, 32-Bit RGBA, 32-Bit `BI_BITFIELDS` mit byte-ausgerichteten RGB(A)-MaskenPalette, RLE, weitere Bit-Tiefen/MaskenvariantenTIFFSingle-IFD bilevel uncompressed, bilevel CCITT, 8-Bit Gray uncompressed/PackBits/LZW/Deflate mit Predictor 2, 8-Bit RGB uncompressed/PackBits/LZW/Deflate mit Predictor 2, 8-Bit CMYK uncompressed/PackBits/LZW/Deflate mit Predictor 2, 8-Bit Palette uncompressed/PackBits/LZW/DeflateMulti-Page, PlanarConfiguration != 1, Palette-TIFFs mit Predictor und weitere exotische VariantenWebPoptional ueber vorhandene GD-WebP-Runtime, Single-Frame, verlustbehaftet und je nach Runtime auch lossless, RGB mit optionaler Soft-Mask aus Alpha; im Docker-PHP-Container ist GD+WebP jetzt vorbereitetohne GD-WebP-Support bleibt der Importpfad explizit gesperrt; Animation bleibt bewusst ausgeschlossenDie Import-Architektur ist in [docs/image-import.md](/home/skalicki/Projekte/kalle/pdf2/docs/image-import.md) kurz dokumentiert.

Graphics
--------

[](#graphics)

`pdf2` stellt jetzt eine kleine explizite Public API fuer grafische Primitive bereit. Die erste Iteration deckt Linie, Rechteck, Rounded Rectangle und freie Pfade ab und bleibt bewusst konservativ: keine Raw-Content-Injection, keine Opacity-API und keine breite Formensammlung aus dem Altprojekt `pdf(1)`.

```
use Kalle\Pdf\Color\Color;
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Drawing\GraphicsAccessibility;
use Kalle\Pdf\Drawing\Path;
use Kalle\Pdf\Drawing\StrokeStyle;

$triangle = Path::builder()
    ->moveTo(60, 640)
    ->lineTo(120, 720)
    ->lineTo(180, 640)
    ->close()
    ->build();

$document = DefaultDocumentBuilder::make()
    ->line(40, 760, 200, 760, new StrokeStyle(1.5, Color::gray(0.25)))
    ->rectangle(40, 680, 120, 50, fillColor: Color::hex('#dbeafe'))
    ->roundedRectangle(180, 680, 120, 50, 10, new StrokeStyle(1, Color::hex('#1d4ed8')))
    ->path(
        $triangle,
        new StrokeStyle(1, Color::hex('#991b1b')),
        Color::hex('#fecaca'),
        GraphicsAccessibility::alternativeText('Warning triangle'),
    )
    ->build();
```

In Tagged-PDF-Profilen rendert die Graphics-API standardmaessig weiter als `/Artifact`. Semantisch relevante Vektorgrafik kann explizit ueber `GraphicsAccessibility::alternativeText(...)` als `/Figure` in die Tagged-Struktur aufgenommen werden; dekorative Grafik bleibt Artifact.

Header und Footer
-----------------

[](#header-und-footer)

Dokumentweite Seitendekorationen koennen ueber `header()` und `footer()` registriert werden. Beide Callbacks laufen fuer jede erzeugte Seite, auch bei `newPage()` und automatischen Seitenumbruechen. Sie rendern in einem isolierten Seitendekorations-Context und greifen daher nicht in den normalen Content-Flow ein.

Ausführbare Beispiele liegen in `examples/header-footer.php` und `examples/header-footer-filters.php`.

- Signatur: `static function (PageDecorationContext $page, int $pageNumber): void`
- Reihenfolge pro Seite: Header, regulaerer Seiteninhalt, Footer
- Der Context stellt ueber `$page->page()` die aktuelle Seite und deren `contentArea()` bereit.
- Die Seitennummer ist 1-basiert.
- Fuer Seitennummern wie `Seite X von Y` steht zusaetzlich `$page->totalPages()` bereit.
- Bedingte Dekorationen koennen direkt im Callback ueber `$page->isFirstPage()`, `$page->isLastPage()` oder `$page->pageNumber()` umgesetzt werden.
- Fuer haeufige Faelle gibt es ausserdem `pageNumbers(...)` sowie praedikatbasierte Varianten `headerOn(...)` und `footerOn(...)`.
- In Tagged-PDF-Profilen werden Header/Footer als Artefakt behandelt und nicht in die logische Dokumentstruktur aufgenommen.

```
use Kalle\Pdf\Document\PageDecorationContext;
use Kalle\Pdf\Text\TextOptions;

$document = DefaultDocumentBuilder::make()
    ->header(static function (PageDecorationContext $page, int $pageNumber): void {
        $page->text('Projektstatus', TextOptions::make(
            left: $page->page()->contentArea()->left,
            bottom: $page->page()->contentArea()->top,
            fontSize: 12,
        ));
    })
    ->footer(static function (PageDecorationContext $page, int $pageNumber): void {
        $page->text('Seite ' . $pageNumber, TextOptions::make(
            left: $page->page()->contentArea()->left,
            bottom: $page->page()->contentArea()->bottom + 12,
            fontSize: 10,
        ));
    })
    ->text('Langer Inhalt ...')
    ->build();
```

```
$document = DefaultDocumentBuilder::make()
    ->pageNumbers(
        TextOptions::make(left: 40, bottom: 20, fontSize: 10),
        'Seite {{page}} von {{pages}}',
    )
    ->headerOn(
        static fn (PageDecorationContext $page): bool => !$page->isFirstPage(),
        static function (PageDecorationContext $page): void {
            $page->text('Kapitelkopf', TextOptions::make(
                left: $page->page()->contentArea()->left,
                bottom: $page->page()->contentArea()->top,
                fontSize: 12,
            ));
        },
    )
    ->text('Langer Inhalt ...')
    ->build();
```

Links
-----

[](#links)

Die erste Annotations-Anbindung unterstützt aktuell schlanke Link-Annotationen mit explizitem Rechteck auf der Seite, sowohl fuer externe URLs als auch fuer interne Spruenge auf andere Seiten, Zielpositionen oder Named Destinations. Text kann ausserdem direkt mit `TextOptions(link: ...)` oder mit mehreren unterschiedlich verlinkten `TextSegment`-Runs an Link-Annotationen gebunden werden. Fuer explizitere Inline-Link-Spans steht `TextLink` zur Verfuegung, damit sichtbarer Text, Annotation-`/Contents`, PDF/UA-Alternativtext und Gruppierung getrennt steuerbar bleiben.

Fuer explizite Zeilen ohne eingebettete Newline-Strings steht ausserdem `textLines(...)` zur Verfuegung:

```
$document = DefaultDocumentBuilder::make()
    ->textLines([
        'DEIN FIRMENNAME',
        'Strasse Hausnummer',
        '12345 Musterstadt',
    ], TextOptions::make(fontSize: 12))
    ->build();
```

Explizite Tagged-PDF-Leaf-Rollen fuer Text koennen direkt ueber `TextOptions(tag: ...)` gesetzt werden:

```
$document = DefaultDocumentBuilder::make()
    ->profile(\Kalle\Pdf\Document\Profile::pdfA1a())
    ->title('Archive Copy')
    ->language('de-DE')
    ->text('Zitat', TextOptions::make(
        embeddedFont: \Kalle\Pdf\Font\EmbeddedFontSource::fromPath('/path/to/font.ttf'),
        tag: \Kalle\Pdf\Document\TaggedPdf\TaggedStructureTag::BLOCK_QUOTE,
    ))
    ->build();
```

Dekorativer Text wie Briefkopf, Seitenkopf oder Wasserzeichen kann ueber `TextOptions(semantic: TextSemantic::ARTIFACT)` aus der logischen Struktur herausgenommen werden:

```
$document = DefaultDocumentBuilder::make()
    ->profile(\Kalle\Pdf\Document\Profile::pdfA1a())
    ->title('Archive Copy')
    ->language('de-DE')
    ->textLines(['DEIN FIRMENNAME', 'Strasse Hausnummer'], TextOptions::make(
        semantic: \Kalle\Pdf\Text\TextSemantic::ARTIFACT,
    ))
    ->build();
```

Dokumentweite PDF-Outlines/Bookmarks werden ebenfalls unterstuetzt. Neben Top-Level-Bookmarks koennen Outlines explizit verschachtelt, offen oder geschlossen markiert und mit Stilinformationen versehen werden. Zusaetzlich zu `XYZ` werden auch `Fit`, `FitH`, `FitR`, benannte Ziele sowie lokale und externe `GoTo`-Actions unterstuetzt.

```
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Document\Outline;
use Kalle\Pdf\Document\OutlineStyle;
use Kalle\Pdf\Color\Color;

$document = DefaultDocumentBuilder::make()
    ->namedDestination('intro')
    ->outline('Start')
    ->text('Seite 1')
    ->newPage()
    ->outlineAt('Kapitel 1', 2)
    ->outlineChild('Abschnitt 1.1')
    ->outlineSiblingClosed('Abschnitt 1.2')
    ->addOutline(
        Outline::fitHorizontal('Anhang', 3, 720)
            ->withStyle((new OutlineStyle())->withColor(Color::hex('#1d4ed8'))->withBold())
            ->asGoToAction(),
    )
    ->addOutline(Outline::named('Einleitung', 'intro', 1))
    ->addOutline(Outline::fit('Externes PDF', 4)->withDestination(
        Outline::fit('Externes PDF', 4)->destination->asRemoteGoTo('appendix.pdf', true),
    ))
    ->text('Seite 2')
    ->build();
```

Fuer manuelle Overflow-Seiten gibt es zusaetzlich `startOverflowPage()`. Im Unterschied zu `newPage()` uebernimmt diese Methode die aktuelle Seitengroesse, Raender und weitere aktive Seiteneinstellungen der laufenden Seite:

```
$document = DefaultDocumentBuilder::make()
    ->text('Einleitung')
    ->startOverflowPage()
    ->text('Fortsetzung auf der naechsten Overflow-Seite')
    ->build();
```

Der aktuelle automatische Seitenumbruch betrifft vor allem Tabellen. Falls dieser Pfad bewusst unterbunden werden soll, kann er ueber `disableAutoPageBreak()` deaktiviert werden:

```
$builder = DefaultDocumentBuilder::make()
    ->disableAutoPageBreak()
    ->table($table);
```

Die rechteckbasierten Annotationen (`Link`, `Text`, `Highlight`) nutzen ausserdem jetzt ein kleines gemeinsames Metadaten-Fundament ueber `AnnotationMetadata`, auf das die jeweiligen `...Options`-Value-Objects aufsetzen. Dasselbe Muster deckt inzwischen auch weitere Markup- und Geometrie-Typen wie `Underline`, `StrikeOut`, `Squiggly`, `Stamp`, `Square`, `Circle`, `Caret`, `Ink`, `Line`, `PolyLine` und `Polygon` ab. Popups koennen ueber `popupAnnotation(...)` weiterhin an die zuletzt hinzugefuegte kompatible Seitenannotation gebunden werden oder explizit ueber `lastPageAnnotationReference()` plus `popupAnnotationFor(...)`. Seitengebundene Dateianhaenge setzen auf derselben Builder-API auf und koennen entweder neue Attachments anlegen oder vorhandene Dokument-Attachments ueber `existingFileAttachmentAnnotation(...)` wiederverwenden.

Fuer einfache Kommentar-Notizen gibt es ausserdem eine kleine `Text`-Annotation mit festem Rechteck und eigenem `/AP`-Stream, die sich damit auch fuer den aktuellen PDF/A-2u-Pfad eignet. Dasselbe gilt jetzt fuer eine schlanke `Highlight`-Annotation mit festen `QuadPoints` und fuer `FreeText`, das seinen Appearance-Stream mit dem verwendeten Seitenfont rendert.

```
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Page\FreeTextAnnotationOptions;
use Kalle\Pdf\Page\HighlightAnnotationOptions;
use Kalle\Pdf\Page\LinkAnnotationOptions;
use Kalle\Pdf\Page\TextAnnotationOptions;
use Kalle\Pdf\Text\TextLink;
use Kalle\Pdf\Text\TextSegment;

$builder = DefaultDocumentBuilder::make()
    ->text('Projektseite')
    ->link('https://example.com', 40, 500, 120, 16, 'Projektseite oeffnen')
    ->namedDestination('intro')
    ->newPage()
    ->linkToPage(1, 40, 500, 120, 16, 'Zurueck zur ersten Seite')
    ->linkToPagePosition(1, 72, 720, 40, 470, 160, 16, 'Zur Ueberschrift')
    ->text('Zur Einleitung', new \Kalle\Pdf\Text\TextOptions(
        link: \Kalle\Pdf\Page\LinkTarget::namedDestination('intro'),
    ))
    ->text([
        TextSegment::link(
            'Docs',
            TextLink::externalUrl(
                'https://example.com/docs',
                contents: 'Open docs section',
                accessibleLabel: 'Read the documentation section',
                groupKey: 'docs-link',
            ),
        ),
        new TextSegment(' und '),
        TextSegment::link('API', TextLink::externalUrl('https://example.com/api')),
    ])
    ->linkWithOptions(
        'https://example.com/spec',
        40,
        500,
        140,
        16,
        new LinkAnnotationOptions(
            contents: 'Open specification',
            accessibleLabel: 'Read the specification document',
            groupKey: 'spec-link',
        ),
    )
    ->textAnnotationWithOptions(40, 450, 18, 18, 'Kurzer Kommentar', new TextAnnotationOptions(
        title: 'QA',
        icon: 'Comment',
        open: true,
    ))
    ->highlightAnnotationWithOptions(40, 420, 120, 12, new HighlightAnnotationOptions(
        color: \Kalle\Pdf\Color\Color::rgb(1, 1, 0),
        contents: 'Markierung',
        title: 'QA',
    ))
    ->underlineAnnotation(40, 400, 120, 12, \Kalle\Pdf\Color\Color::rgb(1, 0, 0), 'Unterstrichen', 'QA')
    ->squareAnnotationWithOptions(40, 350, 140, 36, new \Kalle\Pdf\Page\ShapeAnnotationOptions(
        borderColor: \Kalle\Pdf\Color\Color::rgb(0.8, 0.1, 0.1),
        fillColor: \Kalle\Pdf\Color\Color::rgb(1, 0.98, 0.9),
        borderStyle: \Kalle\Pdf\Page\AnnotationBorderStyle::dashed(1.5),
        contents: 'Bereichshinweis',
        title: 'QA',
        subject: 'Freigabebereich',
    ))
    ->lineAnnotationWithOptions(40, 310, 180, 330, new \Kalle\Pdf\Page\LineAnnotationOptions(
        color: \Kalle\Pdf\Color\Color::rgb(0, 0.2, 0.8),
        startStyle: \Kalle\Pdf\Page\LineEndingStyle::CIRCLE,
        endStyle: \Kalle\Pdf\Page\LineEndingStyle::CLOSED_ARROW,
        metadata: new \Kalle\Pdf\Page\AnnotationMetadata(
            contents: 'Verweislinie',
            title: 'QA',
            subject: 'Pruefpfad',
        ),
    ));

$lineAnnotation = $builder->lastPageAnnotationReference();

$document = $builder
    ->popupAnnotationFor($lineAnnotation, 190, 300, 160, 70, true)
    ->attachment('demo.txt', 'hello', 'Demo attachment', 'text/plain')
    ->existingFileAttachmentAnnotation(
        'demo.txt',
        40,
        270,
        12,
        14,
        'Graph',
        'Anhang',
    )
    ->fileAttachmentAnnotationWithOptions(
        'demo.txt',
        new \Kalle\Pdf\Document\Attachment\EmbeddedFile('hello', 'text/plain'),
        70,
        270,
        12,
        14,
        new \Kalle\Pdf\Page\FileAttachmentAnnotationOptions(
            description: 'Demo attachment',
            icon: 'Graph',
            contents: 'Anhang',
        ),
    )
    ->freeTextAnnotation(
        'Kommentar im Dokument',
        40,
        380,
        140,
        36,
        new \Kalle\Pdf\Text\TextOptions(fontSize: 12, color: \Kalle\Pdf\Color\Color::rgb(0, 0, 0.4)),
        \Kalle\Pdf\Color\Color::rgb(0.2, 0.2, 0.2),
        \Kalle\Pdf\Color\Color::rgb(1, 1, 0.8),
        'QA',
    )
    ->freeTextAnnotationWithOptions(
        'Zweiter Kommentar',
        40,
        330,
        140,
        36,
        new \Kalle\Pdf\Text\TextOptions(fontSize: 12),
        new FreeTextAnnotationOptions(
            textColor: \Kalle\Pdf\Color\Color::rgb(0, 0, 0.4),
            borderColor: \Kalle\Pdf\Color\Color::rgb(0.2, 0.2, 0.2),
            fillColor: \Kalle\Pdf\Color\Color::rgb(1, 1, 0.8),
            metadata: new \Kalle\Pdf\Page\AnnotationMetadata(title: 'QA'),
        ),
    )
    ->build();
```

Die aktuelle `pdf2`-Implementierung deckt damit nun auch Popups und seitengebundene Dateianhang-Annotationen ab. Fuer PDF/UA-1 werden neben Links und Formularfeldern jetzt auch allgemeine Seitenannotationen als getaggte `Annot`-StructElems mit `/OBJR`, `/StructParent` und Alternativtext geschrieben. Die Freigabe bleibt dabei bewusst streng und explizit: nur Annotationtypen mit dediziertem PDF/UA-Opt-in werden in diesem Pfad akzeptiert, und fehlt fuer eine solche Annotation ein brauchbarer Alternativtext, scheitert `pdf2` weiterhin explizit.

```
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Document\TableOfContents\TableOfContentsLeaderStyle;
use Kalle\Pdf\Document\TableOfContents\TableOfContentsOptions;
use Kalle\Pdf\Document\TableOfContents\TableOfContentsPlacement;
use Kalle\Pdf\Document\TableOfContents\TableOfContentsStyle;

$document = DefaultDocumentBuilder::make()
    ->outline('Start')
    ->text('Seite 1')
    ->newPage()
    ->outlineAt('Details', 2, 72, 640)
    ->text('Seite 2')
    ->tableOfContents(new TableOfContentsOptions(
        placement: TableOfContentsPlacement::start(),
        style: new TableOfContentsStyle(
            leaderStyle: TableOfContentsLeaderStyle::DOTS,
        ),
    ))
    ->build();
```

Alternativ koennen TOC-Eintraege explizit ueber `tableOfContentsEntry(...)` oder `tableOfContentsEntryAt(...)` registriert werden. In der aktuellen ersten Iteration verwendet die TOC entweder die expliziten TOC-Eintraege oder, wenn keine gesetzt sind, die vorhandenen Outlines. Platzierungen am Anfang, Ende oder nach einer bestimmten Seite werden unterstuetzt; logische Seitennummerierung und Header/Footer auf automatisch erzeugten TOC-Seiten sind gegenueber dem Altprojekt noch nicht uebernommen.

Ein ausfuehrbares Beispiel liegt in `examples/table-of-contents.php`.

Tabellen
--------

[](#tabellen)

Die aktuelle Tabellen-API unterstützt Textzellen mit festen, automatischen oder proportionalen Spaltenbreiten, Padding, Borders, `colspan`/`rowspan`, optionale Caption-, Header- und Footer-Zeilen sowie deterministische Seitenumbrüche zwischen ganzen Zeilen bzw. zusammenhängenden `rowspan`-Gruppen. Header- und Footer-Zeilen können auf Folgeseiten wiederholt werden. Für Tagged-PDF-Profile wird zusätzlich eine minimale Tabellenstruktur mit `Table`, `Caption`, `TR`, `TH` und `TD` geschrieben.

```
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Document\Table;
use Kalle\Pdf\Document\TableCaption;
use Kalle\Pdf\Document\TableColumn;
use Kalle\Pdf\Document\TableHeaderScope;
use Kalle\Pdf\Document\TableOptions;
use Kalle\Pdf\Document\TablePlacement;
use Kalle\Pdf\Document\TableRow;
use Kalle\Pdf\Layout\Table\Border;
use Kalle\Pdf\Layout\Table\CellPadding;
use Kalle\Pdf\Text\TextAlign;
use Kalle\Pdf\Text\TextLink;
use Kalle\Pdf\Text\TextOptions;
use Kalle\Pdf\Text\TextSegment;

$table = Table::define(
    TableColumn::auto(),
    TableColumn::proportional(1),
    TableColumn::auto(),
)
    ->withOptions(TableOptions::make(
        caption: TableCaption::text('Produktuebersicht'),
        placement: TablePlacement::absolute(left: 72, top: 321.89, width: 320),
        cellPadding: CellPadding::all(6),
        border: Border::all(0.5),
        textOptions: TextOptions::make(fontSize: 10, lineHeight: 12),
        repeatHeaderOnPageBreak: true,
        repeatFooterOnPageBreak: true,
    ))
    ->withHeaderRows(
        TableRow::fromTexts('Artikel', 'Beschreibung', 'Betrag'),
    )
    ->withRows(
        TableRow::fromCells(
            \Kalle\Pdf\Document\TableCell::text('A-100', rowspan: 2)
                ->withHeaderScope(TableHeaderScope::ROW)
                ->withPadding(CellPadding::symmetric(8, 10)),
            \Kalle\Pdf\Document\TableCell::segments(
                TextSegment::plain('Kompakter Einstieg in das Tabellenlayout von pdf2. '),
                TextSegment::link('Mehr dazu', TextLink::externalUrl('https://example.com/docs/tables')),
            )
                ->withHorizontalAlign(TextAlign::RIGHT)
                ->withBorder(Border::all(1)),
            \Kalle\Pdf\Document\TableCell::text('490,00 EUR')
                ->withHorizontalAlign(TextAlign::RIGHT),
        ),
        TableRow::fromCells(
            \Kalle\Pdf\Document\TableCell::text('Mit zusammenhängender Zeilengruppe über zwei Tabellenzeilen.'),
            \Kalle\Pdf\Document\TableCell::text('90,00 EUR')
                ->withHorizontalAlign(TextAlign::RIGHT),
        ),
    )
    ->withFinalFooterRows(
        TableRow::fromTexts('Summe', '2 Positionen', '580,00 EUR'),
    );

$document = DefaultDocumentBuilder::make()
    ->table($table)
    ->build();
```

`TableColumn::auto()` misst die Mindestbreite einer Spalte aus dem längsten untrennbaren Zellinhalt plus Padding. Das ist nützlich für Datums-, Ticket-, Mengen- oder Betragsspalten. `TableColumn::proportional(...)` verteilt den nach `fixed(...)`- und `auto()`-Spalten verbleibenden Platz.

`withFooterRows(...)` bleibt als Alias für `withFinalFooterRows(...)` erhalten. Für Mehrseiten-Layouts mit Übertrag und Schlussbetrag stehen zusätzlich `withRepeatedFooterRows(...)` und `withFinalFooterRows(...)` zur Verfügung.

Für laufende Seitensummen oder andere seitenabhängige Werte können `withRepeatedFooterRows(...)` und `withFinalFooterRows(...)` alternativ auch einen einzelnen Callback statt fester `TableRow`-Objekte annehmen. Der Callback erhält einen `TableFooterContext` mit `pageNumber`, `completedBodyRowCount`, `totalBodyRowCount` und `isLastPage`.

Ein umfangreicheres Beispiel liegt in `examples/table.php`; ein professionelles Mehrseiten-Beispiel mit wiederholtem Header, wiederholtem Zwischenfooter und finalem Schlussfooter in `examples/table-repeated-footer.php`. Die aktuelle Tabellen-API deckt damit auch absolutes Start-`y`, Rich-Text-Zellen sowie Tagged-Table-Abschnitte `THead`, `TBody` und `TFoot` ab.

Listen
------

[](#listen)

Listen laufen ueber genau einen Builder-Einstieg. `ListOptions` steuert nur das Listenverhalten, `TextOptions` weiterhin das Textlayout.

```
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Document\ListOptions;
use Kalle\Pdf\Document\ListType;
use Kalle\Pdf\Text\TextOptions;

$document = DefaultDocumentBuilder::make()
    ->list(
        ['Erster Punkt', 'Zweiter Punkt'],
        new ListOptions(type: ListType::BULLET),
        TextOptions::make(left: 72, bottom: 720, width: 220, fontSize: 12),
    )
    ->list(
        ['Schritt eins', 'Schritt zwei'],
        new ListOptions(type: ListType::NUMBERED, start: 3, marker: '%d)'),
        TextOptions::make(width: 220, fontSize: 12, spacingAfter: 12),
    )
    ->build();
```

PDF/A-1a Umfang
---------------

[](#pdfa-1a-umfang)

Der folgende Umfang ist der offizielle `PDF/A-1a`-Support-Scope der aktuellen `pdf2`-Version. Abgesichert und freigegeben sind Ueberschriften und Absaetze (`H1` bis `H6`, `P`), zusaetzliche Text-Strukturtypen (`BibEntry`, `BlockQuote`, `Code`, `Em`, `Note`, `Quote`, `Reference`, `Span`, `Strong`, `Title`), frei modellierbare Struktur-Container (`Art`, `BlockQuote`, `Div`, `Index`, `NonStruct`, `Note`, `Part`, `Private`, `Sect`, `TOC`, `TOCI`), Listen (`L`, `LI`, `Lbl`, `LBody`), Tabellen (`Table`, `Caption`, `TR`, `TH`, `TD`), Bilder mit Alternativtext als `Figure`, getaggte Seitenannotationen als `Annot`, getaggte Formular-/Widgetfelder als `Form` sowie Link-Annotationen. Dokumente in diesem Pfad brauchen ausserdem einen gesetzten Sprachwert auf Dokumentebene (`/Lang`). Fuer diese Struktur prueft der Build-Pfad jetzt nicht nur die Existenz von Tagged Content, sondern auch die Konsistenz von `StructTreeRoot`, Dokumentwurzel, `ParentTree`, `/StructParents`, `MCID`-Zuordnung und der unterstuetzten Strukturelement-Hierarchie. Interne `PDF/A-1a`-Dokumentmodelle mit nicht freigegebenen Strukturtypen, mit leeren Tagged-Containern/Listen/Tabellen oder mit inkonsistenten Tagged-Referenzen werden explizit verworfen. Fuer Annotationen und Formularfelder erzwingt der `PDF/A-1a`-Pfad Alternativtexte, `/StructParent`-Eintraege und `OBJR`-basierte Strukturreferenzen.

Der aktuelle Formularumfang im offiziell freigegebenen `PDF/A-1a`-Pfad ist bewusst enger und umfasst nur `TextField`, `ComboBoxField` und `ListBoxField`. Checkboxen, Radio-Button-Gruppen, Push Buttons und Signaturfelder bleiben dort weiterhin gesperrt.

Verschluesselung
----------------

[](#verschluesselung)

Die aktuelle Encryption-API bleibt bewusst klein: Das Dokument bekommt ein explizites `Encryption`-Value-Object. Unterstuetzt sind aktuell `RC4-128`, `AES-128` und `AES-256` sowie eine kleine `Permissions`-API. PDF/A-Profile verbieten weiterhin Verschluesselung und scheitern deshalb mit einer klaren Exception.

```
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Document\Profile;
use Kalle\Pdf\Encryption\Encryption;
use Kalle\Pdf\Encryption\Permissions;

$document = DefaultDocumentBuilder::make()
    ->profile(Profile::pdf17())
    ->title('Encrypted Example')
    ->author('Kalle PDF')
    ->encryption(
        Encryption::aes256('user-secret', 'owner-secret')->withPermissions(
            new Permissions(
                print: false,
                modify: true,
                copy: false,
                annotate: true,
            ),
        ),
    )
    ->text('Confidential content')
    ->build();
```

Ein kleines ausfuehrbares Beispiel liegt in `examples/encryption.php`. Die externe `qpdf`-Regression fuer Permissions ist ueber `bin/test-encryption-permissions-regression.sh` abgedeckt.

Attachments
-----------

[](#attachments)

Dokumentweite eingebettete Dateien koennen direkt ueber den Builder registriert werden. Der aktuelle Stand unterstuetzt nur dokumentweite Associated Files am Catalog (`/AF`), keine objekt- oder seitennahe Zuordnung. Fuer PDF/A-3 werden solche Dokument-Attachments standardmaessig als Associated Files mit `AFRelationship /Data` serialisiert, solange keine explizite Beziehung gesetzt wird. Im PDF/A-Pfad sind Attachment-Dateinamen eindeutig zu halten und ein MIME-Typ ist fuer den Embedded-File-Stream Pflicht. Das gleiche Defaulting ist im aktuell freigegebenen, aber engen PDF/A-4f-Scope abgesichert; Basis-PDF/A-4 und PDF/A-4e blockieren Dokument-Attachments weiterhin. Standard-PDF 2.0 kann dokumentweite Associated Files mit explizitem `AFRelationship` ebenfalls serialisieren.

```
use Kalle\Pdf\Document\Attachment\AssociatedFileRelationship;
use Kalle\Pdf\Document\DefaultDocumentBuilder;
use Kalle\Pdf\Document\Profile;

$document = DefaultDocumentBuilder::make()
    ->profile(Profile::pdfA3b())
    ->title('Invoice with source data')
    ->attachment(
        'invoice.xml',
        '',
        'Machine-readable source data',
        'application/xml',
        AssociatedFileRelationship::DATA,
    )
    ->text('Human-readable invoice preview')
    ->build();
```

Alternativ laesst sich eine Datei direkt von der Platte einlesen:

```
$document = DefaultDocumentBuilder::make()
    ->attachmentFromFile(
        __DIR__ . '/fixtures/source-data.xml',
        description: 'Imported XML payload',
        mimeType: 'application/xml',
    )
    ->build();
```

Groessere Hybridbeispiele mit eingebettetem E-Rechnungs-XML liegen in `examples/e-invoicing/factur-x.php` und `examples/e-invoicing/xrechnung.php`.

Formulare
---------

[](#formulare)

Die aktuelle AcroForm-API deckt fuer Standard-PDFs Textfelder, Checkboxen, Radio Buttons, ComboBoxen, ListBoxen, Push Buttons und Signaturfelder ab. Fuer Tagged-Profile ist der Strukturpfad inzwischen breiter: Textfelder, Checkboxen, Radio-Button-Gruppen, ComboBoxen, ListBoxen, Signaturfelder und inerte Push Buttons werden als `/Form`-Strukturelemente mit `OBJR`/`ParentTree` serialisiert. Das gilt fuer den aktuellen `PDF/UA-1`-Pfad und fuer den offiziell freigegebenen `PDF/A-1a`-Form-Scope. Push Buttons mit URI-Aktionen bleiben fuer `PDF/A-1a` weiterhin explizit gesperrt, weil der Widget-`/A`-Pfad veraPDF-relevante PDF/A-1a-Verstoesse erzeugt. Sichtbare Signaturfelder werden direkt ueber die Formular-API erzeugt; die kryptographische Signaturintegration fuer unterstuetzte Dokumente ist unten als separater Signier-Schritt beschrieben.

```
use Kalle\Pdf\Document\DefaultDocumentBuilder;

$document = DefaultDocumentBuilder::make()
    ->text('Customer details')
    ->textField('customer_name', 40, 720, 180, 18, 'Ada Lovelace', 'Customer name')
    ->checkbox('accept_terms', 40, 680, 14, true, 'Accept terms')
    ->radioButton('delivery', 'standard', 40, 640, 12, true, groupAlternativeName: 'Delivery method')
    ->radioButton('delivery', 'express', 80, 640, 12, alternativeName: 'Express delivery')
    ->comboBox('status', 40, 600, 140, 18, ['new' => 'New', 'done' => 'Done'], 'done', 'Status')
    ->listBox('skills', 40, 540, 140, 48, ['php' => 'PHP', 'pdf' => 'PDF'], ['php', 'pdf'], 'Skills')
    ->pushButton('open_docs', 'Open docs', 40, 470, 120, 20, 'Open documentation', 'https://example.com/docs')
    ->signatureField('approval_signature', 40, 420, 140, 28, 'Approval signature')
    ->build();
```

PDF-Signaturen
--------------

[](#pdf-signaturen)

Kryptographische PDF-Signaturen koennen fuer vorhandene `signatureField(...)`-Widgets direkt ueber die OpenSSL-PHP-Erweiterung erzeugt werden. Der aktuelle Stand unterstuetzt detached CMS-/PKCS#7-Signaturen fuer unverschluesselte Standard-PDFs. Verschluesselte Dokumente und mehrere inkrementelle Signaturrunden sind bewusst noch nicht Teil der API.

```
use Kalle\Pdf\Document\Signature\OpenSslPemSigningCredentials;
use Kalle\Pdf\Document\Signature\PdfSignatureOptions;
use Kalle\Pdf\Pdf;

$document = Pdf::document()
    ->text('Approval')
    ->signatureField('approval_signature', 40, 420, 140, 28, 'Approval signature')
    ->build();

$signedPdf = Pdf::signedContents(
    $document,
    new OpenSslPemSigningCredentials($certificatePem, $privateKeyPem, $privateKeyPassphrase),
    new PdfSignatureOptions(
        fieldName: 'approval_signature',
        signerName: 'Ada Lovelace',
        reason: 'Approval',
        location: 'Berlin',
        contactInfo: 'ada@example.com',
    ),
);
```

Docker
------

[](#docker)

Die Entwicklung kann innerhalb des Docker-Containers erfolgen. Der Projektordner wird per Bind-Mount nach `/app` eingebunden, dadurch sind lokale Dateien direkt im Container sichtbar. Die `make`-Targets reichen dabei automatisch deine lokale `UID` und `GID` an Docker weiter, damit Dateien im gemounteten Projektordner nicht `root` gehören.

### Voraussetzungen

[](#voraussetzungen)

- Docker
- Docker Compose
- `make`

### Image bauen

[](#image-bauen)

Vor der ersten Nutzung das PHP-Image bauen:

```
make build
```

Wenn sich deine lokale Benutzer- oder Gruppen-ID geändert hat oder das Basis-Image aktualisiert wurde, das Image danach neu bauen:

```
make rebuild
```

### Arbeiten über Make

[](#arbeiten-über-make)

Eine Shell im Container starten:

```
make shell
```

Composer-Abhängigkeiten im Container installieren:

```
make composer-install
```

PHPStan im Container ausführen:

```
make phpstan
```

PHP-CS-Fixer im Container ausführen:

```
make cs
```

PHP-CS-Fixer im Prüfmodus ausführen:

```
make cs-check
```

Rector im Prüfmodus ausführen:

```
make rector-check
```

Rector-Änderungen anwenden:

```
make rector
```

PHPUnit im Container ausführen:

```
make test
```

veraPDF im Container ausführen:

```
make verapdf-version
make validate-pdfa PDF=var/example.pdf
make validate-pdfua PDF=var/example.pdf
make test-pdfa1b-regression
make test-pdfa1b-regressions
make test-pdfa1a-regression
make test-pdfa1a-list-regression
make test-pdfa1a-table-regression
make test-pdfa1a-mixed-regression
make test-pdfa1a-multipage-regression
make test-pdfa1a-forms-and-annotations-regression
make test-pdfa1a-radio-regression
make test-pdfa1a-choice-fields-regression
make test-pdfa1a-negative-regressions
make check-pdf PDF=var/example.pdf
```

Dieselben `PDF/A-1a`-veraPDF-Regressionslaeufe laufen auch automatisiert in GitHub Actions ueber `.github/workflows/pdfa-regressions.yml`.

Alternativ direkt über die Skripte:

```
bin/validate-pdfa.sh var/example.pdf
bin/validate-pdfua.sh var/example.pdf
bin/test-pdfa1b-regression.sh
bin/test-pdfa1b-regressions.sh
bin/test-pdfa1a-regression.sh
bin/test-pdfa1a-list-regression.sh
bin/test-pdfa1a-table-regression.sh
bin/test-pdfa1a-mixed-regression.sh
bin/test-pdfa1a-multipage-regression.sh
bin/test-pdfa1a-forms-and-annotations-regression.sh
bin/test-pdfa1a-radio-regression.sh
bin/test-pdfa1a-choice-fields-regression.sh
bin/test-pdfa1a-negative-regressions.sh
```

Compose-Services starten:

```
make up
```

Compose-Services stoppen:

```
make down
```

### PHP-Version prüfen

[](#php-version-prüfen)

Die verlässliche Prüfung der Container-Version ist:

```
make php-version
```

Ein lokales `php -v` prüft dagegen nur die Host-Installation.

###  Health Score

35

—

LowBetter than 77% of packages

Maintenance84

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity39

Early-stage or recently created project

 Bus Factor1

Top contributor holds 96.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~9 days

Total

2

Last Release

81d ago

Major Versions

0.1.0-alpha1 → v2.x-dev2026-04-16

PHP version history (2 changes)0.1.0-alpha1PHP ^8.4

v2.x-devPHP ^8.5

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/761259?v=4)[Sebastian Kalicki](/maintainers/kalicki2k)[@kalicki2k](https://github.com/kalicki2k)

---

Top Contributors

[![skalicki](https://avatars.githubusercontent.com/u/155432417?v=4)](https://github.com/skalicki "skalicki (276 commits)")[![kalicki2k](https://avatars.githubusercontent.com/u/761259?v=4)](https://github.com/kalicki2k "kalicki2k (9 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (1 commits)")

---

Tags

pdfgeneratordocumentRendering

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan, Rector

Code StylePHP CS Fixer

Type Coverage Yes

### Embed Badge

![Health badge](/badges/kalle-pdf/health.svg)

```
[![Health](https://phpackages.com/badges/kalle-pdf/health.svg)](https://phpackages.com/packages/kalle-pdf)
```

###  Alternatives

[phpoffice/phpword

PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)

7.6k39.0M237](/packages/phpoffice-phpword)[anourvalar/office

Generate documents from existing Excel &amp; Word templates | Export tables to Excel (Grids)

24095.2k](/packages/anourvalar-office)[tightenco/jigsaw

Simple static sites with Laravel's Blade.

2.3k453.6k30](/packages/tightenco-jigsaw)[infyomlabs/adminlte-templates

AdminLTE templates for InfyOm Laravel Generator

2661.6M7](/packages/infyomlabs-adminlte-templates)[infyomlabs/generator-builder

InfyOm Laravel Generator GUI Builder

134443.0k](/packages/infyomlabs-generator-builder)[helhum/typoscript-rendering

Can render a TypoScript path by URL, especially useful for Ajax dispatching

68663.3k12](/packages/helhum-typoscript-rendering)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
