Article technique

Échecs de chargement PDF muets en Delphi : le rapport PDFium

Dans le PDFium Component pour Delphi et Lazarus, assigner TPdf.Active := True ne lève jamais rien quand un PDF échoue à se charger : TPdf.SetActive attrape chaque exception et laisse le composant inactif. Pour voir la vraie erreur, appelez plutôt TPdf.LoadDocument(Options, Report). Cette surcharge relance l'exception d'origine et remplit un TPdfLoadReport avec le statut de chargement, le code d'erreur natif PDFium et l'indication de savoir si la table de références croisées a dû être reconstruite

Le problème apparaît d'ordinaire dans du code de lot. Un travail d'extraction de tables parcourt un dossier de 13 PDF du monde réel avec un TPdf partagé, et 7 d'entre eux reviennent en échecs. Aucun des 7 fichiers n'est réellement cassé. Les blocs except autour du chargement ne se déclenchent jamais, le journal accuse les mauvais noms de fichiers, et la première erreur visible est un EPdfError nu à propos d'un composant inactif, levé depuis une lecture de propriété plusieurs lignes après le chargement qui a vraiment échoué. Deux comportements séparés se superposent pour produire ce tableau, et les deux fonctionnent comme conçus

Pourquoi TPdf.Active := True ne lève-t-il rien quand un PDF échoue à se charger ?

TPdf.SetActive enveloppe LoadDocument dans un try..except qui avale toute classe d'exception et laisse simplement le composant inactif. L'avachalement est délibéré : le même setter tourne quand un concepteur de fiches bascule Active dans l'IDE, et un mauvais chemin ne doit pas faire planter l'IDE. À l'exécution, TPdf.Active se contente de rapporter si un handle de document natif existe, donc après un chargement raté, il se lit False et rien d'autre ne se passe. Ce qui avait été levé est perdu, que ce soit un EPdfError de l'analyseur, une erreur de flux ou un EAccessViolation venu d'un pdfium.dll à moitié lié. Les messages DLL détaillés décrits dans diagnostiquer les échecs de chargement de pdfium.dll en Delphi n'atteignent votre gestionnaire qu'à travers un appel qui ne les avale pas

Deux chemins de chargement dans PDFium Component : assigner Active true avale chaque exception dans le setter et reporte l'échec au premier appel gardé, où CheckActive lève un EPdfError à propos d'un composant inactif, tandis que LoadDocument avec TPdfLoadOptions et un TPdfLoadReport audite l'en-tête, startxref, le xref et la marque de fin de fichier, puis relance l'exception d'origine avec la vraie cause attachée
L'avachalement est délibéré parce que le concepteur de l'IDE partage le setter ; le code de lot a besoin de la surcharge qui lève, rapporte et raconte la vraie histoire du fichier
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive avale toute exception de chargement
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // ne s'exécute jamais
end;
// L'échec apparaît ici à la place, comme un EPdfError générique :
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Correction minimale pour le code existant : testez Active juste après l'assignation ;
// depuis la v3.122.1, LastLoadReport garde le texte de l'erreur avalée
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

L'échec finit par se montrer au premier appel gardé. TPdf.PageCount, comme la plupart des propriétés de document, commence par CheckActive, qui lève un EPdfError nommant le composant mais pas le fichier ni la cause. Tester Pdf.Active immédiatement après l'assignation transforme un plantage mal attribué en entrée « échec » honnête. Avant PDFiumPas v3.122.1, la raison était perdue à ce stade ; depuis la v3.122.1, l'assignation ratée remplace LastLoadReport par un rapport plsFailed qui porte le texte d'erreur, si bien que la cause survit. L'objet exception lui-même et l'audit au niveau octet exigent toujours un autre point d'entrée

Pourquoi réutiliser un TPdf échoue-t-il à partir du second fichier ?

TPdf.FileName ne peut être assigné que quand le composant est inactif, si bien qu'une instance partagée rejette le second fichier avant même d'essayer de le charger. TPdf.SetFileName commence par CheckInactive, et la même garde protège Password et FormFill. Après le premier chargement réussi, l'instance reste active, l'assignation suivante lève, et si la boucle de lot attrape cette exception et continue, l'erreur atterrit sous le nouveau nom de fichier tandis que l'ancien document est encore ouvert. Mélangés aux échecs de chargement avalés, le journal cesse de coller à la réalité. Dans la reproduction à 13 fichiers, une instance partagée rapportait 7 échecs, tandis qu'un TPdf.Create(nil) neuf par document ouvrait les 13. Mettre Active := False entre les fichiers marche aussi, mais une instance par document garde chaque fichier isolé par construction

Chronologie d'un TPdf PDFium partagé qui échoue à partir du second fichier : après le premier chargement, l'instance reste active, l'assignation FileName suivante lève dans CheckInactive avant toute tentative de chargement, et la boucle de lot journalise l'erreur sous le nouveau nom de fichier tandis que l'ancien document est encore ouvert, le piège derrière 7 faux échecs dans un lot de 13 fichiers
SetFileName se garde avec CheckInactive, si bien qu'une instance partagée rejette le second fichier avant de l'essayer ; isolez chaque document avec son propre TPdf et le journal colle de nouveau à la réalité

