Műszaki cikk

Csendes PDF-betöltési hibák Delphiben: a PDFium load riport

A Delphihez és Lazarushoz készült PDFium Componentben a TPdf.Active := True sosem dob, ha egy PDF nem tölt be: a TPdf.SetActive minden kivételt elnyel, és a komponenst inaktívan hagyja. A valódi hibáért hívja inkább a TPdf.LoadDocument(Options, Report) túlterhelést. Az újradobja az eredeti kivételt, és egy TPdfLoadReport-ot tölt meg a betöltési állapottal, a natív PDFium hibakóddal, valamint azzal, hogy az xref táblát újjá kellett-e építeni

A probléma jellemzően kötegelt kódban mutatkozik meg. Egy táblakinyerő feladat végigsétál 13 valós világú PDF-en egyetlen közös TPdf-fel, és 7 közülük hibaként jön vissza. Az a 7 fájl közül egyik sem törött ténylegesen. A betöltés körüli except blokkok sosem futnak le, a napló rossz fájlneveket vádol, és az első látható hiba egy csupasz EPdfError egy inaktív komponensről, amely a ténylegesen elbukott betöltés után több sorral egy tulajdonságolvasásból érkezik. Két külön viselkedés rakódik egymásra, hogy ezt a képet adja, és mindkettő úgy működik, ahogy tervezték

Miért nem dob a TPdf.Active := True, ha egy PDF nem tölt be?

A TPdf.SetActive a LoadDocument-et egy try..except-be csomagolja, amely minden kivételosztályt elnyel, és egyszerűen inaktívan hagyja a komponenst. Az elnyelés szándékos: ugyanez a setter fut, amikor egy formtervező az Active-ot állítgatja az IDE-ben, és egy rossz út nem döntheti félre az IDE-t. Futásidőben a TPdf.Active egyszerűen azt jelenti, hogy létezik-e natív dokumentumhandle, így egy elbukott betöltés után False-ot olvas, és semmi más nem történik. Amit dobottak, az eltűnt — legyen az az elemzőtől való EPdfError, egy stream hiba vagy egy félig bekötött pdfium.dll-től való EAccessViolation. A pdfium.dll betöltési hibáinak diagnosztizálásáról Delphiben szóló, részletes DLL üzenetek csak olyan híváson át érnek el az Ön kezelőjéhez, amely nem nyeli el őket

Két betöltési út a PDFium Componentben: az Active true értékadása minden kivételt elnyel a setterben, és a hibát az első védett hívásra halasztja, ahol a CheckActive EPdfError-t dob egy inaktív komponensről, a LoadDocument pedig TPdfLoadOptions-szal és TPdfLoadReport-tal auditálja a fejlécet, a startxref-et, az xref-et és a fájlvégi jelölőt, majd újradobja az eredeti kivételt a valódi okkal csatolva
Az elnyelés szándékos, mert az IDE tervezője is használja a settert; a kötegelt kódnak az a túlterhelés kell, amely dob, jelent és elmondja a fájl valódi történetét
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // a SetActive elnyel minden betöltési kivételt
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // sosem fut le
end;
// A hiba itt bukik fel helyette, általános EPdfError-ként:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Minimális javítás a meglévő kódhoz: az értékadás után azonnal tesztelje az Active-ot;
// a v3.122.1 óta a LastLoadReport őrzi az elnyelt hiba szövegét
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

A hiba végre az első védett hívásnál mutatkozik meg. A TPdf.PageCount, a legtöbb dokumentumtulajdonsághoz hasonlóan, CheckActive-cal kezdődik, amely EPdfError-t dob, megnevezve a komponenst, de nem a fájlt és nem az okot. A Pdf.Active azonnali tesztelése az értékadás után félreattribuált összeomlást becsületes „elbukott” bejegyzéssé alakít. A PDFiumPas v3.122.1 előtt az ok ezen a ponton elveszett; a v3.122.1 óta az elbukott értékadás plsFailed riporttal helyettesíti a LastLoadReport-ot, amely hordozza a hibaszöveget, tehát az ok túlél. Maga a kivételobjektum és a bájtszintű audit más belépési pontot kíván

Miért bukik el egyetlen TPdf újrafelhasználása a második fájltól kezdve?

