Teknisk artikel

Lydløse PDF-loadfejl i Delphi: brug en PDFium load report

I PDFium Component til Delphi og Lazarus rejser en tildeling af TPdf.Active := True aldrig, når en PDF fejler at loade: TPdf.SetActive opsluger hver exception og efterlader komponenten inaktiv. For at se den reelle fejl, så kald TPdf.LoadDocument(Options, Report) i stedet. Den overload re-rejser den originale exception og udfylder en TPdfLoadReport med load-status, den native PDFium-fejlkode og, om cross-reference-tabellen måtte genopbygges

Problemet viser sig normalt i batchkode. Et tabeludtrækningsjob går en mappe med 13 virkelighedens PDF'er igennem med én delt TPdf, og 7 af dem kommer tilbage som fiaskoer. Ingen af de 7 filer er faktisk brudt. except-blokkene omkring loadet fyres aldrig, loggen bebrejder de forkerte filnavne, og den første synlige fejl er en bar EPdfError om en inaktiv komponent, rejst fra en egenskablæsning flere linjer efter det load, der reelt fejlede. To separate adfærdsmønstre stabler sig op og producerer det billede, og begge virker som designet

Hvorfor rejser TPdf.Active := True ikke, når en PDF fejler at loade?

TPdf.SetActive wrapper LoadDocument i en try..except, der opsluger hver exceptionklasse og simpelthen efterlader komponenten inaktiv. Opslugningen er bevidst: samme setter kører, når en form designer toggle'r Active i IDE'en, og en dårlig sti må ikke crash'e IDE'en. Ved run time rapporterer TPdf.Active blot, om et native dokumenthandle findes, så efter et fejlet load læser den False, og intet andet sker. Hvad end der blev rejst, er væk — om det var en EPdfError fra parseren, en stream-fejl eller en EAccessViolation fra en halvbundet pdfium.dll. De detaljerede DLL-beskeder, der er beskrevet i diagnosing pdfium.dll load failures in Delphi, når kun din handler gennem et kald, der ikke opsluger dem

To load-veje i PDFium Component: en tildeling af Active true opsluger hver exception i setteren og udskyder fejlen til det første bevogtede kald, hvor CheckActive rejser en EPdfError om en inaktiv komponent, mens LoadDocument med TPdfLoadOptions og en TPdfLoadReport auditor header, startxref, xref og end of file-markeret og derefter re-rejser den originale exception med den reelle årsag vedhæftet
Opslugningen er bevidst, fordi IDE-designeren deler setteren; batchkode behøver overloaden, der rejser, rapporterer og fortæller filens reelle historie
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive opsluger enhver load-exception
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // udføres aldrig
end;
// Fejlen kommer til syne her i stedet, som en generisk EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Minimal fix for eksisterende kode: test Active lige efter tildelingen;
// siden v3.122.1 beholder LastLoadReport teksten af den opslugte fejl
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Fejlen dukker endelig op ved det første bevogtede kald. TPdf.PageCount, som de fleste dokumentegenskaber, starter med CheckActive, der rejser en EPdfError og navngiver komponenten, men ikke filen og ikke årsagen. At teste Pdf.Active umiddelbart efter tildelingen forvandler en fejlattribueret crash til et ærligt "failed"-entry. Før PDFiumPas v3.122.1 var årsagen tabt på det tidspunkt; siden v3.122.1 erstatter den fejlede tildeling LastLoadReport med en plsFailed-rapport, der bærer fejlteksten, så årsagen overlever. Exception-objektet selv og byte-niveau-auditten kræver stadig et andet indgangspunkt

Hvorfor fejler genbrug af én TPdf fra den anden fil og frem?

TPdf.FileName kan kun tildeles, mens komponenten er inaktiv, så en delt instans afviser den anden fil, før den nogensinde forsøger at loade den. TPdf.SetFileName starter med CheckInactive, og samme vagt beskytter Password og FormFill. Efter det første succesfulde load forbliver instansen aktiv, den næste tildeling rejser, og hvis batchløkken fanger den exception og går videre, lander fejlen under det nye filnavn, mens det gamle dokument stadig er åbent. Blandet med de opslugte load-fejl holder loggen op med at matche virkeligheden. I 13-fil-reproduktionen rapporterede en delt instans 7 fiaskoer, mens en frisk TPdf.Create(nil) pr. dokument åbnede alle 13. At sætte Active := False mellem filer virker også, men én instans pr. dokument holder hver fil isoleret af konstruktion

Tidslinje for en delt PDFium TPdf, der fejler fra den anden fil og frem: efter det første load forbliver instansen aktiv, den næste FileName-tildeling rejser i CheckInactive før noget load-forsøg, og batchløkken logger fejlen under det nye filnavn, mens det gamle dokument stadig er åbent — fælden bag 7 falske fiaskoer i en 13-fils batch
SetFileName vagter med CheckInactive, så en delt instans afviser fil to, før den prøver den; isolér hvert dokument med sin egen TPdf, og loggen matcher virkeligheden igen

