Technischer Artikel

Batch-PDF-Preflight-Berichte in Delphi mit der PDFium Component CLI

Ein Scan-Dienstleister, den ich beraten habe, ließ jede Nacht einen Job laufen, der Tausende digitalisierte Akten als „archivreif“ stempelte. Nach sechs Monaten nahm ein externer Auditor Stichproben aus dem Archiv mit veraPDF und fand PDF/A-Verstöße in Dateien, die der Job durchgewunken hatte. Der Job hatte nicht direkt gelogen; er hatte das falsche Profil geprüft, jedes Ergebnis auf ein einzelnes Bestanden/Nicht-bestanden-Bit reduziert und die Berichtsdateien verworfen, die die Abweichung sofort sichtbar gemacht hätten. Behalten Sie diesen Vorfall im Kopf, wenn Sie die Preflight-Engine von PDFium Component, einer PDF-Bibliothek mit Quellcode für Delphi, C++Builder und Lazarus, in ein Kommandozeilenwerkzeug einbauen: Die Validierungsaufrufe sind der leichte Teil, und der Vertrag darum herum, also Profile, Exitcodes und Berichtsspeicherung, entscheidet darüber, ob Batch-Preflight funktioniert oder leise verrottet.

Der Vertrag: Was ein Scheduler tatsächlich sieht

Ein CI-Runner oder Windows Task Scheduler sieht von Ihrem Werkzeug genau zwei Dinge: den Exitcode und die Dateien, die es hinterlässt. Alles andere, Logzeilen, Konsolenfarben und Fortschrittsausgaben, ist für Menschen, die live zusehen. Legen Sie deshalb vor dem ersten API-Aufruf ein schlichtes Exitcode-Vokabular fest:

  • 0 — jede Datei erfüllte jedes angeforderte Profil
  • 1 — mindestens eine Datei erzeugte Validierungsbefunde
  • 2 — das Werkzeug selbst scheiterte an mindestens einer Datei (beschädigte Eingabe, Sperre, Absturz)

Die Unterscheidung zwischen Code 1 und 2 ist genau die, die Teams überspringen und später bereuen. Ein beschädigtes PDF, das nicht geöffnet werden kann, ist kein Validierungsfehler. Wenn es als solcher behandelt wird, taucht eine Lkw-Ladung defekter Scans in Ihren Kennzahlen als plötzlicher Konformitätseinbruch auf, statt als der operative Vorfall, der sie tatsächlich ist.

Zwei weitere Vertragsbestandteile brauchen jeweils eine Markierung: ein Timeout pro Datei und ein Quarantäneverzeichnis. Ein pathologisches PDF mit Tausenden Seiten und tief verschachtelten Objektstrukturen kann einen Validierungslauf minutenlang blockieren, und einem nächtlichen Zeitfenster ist der Grund egal. Beenden Sie den Job der Datei zur Frist, zählen Sie ihn als Werkzeugfehler, legen Sie die Eingabe zur Prüfung beiseite und halten Sie den Batch in Bewegung. Das Quarantäneverzeichnis wird zugleich zu einem selbstsammlenden Korpus der schlimmsten Dokumente, die Ihre Kunden wirklich senden. Für Release-Tests ist das wertvoller als jedes synthetische Beispiel.

Standards wählen: PDF/A-2b ist nicht PDF/A-3a

Die Enumeration TPdfPreflightStandard wählt die Standardfamilien aus, die in der Praxis relevant sind: ppsPdfA für Archivkonformität nach ISO 19005, ppsPdfUa für Barrierefreiheit nach ISO 14289, ppsPdfX für den Druckaustausch sowie ppsPdfE, ppsPdfR und ppsPdfVT für Engineering-, Raster- und Variable-Data-Workflows. Innerhalb einer Familie erkennt die Engine die Konformitätsstufe, die das Dokument beansprucht, und meldet sie pro Standard im ConformanceName des Ergebnisses. Diese Stufe zählt. PDF/A-2b verspricht nur visuelle Reproduzierbarkeit; PDF/A-3a verlangt zusätzlich logische Strukturtags und erlaubt eingebettete Quelldateien, also eine strengere und für Scanmaterial deutlich schwierigere Hürde. Wenn Ihre Aufbewahrungsrichtlinie PDF/A-2b fordert, fluten Fehler wegen fehlender Strukturtags den Bericht mit Befunden, die niemand beheben will. Irgendein PDF/A-Label ohne Prüfung der Stufe zu akzeptieren, verspricht dagegen zu wenig. Staatliche Barrierefreiheitsvorgaben ergänzen immer häufiger PDF/UA, und das kostet nichts zusätzlich, weil BuildPdfPreflightReport (aus der Unit FPdfPreflightReport) eine Menge akzeptiert:

Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

