Teknisk artikel

Tysta PDF-inläsningsfel i Delphi: använd en PDFium-rapport

I PDFium Component för Delphi och Lazarus kastar tilldelningen TPdf.Active := True aldrig när en PDF misslyckas att läsas in: TPdf.SetActive fångar varje undantag och lämnar komponenten inaktiv. För att se det verkliga felet, anropa i stället TPdf.LoadDocument(Options, Report). Den överlagringen kastar om originalundantaget och fyller en TPdfLoadReport med inläsningsstatus, den nativa PDFium-felkoden och om referenstabellen behövde byggas om

Problemet visar sig oftast i batchkod. Ett tabellutdragsjobb går igenom en mapp med 13 verkliga PDF:er med en gemensam TPdf, och 7 av dem kommer tillbaka som misslyckanden. Ingen av de 7 filerna är faktiskt söndrig. except-blocken runt inläsningen avfyras aldrig, loggen beskyller fel filnamn, och det första synliga felet är en bar EPdfError om en inaktiv komponent, kastad från en egenskapsläsning flera rader efter den inläsning som verkligen fallerade. Två skilda beteenden staplas för att ge den bilden, och båda fungerar som konstruerat

Varför kastar TPdf.Active := True inte när en PDF misslyckas att läsas in?

TPdf.SetActive slår in LoadDocument i en try..except som sväljer varje undantagsklass och helt enkelt lämnar komponenten inaktiv. Svalget är medvetet: samma setter körs när en formulärdesigner växlar Active i IDE:n, och en felaktig sökväg får inte krascha IDE:n. Vid körning rapporterar TPdf.Active bara om ett nativt dokumenthandtag finns, så efter en misslyckad inläsning läser den False och ingenting annat händer. Vad som än kastades är borta, oavsett om det var en EPdfError från tolkaren, ett strömfel eller en EAccessViolation från en halvt bunden pdfium.dll. De detaljerade DLL-meddelanden som beskrivs i att diagnostisera pdfium.dll-inläsningsfel i Delphi når din hanterare bara genom ett anrop som inte sväljer dem

Två inläsningsvägar i PDFium Component: tilldelning av Active true sväljer varje undantag i settern och skjuter upp felet till första bevakade anropet, där CheckActive kastar en EPdfError om en inaktiv komponent, medan LoadDocument med TPdfLoadOptions och en TPdfLoadReport granskar huvudet, startxref, xref och slutet-av-fil-markören, och sedan kastar om originalundantaget med den verkliga orsaken bifogad
Svalget är medvetet för att IDE-designern delar settern; batchkod behöver överlagringen som kastar, rapporterar och berättar filens verkliga historia
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive sväljer varje inläsningsundantag
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // körs aldrig
end;
// Felet visar sig här i stället, som en generisk EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Minimal fix för befintlig kod: testa Active direkt efter tilldelningen;
// sedan v3.122.1 behåller LastLoadReport texten av det svalda felet
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Felet dyker till slut upp vid det första bevakade anropet. TPdf.PageCount, som de flesta dokumentegenskaper, börjar med CheckActive, som kastar en EPdfError som namnger komponenten men inte filen och inte orsaken. Att testa Pdf.Active omedelbart efter tilldelningen förvandlar en feltillskriven krasch till en ärlig "misslyckad"-post. Före PDFiumPas v3.122.1 var orsaken förlorad vid den punkten; sedan v3.122.1 ersätter den misslyckade tilldelningen LastLoadReport med en plsFailed-rapport som bär feltexten, så att orsaken överlever. Undantagsobjektet självt och granskningen på bytenivå kräver fortfarande en annan ingång

Varför faller återanvändning av en TPdf från den andra filen och framåt?

TPdf.FileName kan bara tilldelas medan komponenten är inaktiv, så en gemensam instans avvisar den andra filen innan den ens försöker läsa in den. TPdf.SetFileName börjar med CheckInactive, och samma vakt skyddar Password och FormFill. Efter den första lyckade inläsningen förblir instansen aktiv, nästa tilldelning kastar, och om batchloopen fångar det undantaget och går vidare hamnar felet under det nya filnamnet medan det gamla dokumentet fortfarande är öppet. Blandat med de svalda inläsningsfelen slutar loggen stämma med verkligheten. I 13-filsreproduktionen rapporterade en gemensam instans 7 misslyckanden, medan en färsk TPdf.Create(nil) per dokument öppnade alla 13. Att sätta Active := False mellan filerna fungerar också, men en instans per dokument håller varje fil isolerad av konstruktion

Tidslinje för en gemensam PDFium-TPdf som faller från den andra filen och framåt: efter den första inläsningen förblir instansen aktiv, nästa FileName-tilldelning kastar i CheckInactive innan något inläsningsförsök, och batchloopen loggar felet under det nya filnamnet medan det gamla dokumentet fortfarande är öppet — fällan bakom 7 falska misslyckanden i en 13-filsbatch
SetFileName bevakar med CheckInactive, så en gemensam instans avvisar fil två innan den försöker den; isolera varje dokument med sin egen TPdf och loggen stämmer med verkligheten igen