Hvad giver TPdf.LoadDocument med en TPdfLoadReport dig?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) rejser den reelle exception og fortæller dig også, hvad der skete, i struktureret form. Fil-overloaden loader FileName; søster-overloads tager TBytes eller en pointer og en størrelse, og LoadCustomDocument(AStream, AOwnsStream, Options, Report) dækker streams. Hver eneste validerer options, tjekker, at instansen er inaktiv, kører en byte-niveau-audit af headeren, startxref, xref-sektionerne og %%EOF-markeret og udfører derefter det native load. Auditten er afgrænset af samme slags grænser, som er diskuteret i parser resource budgets for untrusted PDFs: TPdfLoadOptions.Default sætter AuditByteLimit til 256 MiB, MaxIssues til 256, MaxXrefSections til 1024 og MaxXrefEntries til 4.000.000. Ved fejl sætter metoden Report.Status := plsFailed og re-rejser; fordi Report skrives in place, overlever dens indhold exceptionen, og en kopi gemmes i TPdf.LastLoadReport

Rapportfelterne svarer på de spørgsmål, en batchlog reelt har brug for. Status er én af plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected eller plsFailed. NativeErrorCode holder FPDF_GetLastError, så FPDF_ERR_PASSWORD (4) adskiller en manglende eller forkert adgangskode fra en beskadiget fil rapporteret som FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid og RecoveryRoute siger, om PDFium måtte genopbygge xref-tabellen, og Issues lister hvert audit-fund med Code, Severity, Offset, ObjectNumber og MessageText, med IssuesTruncated sat, når MaxIssues klippede listen kort

LoadDocument-pipelinen i PDFium Component og dens TPdfLoadReport: optionvalidering og inaktiv-tjekket rejser, før nogen rapport findes, en byte-audit går igennem header, startxref, xref-sektioner og end of file-marker, det native load registrerer FPDF_GetLastError, og udfaldene forgrener sig i loaded, loaded with recovery efter en xref-genopbygning, strict rejection eller failure
Status, NativeErrorCode og issue-listen svarer på, hvad en batchlog behøver; kun en options-overload tilføjer byte-auditten, mens en fejlet Active := True siden v3.122.1 stadig registrerer plsFailed i 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);          // én instans pr. dokument
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report udfyldes, selvom LoadDocument rejste
          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;

Hvornår bør du loade med plmStrict?

Brug plmStrict, hver gang en stille repareret fil er værre end en afvist — som arkivindtag, evidence-håndtering eller en signeringspipeline. PDFium rekonstruerer stille en brudt cross-reference-tabel (ISO 32000-1 §7.5.4) ved at scanne filen for objekter, hvilket er fint for en viewer og et problem for alt, der skal procese præcis de bytes, det fik. Efter det native load spørger komponenten FPDF_DocumentHasValidCrossReferenceTable. I plmCompatible-tilstand giver en genopbygning plsLoadedWithRecovery plus en plicNativeCrossReferenceRebuild-advarsel. I plmStrict-tilstand unlåder komponenten dokumentet, sætter plsRejected, tilføjer plicStrictModeRejected og rejser en EPdfError med "Strict PDF load rejected the document". Strict-tilstand afviser også enhver audit-fejl, og TPdfLoadOptions.Default(plmStrict) slår RequireFinalEndOfFileMarker til, hvilket forfremmer en manglende %%EOF eller data efter den sidste (§7.5.5) fra advarsel til fejl. Xref-auditten komplementerer objektniveau-tjekkene i validating object and xref streams with 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;               // gyldig xref, ingen audit-fejl
    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;

Hvor holder TPdf.LastLoadReport op med at sige sandheden?

TPdf.LastLoadReport er kun komplet efter en LoadDocument-overload, der tager options, for kun de overloads kører byte-auditten. Et succesfuldt Active := True skriver en compatible-mode-rapport uden byte-audit, så AuditAttempted forbliver False. Før PDFiumPas v3.122.1 skrev et fejlet en intet, hvilket betød, at LastLoadReport på en delt instans stadig beskrev den forrige fil, ofte med en beroligende plsLoaded. Siden v3.122.1 erstatter hvert fejlet load rapporten: et fejlet Active := True, som stadig efterlader komponenten inaktiv uden at rejse, og et fejlet kald til den blotte LoadDocument eller LoadCustomDocument registrerer plsFailed med fejlteksten — igen uden en audit. Yderligere to huller betyder noget i praksis. Optionvalidering og CheckInactive kører, før rapporten initialiseres, så en negativ AuditByteLimit eller en allerede aktiv instans rejser uden at producere en rapport. Og NativeErrorCode er kun meningsfuld, når PDFium reelt forsøgte parsingen; for en manglende fil rejser wrapperen, før PDFium kører, så log ErrorMessage og exception-teksten i stedet

Den praktiske regel er kort. Behold Active := True til designer-bundne viewers, hvor en inaktiv komponent er et acceptabelt udfald. Alle andre steder, og frem for alt i batch- og serverkode, så opret én TPdf pr. dokument, kald LoadDocument(Options, Report), fang exceptionen, den rejser, og log Report.Status, NativeErrorCode og fejl-niveau-Issues sammen med filnavnet. Prisen er et par linjer pr. kaldsted, og hver fiasko bliver tilskrevet den rigtige fil med sin reelle årsag

Load report-API'en, strict mode og byte-niveau-auditten skibes med PDFium Component for Delphi, C++Builder and Lazarus, side om side med rendering, tekstudtrækning, formudfyldning og PDF/A-validering