A TPdf.FileName csak akkor rendelhető hozzá, amíg a komponens inaktív, így egy megosztott instance még megpróbálás előtt elutasítja a második fájlt. A TPdf.SetFileName CheckInactive-cal kezdődik, és ugyanez az őr védi a Password-t és a FormFill-t is. Az első sikeres betöltés után az instance aktív marad, a következő értékadás dob, és ha a kötegelt hurok elkapja azt a kivételt, és továbblép, a hiba az új fájlnév alá landol, miközben a régi dokumentum még nyitva van. Az elnyelt betöltési hibákkal keveredve a napló abbahagyja a valóság tükrözését. A 13 fájlos reprodukcióban egy megosztott instance 7 hibát jelentett, miközben dokumentumonként egy friss TPdf.Create(nil) mind a 13-at megnyitotta. A Active := False beállítása a fájlok közt szintén működik, de dokumentumonként egy instance konstrukcióval tartja minden fájlt elkülönítve

Egy megosztott PDFium TPdf idővonala, amely a második fájltól kezdve bukik el: az első betöltés után az instance aktív marad, a következő FileName értékadás a CheckInactive-ben dob, még bármilyen betöltési kísérlet előtt, a kötegelt hurok pedig az új fájlnév alatt naplózza a hibát, miközben a régi dokumentum még nyitva van — 7 hamis hiba csapdája egy 13 fájlos kötegben
A SetFileName CheckInactive-cal őr, így egy megosztott instance a második fájlt megpróbálás előtt utasítja el; minden dokumentumot külön TPdf-fel izoláljon, és a napló újra egyezik a valósággal

Mit ad Önnek a TPdf.LoadDocument TPdfLoadReport-tal?

A TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) felhányja a valódi kivételt, és strukturált formában is elmondja, mi történt. A fájl túlterhelés a FileName-t tölti be; a testvér túlterhelések TBytes-t vagy pointert és méretet kapnak, a LoadCustomDocument(AStream, AOwnsStream, Options, Report) pedig a streameket fedi le. Mindegyik érvényesíti az opciókat, ellenőrzi, hogy az instance inaktív-e, bájtszintű auditot futtat a fejlécre, a startxref-re, az xref szekciókra és a %%EOF jelölőre, majd végrehajtja a natív betöltést. Az auditot ugyanolyan jellegű korlátok határolják, amelyekről a parser erőforrás-keretei megbízhatatlan PDF-ekhez szólnak: a TPdfLoadOptions.Default az AuditByteLimit-et 256 MiB-re, a MaxIssues-t 256-ra, a MaxXrefSections-t 1024-re, a MaxXrefEntries-t 4 000 000-ra állítja. Hibánál a metódus Report.Status := plsFailed-re áll, és újradobja; mivel a Report a helyén íródik, tartalma túléli a kivételt, és egy másolat a TPdf.LastLoadReport-ba kerül

A riport mezői megválaszolják azokat a kérdéseket, amelyeket egy kötegelt napló ténylegesen feltesz. A Status a plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected vagy plsFailed egyike. A NativeErrorCode a FPDF_GetLastError-t tartja, így a FPDF_ERR_PASSWORD (4) elválasztja a hiányzó vagy rossz jelszót a sérült fájltól, amely FPDF_ERR_FORMAT-ként (3) jelentkezik. A UsedRecovery, a CrossReferenceTableValid és a RecoveryRoute elmondja, hogy a PDFiumnak újjá kellett-e építenie az xref táblát, az Issues pedig felsorolja az egyes audit megállapításokat Code-dal, Severity-vel, Offset-tal, ObjectNumber-rel és MessageText-tel, IssuesTruncated-dal arra az esetre, amikor a MaxIssues megrövidíti a listát

A PDFium Component LoadDocument futószalagja és a TPdfLoadReport-ja: az opcióvalidáció és az inaktív-ellenőrzés dob, mielőtt bármilyen riport létezne, a bájtaudit végigsétál a fejlécen, a startxref-en, az xref szekciókon és a fájlvégi jelölőn, a natív betöltés rögzíti a FPDF_GetLastError-t, a kimenetelek pedig betöltésre, xref-újraépítés utáni helyreállításos betöltésre, szigorú elutasításra vagy hibára ágaznak
A Status, a NativeErrorCode és a problémalista megválaszolja, amire egy kötegelt naplónak szüksége van; csak az opciós túlterhelés adja a bájtauditot, miközben a v3.122.1 óta egy elbukott Active := True is plsFailed-et rögzít a LastLoadReport-ban
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);          // dokumentumonként egy instance
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // a Report kitöltődik, holott a LoadDocument kivételt dobott
          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;

