Tekninen artikkeli

Hiljaiset PDF-latausviat Delphissä: PDFium-latausraportti

PDFium Component for Delphi and Lazarusissa TPdf.Active := Truein sijoittaminen ei koskaan nosta kun PDF:n lataus epäonnistuu: TPdf.SetActive nappaa jokaisen poikkeuksen ja jättää komponentin inaktiiviseksi. Nähdäksesi oikean virheen kutsu sen sijaan TPdf.LoadDocument(Options, Report)ia. Kyseinen overload nostaa alkuperäisen poikkeuksen uudelleen ja täyttää TPdfLoadReportin latausstatuksella, natiivilla PDFium-virhekoodilla ja sillä tiedolla piti ristiviittaustaulukko rakentaa uudelleen

Ongelma nousee yleensä pintaan eräajokoodissa. Taulukointityö kävelee 13 oikean maailman PDF:n kansion läpi yhdellä jaetulla TPdfillä, ja 7 niistä palautuu epäonnistumisina. Yksikään kyseisistä 7 tiedostosta ei oikeasti ole rikki. Latauksen ympärillä olevat except-lohkot eivät koskaan laukea, loki syyttää vääriä tiedostonimiä, ja ensimmäinen näkyvä virhe on paljas EPdfError inaktiivisesta komponentista, nostettuna propertyn lukemisesta useita rivejä sen latauksen jälkeen joka oikeasti epäonnistui. Kaksi erillistä käyttäytymistä pinoavat kuvan, ja molemmat toimivat suunnitelmansa mukaan

Miksi TPdf.Active := True ei nosta kun PDF:n lataus epäonnistuu?

TPdf.SetActive käärii LoadDocumentin try..exceptin sisään joka nielee jokaisen poikkeusluokan ja jättää komponentin yksinkertaisesti inaktiiviseksi. Nielaisu on tahallinen: sama setteri ajaa kun lomakemuotoilija kytkkee Activeia IDE:ssä, eikä huono polku saa kaataa IDE:tä. Ajonaikana TPdf.Active raportoi vain onko natiivia dokumenttihandlea olemassa, joten epäonnistuneen latauksen jälkeen se lukee Falsen eikä muuta tapahdu. Mikä ikinä nostettiin on poissa, olipa se jäsentimen EPdfError, streamivirhe tai puolisidonnaisen pdfium.dll:n EAccessViolation. Artikkelissa pdfium.dll-latausvikojen diagnosoimisesta Delphissä kuvatut yksityiskohtaiset DLL-viestit saavuttavat käsittelijäsi vain kutsun kautta joka ei niele niitä

Kaksi latauspolkua PDFium Componentissa: Active true -sijoittaminen nielee jokaisen poikkeuksen setterissä ja siirtää epäonnistumisen ensimmäiselle vartiodulle kutsulle jossa CheckActive nostaa EPdfErrorin inaktiivisesta komponentista, kun taas LoadDocument TPdfLoadOptionsilla ja TPdfLoadReportilla auditoi headerin, startxrefin, xrefin ja tiedoston lopun merkin, ja nostaa sitten alkuperäisen poikkeuksen uudelleen todellinen syy mukanaan
Nielaisu on tahallinen koska IDE:n suunnittelija jakaa setterin; eräajokoodi tarvitsee overloadin joka nostaa, raportoi ja kertoo tiedoston todellisen tarinan
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive nielaisee minkä tahansa latauspoikkeuksen
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // ei koskaan suoritu
end;
// Virhe nousee pintaan täällä sen sijaan, yleisenä EPdfErrorina:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Minimikorjaus olemassa olevaan koodiin: testaa Active heti sijoituksen jälkeen;
// versiosta v3.122.1 alkaen LastLoadReport säilyttää nielaistun virheen tekstin
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Virhe näkyy vihdoin ensimmäisessä vartiodussa kutsussa. TPdf.PageCount, kuten useimmat dokumenttipropertyt, alkaa CheckActiveilla, joka nostaa EPdfErrorin joka nimeää komponentin muttei tiedostoa eikä syytä. Pdf.Activein testaus heti sijoituksen jälkeen kääntää väärin attribuoidun kaatumisen rehelliseksi "epäonnistui"-merkinnäksi. Ennen PDFiumPas v3.122.1:ää syy oli menetetty kyseisessä pisteessä; versiosta v3.122.1 alkaen epäonnistunut sijoitus korvaa LastLoadReportin plsFailed-raportilla joka kantaa virhetekstiä, joten syy selviää hengissä. Poikkeusobjekti itse ja tavutason auditointi vaativat yhä toisen sisääntulopisteen

