Technischer Artikel

PDF-Eingangsprüfung in Delphi mit PDFium Component aufbauen

Eine PDF-Eingangsprüfung ist ein kleines Programm mit einer einzigen Aufgabe: jede Datei anzusehen, bevor irgendetwas Downstream sie anfassen darf. Um diese Aufgabe zu erledigen, muss sie eine Handvoll Fähigkeiten in einem Durchlauf vereinen. Sie öffnet die Datei (ohne ihr zu vertrauen), liest, was die Datei über sich selbst behauptet, sucht nach Inhalten, die einen naiven Extraktor in die Irre führen oder einen Angriff tragen, entscheidet, ob es überhaupt extrahierbaren Text gibt, und leitet das Dokument dann anhand dessen, was sie gefunden hat, in eine Warteschlange. Lassen Sie die Inspektion weg, sind die Fehlschläge stille: Ein mit einem Besitzer-Passwort verschlüsseltes PDF, das ein XFA-Formular umhüllt, segelt als leere Strings durch einen Textextraktor, wird als leeres Dokument indiziert, und niemand merkt es, bis jemand Downstream nach Inhalt sucht, der nie gelesen wurde. Die PDFium-Komponente ist eine Quellcode-VCL/LCL-Viewer- und Inspektionsbibliothek für Delphi, C++Builder und Lazarus, und sie legt die Introspektions-Aufrufe offen, die diese Werkbank braucht. Die folgenden Abschnitte gehen durch, welcher Aufruf welche Frage beantwortet, und die zwei Stellen, an denen der naheliegende Aufruf Ihnen eine selbstbewusst falsche Antwort gibt

Fünf Fragen, die vor der Einordnung einer Datei zu beantworten sind

Blendet man das Raster und die Miniaturansichten-Leiste weg, reduziert sich die Eingangs-Triage auf fünf Fragen:

Diagramm einer Delphi-PDF-Aufnahmewerkbank, die fünf Sichtungsfragen in einem billigen Öffnen beantwortet und Dateien in die Zustände bereit, Prüfung, blockiert oder beschädigt routet
Die Aufnahme-Triage beantwortet fünf Fragen in einem einzigen billigen Öffnen und routet die Datei zu bereit, prüfen, blockiert oder beschädigt
  • Lässt sich die Datei überhaupt öffnen, und mit welchem Passwort?
  • Was gibt sie über sich an: Titel, Autor, Erstellungsdatum?
  • Trägt sie aktive oder riskante Inhalte wie JavaScript, ein XFA-Formular oder eingebettete Dateien?
  • Gibt es extrahierbaren Text, oder ist es ein Scan auf dem Weg zur OCR?
  • Vor dem Hintergrund all dessen, in welche Warteschlange gehört sie: Durchlaufverarbeitung, manuelle Prüfung oder Quarantäne?

Jede Frage bildet sich auf ein oder zwei Aufrufe der PDFium-Komponente ab. Zwei dieser Abbildungen haben scharfe Kanten, die für die meisten falsch einsortierten Dateien verantwortlich sind, die in Produktion debuggt werden mussten. Dokumentmetadaten leben an zwei verschiedenen Orten, die einander widersprechen können, und Verschlüsselung hält ein Dokument nicht unbedingt vom Öffnen ab

Günstig öffnen: Formularausfüllung aus, null Seiten gerendert

Die Triage sollte das günstigmögliche Öffnen sein. FormFill := False vor Active := True weist die Komponente an, die Formularausfüllungs-Umgebung komplett zu überspringen. Das verkürzt die Ladezeit und verhindert, genauso wichtig für Dateien unbekannter Herkunft, die Initialisierung jeglichen Dokument-JavaScripts. Keine der unten verwendeten Inspektionseigenschaften erfordert das Rendern einer Seite, deshalb muss ein Triage-Durchlauf nie ein einziges Bitmap erzeugen

