Eine Pipeline zur Bearbeitung von Versicherungsschäden, an der ich gearbeitet habe, verlor durch eine einzige eingehende Datei einen halben Tag. Der „signierte Vertrag“, den ein Makler hochgeladen hatte, war ein mit Besitzerkennwort verschlüsseltes PDF, das ein XFA-Formular enthielt: Der nachgelagerte Textextraktor lieferte leere Zeichenfolgen, der Indexer legte den Schaden als leeres Dokument ab, und niemand bemerkte es, bis der Versicherungsnehmer anrief. Der Fehler lag nicht im Extraktor. Es hatte schlicht kein Code wirklich auf die Datei geschaut, bevor sie geroutet wurde. Jedes Team, das PDFs aus der Außenwelt annimmt, baut irgendwann dasselbe Werkzeug: eine Eingangs-Workbench, die jedes Dokument untersucht und entscheidet, wohin es weitergehen darf. PDFium Component, ein VCL/LCL-Dokumentviewer und eine Inspektionsbibliothek mit Quellcode für Delphi, C++Builder und Lazarus, liefert die Introspektionsaufrufe für diese Workbench; der Rest dieses Artikels zeigt, welche Aufrufe welche Fragen beantworten und wo sie in die Irre führen können.
Fünf Fragen, bevor eine Datei geroutet wird
Wenn man Raster und Vorschaustreifen ausblendet, bleiben bei der Eingangstriage fünf Fragen:
- Kann die Datei überhaupt geöffnet werden, und mit welchem Kennwort?
- Was behauptet sie zu sein: Titel, Autor, Erstellungsdatum?
- Enthält sie aktive oder riskante Inhalte: JavaScript, ein XFA-Formular, eingebettete Dateien?
- Gibt es extrahierbaren Text, oder ist sie ein Scan für OCR?
- Welche Warteschlange bekommt sie unter diesen Voraussetzungen: Dunkelverarbeitung, manuelle Prüfung oder Quarantäne?
Jede Frage lässt sich auf einen oder zwei Aufrufe von PDFium Component abbilden. Zwei dieser Zuordnungen haben scharfe Kanten, die für die meisten falsch gerouteten Dateien verantwortlich waren, die ich in Produktion debuggt habe: Dokumentmetadaten liegen an zwei verschiedenen Stellen, und Verschlüsselung verhindert nicht zwingend, dass ein Dokument geöffnet wird.
Billig öffnen: Formularausfüllung aus, keine Seite rendern
Triagieren sollte der billigste mögliche Öffnungsvorgang sein. Wenn FormFill := False vor Active := True gesetzt wird, überspringt die Komponente die Formularumgebung vollständig. Das verkürzt die Ladezeit und verhindert, was bei Dateien unbekannter Herkunft ebenso wichtig ist, dass dokumentweites JavaScript initialisiert wird. Keine der unten verwendeten Inspektionseigenschaften erfordert das Rendern einer Seite, daher muss ein Triage-Durchlauf kein 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; // no form environment, no JavaScript init
Pdf.Active := True; // failure is silent: Active simply stays False
if not Pdf.Active then
begin
Rec.OpenFailed := True; // damaged file or user-password lock
Exit; // the finally block still runs
end;
Rec.PageCount := Pdf.PageCount;
CollectIdentity(Pdf, IncomingPath, Rec);
CollectRiskSignals(Pdf, Rec);
finally
Pdf.Active := False;
Pdf.Free; // never leak the instance on a malformed file
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 dann fröhlich PageCount aus einem Dokument, das nie geöffnet wurde. Wenn der Ablehnungsworkflow den tatsächlichen Fehlertext der Engine braucht, lesen Sie die Datei in ein Byte-Array und rufen Sie die LoadDocument-Überladung auf, die TBytes entgegennimmt; dieser Pfad wirft EPdfError mit der Meldung, einschließlich des Kennwortfalls. Das try..finally bleibt trotzdem sinnvoll: Eingangsdienste laufen wochenlang unbeaufsichtigt, und keine spätere Exception darf die TPdf-Instanz lecken oder eine Sperre halten, über die der nächste Wiederholungsversuch stolpert.
Der Durchsatz wird selten zum Engpass. Bei deaktivierter Formularausfüllung und ohne Rendering wird ein Triage-Öffnen von I/O dominiert, und ein einzelner Worker untersucht bequem mehrere Dateien pro Sekunde von lokaler Platte. Falls das Eingangsvolumen irgendwann doch über einen Worker hinauswächst, partitionieren Sie die Arbeit nach Datei statt nach Prüfung. Die fünf Fragen teilen sich einen Öffnungsvorgang; sie über Prozesse aufzuteilen würde den teuersten Schritt vervielfachen, statt ihn zu amortisieren.
Metadaten liegen an zwei Stellen und widersprechen sich
ISO 32000-1 definiert zwei Speicherorte für Dokumentmetadaten: das Document Information Dictionary (Abschnitt 14.3.3) und ein am Katalog hängendes XMP-Paket (Abschnitt 14.3.2). Die Eigenschaften Title, Author, Subject und CreationDate lesen das Info-Dictionary (mit MetaText[] für andere Schlüssel und DecodeDate zum Parsen der Datumszeichenfolge D:YYYYMMDD...). Der Haken ist, dass moderne Erzeuger immer häufiger nur XMP schreiben, eine Richtung, die ISO 32000-2 offiziell macht, indem die meisten Info-Dictionary-Schlüssel in PDF 2.0 als veraltet markiert werden. In einem Eingangswerkzeug zeigt sich das sehr konkret: Ihre Workbench zeigt einen leeren Titel, während Adobe Acrobat einen anzeigt, weil Acrobat auf dc:title im XMP-Paket zurückfällt, das die Info-Dictionary-Eigenschaften nie berühren.
procedure CollectIdentity(Pdf: TPdf; const FilePath: string;
var Rec: TIntakeRecord);
begin
Rec.Title := Pdf.Title; // Info dictionary value
Rec.Author := Pdf.Author;
Rec.CreatedAt := Pdf.CreationDate; // raw PDF date string ("D:2026...")
// An empty Info title does not mean the document is untitled. The
// component does not expose the XMP packet, so probe the raw file
// bytes for the dc:title element before trusting the blank.
if (Rec.Title = '') and FileContainsText(FilePath, 'dc:title') then
Include(Rec.Flags, ifTitleInXmpOnly);
end;
Selbst die grobe Teilzeichenfolgenprobe oben lohnt sich: „Metadaten vorhanden, aber nicht dort, wo Legacy-Werkzeuge suchen“ ist für jede Archivpipeline, die nach Titel oder Autor indexiert, eine routingrelevante Tatsache. Wenn Ihr nachgelagerter Index nur das Info-Dictionary liest, werden so markierte Dateien stillschweigend unauffindbar.
Verschlüsselte Dateien, die trotzdem aufgehen
Ein verschlüsseltes Dokument muss beim Öffnen nicht scheitern. Der Standard Security Handler (ISO 32000-1 Abschnitt 7.6.3) unterscheidet ein Benutzerkennwort, das zum Öffnen des Dokuments erforderlich ist, von einem Besitzerkennwort, das lediglich Berechtigungen wie Drucken und Kopieren steuert. Ein großer Anteil „geschützter“ Geschäftsdokumente ist mit einem Besitzerkennwort und einem leeren Benutzerkennwort verschlüsselt. Sie öffnen ohne Nachfrage, werden vollständig entschlüsselt und verlassen sich darauf, dass Viewer die Berechtigungsflags freiwillig beachten. Das ist Richtlinie, nicht Schutz, und Ihre Eingangsstatus sollten den Unterschied abbilden.
Die Erkennung von Verschlüsselung nach erfolgreichem Öffnen braucht einen Engine-Aufruf plus einen Fallback: FPDF_GetSecurityHandlerRevision(Pdf.Document) liefert -1 für ungeschützte Dateien und sonst die Handler-Revision, und wenn Pdf.Permissions etwas anderes als die Maske $FFFFFFFF mit allen Bits gesetzt zurückgibt, ist das das bestätigende Signal. Bei Dateien, die wirklich durch ein Benutzerkennwort gesperrt sind, weisen Sie Password zu, bevor Sie Active := True setzen. Wenn das Öffnen weiterhin scheitert, routen Sie die Datei in einen blockierten Zustand, der Anmeldedaten vom Absender über einen sicheren Kanal anfordert, statt blind erneut zu versuchen. Und widerstehen Sie der Versuchung, „verschlüsselt“ automatisch als Quarantäne zu behandeln: In den meisten dokumentlastigen Branchen sind verschlüsselte, aber öffnende Dateien der Normalfall, nicht der verdächtige.
Aktive Inhalte: JavaScript, XFA und eingebettete Dateien
Drei Befunde sollten immer in die Routingentscheidung einfließen. Erstens JavaScript: Das Ereignis OnUnsupportedFeature meldet strukturelle Merkmale wie XFA oder 3D-Inhalte, sobald die Engine ihnen begegnet, erkennt aber kein JavaScript. Prüfen Sie stattdessen JavaScriptActionCount und behandeln Sie ein Ergebnis ungleich null als aktiven Inhalt. Zweitens XFA: Wenn FormType den Wert ftXfaFull zurückgibt, sind die sichtbaren Seiten oft kaum mehr als eine gerenderte Fassung der XFA-Vorlage, und klassische Textextraktion sieht Boilerplate statt ausgefüllter Werte. Drittens Anhänge: Ein PDF ist ein Containerformat, und AttachmentCount sagt Ihnen, ob dieses Dokument Passagiere mitbringt.
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 is a per-page property; walk the pages to total
// it. Loading a page object renders nothing, so this stays cheap.
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 Dokument, also verwenden Sie ihn niemals ohne Bereinigung als Ausgabepfad. Ein eingebetteter Name wie ..\..\start.exe ist eine Path-Traversal-Falle, die nur auf einen unvorsichtigen Speichervorgang wartet. Und eine Blockliste für Erweiterungen ist ein Stolperdraht, keine Garantie; ihre Aufgabe ist es, eine menschliche Entscheidung zu erzwingen, nicht die Datei sauber zu zertifizieren.
Signale in Routingzustände übersetzen
Ein brauchbares Zustandsmodell braucht weniger Zustände, als die meisten Teams erwarten: ready (keine Blocker, Text vorhanden), review (Öffnen erfolgreich, aber etwas braucht Augen: XFA-Formular, JavaScript, leere Textebene, Titel nur in XMP), blocked (Benutzerkennwort erforderlich) und damaged (Öffnen fehlgeschlagen). Speichern Sie die Belege zusammen mit dem Zustand: Dateihash, Seitenzahl, die exakten Flags, die Engine-Fehlermeldung bei beschädigten Dateien. Wer eine Routingentscheidung hinterfragt, tut das Wochen später, bei einer Datei, die inzwischen ersetzt oder verändert sein kann.
Wenn ein Operator eine Datei aus der Quarantäne wirklich ansehen muss, geben Sie sie nicht an den Standard-Shell-Viewer weiter. Rendern Sie sie in einer gehärteten Oberfläche, in der Skripting und Linkbehandlung deaktiviert sind, wie in einer sicheren PDF-Vorschauoberfläche in Delphi beschrieben. Und wenn Ihr Eingang in ein Archiv mit Konformitätsanforderungen einspeist, ist der Triage-Durchlauf der natürliche Ort, um eine tiefere Prüfung einzuplanen; Batch-Preflight-Validierung gegen PDF/A- und PDF/UA-Profile setzt genau dort an, wo diese Inspektion aufhört.
Häufig gestellte Fragen
Wie prüfe ich in Delphi, ob ein PDF kennwortgeschützt ist?
Öffnen Sie es mit PDFium Component und fragen Sie den Security Handler ab: FPDF_GetSecurityHandlerRevision(Pdf.Document) liefert -1 für ungeschützte Dateien. Wenn Active ohne Kennwort auf False bleibt, verwendet die Datei sehr wahrscheinlich ein Benutzerkennwort; weisen Sie Password zu und versuchen Sie es erneut. Wenn sie problemlos öffnet, aber ein Security Handler vorhanden ist, trägt die Datei nur Besitzerkennwortschutz: Sie ist vollständig lesbar, und die Berechtigungsflags in Permissions sind beratend.
Warum liefert die Title-Eigenschaft eine leere Zeichenfolge, obwohl Acrobat einen Titel zeigt?
Der Titel ist nur im XMP-Metadatenpaket gespeichert, nicht im Document Information Dictionary, das Title liest. Die Komponente stellt das XMP-Paket nicht bereit, also durchsuchen Sie die rohen Dateibytes nach dc:title und markieren Sie die Datei für Pipelines, die auf Info-Dictionary-Metadaten indexieren.
Kann PDFium Component JavaScript in einem PDF erkennen?
Ja. Prüfen Sie JavaScriptActionCount oder enumerieren Sie die dokumentweiten Aktionen über JavaScriptActions. Verlassen Sie sich dafür nicht auf das Ereignis OnUnsupportedFeature; es meldet Merkmale wie XFA und 3D, aber kein Skripting.
Die Produktseite der Komponente behandelt Lizenzierung, die vollständige Inspektions-API und die mitgelieferten Demos, einschließlich eines Dokumentinspektors im Stil einer Eingangsprüfung: PDFium Component.