Odborný článok

Tiché zlyhania načítania PDF v Delphi a PDFium load report

V PDFium Component pre Delphi a Lazarus priradenie TPdf.Active := True pri zlyhaní načítania PDF nikdy nevyhodí výnimku: TPdf.SetActive zachytí každú výnimku a nechá komponent neaktívny. Skutočnú chybu uvidíte, keď zavoláte TPdf.LoadDocument(Options, Report). Ten overload znova vyhodí pôvodnú výnimku a naplní TPdfLoadReport statusom načítania, natívnym chybovým kódom PDFium a tým, či bolo treba prestavať tabuľku cross-reference

Problém sa zvyčajne ukáže v dávkovom kóde. Úloha na extrakciu tabuliek prejde priečinok s 13 reálnymi PDF s jedným zdieľaným TPdf a 7 z nich sa vráti ako zlyhané. Žiadny z tých 7 súborov nie je v skutočnosti pokazený. Bloky except okolo načítania sa nikdy nespustia, log obviní zlé mená súborov a prvá viditeľná chyba je holé EPdfError o neaktívnom komponente, vyhodené z čítania vlastnosti niekoľko riadkov po načítaní, ktoré naozaj padlo. Toto utvárajú dve samostatné správania a obe fungujú, ako boli navrhnuté

Prečo TPdf.Active := True nevyhodí výnimku, keď sa PDF nepodarí načítať?

TPdf.SetActive obaľuje LoadDocument do try..except, ktoré prehltne každú triedu výnimky a jednoducho nechá komponent neaktívny. To prehltnutie je zámyselné: ten istý setter beží, keď form designer prepína Active v IDE a zlá cesta nesmie zhodiť IDE. Za behu len TPdf.Active hlási, či existuje natívny handle dokumentu, takže po zlyhanom načítaní sa prečíta False a nič iné sa nedeje. Čokoľvek, čo bolo vyhodené, je preč, či to bol EPdfError od parsera, chyba streamu alebo EAccessViolation z polovicou naviazanej pdfium.dll. Detailné DLL hlásenia popísané v diagnostike zlyhaní načítania pdfium.dll v Delphi sa k vášmu handleru dostanú len cez volanie, ktoré ich neprehltnie

Dve cesty načítania v PDFium Component: priradenie Active true prehltne vo setteri každú výnimku a odloží zlyhanie na prvé strážené volanie, kde CheckActive vyhodí EPdfError o neaktívnom komponente, zatiaľ čo LoadDocument s TPdfLoadOptions a TPdfLoadReport skontroluje audit hlavičky, startxref, xref a markera konca súboru a potom znova vyhodí pôvodnú výnimku so skutočnou príčinou pripojenou
Prehltnutie je zámyselné, lebo IDE designer zdieľa ten setter; dávkový kód potrebuje overload, ktorý vyhodí výnimku, nahlási ju a rozpovie skutočný príbeh súboru
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive prehltne ľubovoľnú výnimku načítania
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // nikdy sa nevykoná
end;
// Zlyhanie sa ukáže tu namiesto toho, ako generické EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Minimálna oprava existujúceho kódu: otestujte Active hneď po priradení;
// od v3.122.1 LastLoadReport drží text prehltnutej chyby
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Zlyhanie sa napokon ukáže pri prvom stráženom volaní. TPdf.PageCount, ako väčšina vlastností dokumentu, začína CheckActive, ktorý vyhodí EPdfError pomerujúcu komponent, ale nie súbor a nie príčinu. Otestovanie Pdf.Active bezprostredne po priradení zmení zle pripísaný pád na úprimnú položku "zlyhalo". Pred PDFiumPas v3.122.1 sa dôvod v tom bode strácal; od v3.122.1 zlyhané priradenie nahradí LastLoadReport reportom plsFailed, ktorý nesie text chyby, takže príčina prežije. Samotný objekt výnimky a audit na úrovni bajtov stále vyžadujú iný vstupný bod