Miksi yhden TPdf:n uudelleenkäyttö epäonnistuu toisesta tiedostosta alkaen?

TPdf.FileNamein voi sijoittaa vain kun komponentti on inaktiivinen, joten jaettu instanssi hylkää toisen tiedoston ennen kuin se koskaan yrittää ladata sitä. TPdf.SetFileName alkaa CheckInactiveilla, ja sama vartio suojaa Passwordia ja FormFillia. Ensimmäisen onnistuneen latauksen jälkeen instanssi pysyy aktiivisena, seuraava sijoitus nostaa, ja jos eräajosilmukka nappaa kyseisen poikkeuksen ja jatkaa matkaansa, virhe laskeutuu uuden tiedostonimen alle kun vanha dokumentti on yhä auki. Sekoitettuna nielaisuihin latausvikoihin loki lakkaa vastaamasta todellisuutta. 13 tiedoston toistossa jaettu instanssi raportoi 7 epäonnistumista, kun taas tuore TPdf.Create(nil) dokumenttia kohti avasi kaikki 13. Active := False:n asettaminen tiedostojen välillä toimii myös, mutta yksi instanssi dokumenttia kohti pitää jokaisen tiedoston eristettynä rakenteesta johtuen

Aikajana jaetun PDFium TPdf:n epäonnistumisesta toisesta tiedostosta alkaen: ensimmäisen latauksen jälkeen instanssi pysyy aktiivisena, seuraava FileName-sijoitus nostaa CheckInactiveissa ennen mitään latausyritystä, ja eräajosilmukka lokittaa virheen uuden tiedostonimen alle kun vanha dokumentti on yhä auki, ansa 13 tiedoston eräajon 7 väärän epäonnistumisen takana
SetFileName vartioi CheckInactiveilla, joten jaettu instanssi hylkää toisen tiedoston ennen kuin yrittää sitä; eristä jokainen dokumentti omalla TPdf:llään ja loki vastaa todellisuutta taas

Mitä TPdf.LoadDocument TPdfLoadReportilla antaa sinulle?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) nostaa oikean poikkeuksen ja kertoo myös mitä tapahtui jäsennellyssä muodossa. Tiedosto-overload lataa FileNamein; sisaroverloadit ottavat TBytesin tai pointterin ja koon, ja LoadCustomDocument(AStream, AOwnsStream, Options, Report) kattaa streamit. Jokainen niistä validoi option, tarkistaa että instanssi on inaktiivinen, ajaa tavutason auditoinnin headerista, startxrefistä, xref-osioista ja %%EOF-merkistä, ja suorittaa sitten natiivin latauksen. Auditointi on rajattu samantyyppisillä rajoilla joita käsittelee artikkeli jäsentimen resurssibudjetit epäluotettaville PDF:eille: TPdfLoadOptions.Default asettaa AuditByteLimitin arvoon 256 MiB, MaxIssuesin arvoon 256, MaxXrefSectionsin arvoon 1024 ja MaxXrefEntriesin arvoon 4 000 000. Epäonnistuessa metodi asettaa Report.Status := plsFailedin ja nostaa uudelleen; koska Report kirjoitetaan paikalleen, sen sisältö selviää poikkeuksesta, ja kopio tallennetaan TPdf.LastLoadReportiin

Raportin kentät vastaavat kysymyksiin joita eräajoloki oikeasti tarvitsee. Status on yksi arvoista plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected tai plsFailed. NativeErrorCode kantaa FPDF_GetLastErrorin, joten FPDF_ERR_PASSWORD (4) erottaa puuttuvan tai väärän salasanan vahingoittuneesta tiedostosta joka raportoidaan FPDF_ERR_FORMATina (3). UsedRecovery, CrossReferenceTableValid ja RecoveryRoute kertovat joutuiko PDFium rakentamaan xref-taulukon uudelleen, ja Issues listaa jokaisen auditointilöydöksen arvoilla Code, Severity, Offset, ObjectNumber ja MessageText, IssuesTruncated asetettuna kun MaxIssues typisti listan

