Technischer Artikel

PDF/A-Archivierungskonformität in Delphi mit PDFium VCL

Sie liefern einen Konverter aus, der jede Datei als PDF/A-1b kennzeichnet, das Archivsystem des Kunden liest diese ein Jahr lang ein, und dann führt eine Prüfung die gesamte Charge durch veraPDF aus, wobei ein Drittel als nicht konform zurückgegeben wird. Nichts stürzte ab, es wurde keine Exception ausgelöst, die Dateien lassen sich in jedem Viewer auf Ihrem Schreibtisch einwandfrei öffnen. Sie entsprachen lediglich nicht dem Standard, mit dem Sie sie versehen haben. Dies ist das normale Fehlerszenario für Archiv-PDFs, und deshalb ist die Behauptung „Wir haben das Flag gesetzt“ niemals dasselbe wie „Es lässt sich validieren“

Das Erste, was man über PDFium und PDF/A verstehen muss, ist, dass die Engine selbst nichts damit zu tun hat. PDFium rendert, parst und schreibt PDFs, aber seine öffentliche Schnittstelle hat kein ConvertToPDFA, keinen OutputIntent-Writer und keine XMP-API. Jeder Teil der Archivierungskonformität – das XMP-Paket, der OutputIntent und sein ICC-Profil, die Katalog-Marker, die Validierung – lebt in PDFiumPas selbst, in einer etwa 2.000 Zeilen langen, reinen Pascal-Unit (FPdfPdfa.pas) die die gespeicherten Bytes parst und sie durch ein inkrementelles Update neu schreibt. Zu wissen, wo die Arbeit stattfindet, sagt Ihnen, wo sich die Fehler verstecken, und sie verstecken sich nicht in PDFium

Was PDF/A tatsächlich verlangt und wo die Probleme liegen

PDF/A ist nicht ein einziges Format. ISO 19005 definiert drei Teile (PDF/A-1, -2, -3) und darin jeweils Konformitätsstufen, die unterschiedliche Garantien bieten. Level B (basic) garantiert nur, dass das visuelle Erscheinungsbild reproduzierbar ist. Level A (accessible) fügt darüber hinaus eine getaggte Struktur (Structure Tree) und Unicode-Mapping hinzu. Level U, das nur für die Teile 2 und 3 existiert, liegt dazwischen: zuverlässiger Unicode-Text ohne den vollständigen Strukturbaum. ISO 19005-1 kennt kein Level U – eine Einschränkung, die die Bibliothek direkt kodiert

Eine Handvoll Regeln des Formats sind diejenigen, die in der Praxis zu Problemen führen. Verschlüsselung ist grundsätzlich verboten (ISO 19005-1 §6.1.3 und Nachfolger): Eine PDF/A-Datei darf kein /Encrypt-Dictionary enthalten. Das Dokument muss eine Ausgabe-Rendering-Bedingung über einen OutputIntent deklarieren, dessen Ziel ein gültiges ICC-Profil ist (§6.2.3.2). Der Konformitätsanspruch selbst muss als XMP-Metadaten unter dem PDF/A-Identifikationsschema erscheinen. Level A erfordert zusätzlich die logische Struktur gemäß §6.8, also den Strukturbaum (Tag Tree), der das Dokument maschinenlesbar macht. Wenn Sie eines dieser Kriterien verfehlen, lehnt ein Konformitätsprüfer die Datei ab, selbst wenn sie perfekt gerendert wird

Der eine Aufruf, der ein Archiv erzeugt

PDFiumPas stellt die gesamte Pipeline hinter TPdf.SaveAsPdfA zur Verfügung. Die einfache Überladung nimmt eine Zielkonformität entgegen und verwendet standardmäßig PDF/A-1b, was die richtige Standardeinstellung für den häufigen Fall „Mache dies für immer darstellbar“ ist

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // Default conformance is pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // file now carries XMP, sRGB OutputIntent, and catalog markers
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

Hinter den Kulissen ist dies ein zweistufiger Prozess. SaveAsPdfA bittet PDFium zunächst, das Dokument mit FPDF_SaveAsCopy- zu serialisieren, und übergibt diesen Byte-Stream dann an InjectPdfAMarkers, das die XMP-Metadaten, den sRGB-OutputIntent mit seinem eingebetteten ICC-Profil und einen neu geschriebenen Katalog als inkrementelles Update anhängt. Die Quelle wird ab Position Null gelesen und das Ziel ab Position Null geschrieben; der ursprüngliche Objektbaum bleibt intakt und die Marker werden nach dem bestehenden %%EOF eingefügt. Wenn Sie die Bytes anstelle einer Datei benötigen, nimmt SaveAsPdfAToStream einen TStream und dieselben Optionen entgegen

Auswahl der Konformität mit dem Options-Record

