Tehnični članak

Tiha spodletela nalaganja PDF v Delphiju: poročilo PDFium

V PDFium Component za Delphi in Lazarus dodelitev TPdf.Active := True nikoli ne sproži, ko nalaganje PDF spodleti: TPdf.SetActive ujame vsako izjemo in komponento pusti nedejavno. Da vidite pravo napako, namesto tega pokličite TPdf.LoadDocument(Options, Report). Ta preobremenitev znova sproži izvirno izjemo in napolni TPdfLoadReport s statusom nalaganja, izvorno kodo napake PDFium in s tem, ali je bilo treba tabelo navzkrižnih sklicev znova zgraditi

Težava se običajno pokaže v paketni kodi. Opravilo za izločanje tabel obide mapo 13 PDF-jev iz resničnega sveta z enim skupnim TPdf, 7 od njih pa se vrne kot spodleteli. Nobena od teh 7 datotek ni dejansko pokvarjena. Bloki except okoli nalaganja se nikoli ne sprožijo, dnevnik krivi napačna imena datotek, prva vidna napaka pa je goli EPdfError o nedejavni komponenti, sprožen iz branja lastnosti nekaj vrstic po nalaganju, ki je res spodletelo. Dve ločeni obnašanji se zložita in izdelata to sliko, obe pa delujeta, kot je načrtovano

Zakaj TPdf.Active := True ne sproži, ko nalaganje PDF spodleti?

TPdf.SetActive ovije LoadDocument v try..except, ki pogoltne vsak razred izjem in komponento preprosto pusti nedejavno. Pogltanje je namerno: isti nastavljavnik teče, ko oblikovalec obrazcev preklopi Active v IDE, in napačna pot ne sme podreti IDE. Ob izvajanju TPdf.Active le poroča, ali obstaja izvorni ročaj dokumenta, tako da po spodletelem nalaganju prebere False in se nič drugega ne zgodi. Karkoli je bilo sproženo, je izginilo, naj bo to EPdfError razčlenjevalnika, napaka toka ali EAccessViolation od polvezane pdfium.dll. Podrobna sporočila DLL, opisana v diagnosticiranju spodletelih nalaganj pdfium.dll v Delphiju, dosežejo vaš ročaj le skozi klic, ki jih ne pogoltne

Dve poti nalaganja v PDFium Component: dodelitev Active true pogoltne vsako izjemo v nastavljavniku in spodletelost preloži na prvi branjeni klic, kjer CheckActive sproži EPdfError o nedejavni komponenti, LoadDocument s TPdfLoadOptions in TPdfLoadReport pa pregleda glavo, startxref, xref in marker konca datoteke, nato pa znova sproži izvirno izjemo s pristavljenim pravim vzrokom
Pogltanje je namerno, ker oblikovalec IDE deli nastavljavnik; paketna koda potrebuje preobremenitev, ki sproži, poroča in pove pravo zgodbo datoteke
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive pogoltne vsako izjemo nalaganja
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // se nikoli ne izvede
end;
// Spodletelost se pokaže tukaj namesto tega, kot generični EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Minimalen popravek za obstoječo kodo: preizkusite Active takoj po dodelitvi;
// od v3.122.1 LastLoadReport obdrži besedilo poglotjene napake
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Spodletelost se končno pokaže pri prvem branjenem klicu. TPdf.PageCount, kot večina lastnosti dokumenta, začne s CheckActive, ki sproži EPdfError, ki poimenuje komponento, ne datoteke in ne vzroka. Preizkus Pdf.Active takoj po dodelitvi spremeni napačno pripisan trk v iskreni vnos »spodletelo«. Pred PDFiumPas v3.122.1 je bil vzrok na tej točki izgubljen; od v3.122.1 spodletela dodelitev zamenja LastLoadReport s poročilom plsFailed, ki nosi besedilo napake, tako da vzrok preživi. Objekt izjeme sam in pregled na ravni bajtov pa še vedno zahtevata drugačno vstopno točko