Mikor töltse be plmStrict-tel?

Akkor használja a plmStrict-et, amikor egy csendben javított fájl rosszabb, mint egy elutasított — például archívum-bevételnél, bizonyítékkezelésnél vagy alágírási futószalagnál. A PDFium csendben újjáépít egy törött kereszthivatkozási táblát (ISO 32000-1 §7.5.4) az objektumokért végigsöprve a fájlt, ami nagyszerű egy nézőnek, és probléma bárminek, amelynek pontosan azokat a bájtokat kell feldolgoznia, amelyeket kapott. A natív betöltés után a komponens rákérdez a FPDF_DocumentHasValidCrossReferenceTable-re. plmCompatible módban az újraépítés plsLoadedWithRecovery-t ad plusz egy plicNativeCrossReferenceRebuild figyelmeztetést. plmStrict módban a komponens kirakja a dokumentumot, plsRejected-et állít, felveszi a plicStrictModeRejected-et, és EPdfError-t dob „Strict PDF load rejected the document” üzenettel. A szigorú mód minden audit hibát is elutasít, a TPdfLoadOptions.Default(plmStrict) pedig bekapcsolja a RequireFinalEndOfFileMarker-t, amely a hiányzó %%EOF-ot vagy az utolsó utáni adatot (§7.5.5) figyelmeztetésből hibává lépteti. Az xref audit kiegészíti az objektumszintű ellenőrzéseket, amelyeket a objektum- és xref streamek érvényesítése PDFium VCL-lel tárgyal

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;               // érvényes xref, nincs audit hiba
    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;

Hol hagyja abba a TPdf.LastLoadReport az igazmondást?

A TPdf.LastLoadReport csak az opciókat kapó LoadDocument túlterhelés után teljes, mert csak azok a túlterhelések futtatják a bájtauditot. Egy sikeres Active := True kompatibilis módú riportot ír bájtaudit nélkül, így az AuditAttempted False marad. A PDFiumPas v3.122.1 előtt egy elbukott semmit sem írt, ami azt jelentette, hogy megosztott instancen a LastLoadReport továbbra is az előző fájlt írta le, gyakran megnyugtató plsLoaded-dal. A v3.122.1 óta minden elbukott betöltés helyettesíti a riportot: egy elbukott Active := True, amely továbbra is inaktívan hagyja a komponenst dobás nélkül, és egy elbukott sima LoadDocument vagy LoadCustomDocument hívás plsFailed-et rögzít a hibaszöveggel, szintén audit nélkül. Két további rés számít a gyakorlatban. Az opcióvalidáció és a CheckInactive a riport inicializálása előtt fut, tehát negatív AuditByteLimit vagy már aktív instance riport nélkül dob. És a NativeErrorCode csak akkor értelmes, ha a PDFium ténylegesen megkísérelte az elemzést; hiányzó fájlnál a wrapper még a PDFium futása előtt dob, ezért naplózza inkább az ErrorMessage-et és a kivételszöveget

A gyakorlati szabály rövid. Tartsa meg az Active := True-t tervezőhöz kötött nézőknél, ahol az inaktív komponens elfogadható kimenet. Minden más helyen, és mindenekelőtt kötegelt és szerverkódban legyen dokumentumonként egy TPdf, hívjon LoadDocument(Options, Report)-ot, kapja el a dobott kivételt, és naplózza a Report.Status-t, a NativeErrorCode-ot és a hibaszintű Issues-t a fájlnévvel együtt. Az ára néhány sor hívóhelyenként, és minden hiba a helyes fájlhoz kerül a valódi okával együtt

A load riport API, a szigorú mód és a bájtszintű audit a Delphihez, C++Builderhez és Lazarushoz készült PDFium Componenttel érkezik, a rajzolás, a szövegkinyerés, az űrlapkitöltés és a PDF/A-ellenőrzés mellett