Prečo zlyháva znovupoužitie jedného TPdf od druhého súboru?

TPdf.FileName sa dá priradiť len vtedy, keď je komponent neaktívny, takže zdieľaná inštancia odmietne druhý súbor skôr, než sa vôbec pokúsi ho načítať. TPdf.SetFileName začína CheckInactive a tým istým strážcom sú chránené Password a FormFill. Po prvom úspešnom načítaní zostáva inštancia aktívna, ďalšie priradenie vyhodí výnimku a keď dávkový cyklus tú výnimku chytí a ide ďalej, chyba pristane pod novým menom súboru, zatiaľ čo starý dokument je stále otvorený. Zmiešané s prehltnutými zlyhaniami načítania sa log prestane zhodovať s realitou. V reprodukcii s 13 súbormi hlásila zdieľaná inštancia 7 zlyhaní, zatiaľ čo čerstvé TPdf.Create(nil) na dokument otvorilo všetkých 13. Nastavenie Active := False medzi súbormi tiež funguje, ale jedna inštancia na dokument drží každý súbor izolovaný konštrukciou

Časová os zdieľaného TPdf z PDFium zlyhávajúceho od druhého súboru: po prvom načítaní zostáva inštancia aktívna, ďalšie priradenie FileName vyhodí výnimku v CheckInactive skôr, než sa akýkoľvek pokus o načítanie, a dávkový cyklus loguje chybu pod novým menom súboru, kým starý dokument je stále otvorený, pasca za 7 falošných zlyhaní v dávke 13 súborov
SetFileName stráži cez CheckInactive, takže zdieľaná inštancia odmietne druhý súbor skôr, než ho skúsi; izolujte každý dokument vlastným TPdf a log sa vráti k realite

Čo dáva TPdf.LoadDocument s TPdfLoadReport?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) vyhodí skutočnú výnimku a navyše povie, čo sa stalo, v štruktúrovanej podobe. Súborový overload načíta FileName; súrodenecké overloady berú TBytes alebo pointer a veľkosť a LoadCustomDocument(AStream, AOwnsStream, Options, Report) pokrýva streamy. Každý z nich validuje options, skontroluje, že inštancia je neaktívna, spustí audit na úrovni bajtov hlavičky, startxref, xref sekcií a markera %%EOF a potom vykoná natívne načítanie. Audit je ohraničený rovnakým druhom limitov, aké rozoberá rozpočet zdrojov parsera pre nedôveryhodné PDF: TPdfLoadOptions.Default nastavuje AuditByteLimit na 256 MiB, MaxIssues na 256, MaxXrefSections na 1024 a MaxXrefEntries na 4 000 000. Pri zlyhaní metóda nastaví Report.Status := plsFailed a znova vyhodí výnimku; keďže Report sa zapisuje na miesto, jeho obsah prežije výnimku a kópia sa uloží do TPdf.LastLoadReport

Polia reportu odpovedajú na otázky, ktoré dávkový log naozaj potrebuje. Status je jedno z plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected alebo plsFailed. NativeErrorCode drží FPDF_GetLastError, takže FPDF_ERR_PASSWORD (4) oddelí chýbajúce alebo zlé heslo od poškodeného súboru hláseného ako FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid a RecoveryRoute povedia, či musel PDFium prestavať tabuľku xref, a Issues vypíše každý nález auditu s Code, Severity, Offset, ObjectNumber a MessageText, s IssuesTruncated nastaveným, keď MaxIssues zoznam useklo