Um einen bestimmten Teil und eine bestimmte Stufe anzusteuern, übergeben Sie einen TPdfASaveOptions-Record. Sein Feld Conformance nimmt einen TPdfAConformance-Wert auf. Die Aufzählung deckt jede gültige Kombination ab und nichts anderes: pac1b, pac1a für Teil 1; pac2b, pac2u, pac2a für Teil 2; pac3b, pac3u, pac3a für Teil 3, plus pacUnknown und pacNone für die Validierungsseite. Es gibt kein pac1u, da diese Stufe im Standard nicht existiert

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf');
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: reliable Unicode text
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

Der Großteil des Records kann leer bleiben. Lassen Sie Title, Author, Subject, Keywords, Creator und Producer leer, und SaveAsPdfA füllt sie automatisch aus dem Info-Dictionary des Dokuments über FPDF_GetMetaText aus. Lassen Sie CreationDate and ModDate leer, und es wird die aktuelle UTC-Zeit für beide XMP-Daten verwendet. Lassen Sie DocumentId und InstanceId leer, und die Bibliothek füllt sie vorab aus FPDF_GetFileIdentifier aus, mit einem Fallback auf eine deterministische ID, die aus den Quell-Bytes abgeleitet wird. Das einzige Feld, das Sie möglicherweise bewusst überschreiben möchten, ist IccProfileData: Leer bedeutet das gebündelte sRGB IEC61966-2.1-Profil, aber ein CMYK- oder Graustufen-Workflow sollte sein eigenes bereitstellen

Warum Level A herabgestuft wird und warum das die ehrlichere Wahl ist

Hier ist eine Feinheit, über die Personen stolpern, die erwarten, dass ein Flag eine Garantie ist. Sie können pac1a für ein Dokument anfordern, das keinen Strukturbaum hat, aber PDF/A-1a erfordert die logische Struktur gemäß §6.8, und die Bibliothek kann keinen Strukturbaum aus einem ungetaggten PDF erzeugen. Anstatt eine Datei auszugeben, die Level A beansprucht, obwohl sie es nicht erfüllt, sucht SaveAsPdfA nach einer echten getaggten Struktur (/StructTreeRoot plus /MarkInfo mit /Marked true) und stuft den Anspruch herab, falls diese fehlt: Aus pac1a wird pac1b, aus pac2a wird pac2b und so weiter über alle drei Teile hinweg. Die internen Hilfsfunktionen sind PdfAIsLevelA und PdfADowngradeToLevelB

Die Begründung ist es wert, klar ausgesprochen zu werden: Eine Datei, die ehrlich die Stufe deklariert, die sie erfüllt, ist nützlicher als eine, die über eine Stufe lügt, die sie nicht einhält. Level U wird anders gehandhabt. Das Erkennen einer echten Unicode-Abdeckung würde bedeuten, dass ein naiver Test auf „Hat es /ToUnicode“ legitime Dokumente zu stark herabstuft (WinAnsi und ähnliche Kodierungen sind davon ausgenommen). Daher gibt die Speicherseite den U-Anspruch so aus, wie der Aufrufer ihn deklariert hat, und überlässt es der Validierungsseite, etwaige Abweichungen zu kennzeichnen. Wenn Sie ein garantiertes Level-A-Archiv benötigen, taggen Sie das Dokument vor der Konvertierung; der Konverter erfindet keine Struktur, die nicht da ist

Die ICC-Falle, die nur ein echter Validator aufdeckt

Dies ist der Fehler, der uns die härteste Lektion gelehrt hat, da der bibliothekseigene Prüfer ihn bestand, veraPDF – der ISO-19005-Referenz-Validator – hingegen nicht. PDF/A erfordert, dass das Zielprofil des OutputIntents ein gültiger ICCBased-Stream ist, und §6.2.3.2 schreibt vor, dass ein Prüfer diesen Stream als Farbraum validiert. Ein ICCBased-Stream muss /N deklarieren, die Anzahl der Farbkomponenten. Eine frühe Version des Injektors schrieb das ICC-Stream-Dictionary nur mit /Length und ohne /N, und veraPDF lehnte das Ergebnis mit „The N entry (value null)... is missing“ ab

Was es tückisch machte, war, dass die Ablehnung nur für PDF/A-1b und -1a ausgelöst wurde. Die Konformitätsmodelle für Teil 2 und Teil 3 führten diese spezielle Prüfung für das Zielprofil nicht durch. Daher wurde dieselbe injizierte Struktur unter pac2b, pac3b und pac2u validiert, schlug jedoch unter pac1b allein aufgrund des Werts von pdfaid:part fehl. Ein Unit-Test konnte dies niemals feststellen, da das bibliothekseigene ValidatePdfACompliance nur prüfte, ob der Schlüssel /DestOutputProfile existierte, nicht aber, was sich im Stream-Dictionary befand. Interne Tests blieben grün; die reale Archivierungsvalidierung schlug fehl