PDFium Componentin LoadDocument-putki ja sen TPdfLoadReport: option validointi ja inaktiivisuustarkistus nostavat ennen kuin raporttia on olemassa, tavuauditointi kävelee headerin, startxrefin, xref-osiot ja tiedoston lopun merkin läpi, natiivi lataus kirjaa FPDF_GetLastErrorin, ja lopputulokset haarautuvat ladattuun, ladattuun palautuksella xrefin uudelleenrakentamisen jälkeen, tiukkaan hylkäykseen tai epäonnistumiseen
Status, NativeErrorCode ja ongelmalista vastaavat siihen mitä eräajoloki tarvitsee; vain options-overload lisää tavuauditoinnin, kun taas versiosta v3.122.1 alkaen epäonnistunut Active := True kirjaa yhä plsFailedin LastLoadReportiin
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);          // yksi instanssi dokumenttia kohti
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report täyttyy vaikka LoadDocument nosti poikkeuksen
          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;

Milloin kannattaa ladata plmStrictilla?

Käytä plmStrictia aina kun hiljaisena korjattu tiedosto on pahempi kuin hylätty, kuten arkistovastaanotossa, todisteiden käsittelyssä tai allekirjoitusputkessa. PDFium rakentaa rikkinäisen ristiviittaustaulukon (ISO 32000-1 §7.5.4) hiljaisena uudelleen skannaamalla tiedostoa objektien varalta, mikä on loistavaa katselimelle ja ongelma kaikelle joka on käsiteltävä täsmälleen saamansa tavut. Natiivin latauksen jälkeen komponentti kysyy FPDF_DocumentHasValidCrossReferenceTableia. plmCompatible-modessa uudelleenrakennus tuottaa plsLoadedWithRecoveryin plus plicNativeCrossReferenceRebuild-varoituksen. plmStrict-modessa komponentti purkaa dokumentin, asettaa plsRejectedin, lisää plicStrictModeRejectedin ja nostaa EPdfErrorin viestillä "Strict PDF load rejected the document". Tiukka moda hylkää myös minkä tahansa auditointivirheen, ja TPdfLoadOptions.Default(plmStrict) kytkee päälle RequireFinalEndOfFileMarkerin, joka ylentää puuttuvan %%EOFin tai datan viimeisen jälkeisen (§7.5.5) varoituksesta virheeksi. xref-auditointi täydentää objektitason tarkistukset artikkelissa objekti- ja xref-streamien validoinnissa PDFium VCL:llä

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;               // kelvollinen xref, ei auditointivirheitä
    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;

Missä TPdf.LastLoadReport lakkaa kertomasta totuutta?

TPdf.LastLoadReport on täydellinen vain option ottavan LoadDocument-overloadin jälkeen, koska vain kyseiset overloadit ajavat tavuauditoinnin. Onnistunut Active := True kirjoittaa compatible-mode-raportin ilman tavuauditointia, joten AuditAttempted pysyy Falsena. Ennen PDFiumPas v3.122.1:ää epäonnistunut ei kirjoittanut mitään, mikä tarkoitti että jaetulla instanssilla LastLoadReport kuvasi yhä edellistä tiedostoa, usein rauhoittavalla plsLoadedilla. Versiosta v3.122.1 alkaen jokainen epäonnistunut lataus korvaa raportin: epäonnistunut Active := True, joka yhä jättää komponentin inaktiiviseksi nostamatta, ja epäonnistunut pelkkä LoadDocument- tai LoadCustomDocument-kutsu kirjaavat plsFailedin virhetekstillä, jälleen ilman auditointia. Kaksi muuta aukkoa merkitsevät käytännössä. Option validointi ja CheckInactive ajavat ennen kuin raportti alustetaan, joten negatiivinen AuditByteLimit tai jo aktiivinen instanssi nostaa tuottamatta raporttia. Ja NativeErrorCode on merkityksellinen vain kun PDFium oikeasti yritti jäsennystä; puuttuvalle tiedostolle kääre nostaa ennen kuin PDFium ajaa, joten lokita sen sijaan ErrorMessage ja poikkeusteksti

Käytännön sääntö on lyhyt. Pidä Active := True suunnittelijaan sidotuille katselimille joissa inaktiivinen komponentti on hyväksyttävä lopputulos. Kaikkialla muualla, ja ennen kaikkea eräajo- ja palvelinkoodissa, luo yksi TPdf dokumenttia kohti, kutsu LoadDocument(Options, Report)ia, nappaa sen nostama poikkeus ja lokita Report.Status, NativeErrorCode ja virhetason Issuesit tiedostonimen kanssa. Hintana on muutama rivi kutsumispaikkaa kohden, ja jokainen epäonnistuminen attribuoidaan oikealle tiedostolle todellisella syyllään

Latausraportti-API, tiukka moda ja tavutason auditointi toimitetaan mukana PDFium Component for Delphi, C++Builder and Lazarus -paketissa, renderöinnin, tekstin poimin, lomakkeiden täytön ja PDF/A-validoinnin rinnalla