Technischer Artikel

Stille PDF-Ladefehler in Delphi: PDFium-Load-Report nutzen

In der PDFium Component for Delphi and Lazarus wirft die Zuweisung TPdf.Active := True nie, wenn ein PDF nicht lädt: TPdf.SetActive fängt jede Exception und lässt die Komponente inaktiv. Um den echten Fehler zu sehen, rufen Sie stattdessen TPdf.LoadDocument(Options, Report) auf. Dieses Overload hebt die ursprüngliche Exception erneut aus und füllt einen TPdfLoadReport mit dem Ladestatus, dem nativen PDFium-Fehlercode und der Auskunft, ob die Cross-Reference-Tabelle neu aufgebaut werden musste

Das Problem zeigt sich meist in Batch-Code. Ein Tabellenextraktions-Job läuft über einen Ordner mit 13 realweltlichen PDFs, die sich einen gemeinsamen TPdf teilen, und 7 davon kehren als Fehlschläge zurück. Keine der 7 Dateien ist tatsächlich kaputt. Die except-Blöcke um das Laden feuern nie, das Log macht die falschen Dateinamen verantwortlich, und der erste sichtbare Fehler ist ein nacktes EPdfError über eine inaktive Komponente, geworfen aus einem Property-Read mehrere Zeilen nach dem Laden, das wirklich scheiterte. Zwei getrennte Verhalten stapeln sich zu diesem Bild, und beide funktionieren wie entworfen

Warum wirft TPdf.Active := True nicht, wenn ein PDF nicht lädt?

TPdf.SetActive wickelt LoadDocument in ein try..except, das jede Exception-Klasse schluckt und die Komponente schlicht inaktiv lässt. Das Schlucken ist absichtlich: Derselbe Setter läuft, wenn ein Form-Designer im IDE Active umlegt, und ein schlechter Pfad darf die IDE nicht abschießen. Zur Laufzeit meldet TPdf.Active nur, ob ein natives Dokument-Handle existiert, liest also nach gescheitertem Laden False, und sonst passiert nichts. Was auch immer geworfen wurde, ist weg – ein EPdfError aus dem Parser, ein Stream-Fehler oder eine EAccessViolation aus einer halb gebundenen pdfium.dll. Die detaillierten DLL-Meldungen aus Diagnose von pdfium.dll-Ladefehlern in Delphi erreichen Ihren Handler nur über einen Aufruf, der sie nicht schluckt

Zwei Ladepfade in PDFium Component: Active auf true zu setzen schluckt jede Exception im Setter und schiebt das Scheitern auf den ersten bewachten Aufruf, wo CheckActive ein EPdfError über eine inaktive Komponente wirft, während LoadDocument mit TPdfLoadOptions und einem TPdfLoadReport Header, startxref, xref und End-of-File-Marker auditiert und dann die ursprüngliche Exception mit der echten Ursache erneut auslöst
Das Schlucken ist absichtlich, weil der IDE-Designer denselben Setter teilt; Batch-Code braucht das Overload, das wirft, berichtet und die wahre Geschichte der Datei erzählt
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive schluckt jede Lade-Exception
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // läuft nie
end;
// Das Scheitern taucht stattdessen hier auf, als generisches EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Minimaler Fix für bestehenden Code: Active direkt nach der Zuweisung testen;
// seit v3.122.1 hält LastLoadReport den Text der geschluckten Fehler
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Der Fehlschlag taucht schließlich am ersten bewachten Aufruf auf. TPdf.PageCount beginnt wie die meisten Dokument-Properties mit CheckActive, das ein EPdfError mit dem Komponentennamen wirft, aber weder Datei noch Ursache nennt. Pdf.Active direkt nach der Zuweisung zu testen macht aus einer falsch zugeschriebenen Absturzmeldung einen ehrlichen „failed“-Eintrag. Vor PDFiumPas v3.122.1 war der Grund an der Stelle verloren; seit v3.122.1 ersetzt die gescheiterte Zuweisung LastLoadReport durch einen plsFailed-Report mit dem Fehlertext, also überlebt die Ursache. Das Exception-Objekt selbst und das Byte-Level-Audit verlangen weiterhin einen anderen Einstiegspunkt

Warum scheitert ein wiederverwendeter TPdf ab der zweiten Datei?

TPdf.FileName lässt sich nur zuweisen, solange die Komponente inaktiv ist, also weist eine geteilte Instanz die zweite Datei zurück, bevor sie überhaupt versucht zu laden. TPdf.SetFileName beginnt mit CheckInactive, und derselbe Guard bewacht Password und FormFill. Nach dem ersten erfolgreichen Laden bleibt die Instanz aktiv, die nächste Zuweisung wirft, und wenn die Batch-Schleife diese Exception fängt und weiterzieht, landet der Fehler unter dem neuen Dateinamen, während das alte Dokument noch offen ist. Vermischt mit den geschluckten Ladefehlern hört das Log auf, zur Realität zu passen. In der 13-Datei-Reproduktion meldete eine geteilte Instanz 7 Fehlschläge, während ein frisches TPdf.Create(nil) pro Dokument alle 13 öffnete. Zwischen den Dateien Active := False zu setzen funktioniert auch, aber eine Instanz pro Dokument hält jede Datei von Konstruktion wegen isoliert

