Articolo tecnico

Fallimenti di load PDF silenziosi: il load report di PDFium

Nel PDFium Component per Delphi e Lazarus, assegnare TPdf.Active := True non solleva mai quando un PDF non si carica: TPdf.SetActive cattura ogni eccezione e lascia il componente inattivo. Per vedere l'errore vero, chiamate invece TPdf.LoadDocument(Options, Report). Quell'overload rilancia l'eccezione originale e riempie un TPdfLoadReport con lo stato di caricamento, il codice errore nativo di PDFium e se la tabella cross-reference abbia dovuto essere ricostruita

Il problema di solito emerge nel codice batch. Un lavoro di estrazione tabelle percorre una cartella di 13 PDF reali con un solo TPdf condiviso, e 7 di loro tornano come fallimenti. Nessuno dei 7 file è davvero rotto. I blocchi except attorno al caricamento non scattano mai, il log incolpa i nomi di file sbagliati, e il primo errore visibile è un nudo EPdfError su un componente inattivo, sollevato da una lettura di proprietà diverse righe dopo il caricamento che ha davvero fallito. Due comportamenti separati si impilano per produrre quel quadro, ed entrambi funzionano come progettato

Perché TPdf.Active := True non solleva quando un PDF non si carica?

TPdf.SetActive incapsula LoadDocument in un try..except che ingoia ogni classe di eccezione e lascia semplicemente il componente inattivo. L'ingozzata è deliberata: lo stesso setter gira quando un designer di form commuta Active nell'IDE, e un percorso sbagliato non deve crashare l'IDE. A run time TPdf.Active riferisce solo se esiste un handle di documento nativo, quindi dopo un caricamento fallito si legge False e non succede nient'altro. Qualunque cosa sia stata sollevata è sparita, che fosse un EPdfError dal parser, un errore di stream o un EAccessViolation da un pdfium.dll mezzo collegato. I messaggi dettagliati della DLL descritti in diagnosticare i fallimenti di caricamento di pdfium.dll in Delphi raggiungono il vostro handler solo attraverso una chiamata che non li ingoia

Due percorsi di caricamento in PDFium Component: assegnare Active true ingoia ogni eccezione nel setter e rinvia il fallimento alla prima chiamata protetta, dove CheckActive solleva un EPdfError su un componente inattivo, mentre LoadDocument con TPdfLoadOptions e un TPdfLoadReport fa audit su header, startxref, xref e marcatore di fine file, poi rilancia l'eccezione originale con la causa vera attaccata
L'ingozzata è deliberata perché il designer dell'IDE condivide il setter; il codice batch ha bisogno dell'overload che solleva, riferisce e racconta la storia vera del file
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive ingoia qualunque eccezione di caricamento
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // non esegue mai
end;
// Il fallimento emerge invece qui, come un generico EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Correzione minima per codice esistente: testate Active subito dopo l'assegnamento;
// dalla v3.122.1 LastLoadReport conserva il testo dell'errore ingoiato
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Il fallimento alla fine si mostra alla prima chiamata protetta. TPdf.PageCount, come la maggior parte delle proprietà del documento, inizia con CheckActive, che solleva un EPdfError nominando il componente ma non il file e non la causa. Testare Pdf.Active subito dopo l'assegnamento trasforma un crash attribuito male in una voce onesta "fallito". Prima di PDFiumPas v3.122.1 la ragione si perdeva in quel punto; dalla v3.122.1 l'assegnamento fallito sostituisce LastLoadReport con un report plsFailed che porta il testo dell'errore, quindi la causa sopravvive. L'oggetto eccezione in sé e l'audit a livello di byte richiedono comunque un punto di ingresso diverso

Perché riutilizzare un solo TPdf fallisce dal secondo file in poi?

TPdf.FileName può essere assegnato solo mentre il componente è inattivo, quindi un'istanza condivisa rifiuta il secondo file prima ancora di provare a caricarlo. TPdf.SetFileName inizia con CheckInactive, e la stessa guardia protegge Password e FormFill. Dopo il primo caricamento riuscito l'istanza resta attiva, l'assegnamento successivo solleva, e se il loop batch cattura quell'eccezione e va avanti, l'errore atterra sotto il nuovo nome di file mentre il vecchio documento è ancora aperto. Mescolati ai fallimenti di caricamento ingoiati, il log smette di corrispondere alla realtà. Nella riproduzione da 13 file un'istanza condivisa riferiva 7 fallimenti, mentre un fresco TPdf.Create(nil) per documento ne apriva tutti e 13. Impostare Active := False tra i file funziona anch'esso, ma un'istanza per documento tiene ogni file isolato per costruzione

Cronologia di un TPdf PDFium condiviso che fallisce dal secondo file in poi: dopo il primo caricamento l'istanza resta attiva, il successivo assegnamento di FileName solleva in CheckInactive prima di ogni tentativo di caricamento, e il loop batch scrive l'errore nel log sotto il nuovo nome di file mentre il vecchio documento è ancora aperto, la trappola dietro 7 falsi fallimenti in un batch da 13 file
SetFileName protegge con CheckInactive, quindi un'istanza condivisa rifiuta il file due prima di provarlo; isolate ogni documento col suo TPdf e il log torna a corrispondere alla realtà