Zakaj ponovna uporaba enega TPdf spodleti od druge datoteke naprej?

TPdf.FileName se sme dodeliti le, medtem ko je komponenta nedejavna, tako da skupni primerek zavrne drugo datoteko, preden kdaj poskusi, da bi jo naložil. TPdf.SetFileName začne s CheckInactive, isti branilec pa ščiti Password in FormFill. Po prvem uspešnem nalaganju primerek ostane dejaven, naslednja dodelitev sproži, in če paketna zanka ujame to izjemo ter gre naprej, napaka pristne pod novim imenom datoteke, medtem ko je stari dokument še vedno odprt. Pomešano s poglotjenimi spodletelimi nalaganji se dnevnik preneha ujemati z realnostjo. V reprodukciji s 13 datotekami je skupni primerek prijavil 7 spodletelosti, svež TPdf.Create(nil) na dokument pa je odprl vseh 13. Nastavitev Active := False med datotekami deluje tudi, en primerek na dokument pa drži vsako datoteko izolirano po zgradbi

Časovnica skupnega TPdf pri PDFium, ki spodleti od druge datoteke naprej: po prvem nalaganju primerek ostane dejaven, naslednja dodelitev FileName sproži v CheckInactive, preden kateri koli poskus nalaganja, paketna zanka pa zabeleži napako pod novim imenom datoteke, medtem ko je stari dokument še vedno odprt — past za 7 napačnih spodletelosti v paketu 13 datotek
SetFileName brani s CheckInactive, tako da skupni primerek zavrne drugo datoteko, preden jo poskusi; izolirajte vsak dokument s svojim TPdf in dnevnik spet ujema realnost

Kaj vam da TPdf.LoadDocument s TPdfLoadReport?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) sproži pravo izjemo in vam v strukturirani obliki pove tudi, kaj se je zgodilo. Preobremenitev datoteke naloži FileName; sesterske preobremenitve vzamejo TBytes ali kazalec in velikost, LoadCustomDocument(AStream, AOwnsStream, Options, Report) pa pokriva tokove. Vsaka validira možnosti, preveri, da je primerek nedejaven, požene pregled na ravni bajtov po glavi, startxref, odsekih xref in markerju %%EOF, nato pa izvede izvorno nalaganje. Pregled je omejen s tovrstnimi mejami, kot jih razpravlja proračun virov razčlenjevalnika za nezaupljive PDF-je: TPdfLoadOptions.Default nastavi AuditByteLimit na 256 MiB, MaxIssues na 256, MaxXrefSections na 1024 in MaxXrefEntries na 4.000.000. Ob spodletelosti metoda nastavi Report.Status := plsFailed in znova sproži; ker se Report zapiše na mestu, njegova vsebina preživi izjemo, kopija pa se shrani v TPdf.LastLoadReport

Polja poročila odgovorijo na vprašanja, ki jih paketni dnevnik dejansko potrebuje. Status je en od plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected ali plsFailed. NativeErrorCode drži FPDF_GetLastError, tako da FPDF_ERR_PASSWORD (4) loči manjkajoče ali napačno geslo od pokvarjene datoteke, prijavljene kot FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid in RecoveryRoute povedo, ali je moral PDFium znova zgraditi tabelo xref, Issues pa našteva vsako ugotovitev pregleda s Code, Severity, Offset, ObjectNumber in MessageText, z IssuesTruncated, nastavljenim, kadar je MaxIssues seznam skrajšal

Cevovod LoadDocument pri PDFium Component in njegovo poročilo TPdfLoadReport: validacija možnosti in preizkus nedejavnosti sprožita, preden katero koli poročilo obstaja, bajtni pregled obide glavo, startxref, odseke xref in marker konca datoteke, izvorno nalaganje zabeleži FPDF_GetLastError, izidi pa se razvejajo v naloženo, naloženo z obnovo po znovni zgradbi xref, zavrnitev v strogem načinu ali spodletelost
Status, NativeErrorCode in seznam ugotovitev odgovorijo, kaj potrebuje paketni dnevnik; bajtni pregled doda le preobremenitev z možnostmi, od v3.122.1 pa spodletelo Active := True še vedno zabeleži plsFailed v 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 primerek na dokument
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report je napolnjen, čeprav je LoadDocument sprožil
          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;

