Zum Inhalt springen
getnextpdf.com

Ein PDF ist ein Container: eingebettete Dateien und zugeordnete Daten

Spec: ISO 32000-2, §7.11.4Spec: ISO 32000-2, §14.13Spec: ISO 19005-3, PDF/A-3

Die meisten Menschen stellen sich ein PDF als einen Stapel Seiten vor. Das ist der Teil, den Sie sehen. Aber ein PDF ist auch ein Container, und es kann ganze andere Dateien in sich tragen – eine Tabellenkalkulation, eine XML-Nutzlast, das ursprüngliche Quelldokument – gebündelt in derselben einzelnen Datei, die Sie jemand anderem übergeben.

Diese Seite erklärt, wie das funktioniert: den eingebetteten Dateistream, der die Bytes speichert, den Namensbaum, der sie auflistet, und den einen Schlüssel, der entscheidet, ob ein Anhang einfach nur dasitzt oder tatsächlich etwas bedeutet.

Ein untypisierter und ein typisierter Anhang sehen für einen Menschen identisch aus. Beide sind eine Datei, die in einem PDF mitreist, und – in dieser Engine – sind beide dem Dokument zugeordnet. Der Unterschied besteht darin, dass der eine einer Maschine sagt, wofür er da ist, und der andere die Beziehung leer lässt, damit die Maschine rät.

Dieser Unterschied entscheidet alles bei einer hybriden E-Rechnung. Eine Steuerplattform liest nicht Ihre Rechnungsseite; sie liest das XML, das Sie eingebettet haben. Wenn dieses XML als undifferenzierter Blob angehängt ist statt als die Rechnungs-Daten für das sichtbare Dokument, hat ein konformer Reader keine verlässliche Möglichkeit zu erkennen, dass es die zu verarbeitende Nutzlast ist. Die Seite sieht perfekt aus. Die Rechnung wird abgelehnt. Der Fehler trifft Tage später ein, mit einer zurückgehaltenen Zahlung im Schlepptau.

Die Beziehung in der Schicht, die die Datei erzeugt, richtig zu setzen, ist weitaus günstiger, als sie eine abgelehnte Rechnung nach der anderen herauszufinden.

  • Ein PDF kann die Bytes jeder beliebigen Datei einbetten als eingebetteten Dateistream (Spec: ISO 32000-2, §7.11.4). Der Stream trägt die Daten plus ein kleines Parameter-Dictionary: ursprüngliche Größe, Daten und eine Prüfsumme.
  • Eingebettete Dateien werden im EmbeddedFiles-Namensbaum katalogisiert, sodass ein Reader sie namentlich aufzählen kann, ohne das gesamte Dokument zu durchsuchen.
  • Eine zugeordnete Datei geht einen Schritt weiter: Sie deklariert eine AFRelationship (Spec: ISO 32000-2, §7.11.3) – einen von acht Standardwerten (Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, Unspecified) oder einen benutzerdefinierten Wert –, der angibt, wie die Datei mit dem Inhalt zusammenhängt, an den sie angehängt ist.
  • Diese typisierte Beziehung ist der Mechanismus hinter hybriden E-Rechnungen (ZUGFeRD / Factur-X) und PDF/A-3-Anhängen (Spec: ISO 19005-3, PDF/A-3).
  • NextPDF unterstützt die rohen Container-Primitiven im Core: embedFile() und embedFileFromString() mit einer expliziten Beziehung. Advanced-Editionen fügen den dedizierten EN-16931- / ZUGFeRD- / Factur-X-E-Rechnungs-Embedder auf diesen Primitiven obendrauf hinzu.

Stellen Sie es sich als zwei aufeinandergestapelte Schichten vor.

Die untere Schicht ist die Speicherung. Ein eingebetteter Dateistream (Spec: ISO 32000-2, §7.11.4) sind die Bytes der Originaldatei, eingehüllt in ein PDF-Stream-Objekt, mit einem Parameter-Dictionary, das die ursprüngliche Größe, das Änderungsdatum und eine Prüfsumme der unkomprimierten Daten erfasst. Der Stream wird über ein Dateispezifikations-Dictionary erreicht, dessen /EF-Dictionary auf den eingebetteten Dateistream zeigt – der Stream selbst trägt kein /EF. Ein Reader kann die Datei Byte für Byte wieder herausziehen. Damit diese Dateien auffindbar sind, hält der Dokumentkatalog einen EmbeddedFiles-Namensbaum – eine sortierte Zuordnung von einem Namen zu jeder Dateispezifikation –, sodass ein Reader auflisten kann „hier sind die 3 Dateien in diesem PDF”, ohne jede Seite zu durchlaufen.

