Dateien einbetten und PDF-Portfolios erstellen
Auf einen Blick
Abschnitt betitelt „Auf einen Blick“Dieses Recipe hängt eine oder mehrere Dateien an ein PDF an und ordnet sie, sobald mehrere Anhänge vorhanden sind, als PDF-Portfolio an. Verwenden Sie es, wenn ein Dokument seine Belege innerhalb derselben Datei mitführen soll: eine Rechnung, die zusammen mit dem zugrunde liegenden Stundennachweis ausgeliefert wird, ein Produktdatenblatt, das einen Computer-Aided-Design-Export (CAD-Export) bündelt, oder ein Archivdatensatz, der die Quelltabelle neben dem gerenderten Bericht aufbewahrt.
NextPDF stellt zwei Einstiegspunkte am Dokumentobjekt bereit. embedFile() liest eine Datei von der Festplatte; embedFileFromString() bettet Bytes aus dem Arbeitsspeicher ein, die Sie zur Laufzeit erzeugt haben. Beide Methoden registrieren den Anhang. Bei save() schreibt die Engine jeden Anhang als eingebetteten Dateistream, verpackt ihn in ein File-Specification-Dictionary und verknüpft jede File-Specification mit dem dokumentweiten EmbeddedFiles-Namensbaum. ISO 32000-2 definiert diesen Namensbaum als die Stelle, an der eingebettete Dateistreams über das Name-Dictionary mit dem Dokument als Ganzem verknüpft werden.
Dies ist eine Core-Funktion ohne kommerzielles Gate. Die Attachment-API ist seit 1.0.0 stabil und läuft über die gesamte 8.1-8.4-Backport-Matrix.
Installation
Abschnitt betitelt „Installation“composer require nextpdf/core:^3Es ist keine optionale Erweiterung erforderlich.
Konzeptioneller Überblick
Abschnitt betitelt „Konzeptioneller Überblick“Ein Anhang durchläuft drei PDF-Strukturen. Wenn Sie diese kennen, können Sie die Ausgabe leichter lesen und eine nicht konforme Datei gezielter debuggen.
- Eingebetteter Dateistream. Die rohen Bytes der angehängten Datei werden Flate-komprimiert und als Stream-Objekt geschrieben, dessen
/Typeden Wert/EmbeddedFilehat. NextPDF hält die ursprüngliche Größe, eine MD5-Prüfsumme und das Änderungsdatum im Parameter-Dictionary des Streams fest. Den erkannten Multipurpose-Internet-Mail-Extensions-Typ (MIME-Typ) kodiert es als/Subtypedes Streams. - File-Specification-Dictionary. Der Metadaten-Wrapper. Er trägt den angezeigten Dateinamen (
/Fund den Unicode-Namen/UF), eine menschenlesbare Beschreibung (/Desc), eine Referenz auf den eingebetteten Stream (/EF) und die Beziehung, die die Datei zum Host-Dokument hat (/AFRelationship). EmbeddedFiles-Namensbaum. Ein einziger dokumentweiter Index, der den Namen jedes Anhangs auf seine File-Specification abbildet. ISO 32000-2 verlangt, dass jede über diesen Baum erreichte File-Specification einenEF-Eintrag enthält, dessen Wert auf einen eingebetteten Dateistream verweist. NextPDF baut und balanciert diesen Baum beisave()für Sie.
Der Wert der Beziehung ist für die Konformität entscheidend. Die PDF Association Application Note 0002 legt fest, dass eine zugeordnete Datei einen AFRelationship-Eintrag benötigt, der aus der festen PDF 2.0-Wertemenge gewählt wird: Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema oder Unspecified. NextPDF modelliert diese Menge als AFRelationship-Enum und weist jeden anderen Wert ab. Wählen Sie den Begriff, der beschreibt, warum die Datei vorhanden ist: Ein Stundennachweis zu einer Rechnung ist Source; ein maschinenlesbarer Datensatz zu einem Diagramm ist Data.
Ein PDF-Portfolio (in ISO 32000-2 als Collection bezeichnet) ist die nächsthöhere Schicht. Wenn ein Dokument mehrere Anhänge enthält, teilt das Katalog-Collection-Dictionary dem Reader mit, wie sie dargestellt werden: als sortierbare Detailtabelle, als Kachel-Layout oder als verborgener Umschlag. ISO 32000-2 beschreibt das Collection-Dictionary als das Steuerelement, mit dem ein PDF-Prozessor Dateianhänge als organisiertes Portfolio darstellt. NextPDF modelliert dies als CollectionDictionary-Value-Object und verwendet CollectionSort für die Spaltenreihenfolge einer Detailansicht.
API-Oberfläche
Abschnitt betitelt „API-Oberfläche“Die dokumentweiten Methoden (aus dem HasFileAttachments-Concern auf \NextPDF\Core\Document):
embedFile(string $path, string $description = ''): static— liest eine Datei aus$pathund hängt sie an. Der MIME-Typ wird aus der Dateiendung erkannt; die Beziehung hat standardmäßig den WertUnspecified. Liest bis zu 100 MB; verwenden SieembedFileFromString()für größere Payloads. Gibt das Dokument zur Verkettung zurück.embedFileFromString(string $data, string $filename, string $description = '', string $afRelationship = '/Unspecified'): static— hängt Bytes aus dem Arbeitsspeicher unter dem Anzeigenamen$filenamean. Übergeben Sie einAFRelationship-Literal (mit oder ohne führenden Schrägstrich), um die Beziehung zu setzen. Gibt das Dokument zur Verkettung zurück.
Die unterstützenden Typen (Namespace \NextPDF\Navigation und \NextPDF\Document):
\NextPDF\Navigation\AFRelationship— das Enum der acht gültigen Beziehungswerte.AFRelationship::coerce()normalisiert einen String oder einen Enum-Case und wirft bei einem unbekannten Wert eine Exception.toPdfName()gibt das/Name-Literal aus.\NextPDF\Document\CollectionDictionary— baut das Katalog-Collection-Dictionary. Die KonstantenVIEW_DETAILS,VIEW_TILE,VIEW_HIDDEN,VIEW_CUSTOMundVIEW_NONEwählen den Darstellungsmodus; der Konstruktor akzeptiert außerdem einen anfänglichen Dokumentnamen und eine optionale Sortierung.\NextPDF\Document\CollectionSort— das Value-Object für die Spaltenreihenfolge eines Portfolios in der Detailansicht.
Codebeispiel — Schnellstart
Abschnitt betitelt „Codebeispiel — Schnellstart“Dieses minimale Beispiel hängt einen erzeugten Comma-Separated-Values-Datensatz (CSV-Datensatz) an eine Rechnungsseite an und deklariert ihn als Source-Daten, aus denen die Rechnung erstellt wurde.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Navigation\AFRelationship;
$doc = Document::createStandalone();$doc->addPage();$doc->setFont('helvetica', 'B', 18);$doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// Attach the line-item dataset the invoice was rendered from.$csv = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n";$doc->embedFileFromString( data: $csv, filename: 'line-items.csv', description: 'Source line items for INV-2026-0042', afRelationship: AFRelationship::Source->value,);
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-with-attachment.pdf');Der Reader zeigt line-items.csv in seinem Anhang-Panel an, und die Beziehung markiert die Datei als Quelle, aus der sich die Rechnung ableitet.
Codebeispiel — Produktion
Abschnitt betitelt „Codebeispiel — Produktion“Dieses vollständige Beispiel hängt eine Datei von der Festplatte und einen Datensatz aus dem Arbeitsspeicher an, validiert den Dateipfad vor dem Lesen gegen ein per Allowlist zugelassenes Basisverzeichnis und baut ein sortierbares Portfolio über die Anhänge. Es fängt die spezifischsten NextPDF-Exceptions ab, die beim Einbetten von Anhängen auftreten können, und gibt anschließend einen definierten Exit-Code zurück, statt Fehler stillschweigend zu verschlucken.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Document\CollectionDictionary;use NextPDF\Document\CollectionSort;use NextPDF\Exception\CompressionException;use NextPDF\Exception\InvalidConfigException;use NextPDF\Exception\PageLayoutException;use NextPDF\Navigation\AFRelationship;
/** * Resolve a caller-supplied filename against an allowed base directory. * * Rejects path traversal and stream wrappers so an embedded attachment can * never read outside the directory the application owns. Returns the * canonical absolute path, or null when the input escapes the base. * * @param non-empty-string $baseDir Absolute path to the allowed directory. * @param non-empty-string $userName Untrusted filename from the request. */function resolveWithinBase(string $baseDir, string $userName): ?string{ $base = \realpath($baseDir); if ($base === false) { return null; }
$candidate = \realpath($base . \DIRECTORY_SEPARATOR . \basename($userName)); if ($candidate === false || !\str_starts_with($candidate, $base . \DIRECTORY_SEPARATOR)) { return null; }
return $candidate;}
$attachmentsDir = __DIR__ . '/attachments';$requestedFile = 'timesheet-2026-05.pdf';
$safePath = resolveWithinBase($attachmentsDir, $requestedFile);if ($safePath === null) { \fwrite(\STDERR, "Rejected attachment path: outside the allowed directory\n"); exit(2);}
try { $doc = Document::createStandalone(); $doc->setTitle('Invoice INV-2026-0042 with supporting documents'); $doc->addPage(); $doc->setFont('helvetica', 'B', 18); $doc->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// 1. A validated file from disk: the supporting timesheet. $doc->embedFile( $safePath, 'Timesheet supporting the billed hours', );
// 2. An in-memory dataset generated at runtime. $lineItems = "sku,qty,unit_price\nA-100,3,49.00\nB-220,1,180.00\n"; $doc->embedFileFromString( data: $lineItems, filename: 'line-items.csv', description: 'Machine-readable line items', afRelationship: AFRelationship::Data->value, );
// Present both attachments as a sortable details portfolio. The sort // keys reference columns declared in the portfolio /Schema; here the // built-in filename and modification-date fields order the view. $portfolio = new CollectionDictionary( view: CollectionDictionary::VIEW_DETAILS, initialDocument: 'line-items.csv', sort: new CollectionSort( keys: ['_Filename', '_ModDate'], ascending: [true, false], ), ); // $portfolio->toPdfDictionary() yields the catalog /Collection literal, // shared with the unencrypted-wrapper envelope path.
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/invoice-portfolio.pdf'; $doc->save($out);
echo "Wrote {$out} with 2 attachments and a details portfolio\n";} catch (PageLayoutException $e) { // Unreadable path, oversized file, null byte, or a MIME-type name that // exceeds the 127-byte PDF name limit. \fwrite(\STDERR, "Attachment rejected: {$e->getMessage()}\n"); exit(1);} catch (CompressionException | InvalidConfigException $e) { // The attachment data could not be compressed, or a config value was invalid. \fwrite(\STDERR, "Write failed: {$e->getMessage()}\n"); exit(1);}CollectionDictionary und CollectionSort sind Value-Objects. Sie validieren ihre Eingaben bei der Konstruktion und serialisieren sich in das Katalog-/Collection-Literal, das die Portfolio-Ansicht im Reader steuert.
Edge Cases & Stolperfallen
Abschnitt betitelt „Edge Cases & Stolperfallen“- Die Pfadeingabe liegt in Ihrer Verantwortung.
embedFile()schützt gegen Null-Bytes und Stream-Wrapper und löst den realen Pfad auf, erzwingt aber keine Allowlist für das Basisverzeichnis. Wenn der Pfad aus einem Request stammt, validieren Sie ihn zuerst, so wie es das Produktionsbeispiel mitresolveWithinBase()tut. - Die Obergrenze von 100 MB gilt nur für
embedFile(). Eine Datei über104,857,600Bytes löstPageLayoutExceptionaus. Bei größeren Payloads streamen Sie die Bytes selbst und übergeben Sie sie anembedFileFromString(). - Lange MIME-Typnamen werden abgewiesen. Der erkannte MIME-Typ wird zum
/Subtypedes eingebetteten Streams, einem PDF-Name-Token, das von ISO 32000-2 auf 127 Bytes begrenzt ist. Ein ungewöhnlich langer Typ (einige Office-Formate kommen an 90 Bytes heran) bleibt deutlich unter dem Limit, aber ein manuell angegebener Typ, der es überschreitet, löstPageLayoutExceptionaus. Lassen Sie die Engine den Typ aus der Dateiendung erkennen, sofern Sie keinen konkreten Grund haben, ihn zu überschreiben. - Eine unbekannte Beziehung wirft eine Exception.
AFRelationship::coerce()weist jeden Wert außerhalb der festen Menge ab, statt aufUnspecifiedherabzustufen. Übergeben Sie einen Enum-Case (AFRelationship::Source->value), damit Tippfehler nicht bis zur Laufzeit unentdeckt bleiben. - Dateinamen müssen im Namensbaum eindeutig sein. Zwei Anhänge mit demselben Anzeigenamen kollidieren im
EmbeddedFiles-Index. Geben Sie jedem Anhang einen eindeutigen Dateinamen. _ModDatewird in Coordinated Universal Time (UTC) festgehalten.embedFile()liest die Änderungszeit der Datei und schreibt sie mitgmdate(), sodass dieselbe Fixture auf allen Maschinen unabhängig von der Zeitzonen-Einstellung ein exakt gleiches Datum erzeugt.
Performance
Abschnitt betitelt „Performance“Jeder Anhang wird einmal mit gzcompress() auf Level 9 komprimiert und bei save() als einzelner Stream geschrieben. Die Kompression dominiert die Kosten und skaliert mit der Größe der angehängten Payload, nicht mit dem Seiteninhalt. Eine Handvoll kleiner Begleitdateien (Datensätze, Tabellen, ein Stundennachweis-PDF) bleibt innerhalb des Budgets von 2000 ms / 64 MB. Bei vielen großen Anhängen bilden die eingebetteten Bytes die Speicheruntergrenze: Ein 50-MB-Anhang, der als String gehalten wird, belegt vor der Kompression mindestens ebenso viel Speicher. Bevorzugen Sie embedFileFromString() mit chunkweiser Erzeugung, statt mehrere große Dateien auf einmal zu laden.
Der Namensbaum wird einmal bei save() gebaut. Bis zu 64 Einträge bleiben in einem flachen Baum mit einer einzigen Wurzel. Darüber hinaus partitioniert NextPDF den Baum in ausbalancierte Kids- und Limits-Bereiche, sodass die Indexkosten bei großen Anhangmengen logarithmisch bleiben.
Sicherheitshinweise
Abschnitt betitelt „Sicherheitshinweise“- Validieren Sie jeden nicht vertrauenswürdigen Pfad gegen eine Allowlist. Das Einbetten liest jede Datei, die der PHP-Prozess erreichen kann. Ohne eine Prüfung des Basisverzeichnisses macht ein manipulierter Dateiname aus dem Anhängen eine Local File Inclusion (LFI). Das Produktionsbeispiel zeigt den Allowlist-Schutz; wenden Sie ihn immer dann an, wenn der Dateiname keine Konstante zur Compile-Zeit ist.
- Behandeln Sie angehängte Bytes auf der konsumierenden Seite als nicht vertrauenswürdig. Eine eingebettete Datei ist für NextPDF undurchsichtig. Die Engine parst oder führt sie nicht aus. Das Risiko liegt dort, wo die Datei später geöffnet wird. Setzen Sie Beziehung und Beschreibung, damit ein nachgelagerter Consumer weiß, was jeder Anhang ist, bevor er ihn extrahiert.
- Keine Geheimnisse in Anhängen oder Beschreibungen. Dateiname, Beschreibung und Bytes werden im Klartext gespeichert, sofern nicht das gesamte Dokument verschlüsselt ist. Um einen Anhang zu schützen, verschlüsseln Sie das Dokument mit einer Berechtigungs-Policy (siehe das zugehörige Recipe). Betten Sie keine Anmeldedaten, Schlüssel oder personenbezogenen Daten ein, die Sie nicht auf die gerenderte Seite setzen würden.
- In diesem Recipe findet kein Netzwerkzugriff statt. Jedes Byte wird aus dem validierten lokalen Pfad gelesen oder im Arbeitsspeicher bereitgestellt.
Konformität
Abschnitt betitelt „Konformität“| Aussage | Spec | Abschnitt | reference_id |
|---|---|---|---|
Eingebettete Dateistreams werden über den EmbeddedFiles-Eintrag im Name-Dictionary mit dem Dokument verknüpft. | ISO 32000-2 | 7.11.4 | |
Der EmbeddedFiles-Namensbaum bildet Namen auf File-Specifications ab, deren EF-Eintrag auf einen eingebetteten Dateistream verweist. | ISO 32000-2 | 7.7.4 | |
Eine zugeordnete Datei benötigt einen AFRelationship-Wert aus der festen PDF 2.0-Menge. | PDF Association AN002 | 3 | |
Das Katalog-Collection-Dictionary steuert die Portfolio-Darstellung von Anhängen. | ISO 32000-2 | 7.11.6 |
Reproduzierbarkeits-Profil — strukturell. Die Trailer-/ID, die Datums-Atome pro Speichervorgang und das /ModDate des eingebetteten Streams variieren zwischen den Durchläufen. Deshalb entfernt ein struktureller Vergleich diese Werte, bevor er den Objektgraphen vergleicht. Dieses Recipe beschreibt, wie NextPDF die Struktur erzeugt. Es sichert keine pauschale PDF/A-4f-Konformität zu, die vom gesamten Dokument abhängt. Für ein Archivierungsprofil, das verlangt, dass jeder Anhang eine Beziehung und eine Beschreibung deklariert, siehe das PDF/A-4-Recipe.