Che cosa vi dà TPdf.LoadDocument con un TPdfLoadReport?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) solleva l'eccezione vera e vi dice anche che cosa è successo in forma strutturata. L'overload per file carica FileName; gli overload fratelli prendono TBytes o un puntatore e una dimensione, e LoadCustomDocument(AStream, AOwnsStream, Options, Report) copre gli stream. Ognuno valida le opzioni, controlla che l'istanza sia inattiva, esegue un audit a livello di byte di header, startxref, le sezioni xref e il marcatore %%EOF, poi esegue il caricamento nativo. L'audit è limitato dallo stesso genere di limiti discussi in i budget di risorse del parser per PDF non fidati: TPdfLoadOptions.Default imposta AuditByteLimit a 256 MiB, MaxIssues a 256, MaxXrefSections a 1024 e MaxXrefEntries a 4.000.000. Al fallimento il metodo imposta Report.Status := plsFailed e rilancia; poiché Report viene scritto sul posto, il suo contenuto sopravvive all'eccezione, e una copia viene memorizzata in TPdf.LastLoadReport

I campi del report rispondono alle domande di cui un log batch ha davvero bisogno. Status è uno tra plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected o plsFailed. NativeErrorCode contiene FPDF_GetLastError, così FPDF_ERR_PASSWORD (4) separa una password mancante o sbagliata da un file danneggiato riferito come FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid e RecoveryRoute dicono se PDFium abbia dovuto ricostruire la tabella xref, e Issues elenca ogni riscontro dell'audit con Code, Severity, Offset, ObjectNumber e MessageText, con IssuesTruncated impostato quando MaxIssues ha tagliato la lista

La pipeline LoadDocument di PDFium Component e il suo TPdfLoadReport: la validazione delle opzioni e il controllo inattivo sollevano prima che esista un report, un audit di byte percorre header, startxref, sezioni xref e marcatore di fine file, il caricamento nativo registra FPDF_GetLastError, e gli esiti si ramificano in loaded, loaded with recovery dopo una ricostruzione xref, rifiuto severo o fallimento
Status, NativeErrorCode e la lista dei problemi rispondono a ciò che serve a un log batch; solo un overload con opzioni aggiunge l'audit dei byte, mentre dalla v3.122.1 un Active := True fallito registra comunque plsFailed in 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);          // una istanza per documento
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report viene riempito anche se LoadDocument ha sollevato
          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;

Quando caricare con plmStrict?

Usate plmStrict ogni volta che un file riparato in silenzio è peggiore di uno rifiutato, come intake di archivio, gestione di prove o una pipeline di firma. PDFium ricostruisce in silenzio una tabella cross-reference rotta (ISO 32000-1 §7.5.4) scandendo il file alla ricerca di oggetti, il che è ottimo per un viewer e un problema per qualsiasi cosa debba processare esattamente i byte che ha ricevuto. Dopo il caricamento nativo, il componente interroga FPDF_DocumentHasValidCrossReferenceTable. In modalità plmCompatible una ricostruzione produce plsLoadedWithRecovery più un warning plicNativeCrossReferenceRebuild. In modalità plmStrict il componente scarica il documento, imposta plsRejected, aggiunge plicStrictModeRejected e solleva EPdfError con "Strict PDF load rejected the document". La modalità strict rifiuta anche ogni errore di audit, e TPdfLoadOptions.Default(plmStrict) attiva RequireFinalEndOfFileMarker, che promuove un %%EOF mancante o dati dopo l'ultimo (§7.5.5) da warning a errore. L'audit xref completa i controlli a livello di oggetto di validare stream di oggetti e xref con 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 valida, nessun errore di 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;

Dove TPdf.LastLoadReport smette di dire la verità?

TPdf.LastLoadReport è completo solo dopo un overload di LoadDocument che prende le opzioni, perché solo quegli overload eseguono l'audit dei byte. Un Active := True riuscito scrive un report in modalità compatibile senza audit dei byte, quindi AuditAttempted resta False. Prima di PDFiumPas v3.122.1 uno fallito non scriveva nulla, il che significava che su un'istanza condivisa LastLoadReport descriveva ancora il file precedente, spesso con un rassicurante plsLoaded. Dalla v3.122.1 ogni caricamento fallito sostituisce il report: un Active := True fallito, che lascia comunque il componente inattivo senza sollevare, e una chiamata fallita a LoadDocument semplice o LoadCustomDocument registrano plsFailed col testo dell'errore, anche qui senza audit. Altri due buchi contano nella pratica. La validazione delle opzioni e CheckInactive girano prima che il report sia inizializzato, quindi un AuditByteLimit negativo o un'istanza già attiva sollevano senza produrre un report. E NativeErrorCode è significativo solo quando PDFium ha davvero tentato il parsing; per un file mancante il wrapper solleva prima che PDFium giri, quindi scrivete nel log ErrorMessage e il testo dell'eccezione

La regola pratica è breve. Tenete Active := True per i viewer legati al designer dove un componente inattivo è un esito accettabile. Ovunque altro, e sopra tutto nel codice batch e server, create un TPdf per documento, chiamate LoadDocument(Options, Report), catturate l'eccezione che solleva e scrivete nel log Report.Status, NativeErrorCode e gli Issues di livello errore insieme al nome del file. Il costo è qualche riga per call site, e ogni fallimento viene attribuito al file giusto con la sua causa vera

L'API del load report, la modalità strict e l'audit a livello di byte viaggiano con il PDFium Component for Delphi, C++Builder and Lazarus, insieme a rendering, estrazione testo, riempimento moduli e validazione PDF/A