Zeitstrahl eines geteilten PDFium-TPdf, das ab der zweiten Datei scheitert: Nach dem ersten Laden bleibt die Instanz aktiv, die nächste FileName-Zuweisung wirft im CheckInactive vor jedem Ladeversuch, und die Batch-Schleife loggt den Fehler unter dem neuen Dateinamen, während das alte Dokument noch offen ist – die Falle hinter 7 falschen Fehlschlägen in einem 13-Datei-Batch
SetFileName bewacht mit CheckInactive, also weist eine geteilte Instanz Datei zwei zurück, bevor sie es versucht; isolieren Sie jedes Dokument mit seinem eigenen TPdf, und das Log passt wieder zur Realität

Was bringt TPdf.LoadDocument mit einem TPdfLoadReport?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) wirft die echte Exception und sagt Ihnen zusätzlich strukturiert, was passiert ist. Das Datei-Overload lädt FileName; Geschwister-Overloads nehmen TBytes oder Pointer und Größe, und LoadCustomDocument(AStream, AOwnsStream, Options, Report) deckt Streams ab. Jedes validiert die Optionen, prüft, dass die Instanz inaktiv ist, führt ein Byte-Level-Audit von Header, startxref, den xref-Abschnitten und dem %%EOF-Marker durch und macht dann das native Laden. Das Audit ist durch dieselbe Art von Grenzen begrenzt, die in Parser-Ressourcenbudgets für nicht vertrauenswürdige PDFs diskutiert werden: TPdfLoadOptions.Default setzt AuditByteLimit auf 256 MiB, MaxIssues auf 256, MaxXrefSections auf 1024 und MaxXrefEntries auf 4.000.000. Beim Scheitern setzt die Methode Report.Status := plsFailed und wirft erneut; weil Report an Ort und Stelle geschrieben wird, überleben seine Inhalte die Exception, und eine Kopie landet in TPdf.LastLoadReport

Die Report-Felder beantworten die Fragen, die ein Batch-Log tatsächlich braucht. Status ist einer von plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected oder plsFailed. NativeErrorCode hält FPDF_GetLastError, also trennt FPDF_ERR_PASSWORD (4) ein fehlendes oder falsches Passwort von einer als FPDF_ERR_FORMAT (3) gemeldeten beschädigten Datei. UsedRecovery, CrossReferenceTableValid und RecoveryRoute sagen, ob PDFium die xref-Tabelle neu aufbauen musste, und Issues listet jeden Audit-Befund mit Code, Severity, Offset, ObjectNumber und MessageText, mit gesetztem IssuesTruncated, wenn MaxIssues die Liste gestutzt hat

Die LoadDocument-Pipeline der PDFium Component und ihr TPdfLoadReport: Optionsvalidierung und der Inaktiv-Check werfen, bevor ein Report existiert, ein Byte-Audit läuft über Header, startxref, xref-Abschnitte und End-of-File-Marker, das native Laden zeichnet FPDF_GetLastError auf, und die Ausgänge verzweigen in loaded, loaded with recovery nach einem xref-Rebuild, strikte Zurückweisung oder Scheitern
Status, NativeErrorCode und die Befundliste beantworten, was ein Batch-Log braucht; nur ein Options-Overload ergänzt das Byte-Audit, und seit v3.122.1 zeichnet ein gescheitertes Active := True trotzdem plsFailed in LastLoadReport auf
uses
  SysUtils, Classes, TypInfo, FPdfView, PDFium;

procedure ProcessBatch(Files, Log: TStrings);
var
  I: Integer;
  Pdf: TPdf;
  Options: TPdfLoadOptions;
  Report: TPdfLoadReport;
begin
  Options := TPdfLoadOptions.Default(plmCompatible);
  for I := 0 to Files.Count - 1 do
  begin
    Pdf := TPdf.Create(nil);          // eine Instanz pro Dokument
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report ist gefüllt, obwohl LoadDocument geworfen hat
          if Report.NativeErrorCode = FPDF_ERR_PASSWORD then
            Log.Add(Files[I] + ': password required')
          else
            Log.Add(Format('%s: %s (%s)', [Files[I],
              GetEnumName(TypeInfo(TPdfLoadStatus), Ord(Report.Status)),
              E.Message]));
          Continue;
        end;
      end;
      if Report.UsedRecovery then
        Log.Add(Files[I] + ': opened after PDFium rebuilt the xref table');
      ExtractTables(Pdf, Log);
    finally
      Pdf.Free;
    end;
  end;