Ein Aufruf, beide Standards, ein konsolidierter Berichtsdatensatz.

Den Bericht lesen: Stille ist keine Konformität

Der Bericht enumeriert Befunde pro Standard. Eine leere Befundliste bedeutet daher „keine Probleme in den Standards gefunden, die gelaufen sind“; das ist nicht dieselbe Aussage wie „die Datei erfüllt den Standard, der Ihnen wichtig ist“. Wenn ein Konfigurationsfehler ppsPdfA aus der Menge entfernt hat, ist die Befundliste ebenso leer. Gehen Sie immer durch Report.Results und prüfen Sie für jeden beabsichtigten Standard zwei Dinge: dass überhaupt ein Ergebniseintrag dafür existiert und dass sein Flag IsCompliant (gestützt durch Status = pfsPass) wahr ist. Genau dieser Fehlermodus steckt hinter der Auditor-Geschichte oben: Der Job setzte „keine Befunde“ mit „archivreif“ gleich, ohne je zu prüfen, welche Standards bewertet worden waren.

Die zweite Falle steckt darin, was ein Befund ist: Jeder TPdfPreflightIssue trägt einen Code, eine Category, eine Description und eine Recommendation. Er benennt die verletzte Regel, nicht eine Seitenzahl. Das prägt die Rückkopplung: Der Bericht sagt dem erzeugenden Team, welche Defektklasse zu beheben ist (eine nicht eingebettete Schrift, eine fehlende XMP-Kennung), und das genaue fehlerhafte Objekt zu finden ist Aufgabe des Reparaturwerkzeugs, nicht des Validators. Schreiben Sie Berichtskonsumenten gegen die stabilen Code-Werte, statt Beschreibungstext zu parsen, der zwischen Releases umformuliert werden kann.

Berichtsdateien für Maschinen und für den Bereitschaftsdienst

Der Berichtsdatensatz schreibt dieselben Befunde in fünf Formaten: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile und SaveMarkdownToFile (mit passenden Funktionen im Stil von ToJson, wenn Sie die Zeichenfolge statt einer Datei brauchen). Wählen Sie nicht nur eines aus. JSON gehört in die Pipeline: Hängen Sie es an den Jobdatensatz und lassen Sie CI Issue-Codes und Status pro Standard parsen. HTML ist für den Operator, der geweckt wird: Es öffnet in jedem Browser ohne Werkzeug. Beides zu schreiben kostet eine zusätzliche Zeile pro Datei und beseitigt die schlechteste Bereitschaftserfahrung in der Batch-Verarbeitung, nämlich um zwei Uhr morgens einen JSON-Blob rückwärts zu verstehen. Halten Sie die Namen deterministisch. Leiten Sie jeden Berichtsnamen vom Eingabedateinamen ab, nie von einem Zeitstempel, sonst verschränken parallele Läufe Berichte, die Sie nicht mehr ihren Eingaben zuordnen können.

Schweregradschwellen gehören in die Konfiguration, nicht in den Code. Derselbe Befund, etwa eine Annotation ohne Alternativbeschreibung, ist für ein PDF/UA-Einreichungsportal ein harter Fehler und für ein internes Archiv eine ignorierbare Notiz. Stellen Sie pro Profil eine Fail-on-Stufe bereit, damit sich die Richtlinie ohne Neukompilieren ändern kann, und zeichnen Sie die wirksame Stufe in der Jobzusammenfassung auf. Im nächsten Quartal erinnert sich niemand mehr daran, mit welchem Schwellenwert der Batch im vergangenen Oktober lief.

Dateien isolieren, damit ein schlechtes PDF den Batch nicht versenkt

procedure RunPreflightBatch(const InputDir, ReportDir: string;
  out FilesWithFindings, ToolFailures: Integer);
var
  SR: TSearchRec;
  Pdf: TPdf;
  Report: TPdfPreflightReport;