Kdaj naj nalagate s plmStrict?

Uporabite plmStrict vsakič, kadar je tiho popravljena datoteka slabša od zavrnjene, na primer pri prevzemu arhivov, ravnanju z dokazi ali cevovodu podpisovanja. PDFium tiho znova zgradi pokvarjeno tabelo navzkrižnih sklicev (ISO 32000-1 §7.5.4) z iskanjem objektov po datoteki, kar je odlično za gledalnik in težava za vse, kar mora obdelati točno bajte, ki jih je dobilo. Po izvornem nalaganju komponenta vpraša FPDF_DocumentHasValidCrossReferenceTable. V načinu plmCompatible znovna zgradba da plsLoadedWithRecovery plus opozorilo plicNativeCrossReferenceRebuild. V načinu plmStrict komponenta razloži dokument, nastavi plsRejected, doda plicStrictModeRejected in sproži EPdfError z »strog način nalaganja PDF je zavrnil dokument«. Strogi način zavrne tudi vsako napako pregleda, TPdfLoadOptions.Default(plmStrict) pa vklopi RequireFinalEndOfFileMarker, ki manjkajoč %%EOF ali podatke za zadnjim (§7.5.5) poviša iz opozorila v napako. Pregled xref dopolnjuje preizkuse na ravni objektov v validiranju tokov objektov in xref 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;               // veljaven xref, brez napak pregleda
    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;

Kje se TPdf.LastLoadReport preneha izjavljati resnično?

TPdf.LastLoadReport je popoln šele po preobremenitvi LoadDocument, ki vzame možnosti, ker le te preobremenitve poženejo bajtni pregled. Uspešno Active := True zapiše poročilo v združljivem načinu brez bajtnega pregleda, tako da AuditAttempted ostane False. Pred PDFiumPas v3.122.1 spodletelo ni zapisalo ničesar, kar je pomenilo, da je na skupnem primerku LastLoadReport še vedno opisoval prejšnjo datoteko, pogosto z umirjajočim plsLoaded. Od v3.122.1 vsako spodletelo nalaganje zamenja poročilo: spodletelo Active := True, ki še vedno pusti komponento nedejavno brez sprožitve, in spodletel klic navadnega LoadDocument ali LoadCustomDocument zabeležita plsFailed z besedilom napake, spet brez pregleda. Dve vrzeli še pomembata v praksi. Validacija možnosti in CheckInactive tečeta, preden se poročilo inicializira, tako da negativen AuditByteLimit ali že dejaven primerek sprožita brez izdelave poročila. In NativeErrorCode je smiselen le, kadar je PDFium dejansko poskusil razčlenitev; za manjkajočo datoteko ovijalnik sproži, preden teče PDFium, zato namesto tega beležite ErrorMessage in besedilo izjeme

Praktično pravilo je kratko. Obdržite Active := True za gledalnike, vezane na oblikovalca, kjer je nedejavna komponenta sprejemljiv izid. Povsod drugje, predvsem pa v paketni in strežniški kodi, ustvarite en TPdf na dokument, pokličite LoadDocument(Options, Report), ujemite izjemo, ki jo sproži, in beležite Report.Status, NativeErrorCode ter Issues na ravni napake skupaj z imenom datoteke. Cena je nekaj vrstic na klicno mesto, vsaka spodletelost pa dobi pripisano pravo datoteko s pravim vzrokom

API poročila o nalaganju, strogi način in pregled na ravni bajtov odplujejo z PDFium Component za Delphi, C++Builder in Lazarus, skupaj z izrisovanjem, izločanjem besedil, izpolnjevanjem obrazcev in validacijo PDF/A