Pipeline LoadDocument v PDFium Component a jej TPdfLoadReport: validácia options a neaktívna kontrola vyhodia výnimku skôr, než existuje akýkoľvek report, bajtový audit prejde hlavičku, startxref, xref sekcie a marker konca súboru, natívne načítanie zaznamená FPDF_GetLastError a výstupy sa rozvetvia na loaded, loaded with recovery po prestavbe xref, prísne zamietnutie alebo zlyhanie
Status, NativeErrorCode a zoznam issue odpovedajú na to, čo dávkový log potrebuje; bajtový audit pridáva len overload s options, kým od v3.122.1 zaznamená zlyhané Active := True do LastLoadReport plsFailed
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);          // jedna inštancia na dokument
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report je vyplnený, aj keď LoadDocument vyhodil výnimku
          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;

Kedy načítať s plmStrict?

Použite plmStrict vždy, keď je poticho opravený súbor horší než zamietnutý, napríklad pri príjme archívu, správe dôkazov alebo podpisovej pipeline. PDFium poticho rekonštruuje pokorenú tabuľku cross-reference (ISO 32000-1 §7.5.4) skenovaním súboru po objektoch, čo je pre viewer skvelé a pre čokoľvek, čo musí spracovať presne tie bajty, ktoré dostalo, problém. Po natívnom načítaní sa komponent pýta FPDF_DocumentHasValidCrossReferenceTable. V režime plmCompatible dáva prestavba plsLoadedWithRecovery plus varovanie plicNativeCrossReferenceRebuild. V režime plmStrict komponent unloaduje dokument, nastaví plsRejected, pridá plicStrictModeRejected a vyhodí EPdfError s "Strict PDF load rejected the document". Prísny režim tiež zamietne akúkoľvek chybu auditu a TPdfLoadOptions.Default(plmStrict) zapína RequireFinalEndOfFileMarker, ktorý povýši chýbajúci %%EOF alebo dáta za posledným (§7.5.5) z varovania na chybu. Audit xref dopĺňa kontroly na úrovni objektov v validácii object a xref streamov s 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;               // platný xref, žiadne chyby auditu
    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;

Kde TPdf.LastLoadReport prestáva hovoriť pravdu?

TPdf.LastLoadReport je kompletný len po overloade LoadDocument, ktorý berie options, lebo len tie overloady spúšťajú bajtový audit. Úspešné Active := True zapíše report v kompatibilnom režime bez bajtového auditu, takže AuditAttempted zostáva False. Pred PDFiumPas v3.122.1 zlyhané nezapisovalo nič, čo znamenalo, že na zdieľanej inštancii stále popisovalo LastLoadReport predchádzajúci súbor, často s upokojujúcim plsLoaded. Od v3.122.1 každé zlyhané načítanie report nahradí: zlyhané Active := True, ktoré aj naďalej necháva komponent neaktívny bez vyhodenia výnimky, a zlyhané holé volanie LoadDocument alebo LoadCustomDocument zaznamená plsFailed s textom chyby, opäť bez auditu. V praxi sa počítajú ešte dve medzery. Validácia options a CheckInactive bežia skôr, než sa report inicializuje, takže záporné AuditByteLimit alebo už aktívna inštancia vyhodí výnimku bez vyrobenia reportu. A NativeErrorCode má význam len vtedy, keď PDFium parse naozaj skúsil; pri chýbajúcom súbore wrapper vyhodí výnimku skôr, než PDFium beží, takže logujte ErrorMessage a text výnimky

Praktické pravidlo je krátke. Držte Active := True pre viewer viazané na dizajnéra, kde je neaktívny komponent prijateľný výsledok. Všade inde, a predovšetkým v dávkovom a serverovom kóde, vytvorte jedno TPdf na dokument, zavolajte LoadDocument(Options, Report), chyťte výnimku, ktorú vyhodí, a logujte Report.Status, NativeErrorCode a chybové úrovne Issues spolu s menom súboru. Náklady sú pár riadkov na call site a každé zlyhanie sa pripíše správnemu súboru so skutočnou príčinou

API load reportu, prísny režim aj audit na úrovni bajtov idú s PDFium Component pre Delphi, C++Builder a Lazarus, spolu s renderovaním, extrakciou textu, vypĺňaním formulárov a validáciou PDF/A