Die obere Schicht ist die Bedeutung. Für sich genommen ist eine eingebettete Datei einfach nur vorhanden. Der Mechanismus der zugeordneten Dateien (Spec: ISO 32000-2, §14.13) hängt eine Datei an etwas an – das gesamte Dokument, eine Seite, ein Grafikobjekt – und versieht sie mit einer AFRelationship. ISO 32000-2 definiert ein kleines Vokabular von acht Standardwerten (Spec: ISO 32000-2, §7.11.3) und erlaubt auch benutzerdefinierte Werte; jeder Standardwert beantwortet eine präzise Frage:

AFRelationshipWas er über die Datei aussagt
SourceDies ist das Quellmaterial, aus dem der sichtbare Inhalt erzeugt wurde (zum Beispiel das ursprüngliche Textverarbeitungsdokument).
DataDies sind strukturierte Daten, die an den sichtbaren Inhalt gebunden sind – der kanonische Fall ist das Rechnungs-XML hinter einer gerenderten Rechnungsseite.
AlternativeDies ist eine alternative Darstellung desselben Inhalts (zum Beispiel eine Audio- oder Videoversion).
SupplementDies ist ergänzendes Material, das den Inhalt erweitert, aber nicht Teil davon ist.
EncryptedPayloadDie eingebettete Datei ist eine verschlüsselte Nutzlast, die das PDF als undurchsichtigen Blob umhüllt.
FormDataDie Datei ist Formulardaten (FDF, XFDF oder eine XML-Formularnutzlast).
SchemaDie Datei ist ein Schema, das die Struktur einer Data-Datei beschreibt (zum Beispiel ein XSD für XML-Daten oder ein JSON Schema).
UnspecifiedDie Beziehung wird bewusst nicht angegeben. Ehrlich, aber es sagt einer Maschine nichts.

Über diese acht hinaus erlaubt der Standard außerdem anwendungsspezifische benutzerdefinierte Beziehungswerte, sodass das Vokabular erweiterbar statt fest ist.

Eine zugeordnete Datei wird durch zwei zusammenwirkende Dinge definiert, nicht durch einen Schlüssel allein. Die /AF-Zuordnung bindet die Dateispezifikation an einen Teil des Dokuments; der AFRelationship-Schlüssel in der Dateispezifikation gibt dann die semantische Beziehung an. Der /AF-Eintrag am Zuordnungspunkt (dem Dokumentkatalog, einer Seite oder einem Objekt) ist ein Array – dieses Array enthält eine oder mehrere Dateispezifikations-Dictionarys, üblicherweise als indirekte Referenzen; /AF ist keine einzelne Referenz. Eine dokumentweite zugeordnete Datei ist die Dateispezifikation, die im /AF-Array des Dokumentkatalogs aufgeführt ist und ihre AFRelationship trägt. Markieren Sie diese Tabellenkalkulation als Unspecified, und Sie haben sie dem Dokument zugeordnet, aber einer Maschine nichts darüber gesagt, warum. Markieren Sie dieselbe Tabellenkalkulation als Data, und Sie haben jedem konformen Reader gesagt, was sie ist und wofür sie da ist. Die Bytes sind dieselben. Die Semantik ist es nicht.

Deshalb ist der E-Rechnungs-Fall nicht „eine XML-Datei anhängen”. Es ist „dieses XML als die Data-zugeordnete Datei für dieses Dokument einbetten, in einem konformen PDF/A-3-Träger” – wobei die Gültigkeit der Rechnung und die rechtliche Akzeptanz separate Prüfungen bleiben, die der Träger nicht durchführt. Der Ablauf hat vier Phasen, und die Reihenfolge ist es, die ihn korrekt hält.

  1. Store the bytesThe file is wrapped in an embedded file stream with its size, dates, and a checksum (ISO 32000-2 §7.11.4).
  2. Register it by nameThe file specification is added to the EmbeddedFiles name tree so a reader can enumerate attachments without scanning the document.
  3. Declare the relationshipAn AFRelationship value (one of the eight standard values such as Source or Data) marks how the file relates to the content, associated at the document level (ISO 32000-2 §14.13.3).
  4. Make it archivalA PDF/A-3 carrier permits the embedded payload to ride inside one conforming archival PDF/A document; invoice validity and legal acceptance remain separate checks (ISO 19005-3).
How a typed attachment becomes a hybrid file end to end: the engine stores the bytes, registers the file by name, declares the relationship, and the archival profile permits it all to ride inside one conforming archival document.