Vad ger TPdf.LoadDocument med en TPdfLoadReport?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) kastar det verkliga undantaget och talar också om vad som hände i strukturerad form. Filöverlagringen läser in FileName; syskonöverlagringar tar TBytes eller en pekare och storlek, och LoadCustomDocument(AStream, AOwnsStream, Options, Report) täcker strömmar. Var och en validerar optionerna, kontrollerar att instansen är inaktiv, kör en granskning på bytenivå av huvudet, startxref, xref-avsnitten och %%EOF-markören, och utför sedan den nativa inläsningen. Granskningen avgränsas av samma sorts gränser som diskuteras i parserresursbudgetar för obetrodda PDF:er: TPdfLoadOptions.Default sätter AuditByteLimit till 256 MiB, MaxIssues till 256, MaxXrefSections till 1024 och MaxXrefEntries till 4 000 000. Vid misslyckande sätter metoden Report.Status := plsFailed och kastar om; eftersom Report skrivs på plats överlever dess innehåll undantaget, och en kopia lagras i TPdf.LastLoadReport

Rapportfälten svarar på de frågor en batchlogg faktiskt behöver. Status är en av plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected eller plsFailed. NativeErrorCode håller FPDF_GetLastError, så att FPDF_ERR_PASSWORD (4) skiljer ett saknat eller fel lösenord från en skadad fil rapporterad som FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid och RecoveryRoute säger om PDFium fick bygga om xref-tabellen, och Issues listar varje granskningsfynd med Code, Severity, Offset, ObjectNumber och MessageText, med IssuesTruncated satt när MaxIssues kapade listan

LoadDocument-pipelinen i PDFium Component och dess TPdfLoadReport: optionvalidering och inaktivitetskontrollen kastar innan någon rapport finns, en bytegranskning går igenom huvudet, startxref, xref-avsnitten och slutet-av-fil-markören, den nativa inläsningen registrerar FPDF_GetLastError, och utfallen förgrenar sig till inläst, inläst med räddning efter en xref-ombyggnad, strikt avvisning eller misslyckande
Status, NativeErrorCode och fyndlistan svarar på vad en batchlogg behöver; bara en options-överlagring lägger till bytegranskningen, medan en misslyckad Active := True sedan v3.122.1 ändå registrerar 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);          // en instans per dokument
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report fylls även om LoadDocument kastade
          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;

När ska du läsa in med plmStrict?

Använd plmStrict närhelst en tyst reparerad fil är värre än en avvisad, som i arkivintag, bevishantering eller en signeringspipeline. PDFium bygger tyst upp en söndrig referenstabell (ISO 32000-1 §7.5.4) genom att skanna filen efter objekt, vilket är utmärkt för en viewer och ett problem för allt som måste behandla exakt de byte det fick. Efter den nativa inläsningen frågar komponenten FPDF_DocumentHasValidCrossReferenceTable. I plmCompatible-läge ger en ombyggnad plsLoadedWithRecovery plus en varning plicNativeCrossReferenceRebuild. I plmStrict-läge laddar komponenten ur dokumentet, sätter plsRejected, lägger till plicStrictModeRejected och kastar EPdfError med "Strict PDF load rejected the document". Strikt läge avvisar också varje granskningsfel, och TPdfLoadOptions.Default(plmStrict) slår på RequireFinalEndOfFileMarker, som höjer en saknad %%EOF eller data efter den sista (§7.5.5) från varning till fel. Xref-granskningen kompletterar objektnivåkontrollerna i att validera objekt- och xref-strömmar med 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;               // giltig xref, inga granskningsfel
    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;

Var slutar TPdf.LastLoadReport säga sanningen?

TPdf.LastLoadReport är fullständig bara efter en LoadDocument-överlagring som tar options, för bara de överlagringarna kör bytegranskningen. En lyckad Active := True skriver en rapport i compatible-mode utan bytegranskning, så att AuditAttempted stannar False. Före PDFiumPas v3.122.1 skrev en misslyckad sådan ingenting, vilket betydde att på en gemensam instans fortfarande beskrev LastLoadReport den föregående filen, ofta med en lugnande plsLoaded. Sedan v3.122.1 ersätter varje misslyckad inläsning rapporten: en misslyckad Active := True, som fortfarande lämnar komponenten inaktiv utan att kasta, och ett misslyckat anrop av enkla LoadDocument eller LoadCustomDocument registrerar plsFailed med feltexten, återigen utan granskning. Två luckor till spelar roll i praktiken. Optionvalidering och CheckInactive körs innan rapporten initierats, så en negativ AuditByteLimit eller en redan aktiv instans kastar utan att producera en rapport. Och NativeErrorCode är meningsfull bara när PDFium faktiskt försökte tolka; för en saknad fil kastar wrappern innan PDFium körs, så logga ErrorMessage och undantagstexten i stället

Den praktiska regeln är kort. Behåll Active := True för designarbundna viewers där en inaktiv komponent är ett acceptabelt utfall. Överallt annars, och framför allt i batch- och serverkod, skapa en TPdf per dokument, anropa LoadDocument(Options, Report), fånga undantaget den kastar och logga Report.Status, NativeErrorCode och felenivå-Issues tillsammans med filnamnet. Kostnaden är några rader per anropsplats, och varje misslyckande tillskrivs rätt fil med sin verkliga orsak

Inläsningsrapport-API:t, strikta läget och granskningen på bytenivå skeppas med PDFium Component för Delphi, C++Builder och Lazarus, tillsammans med rendering, textutdrag, formulärifyllning och PDF/A-validering