Eine PDF/A-3-Checkliste verlangt meist, das XMP-Paket müsse zwei Identifikationseinträge tragen, pdfaid:part und pdfaid:conformance. Eine korrekt geschriebene PDF/A-4-Datei trägt pdfaid:part 4 und pdfaid:rev 2020, und für das Basis-Profil gibt es gar keinen Conformance-Eintrag. Ein ergänzter Buchstabe, um das zu „reparieren", macht die Datei falsch: ISO 19005-4 identifiziert den Basis-Part über Nummer und Revisionsjahr, nur die beiden Erweiterungen behalten einen Buchstaben
PDF/UA-2 hat eine ähnliche Falle eine Ebene tiefer, im Strukturbaum. Eine eigene Rolle im eigenen Namespace zu deklarieren sieht nach der ganzen Arbeit aus, denn Namespaces sind das Feature, das Part 2 ergänzt hat. Es ist die halbe. Eine Rolle wie WidgetGroup in https://example.com/ns/widgets muss trotzdem auf eine Standard-Rolle gemappt werden, bevor ein konformer Checker sie akzeptiert, und nichts auf der gerenderten Seite verrät, dass das Mapping fehlt
PDF/A-4 mit PDFACompliance wählen
In HotPDF sind die PDF/A-4-Profile drei Werte der bestehenden Eigenschaft PDFACompliance: '4' für den Basis-Part, '4E' für technische Dokumente und '4F' für Dateien mit eingebetteten Dateien. Mit '4' bekommt das XMP-Paket pdfaid:part 4 und pdfaid:rev 2020 und sonst nichts aus dem pdfaid-Namespace. '4E' und '4F' ergänzen pdfaid:conformance mit E oder F. Die Decoder-Funktionen melden dieselbe Aufteilung: PDFAPart liefert 4 für alle drei Werte, und PDFAConformanceLevel liefert E, F oder #0 für das Basis-Profil
Alle drei erzwingen einen PDF-2.0-Header. Das Upgrade passiert in EndDoc, ein Dokument, das bei PDF 1.4 startete, kommt also ohne separaten Aufruf als 2.0 heraus. Der eine Fall, der es stoppt, ist StrictVersionLock. Dieses Flag verbietet automatische Upgrades, EndDoc wirft also „PDF/A-4 requires PDF 2.0; StrictVersionLock prevents the version upgrade", statt eine 1.7-Datei zu schreiben, die in den Metadaten Part 4 behauptet. Eine Codebasis, die die Version für einen alten Druck-Workflow festnagelt, sieht genau diese Meldung, wenn jemand zum ersten Mal das Profil umschaltet
Zwei PDF/A-4-Regeln ändern, was im Trailer und im Catalog landet. ISO 19005-4 §6.1.3 erlaubt ein Document-Information-Dictionary nur, wenn der Catalog einen PieceInfo-Eintrag hat, und begrenzt es selbst dann auf ModDate, HotPDF schreibt für Part 4 also kein Info-Dictionary, und der Titel reist als dc:title im XMP. Title und Lang bleiben Pflicht. Eines davon leer gelassen, und EndDoc wirft. Es wirft auch, wenn keine Ausgabeabsicht mit AddPDFAOutputIntent ergänzt wurde, und EnableEncrypt verweigert den Dienst, solange ein PDF/A-Profil gewählt ist, denn eine verschlüsselte Archivdatei ist ein Widerspruch, den der Standard nicht zulässt
Zugeordnete Dateien unter PDF/A-4f und 4e
Zugeordnete Dateien sind der Hauptgrund für PDF/A-4f: eine XML-Rechnung hinter der gerenderten Seite, eine JSON-Nutzlast, die ein nachgelagertes System parst. AddPDFAssociatedFile nimmt einen Dateinamen, einen MIME-Typ, eine Beschreibung, einen AFRelationship-Wert und die Bytes und liefert das indirekte FileSpec-Dictionary zurück, damit man eigene optionale Schlüssel anhängen kann
var
Pdf: THotPDF;
Icc: TFileStream;
begin
Pdf := THotPDF.Create(nil);
Icc := TFileStream.Create('sRGB2014.icc', fmOpenRead or fmShareDenyWrite);
try
Pdf.FileName := 'invoice-archive.pdf';
Pdf.PDFACompliance := '4F'; // Part 4, Rev 2020, Conformance F
Pdf.Title := 'Invoice 2026-0731'; // nur ins XMP-dc:title geschrieben
Pdf.Lang := 'en-US';
Pdf.BeginDoc;
Pdf.AddPDFAOutputIntent('sRGB IEC61966-2.1', 'sRGB IEC61966-2.1',
Icc, 3, 'DeviceRGB');
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-0731');
// LoadInvoiceBytes ist eine eigene Funktion, die TBytes liefert
Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
'Structured invoice data', 'Data', LoadInvoiceBytes);
Pdf.EndDoc;
finally
Icc.Free;
Pdf.Free;
end;
end;
Jeder Aufruf tut vier Dinge. Er schreibt einen EmbeddedFile-Stream, dessen /Subtype den MIME-Typ als PDF-Namen hält, mit einem /Params-Dictionary, das die Größe, ein Änderungsdatum und eine MD5-Prüfsumme trägt. Er baut eine FileSpec mit sowohl /F als auch /UF, der Beschreibung als /Desc und dem Beziehungsnamen. Dann registriert er diese FileSpec zweimal, im /AF-Array des Catalogs und im EmbeddedFiles-Namensbaum. Beide Registrierungen sind Pflicht. Eine Datei, die nur an einem Ort registriert ist, öffnet vielleicht noch aus der Anhangsliste eines Viewers, deshalb beweist eine flüchtige Sichtprüfung über ein Archiv sehr wenig
Der Beziehungsstring muss einer von Source, Data, Alternative, Supplement oder Unspecified sein; alles andere wirft, bevor ein Byte geschrieben wird, ein leerer MIME-Typ ebenso. Auch das Profil wird geprüft. AddPDFAssociatedFile akzeptiert PDF/A-3, PDF/A-4e und PDF/A-4f, und das Basis-Profil '4' wirft, weil es keine zugeordneten Dateien kennt. In die andere Richtung verweigert AddDocumentAttachment den Dienst unter PDF/A-3, 4e und 4f, denn ein schlichter Anhang fehlt der /AF-Registrierung, die diese Profile brauchen. Für PDF/A-3 gegen AddPDFA3AssociatedFile geschriebener Code kompiliert weiter und verhält sich gleich; AddPDFAssociatedFile ist der profilneutrale Name dieser Methode
Die beiden Erweiterungen teilen sich die übrigen Berechtigungen. Unter '4F' stellt EndDoc sicher, dass der Catalog einen EmbeddedFiles-Namensbaum trägt, und AddFileAttachmentAnnotation wird akzeptiert, während das Basis-Profil und 4e sie ablehnen. Unter '4E' sind Add3DAnnotation und AddRichMediaAnnotation erlaubt, zusammen mit SetOCGState und GoTo3DView-Aktionen. Jedes andere PDF/A-Profil weist alle vier ab
Warum scheitert eine namespaced eigene Rolle trotzdem an PDF/UA-2?
Eine eigene Rolle besteht PDF/UA-2 nur, wenn zwei Bedingungen gelten: Ihr /NS-Eintrag zeigt auf ein Namespace-Dictionary, das in der Strukturbaum-Wurzel gelistet ist, und die Rolle löst über das RoleMapNS dieses Namespace zu einer Rolle in einem Standard-Namespace auf. HotPDF verteilt die Arbeit auf drei Methoden. RegisterStructureNamespace erzeugt ein indirektes /Type /Namespace-Dictionary, hängt es an StructTreeRoot /Namespaces an und liefert es zurück; ein zweiter Aufruf mit derselben URI liefert das bestehende Dictionary, statt ein weiteres anzulegen. AddStructureElementNS erzeugt das Element und setzt sein /NS, registriert den Namespace nebenbei. AddStructRoleMapNS schreibt das Mapping
const
PDF2NS = 'http://iso.org/pdf2/ssn';
WidgetNS = 'https://example.com/ns/widgets';
var
DocElem, Group: THPDFDictionaryObject;
begin
Pdf.PDFUACompliance := True;
Pdf.PDFUAPart := 2; // PDF/UA-2 verlangt PDF 2.0
Pdf.Title := 'Widget catalog'; // leerer Title wirft bei EndDoc
Pdf.Lang := 'en-US';
Pdf.BeginDoc;
DocElem := Pdf.AddStructureElement('Document', nil); // an PDF2NS gebunden
Group := Pdf.AddStructureElementNS('WidgetGroup', WidgetNS, DocElem);
// ohne dieses Mapping wirft EndDoc ua2.rolemap.unresolved
Pdf.AddStructRoleMapNS('WidgetGroup', WidgetNS, 'Div', PDF2NS);
Pdf.CurrentPage.SetFont('Arial', [], 18);
Pdf.CurrentPage.BeginMarkedContentMCID('H1', 0);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Widgets');
Pdf.CurrentPage.EndMarkedContent;
Pdf.AddStructureElement('H1', Group, 0, 0); // Seitenindex 0, MCID 0
Pdf.EndDoc;
end;
Ein Teil dieser Registrierung passiert unaufgefordert. Mit PDFUAPart auf 2 bindet AddStructureElement('Document', nil) das Element an den PDF-2.0-Standard-Namespace http://iso.org/pdf2/ssn, denn die Wurzel eines PDF/UA-2-Baums muss ein Document in diesem Namespace sein. Dieselbe Bindung passiert für jede Rolle im PDF-2.0-Vokabular, H1 und P brauchen also kein Namespace-Argument. Rollen, die es nur im 1.7-Set gibt, etwa Note, Code oder BlockQuote, bleiben im 1.7-Namespace
Das Mapping-Ziel ist eine echte Wahl. Ein leerer Ziel-Namespace an AddStructRoleMapNS schreibt die Kurzform, einen nackten Rollennamen, aufgelöst gegen das 1.7-Set. Die PDF-2.0-URI schreibt die zweiteilige [Rolle Namespace]-Form. In einem PDF/UA-2-Dokument auf den PDF-2.0-Namespace mappen: Der ganze Baum löst sich dann in ein Vokabular auf, und Ziele wie Aside oder FENote, die kein 1.7-Gegenstück haben, werden verfügbar. Zwei Mapping-Formen werden an EndDoc rundheraus abgelehnt, eine Kette, die in sich selbst zurückschleift, und eine Rolle in einem expliziten Namespace, gemappt auf eine andere Rolle in ebendiesem Namespace
Die Aufrufreihenfolge ist lockerer, als sie aussieht. Das Mapping darf vor oder nach dem Element ergänzt werden, denn nichts prüft es, bis der Baum fertig ist. Sofort scheitert hingegen AddStructureElementNS, während PDFUAPart auf 1 steht. Part 1 hat keine Namespaces, und der Aufruf wirft „structure namespaces require PDFUAPart=2" auf der Stelle
Was EndDoc prüft, bevor die Datei geschrieben wird
Mit PDFUACompliance an und PDFUAPart auf 2 fährt EndDoc eine vollständige PDF/UA-2-Strukturvalidierung, bevor irgendetwas serialisiert wird, und der erste Fehler wird zur Exception. Vier seiner Prüfungen bewachen das Skelett des Baums. Die Wurzel muss exakt ein Top-Level-Document-Element halten. Jedes Namespace-Dictionary muss indirekt sein, als Namespace typisiert, und eine nicht-leere URI tragen, die nur einmal vorkommt. Jede /NS-Referenz muss zu einem Dictionary auflösen, das im /Namespaces-Array der Wurzel gelistet ist. Und jede Rolle muss zu einem Standard-Namespace auflösen, direkt oder über RoleMap und RoleMapNS. Elemente, die vom Document-Element aus nicht erreichbar sind, werden ebenfalls gemeldet
Derselbe Durchlauf deckt die Anforderungen auf Dokumentebene ab: ein nicht-leeres, gültiges Lang; MarkInfo /Marked auf true; DisplayDocTitle an; und ein XMP-Paket mit nicht-leerem dc:title und pdfuaid:part 2. HotPDF setzt das Marked-Flag, die Viewer-Preference und die XMP-Identifikation selbst, sobald PDF/UA an ist, und schreibt pdfuaid:rev 2024 neben die Part-Nummer. Titel und Sprache muss man selbst liefern, ein leerer Wert für eines davon wirft, bevor der Validator überhaupt startet. Überschriften, Listen, Tabellen, Annotationen und Formulare werden im selben Lauf geprüft
Mit dem Warten bis EndDoc ist Absicht im Spiel. Halbwegs durch die Generierung ist ein Baum legitimerweise unvollständig: Das Document-Element hat vielleicht noch keine Kinder, ein Namespace ist vielleicht registriert, bevor irgendein Element ihn nutzt, ein Mapping kommt vielleicht nach seinem Element. Bei jedem Aufruf zu prüfen würde diese Zustände abweisen. An EndDoc ist der Baum fertig und noch im Speicher, und die Exception nennt einen Regelcode und den Pfad des Elements, der ihn gebrochen hat
try
Pdf.EndDoc;
except
on E: Exception do
begin
// E.Message hat die Form
// PDF/UA-2 validation failed [ua2.heading.skipped at ...]:
// Heading level skips from H2 to H4
LogBuildFailure(Pdf.FileName, E.Message); // eigener Logger
raise;
end;
end;
PDF/A-4-Ausgabe bekommt dieselbe Behandlung von ihrem eigenen Validator. Hält PDFACompliance einen Part-4-Wert, prüft EndDoc den erzeugten Graphen gegen ISO 19005-4 und wirft mit Regelcode und Klausel, etwa pdfa4.filespec.relationship für eine FileSpec ohne gültige Beziehung oder pdfa4.trailer.info_entries für einen herumliegenden Info-Schlüssel. Die Enterprise-Edition legt beide Validatoren als ValidatePDFUA2Structure und ValidatePDFA4 offen, die ein Issue-Array mit jeder zurückgehaltenen Diagnose füllen, statt beim ersten Fehler zu stoppen. Für einen CI-Schritt, der eine komplette Liste ausgeben soll, ist das der bessere Einstiegspunkt
PDF-2.0-Rollen und Überschriften über H6 hinaus
Die typisierte Enumeration THPDFStandardStructureType gewinnt sstDocumentFragment, sstAside, sstTitle, sstFENote, sstSub, sstEm, sstStrong und sstArtifact. Drei davon ändern, wie gewöhnliche Geschäftsdokumente getaggt werden. Aside gibt Seitenleisten und Herausgeber-Zitaten ein Zuhause, das kein zweckentfremdetes Sect ist. FENote markiert Fuß- und Endnoten als Noten, ein Screen-Reader kann sie also auf Wunsch anbieten, statt sie mitten im Satz vorzulesen. Em und Strong tragen Betonung als Struktur, wo ein PDF-1.7-Baum sie nur in einen Span wickeln konnte
Überschriften sind in PDF 2.0 offen. Die Enumeration endet bei sstH6, aber der String-Overload von AddStructureElement akzeptiert H7, H8 und darüber hinaus, denn das PDF-2.0-Set definiert Hn für jedes n ab 1. PDF 1.7 machte bei H6 Schluss, was tiefe technische Dokumente zwang, ihre Gliederung zu nivellieren oder Ebenen wiederzuverwenden. Für Normdokumente, Gesetzestexte und Teilekataloge kann das allein die Umstellung der Ausgabe auf PDF 2.0 rechtfertigen
Tiefere Überschriften kommen mit einer strengeren Regel. Der PDF/UA-2-Durchlauf in EndDoc verlangt, dass Überschriftenebenen aufeinanderfolgen: Die erste Überschrift muss H1 sein, und ein Sprung von H2 zu H4 wirft mit ua2.heading.skipped. Eine Hierarchie darf nur innerhalb eines DocumentFragment niedriger starten, der Rolle für Inhalt, der einem größeren Dokument entnommen wurde. Ein Generator, der eine Markdown- oder HTML-Gliederung direkt auf Hn-Rollen mappt, erbt jede Ebene, die der Autor der Optik wegen übersprungen hat, die Gliederung also normalisieren, bevor sie getaggt wird
Bevor die Produktions-Ausgabe auf PDF 2.0 umstellt
PDF 2.0 beginnt mit einer Header-Änderung, und der Header ist das Erste, was ältere Konsumenten lesen. Ein Ingest-Dienst, ein Druck-Workflow oder ein Line-of-Business-Viewer, der nur bis PDF 1.7 akzeptiert, stoppt an %PDF-2.0, so sauber der Rest der Datei auch ist. Die Wahl eines jeden PDF/A-4-Profils oder von PDF/UA Part 2 wählt PDF 2.0 mit, ob gewollt oder nicht, also jedes verarbeitende System vor der Umstellung bestätigen, nicht danach
Der risikoärmste Rollout hält PDF/A-3 für Dokumente, die zu Lesern gehen, die man nicht kontrolliert, nutzt PDF/A-4f für interne Archive, bei denen man die Ingest-Seite besitzt, und übernimmt PDF/UA-2 dort, wo eine Barrierefreiheits-Richtlinie es nennt. StrictVersionLock dient in der Übergangszeit nebenbei als Wächter: Version auf pdf17 und die Sperre in den Codepfaden setzen, die bei 1.7 bleiben müssen, und eine versehentliche PDF/A-4-Wahl scheitert an EndDoc mit einer Meldung, statt ein System zu erreichen, das die Datei nicht öffnen kann. Die Leitfäden zur PDF/A-, PDF/X- und PDF/UA-Validierung und zu ZUGFeRD- und Factur-X-Hybrid-Rechnungen auf PDF/A-3 behandeln die Profil-Wahlen, die vor der Versionsnummer kommen, und die Hinweise zum automatisierten Preflight-Bericht zeigen, wie man das Urteil zu einem Build-Schritt macht
Das alles läuft als nativer VCL-Code innerhalb der Komponente für Delphi und C++Builder, PDF/A-4- und PDF/UA-2-Ausgabe kommt also mit der eigenen Executable und braucht keine externe Engine. Unterstützte Profile und RAD-Studio-Versionen sind auf der HotPDF-Delphi-Komponentenseite gelistet