procedure InspectIncoming(const IncomingPath: string; var Rec: TIntakeRecord);
var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := IncomingPath;
    Pdf.FormFill := False;     // keine Formularumgebung, keine JavaScript-Initialisierung
    Pdf.Active := True;        // ein Fehlschlag ist still: Active bleibt einfach False

    if not Pdf.Active then
    begin
      Rec.OpenFailed := True;  // beschädigte Datei oder Benutzer-Passwort-Sperre
      Exit;                    // der finally-Block läuft trotzdem
    end;

    Rec.PageCount := Pdf.PageCount;
    CollectIdentity(Pdf, IncomingPath, Rec);
    CollectRiskSignals(Pdf, Rec);
  finally
    Pdf.Active := False;
    Pdf.Free;                  // die Instanz niemals bei einer fehlerhaften Datei verlieren
  end;
end;

Die Prüfung nach der Zuweisung ist nicht optional, und sie ist aus gutem Grund eine Prüfung statt eines Exception-Handlers. Wenn die Engine die Datei nicht laden kann, schluckt die Komponente den internen EPdfError und lässt Active auf False, statt ihn weiterzureichen. Code, der auf eine Exception wartet, liest fröhlich PageCount aus einem Dokument, das nie geöffnet wurde. Wenn der Ablehnungs-Workflow den tatsächlichen Fehlertext der Engine braucht, lesen Sie die Datei in ein Byte-Array und rufen Sie die LoadDocument-Überladung auf, die TBytes nimmt; dieser Weg löst tatsächlich EPdfError mit der Meldung aus, einschließlich des Passwortfalls. Das try..finally verdient seinen Platz weiterhin. Eingangsdienste laufen wochenlang unbeaufsichtigt, und keine spätere Exception darf die TPdf-Instanz verlieren oder eine Sperre halten, über die der Wiederholungsdurchlauf stolpert

Der Durchsatz wird selten zum Flaschenhals. Mit deaktivierter Formularausfüllung und ohne Rendering wird ein Triage-Öffnen von der Ein- und Ausgabe dominiert, und ein einzelner Worker inspiziert von der lokalen Platte bequem mehrere Dateien pro Sekunde. Sollte das Eingangsvolumen irgendwann einen Worker übersteigen, teilen Sie die Arbeit nach Dateien auf, nicht nach Prüfungen. Die fünf Fragen teilen sich ein Öffnen, und ihre Aufteilung auf mehrere Prozesse würde den teuersten Schritt vervielfachen, statt ihn zu verteilen

Metadaten leben an zwei Orten, und sie widersprechen sich

ISO 32000-1 definiert zwei Heime für Dokumentmetadaten: das Dokument-Informationswörterbuch (Klausel 14.3.3) und ein XMP-Paket am Katalog (Klausel 14.3.2). Die Eigenschaften Title, Author, Subject und CreationDate lesen das Info-Wörterbuch, mit MetaText[] für beliebige weitere Schlüssel und DecodeDate, um die Datumszeichenkette D:YYYYMMDD... zu parsen. Der Haken: Moderne Producer schreiben zunehmend nur noch XMP, eine Richtung, die ISO 32000-2 offiziell macht, indem sie die meisten Info-Wörterbuch-Schlüssel in PDF 2.0 für veraltet erklärt. Das Symptom in einem Eingangswerkzeug ist konkret. Ihre Werkbank zeigt einen leeren Titel, während Adobe Acrobat einen anzeigt, weil Acrobat auf dc:title im XMP-Paket zurückgegriffen hat, den die Info-Wörterbuch-Eigenschaften nie berühren

Diagramm von PDF-Metadaten an zwei Orten, dem Info-Wörterbuch und dem XMP-Paket, die sich im Titel widersprechen können, in einem Delphi-Aufnahmewerkzeug
Dokumentmetadaten leben im Info-Dictionary und im XMP-Paket, und die beiden Ablageorte können über den Titel uneins sein
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
  var Rec: TIntakeRecord);