Que vous donne TPdf.LoadDocument avec un TPdfLoadReport ?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) lève la vraie exception et vous dit aussi ce qui s'est passé sous forme structurée. La surcharge fichier charge FileName ; les surcharges sœurs prennent TBytes ou un pointeur et une taille, et LoadCustomDocument(AStream, AOwnsStream, Options, Report) couvre les flux. Chacune valide les options, vérifie que l'instance est inactive, fait un audit au niveau octet de l'en-tête, du startxref, des sections xref et de la marque %%EOF, puis effectue le chargement natif. L'audit est borné par le même genre de limites que celles discutées dans les budgets de ressources de l'analyseur pour les PDF non fiables : TPdfLoadOptions.Default règle AuditByteLimit à 256 Mio, MaxIssues à 256, MaxXrefSections à 1024 et MaxXrefEntries à 4 000 000. En cas d'échec, la méthode pose Report.Status := plsFailed et relance ; parce que Report est écrit sur place, son contenu survit à l'exception, et une copie est stockée dans TPdf.LastLoadReport

Les champs du rapport répondent aux questions dont un journal de lot a réellement besoin. Status vaut l'une de plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected ou plsFailed. NativeErrorCode contient FPDF_GetLastError, si bien que FPDF_ERR_PASSWORD (4) sépare un mot de passe manquant ou faux d'un fichier endommagé rapporté comme FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid et RecoveryRoute disent si PDFium a dû reconstruire la table xref, et Issues liste chaque constat d'audit avec Code, Severity, Offset, ObjectNumber et MessageText, avec IssuesTruncated levé quand MaxIssues a écourté la liste

Le pipeline LoadDocument du PDFium Component et son TPdfLoadReport : la validation des options et le contrôle d'inactivité lèvent avant qu'un rapport n'existe, un audit d'octets parcourt l'en-tête, startxref, les sections xref et la marque de fin de fichier, le chargement natif enregistre FPDF_GetLastError, et les issues se ramifient en chargé, chargé avec récupération après une reconstruction xref, rejet strict ou échec
Status, NativeErrorCode et la liste de constats répondent à ce dont un journal de lot a besoin ; seule une surcharge avec options ajoute l'audit d'octets, tandis que depuis la v3.122.1, un Active := True raté enregistre quand même plsFailed dans LastLoadReport
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);          // une instance par document
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report est rempli même si LoadDocument a levé
          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;

Quand charger avec plmStrict ?

Utilisez plmStrict chaque fois qu'un fichier réparé en silence est pire qu'un fichier rejeté, comme pour une entrée d'archives, la manipulation de preuves ou un pipeline de signature. PDFium reconstruit tranquillement une table de références croisées cassée (ISO 32000-1 §7.5.4) en scannant le fichier à la recherche d'objets, ce qui est super pour une visionneuse et un problème pour tout ce qui doit traiter exactement les octets qu'il a reçus. Après le chargement natif, le composant interroge FPDF_DocumentHasValidCrossReferenceTable. En mode plmCompatible, une reconstruction donne plsLoadedWithRecovery plus un avertissement plicNativeCrossReferenceRebuild. En mode plmStrict, le composant décharge le document, pose plsRejected, ajoute plicStrictModeRejected et lève EPdfError avec « le chargement PDF strict a rejeté le document ». Le mode strict rejette aussi toute erreur d'audit, et TPdfLoadOptions.Default(plmStrict) active RequireFinalEndOfFileMarker, qui promeut un %%EOF manquant ou des données après le dernier (§7.5.5) d'avertissement à erreur. L'audit xref complète les contrôles au niveau objet de valider les flux d'objets et xref avec 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;               // xref valide, aucune erreur d'audit
    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;

Où TPdf.LastLoadReport cesse-t-il de dire la vérité ?

TPdf.LastLoadReport n'est complet qu'après une surcharge de LoadDocument qui prend des options, parce que seules ces surcharges font l'audit d'octets. Un Active := True réussi écrit un rapport en mode compatible sans audit d'octets, si bien que AuditAttempted reste False. Avant PDFiumPas v3.122.1, un échec n'écrivait rien, ce qui faisait que sur une instance partagée, LastLoadReport décrivait toujours le fichier précédent, souvent avec un plsLoaded rassurant. Depuis la v3.122.1, chaque chargement raté remplace le rapport : un Active := True raté, qui laisse toujours le composant inactif sans lever, et un appel LoadDocument ou LoadCustomDocument simple raté enregistrent plsFailed avec le texte d'erreur, là encore sans audit. Deux autres trous comptent en pratique. La validation des options et CheckInactive tournent avant que le rapport ne soit initialisé, si bien qu'un AuditByteLimit négatif ou une instance déjà active lève sans produire de rapport. Et NativeErrorCode ne veut dire quelque chose que quand PDFium a réellement tenté l'analyse ; pour un fichier manquant, le wrapper lève avant que PDFium ne tourne, donc journalisez ErrorMessage et le texte de l'exception à la place

La règle pratique est courte. Gardez Active := True pour les visionneuses liées à un concepteur où un composant inactif est un résultat acceptable. Partout ailleurs, et surtout dans le code de lot et de serveur, créez un TPdf par document, appelez LoadDocument(Options, Report), attrapez l'exception qu'il lève et journalisez Report.Status, NativeErrorCode et les Issues de niveau erreur avec le nom du fichier. Le coût, c'est quelques lignes par site d'appel, et chaque échec se retrouve attribué au bon fichier avec sa vraie cause

L'API de rapport de chargement, le mode strict et l'audit au niveau octet sont livrés avec le PDFium Component pour Delphi, C++Builder et Lazarus, aux côtés du rendu, de l'extraction de texte, du remplissage de formulaires et de la validation PDF/A