end;

Wann sollten Sie mit plmStrict laden?

Nehmen Sie plmStrict, wann immer eine still reparierte Datei schlimmer ist als eine zurückgewiesene, etwa bei Archivaufnahme, Evidence-Handling oder einer Signatur-Pipeline. PDFium rekonstruiert stillschweigend eine kaputte Cross-Reference-Tabelle (ISO 32000-1 §7.5.4), indem es die Datei nach Objekten absucht – großartig für einen Viewer und ein Problem für alles, was exakt die Bytes verarbeiten muss, die es bekam. Nach dem nativen Laden fragt die Komponente FPDF_DocumentHasValidCrossReferenceTable ab. Im plmCompatible-Modus ergibt ein Rebuild plsLoadedWithRecovery plus eine plicNativeCrossReferenceRebuild-Warnung. Im plmStrict-Modus entlädt die Komponente das Dokument, setzt plsRejected, fügt plicStrictModeRejected hinzu und wirft ein EPdfError mit „Strict PDF load rejected the document“. Der Strict-Modus weist außerdem jeden Audit-Fehler zurück, und TPdfLoadOptions.Default(plmStrict) schaltet RequireFinalEndOfFileMarker ein, was ein fehlendes %%EOF oder Daten nach dem letzten (§7.5.5) von Warnung zu Fehler hochstuft. Das xref-Audit ergänzt die Objekt-Level-Checks aus Validieren von Objekt- und xref-Streams mit PDFium VCL

function AcceptForArchive(const FileName: string; out Reason: string): Boolean;
var
  Pdf: TPdf;
  Report: TPdfLoadReport;
  I: Integer;
begin
  Result := False;
  Reason := '';
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := FileName;
    try
      Pdf.LoadDocument(TPdfLoadOptions.Default(plmStrict), Report);
      Result := True;               // gültiges xref, keine Audit-Fehler
    except
      on E: EPdfError do
      begin
        Reason := E.Message;
        for I := 0 to High(Report.Issues) do
          if Report.Issues[I].Severity = plisError then
            Reason := Reason + sLineBreak + Format('  at offset %d: %s',
              [Report.Issues[I].Offset, Report.Issues[I].MessageText]);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Wo hört TPdf.LastLoadReport auf, die Wahrheit zu sagen?

TPdf.LastLoadReport ist erst nach einem LoadDocument-Overload mit Optionen vollständig, denn nur diese Overloads fahren das Byte-Audit. Ein erfolgreiches Active := True schreibt einen Compatible-Mode-Report ohne Byte-Audit, also bleibt AuditAttempted False. Vor PDFiumPas v3.122.1 schrieb ein gescheitertes gar nichts, was bedeutete, dass auf einer geteilten Instanz LastLoadReport weiterhin die vorige Datei beschrieb, oft mit einem beruhigenden plsLoaded. Seit v3.122.1 ersetzt jede gescheiterte Ladung den Report: Ein gescheitertes Active := True, das die Komponente weiterhin inaktiv lässt, ohne zu werfen, und ein gescheiterter schlichter LoadDocument- oder LoadCustomDocument-Aufruf zeichnen plsFailed mit dem Fehlertext auf, ebenfalls ohne Audit. Zwei weitere Lücken zählen in der Praxis. Optionsvalidierung und CheckInactive laufen, bevor der Report initialisiert ist, also wirft ein negatives AuditByteLimit oder eine bereits aktive Instanz, ohne einen Report zu erzeugen. Und NativeErrorCode ist nur aussagekräftig, wenn PDFium den Parse tatsächlich versucht hat; bei einer fehlenden Datei wirft der Wrapper, bevor PDFium läuft, also loggen Sie stattdessen ErrorMessage und den Exception-Text

Die praktische Regel ist kurz. Behalten Sie Active := True für designergebundene Viewer, wo eine inaktive Komponente ein akzeptables Ergebnis ist. Überall sonst, und vor allem in Batch- und Server-Code, erzeugen Sie ein TPdf pro Dokument, rufen LoadDocument(Options, Report) auf, fangen die geworfene Exception und loggen Report.Status, NativeErrorCode und die Fehler-Level-Issues zusammen mit dem Dateinamen. Der Preis sind ein paar Zeilen pro Aufrufstelle, und jeder Fehlschlag wird der richtigen Datei mit ihrer echten Ursache zugeordnet

Die Load-Report-API, der Strict-Modus und das Byte-Level-Audit kommen mit der PDFium Component for Delphi, C++Builder and Lazarus, neben Rendering, Textextraktion, Formularbefüllung und PDF/A-Validierung