begin
  FilesWithFindings := 0;
  ToolFailures := 0;
  if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
  try
    repeat
      Pdf := TPdf.Create(nil);   // fresh instance per file: no state bleed
      try
        try
          Pdf.FileName := InputDir + SR.Name;
          Pdf.Active := True;
          if not Pdf.Active then  // load failures are silent, not raised
            raise EPdfError.Create('Cannot open ' + SR.Name);
          Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
          Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
          Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
          if Report.TotalIssueCount > 0 then
            Inc(FilesWithFindings);
        except
          on E: Exception do
          begin
            Inc(ToolFailures);   // exit-code-2 territory, not a validation verdict
            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
          end;
        end;
      finally
        Pdf.Free;
      end;
    until FindNext(SR) <> 0;
  finally
    FindClose(SR);
  end;
end;

Drei bewusste Entscheidungen stecken in dieser Schleife. Ein frisches TPdf pro Datei garantiert, dass ein Dokument, das Engine-Zustand beschädigt, seine Nachfolger nicht vergiften kann. Die explizite Active-Prüfung zählt, weil das Setzen von Active := True Ladefehler schluckt, statt sie zu werfen. Ohne diese Wache würde eine abgeschnittene Datei in den Validierungsaufruf driften, bevor sie mit einer irreführenden Meldung scheitert. Das innere try..except sitzt innerhalb des Pro-Datei-Bereichs, daher erhöht eine Exception den Fehlerzähler und die Schleife läuft weiter. Der Auditor will Berichte für die 4.999 guten Dateien, auch wenn Datei 5.000 abgeschnitten ist. Und beide Berichtsformate werden geschrieben, bevor das Urteil gezählt wird, sodass die Belege überleben, selbst wenn die Zusammenfassungslogik einen Fehler hat.

Die Exitcode-Zuordnung schrumpft dann im Projektfile auf wenige Zeilen:

begin
  RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
  if Failures > 0 then
    Halt(2)
  else if Findings > 0 then
    Halt(1);
  // falling through exits with 0: every file conformed
end.

Was Preflight Ihnen nicht abnimmt

Die Engine erkennt; sie repariert nicht. Ein Befund zu einer nicht eingebetteten Schrift oder einem geräteabhängigen Farbraum ist ein Arbeitsauftrag für den Erzeuger der Dateien, nichts, was der Validator an Ort und Stelle flicken kann. Planen Sie die Rückkopplung entsprechend: Berichte müssen dort landen, wo das erzeugende Team sie liest, sonst erscheinen dieselben Befunde jede Nacht wieder, bis jemand fragt, warum die Kurve nie fällt. Es lohnt sich außerdem, eine Stichprobe von Urteilen mit einem unabhängigen Validator gegenzuprüfen, etwa veraPDF für PDF/A oder Acrobats Preflight für PDF/X, bevor ein externer Auditor es für Sie tut. Zwei übereinstimmende Engines sind starke Evidenz; eine Engine allein ist eine Meinung.

Häufig gestellte Fragen

Kann ich PDF/A und PDF/UA im selben Durchlauf validieren?

Ja. BuildPdfPreflightReport nimmt eine Menge von Standards entgegen, daher bewertet [ppsPdfA, ppsPdfUa] beide in einem Lauf mit einem konsolidierten Bericht. PDF/UA-Prüfungen passen außerdem natürlich zu Viewer-seitiger Barrierefreiheitsarbeit, etwa den Mustern in einem barrierefreien PDF-Reader in Delphi.

Warum zeigt mein Bericht keine Befunde für eine Datei, die veraPDF ablehnt?

Prüfen Sie zuerst, ob der Standard wirklich gelaufen ist: Gehen Sie durch Report.Results, suchen Sie einen Eintrag, dessen Standard passt, und prüfen Sie sein Flag IsCompliant, statt aus einer leeren Befundliste zu schließen. Wenn die Standards übereinstimmen und die Engines dennoch auseinandergehen, behalten Sie das Dokument als benannten Regressionsfall. Abweichende Urteile an echten Kundendateien sind genau das, was Release-Tests brauchen.

Behebt Preflight die Probleme, die es findet?

Nein. Es meldet Farb-, Schrift-, Struktur- und Metadatenverstöße mit Codes, Kategorien und Empfehlungen; die Behebung passiert im erzeugenden Workflow. Planen Sie diese Schleife ein, nicht nur die Prüfung.

Profile, Berichtsformate und die vollständige Preflight-API sind auf der Produktseite dokumentiert: PDFium Component. Dieselbe Engine treibt interaktive Prüfungen in einer Review-Oberfläche an, sodass diese CLI und Ihre PDF-Eingangsprüfungs-Workbench ein gemeinsames Validierungsvokabular nutzen können.