Diese vierte Phase ist der Grund, warum PDF/A-3 als eigenes Profil existiert. Frühere Archivierungsprofile schränkten ein, was eingebettet werden durfte; PDF/A-3 (Spec: ISO 19005-3, PDF/A-3) ist der Teil, der Dateien jedes Formats erlaubt, in einem konformen Archivdokument mitzureisen. Es erlaubt die eingebettete Nutzlast – es validiert diese Nutzlast nicht und verleiht keinen rechtlichen Status. Ohne ihn könnte die hybride Rechnung – eine Datei, die zugleich die Seite ist, die eine Person liest, und die Daten, die ein Steuersystem verarbeitet – überhaupt kein konformes Archiv-PDF/A-Dokument sein; ob die Rechnung gültig und rechtlich akzeptiert ist, bleibt eine separate Frage. Der dedizierte E-Rechnungs-Embedder, den Advanced-Editionen hinzufügen, ist die Bequemlichkeitsnaht über genau diesem: Er bettet die Nutzlast ein, setzt die Beziehung auf Data und registriert sie korrekt, sodass Sie die Container-Verrohrung nicht von Hand zusammensetzen. Die tiefere Rechnungs- und Archivierungsmechanik liegt auf den zwei unten verlinkten Nachbarseiten; auf dieser Seite geht es um den Container, auf dem beide stehen.

Ein kleines, vollständiges Programm. Die zwei Aufrufe, auf die es ankommt, sind der Unterschied zwischen einer untypisierten und einer typisierten zugeordneten Datei – und die Beziehung ist ein explizites Argument, das Sie setzen sollten. In dieser Engine erzeugen beide Aufrufe eine zugeordnete Datei: embedFile() und embedFileFromString() registrieren die Dateispezifikation stets im /AF-Array des Dokumentkatalogs, sodass das Einzige, was die Beziehung ändert, die Bedeutung der Zuordnung ist. Sie ist standardmäßig Unspecified, was die Datei zuordnet, einer Maschine aber nichts darüber sagt, warum; für eine E-Rechnungs-Nutzlast setzen Sie sie auf Data, damit ein Reader sie findet.

<?php
declare(strict_types=1);
use NextPDF\Core\Document;
use NextPDF\Navigation\AFRelationship;
$document = Document::createStandalone();
$document->addPage();
$document->setFont('helvetica', 'B', 16);
$document->cell(0, 12, 'Invoice INV-2026-0042', newLine: true);
// An UNTYPED associated file: the bytes are embedded AND the file spec is
// added to the document catalog's /AF array, but the relationship says
// nothing about why. A reader can open it; a machine cannot tell its role.
// The relationship is left Unspecified (its default); the second argument is
// the human-readable description. embedFile accepts the AFRelationship enum.
$document->embedFile(
'/srv/invoices/INV-2026-0042-source.docx',
'Original source document',
AFRelationship::Unspecified,
);
// A TYPED associated file: the invoice XML is declared as the DATA behind
// the visible page. This is the relationship a hybrid e-invoice reader
// looks for — the same intent the dedicated e-invoice embedder sets.
// embedFileFromString takes the data, a filename, a description, and a
// relationship as a PDF-name string ('/Data').
$invoiceXml = $generateCiiXml(); // your ERP authors this; the engine never does
$document->embedFileFromString(
$invoiceXml,
'factur-x.xml',
'Factur-X invoice data',
'/Data',
);
$bytes = $document->getPdfData();

Die '/Data'-Beziehung ist unverkennbar. Der erste Anhang – als Unspecified belassen – ist gleichermaßen zugeordnet, nur ohne eine angegebene Bedeutung. Für beide Aufrufe schreibt die Engine den eingebetteten Dateistream, fügt die Datei dem EmbeddedFiles-Namensbaum hinzu, listet ihre Dateispezifikation im /AF-Array des Dokumentkatalogs auf und erfasst die Beziehung, die Sie angegeben haben – sie wählt keine für Sie aus. Diese Engine hat keinen Nur-Namensbaum-Modus: Jede Datei, die Sie auf diese Weise einbetten, ist eine dokumentzugeordnete Datei, sodass die Beziehung der einzige Hebel ist, den Sie kontrollieren.

Die häufige Annahme ist, dass „eingebettet” und „zugeordnet” zwei Wörter für dasselbe sind. Das sind sie nicht. Eingebettet bezieht sich auf die Speicherung – die Bytes sind im PDF. Zugeordnet bezieht sich auf die Bindung – die Dateispezifikation ist in einem /AF-Array an einem Teil des Dokuments aufgeführt, und sie trägt eine AFRelationship. Im abstrakten PDF-Modell kann eine Datei in den Namensbaum eingebettet werden, ohne jemals zugeordnet zu sein; der embedFile()-Pfad von NextPDF belässt es nicht dabei – er schreibt stets die /AF-Zuordnung –, sodass für diese Engine die offene Frage nie ist, ob eine Datei zugeordnet ist, sondern was die Beziehung aussagt.