begin
  Rec.Title := Pdf.Title;             // Wert des Info-Wörterbuchs
  Rec.Author := Pdf.Author;
  Rec.CreatedAt := Pdf.CreationDate;  // roher PDF-Datumsstring ("D:2026...")

  // Ein leerer Info-Titel bedeutet nicht, dass das Dokument unbenannt ist. Die
  // Komponente legt das XMP-Paket nicht offen, also die Rohdatei auf das
  // dc:title-Element prüfen, bevor man dem Leerwert vertraut.
  if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
    Include(Rec.Flags, ifTitleInXmpOnly);
end;

Selbst die grobe Teilzeichenketten-Sonde oben verdient ihren Platz: „Metadaten vorhanden, aber nicht dort, wo Legacy-Werkzeuge hinschauen“ ist eine einordnungsrelevante Tatsache für jede Archiv-Pipeline, die auf Titel oder Autor indiziert. Liest Ihr Downstream-Index nur das Info-Wörterbuch, werden auf diese Weise markierte Dateien still unfindbar

Verschlüsselte Dateien, die sich trotzdem öffnen

Ein verschlüsseltes Dokument schlägt beim Öffnen nicht zwangsläufig fehl. Der Standard-Sicherheits-Handler (ISO 32000-1, Klausel 7.6.3) unterscheidet ein Benutzer-Passwort, das zum Öffnen des Dokuments nötig ist, von einem Besitzer-Passwort, das lediglich Berechtigungen wie Drucken und Kopieren kontrolliert. Ein großer Teil der „geschützten“ Geschäftsdokumente ist mit einem Besitzer-Passwort und einem leeren Benutzer-Passwort verschlüsselt. Sie öffnen sich ohne Nachfrage, entschlüsseln sich vollständig und verlassen sich darauf, dass Viewer die Berechtigungs-Flags freiwillig respektieren. Das ist Richtlinie, kein Schutz, und Ihre Eingangszustände sollten den Unterschied widerspiegeln

Die Verschlüsselungserkennung nach einem erfolgreichen Öffnen kostet einen Engine-Aufruf plus einen Fallback. FPDF_GetSecurityHandlerRevision(Pdf.Document) gibt -1 für ungeschützte Dateien zurück und andernfalls die Handler-Revision, und Pdf.Permissions, das etwas anderes als die Alles-Bits-Maske $FFFFFFFF liefert, ist das bestätigende Signal. Für echt mit Benutzer-Passwort gesperrte Dateien weisen Sie Password zu, bevor Sie Active := True setzen; schlägt das Öffnen weiterhin fehl, leiten Sie die Datei in einen blockierten Zustand, der vom Absender Zugangsdaten über einen sicheren Kanal anfordert, statt blind erneut zu versuchen. Und widerstehen Sie der Versuchung, „verschlüsselt“ als automatische Quarantäne zu behandeln. In den meisten dokumentenlastigen Branchen sind verschlüsselte, aber öffnenbare Dateien der Normalfall, nicht der verdächtige

Aktive Inhalte: JavaScript, XFA und eingebettete Dateien

Drei Befunde sollten es immer bis zur Einordnungsentscheidung schaffen. Erstens JavaScript: Das Ereignis OnUnsupportedFeature meldet strukturelle Merkmale wie XFA oder 3D-Inhalte, sobald die Engine auf sie stößt, erkennt aber kein JavaScript. Prüfen Sie stattdessen JavaScriptActionCount und behandeln Sie ein Ergebnis ungleich null als aktiven Inhalt. Zweitens XFA: Wenn FormType ftXfaFull zurückgibt, sind die sichtbaren Seiten oft wenig mehr als ein Rendering der XFA-Vorlage, und die herkömmliche Textextraktion sieht Boilerplate statt der ausgefüllten Werte. Drittens Anhänge: Ein PDF ist ein Containerformat, und AttachmentCount sagt Ihnen, ob dieses hier Passagiere mitführt