Die Lösung ist IccComponentCount, das die Farbraum-Signatur der Daten bei Offset 16 des ICC-Headers liest und sie auf eine Komponentenanzahl abbildet: GRAY ist 1, RGB , Lab und XYZ sind 3, CMYK ist 4, wobei ein unbekanntes Profil standardmäßig auf 3 gesetzt wird. Diese Anzahl geht als /N in das Stream-Dictionary ein. Sie wird berechnet und ist nicht fest auf 3 codiert, sodass ein Aufrufer, der ein CMYK- oder Graustufenprofil über IccProfileData bereitstellt, immer noch den korrekten Wert erhält. Die allgemeinere Lektion ist methodischer Natur: Der bibliotheksinterne Prüfer und ein autoritativer Validator haben jeweils blinde Flecken, und die PDF/A-Ausgabe muss End-to-End gegen eine Referenzimplementierung wie veraPDF getestet werden, anstatt sich auf Selbstprüfungen zu verlassen. Dieselbe Disziplin der inkrementellen Updates für saubere Archive wird in Validierung von komprimierten Objekt- und Xref-Streams behandelt, was wichtig ist, da moderne PDFs, die der Injektor verarbeitet, häufig auf Cross-Reference-Streams aufbauen

Verschlüsselung, Xref-Streams und andere Grenzfälle

Da ISO 19005 Verschlüsselung verbietet, wird sie beim Speichern entfernt. SaveAsPdfA wendet bei der Serialisierung FPDF_REMOVE_SECURITY an, sodass eine verschlüsselte Quelle (die mit ihrem Passwort geladen wurde) auf dem Weg in das Archiv entschlüsselt wird. Bei einem unverschlüsselten Dokument ist dies ein No-Op und ändert nichts. Die logische Konsequenz ist dieselbe Einschränkung, die HotPDF aus der anderen Richtung erzwingt: Eine einzelne Datei kann nicht sowohl verschlüsselt als auch PDF/A sein. Wenn ein Workflow beides erfordert, besteht die Lösung aus zwei Artefakten: einer verschlüsselten Kopie für die Verteilung und einer separaten sauberen Kopie für das Archiv

Ein weiterer Grenzfall ist unsichtbar, bis er zuschlägt: PDF 1.5+-Dokumente, die einen reinen Cross-Reference-Stream verwenden und kein trailer-Schlüsselwort tragen. Der Injektor liest den Trailer, um das Quell-/Info-Dictionary zu finden und sein inkrementelles Update anzuhängen. Er muss das Xref-Stream-Format akzeptieren, da ein solches Dokument sonst unverändert durchkopiert würde, wobei die Marker stillschweigend verloren gingen. ISO 32000-1 §7.5.6 erlaubt explizit ein klassisches Trailer-Inkrementell-Update für ein Xref-Stream-Dokument, wobei /Prev auf den Xref-Stream-Offset zeigt, was genau der Struktur entspricht, die der Injektor ausgibt. PDFiums eigenes FPDF_SaveAsCopy schreibt immer einen klassischen Trailer, sodass der Injektor im normalen Ablauf niemals auf eine reine Xref-Stream-Quelle trifft, der Lesepfad dies jedoch für Dokumente handhabt, die von anderswo stammen

Verifizieren, bevor man dem Anspruch vertraut

Die Bibliothek wird mit einem Byte-Level-Prüfer namens TPdf.ValidatePdfA ausgeliefert, der ein TPdfAValidationResult zurückgibt. Sein Feld Conformance meldet die erkannte Stufe und Issues ist eine Menge von TPdfAValidationIssue-Werten. Die Komfortmethode IsCompliant ist nur dann wahr, wenn eine echte Stufe erkannt wurde und die Menge der Probleme leer ist. Führen Sie sie als schnelles erstes Tor in einem Batch aus

var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice_archive.pdf');
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
  finally
    Pdf.Free;
  end;
end;

Seien Sie ehrlich, was Ihnen das bringt. Der Byte-Level-Prüfer erkennt strukturelle Probleme (ein fehlender OutputIntent, eine verbotene Aktion, ein vorhandenes /Encrypt, Transparenz, wo Teil 1 dies verbietet) mit hoher Zuverlässigkeit. Die Erkennung der Schrifteinbettung verwendet eine Zählheuristik, die bewusst nur ein Signal mit hoher Zuverlässigkeit meldet, anstatt die Abdeckung pro Glyphe zu verfolgen. Was er nicht tut, ist die Analyse von Content-Stream-Operatoren, was einen vollständigen Content-Parser erfordern würde und designbedingt außerhalb des Rahmens liegt. Kombinieren Sie für ein Release-Gate den bibliotheksinternen Prüfer mit veraPDF: Der Prüfer arbeitet sofort und läuft überall ohne DLL, veraPDF ist autoritativ. Die Einbindung dieser Kombination in einen Batch-Lauf ist Thema des Batch-Preflight-Bericht CLI, wo diese Validierung in einem realen Archiv-Workflow hingehört

Die hier gezeigten APIs SaveAsPdfA, InjectPdfAMarkers und ValidatePdfA werden mit der PDFium-Komponente für Delphi, C++Builder und Lazarus/FPC ausgeliefert. Die Produktseite verlinkt die vollständige API-Referenz, einschließlich der vollständigen Konformitätsaufzählung und des Options-Records hinter diesen Beispielen