Eine zweite Falle: anzunehmen, ein Reader werde „herausfinden”, welcher Anhang die Rechnung ist. Ein konformer Reader soll nicht raten. Er sucht nach der Datei, deren Beziehung Data angibt. Belassen Sie die Beziehung als Unspecified, und Sie haben die Nutzlast zugeordnet, der Maschine aber nichts Nützliches über ihre Rolle gesagt.

Der Container-Mechanismus ist auf eine Weise mächtig, über die man ehrlich sein sollte: embedFile() liest jeden Pfad, den der PHP-Prozess lesen kann. Das ist die Funktion – und es ist zugleich die Grenze. Die Engine hängt die Bytes an, die ihr gegeben werden; sie entscheidet nicht, und kann nicht für Sie entscheiden, ob ein Pfad einer ist, den Sie offenzulegen beabsichtigt haben.

Embedding a file from a caller-supplied path — edition availability
EditionAvailability
Core

embedFile() liest jeden Pfad, auf den der PHP-Prozess Zugriff hat, und bettet seine Bytes wortwörtlich ein. Zu validieren, dass der Pfad sicher und beabsichtigt ist – kein benutzergesteuerter Wert, kein Traversal und kein Geheimnis außerhalb des Geltungsbereichs des Dokuments –, ist die Verantwortung des Integrators. Dies ist ein dokumentierter Sicherheitsvertrag, kein Versäumnis: Die Engine wird nicht stillschweigend raten, welche Pfade legitim sind, weil diese Vermutung Ihrer Anwendung gehört, die die Vertrauensgrenze kennt, die die Engine nicht sehen kann. Leiten Sie von Angreifern beeinflusste Bytes über einen String mit embedFileFromString(), sodass die Pfadschicht nie ins Spiel kommt.

ProNot in this edition
EnterpriseNot in this edition

Zwei weitere Grenzen, die es klar zu benennen gilt:

  • Einbetten ist nicht Validieren. Die Engine trägt die Bytes, die Sie ihr geben. Ob das eingebettete XML eine konforme Rechnungsnutzlast ist, ist eine separate Frage, die ein Validator beantwortet – siehe die Rechnungsseite.
  • Ein typisierter Anhang ist für sich genommen keine konforme Archivdatei. Die hybride Datei zu einem rechtlichen PDF/A-3-Dokument zu machen, erfordert den Archivmodus und eine unabhängige Konformitätsprüfung – siehe die Archivierungsseite.
  • Rechnungen und E-Rechnungen – der Anwendungsfall, den dieser Mechanismus möglich macht: ein hybrides PDF, das eine maschinenlesbare Rechnung als seine Data-zugeordnete Datei trägt.
  • Archivierung und PDF/A – warum der Träger eine PDF/A-3-Datei ist und was Konformität verspricht und was nicht.
  • Die Anatomie einer PDF-Datei – wo der Namensbaum und der Dokumentkatalog in der Dateistruktur sitzen.
  • Streams und Filter – wie die Bytes einer eingebetteten Datei in einem Stream-Objekt gespeichert und komprimiert werden.
  • Eingebetteter Dateistream – ein PDF-Stream-Objekt, das die Bytes einer externen Datei enthält, mit einem Parameter-Dictionary, das ihre ursprüngliche Größe, Daten und eine Prüfsumme erfasst (ISO 32000-2 §7.11.4).
  • EmbeddedFiles-Namensbaum – die sortierte Zuordnung im Dokumentkatalog, die eingebettete Dateien namentlich auflistet, sodass ein Reader Anhänge aufzählen kann, ohne das gesamte Dokument zu durchsuchen.
  • Zugeordnete Datei – eine eingebettete Datei, die durch eine /AF-Zuordnung (am Dokumentkatalog, einer Seite oder einem Objekt) an einen Teil des Dokuments gebunden ist und eine AFRelationship trägt, die angibt, wie sie mit diesem Inhalt zusammenhängt; der dokumentweite Fall – die Dateispezifikation im /AF-Array des Katalogs – ist derjenige, auf den sich diese Seite konzentriert (ISO 32000-2 §14.13.3).
  • AFRelationship – der Dateispezifikationsschlüssel, dessen Wert die Beziehung benennt (ISO 32000-2 §7.11.3). Er nimmt einen von acht Standardwerten an (Source, Data, Alternative, Supplement, EncryptedPayload, FormData, Schema, Unspecified) oder einen benutzerdefinierten Wert; Data ist der Wert, den eine hybride E-Rechnungs-Nutzlast verwendet.
  • PDF/A-3 – das ISO-19005-3-Archivprofil, das das Einbetten von Dateien jedes Formats erlaubt und so ein konformes hybrides Dokument ermöglicht.
  • Hybride Rechnung – eine PDF-Datei, die zugleich eine menschenlesbare Seite und eine maschinenlesbare eingebettete Rechnungsnutzlast ist.