Diagramm der PDF-Aufnahme-Risikosignale in Delphi: Verschlüsselungs-Handler-Revision, JavaScript-Aktionsanzahl, XFA-Formulartyp und gefährliche Anhänge
Verschlüsselungszustand und die JavaScript-, XFA- und Anhangszählungen sind die Signale, die bis in die Routingentscheidung überleben müssen
procedure CollectRiskSignals(Pdf: TPdf; var Rec: TIntakeRecord);
var
  i, PageNo: Integer;
  Ext: string;
begin
  Rec.IsEncrypted := Assigned(FPDF_GetSecurityHandlerRevision) and
    (FPDF_GetSecurityHandlerRevision(Pdf.Document) <> -1);
  Rec.HasForms := Pdf.FormType <> ftNone;
  Rec.IsXfa := Pdf.FormType = ftXfaFull;
  Rec.HasJavaScript := Pdf.JavaScriptActionCount > 0;

  // AnnotationCount ist eine seitenbezogene Eigenschaft; die Seiten durchlaufen,
  // um zu summieren. Das Laden eines Seitenobjekts rendert nichts und bleibt günstig.
  Rec.Annotations := 0;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    Inc(Rec.Annotations, Pdf.AnnotationCount);
  end;

  Rec.Attachments := Pdf.AttachmentCount;

  for i := 0 to Rec.Attachments - 1 do
  begin
    Ext := LowerCase(ExtractFileExt(string(Pdf.AttachmentName[i])));
    if (Ext = '.exe') or (Ext = '.js') or (Ext = '.vbs') or (Ext = '.dll') then
      Include(Rec.Flags, ifDangerousAttachment);
  end;
end;

Zwei Details in dieser Schleife verdienen Aufmerksamkeit. Der Anhangsname kommt aus dem Inneren des Dokuments, verwenden Sie ihn also nie ohne vorherige Bereinigung als Ausgabepfad; ein eingebetteter Name wie ..\..\start.exe ist eine Pfadüberquerung, die nur auf einen unachtsamen Speicher-Aufruf wartet. Und eine Erweiterungs-Blockliste ist ein Stolperdraht, keine Garantie. Ihre Aufgabe ist es, eine menschliche Entscheidung zu erzwingen, nicht die Datei für sauber zu erklären

Aus Signalen Einordnungszustände machen

Ein brauchbares Zustandsmodell braucht weniger Zustände, als die meisten Teams erwarten: bereit (keine Blocker, Text vorhanden), Prüfung (das Öffnen gelang, aber etwas braucht menschliche Augen, etwa ein XFA-Formular, JavaScript, eine leere Textebene oder ein Titel nur im XMP), blockiert (Benutzer-Passwort erforderlich) und beschädigt (Öffnen fehlgeschlagen). Zeichnen Sie die Beweise zusammen mit dem Zustand auf. Der Datei-Hash, die Seitenzahl, die genauen Flags und die Engine-Fehlermeldung für beschädigte Dateien sind alle wichtig, denn die Person, die eine Einordnungsentscheidung anzweifelt, tut das Wochen später, an einer Datei, die seither ersetzt oder verändert worden sein kann

Wenn ein Operator eine quarantänierte Datei tatsächlich ansehen soll, übergeben Sie sie nicht dem Standard-Shell-Viewer. Rendern Sie sie in einem gehärteten Bereich mit deaktivierter Skript- und Link-Behandlung, dem Ansatz aus einer sicheren PDF-Vorschaufläche in Delphi. Und wenn Ihr Eingang ein Archiv mit Konformitätsanforderungen speist, ist der Triage-Durchlauf der natürliche Ort, eine tiefere Prüfung einzuplanen; die Batch-Preflight-Validierung gegen PDF/A- und PDF/UA-Profile setzt genau dort an, wo diese Inspektion aufhört

Die Produktseite der Komponente behandelt Lizenzierung, die vollständige Inspektions-API und die mitgelieferten Demos, darunter ein dokument-inspizierendes Beispiel im Eingangsstil: PDFium Component