Tehnički članak

Tihi neuspeh učitavanja PDF-a: upotrebite PDFium load report

U PDFium Component za Delphi i Lazarus, dodeljivanje TPdf.Active := True nikada ne diže izuzetak kad PDF padne na učitavanju: TPdf.SetActive uhvati svaki izuzetak i ostavi komponentu neaktivnom. Da biste videli pravu grešku, pozovite umesto toga TPdf.LoadDocument(Options, Report). Taj overload ponovo diže prvobitni izuzetak i puni TPdfLoadReport statusom učitavanja, nativnim PDFium kodom greške i podacima da li je tabela unakrsnih referenci morala da se ponovo izgradi

Problem obično isplivava u batch kodu. Posao za vađenje tabela prelazi preko fascikle sa 13 realnih PDF-ova sa jednim deljenim TPdf-om, i 7 njih se vraća kao neuspesi. Nijedan od tih 7 fajlova zapravo nije pokvaren. except blokovi oko učitavanja nikada ne opale, log krivi pogrešna imena fajlova, a prva vidljiva greška goli je EPdfError o neaktivnoj komponenti, podignut iz čitanja svojstva nekoliko redova posle učitavanja koje je zaista palo. Dva odvojena ponašanja naslažu se u tu sliku, i oba rade kako su dizajnirana

Zašto TPdf.Active := True ne diže izuzetak kad PDF padne na učitavanju?

TPdf.SetActive obavija LoadDocument u try..except koji guta svaku klasu izuzetaka i jednostavno ostavi komponentu neaktivnom. Gutnja je namerna: isti setter radi kad form designer prebaci Active u IDE-u, i loša putanja ne sme da sruši IDE. U vreme izvršavanja TPdf.Active samo javlja da li nativni handle dokumenta postoji, pa posle palog učitavanja čita se False i ništa drugo se ne dešava. Šta god je podignuto, nestalo je: bio to EPdfError iz parsera, greška streama ili EAccessViolation iz napola povezanog pdfium.dll-a. Detaljne DLL poruke opisane u tekstu o dijagnostikovanju padova učitavanja pdfium.dll-a u Delphi-ju do vašeg hendlera stižu samo kroz poziv koji ih ne guta

Dve putanje učitavanja u PDFium Component: dodela Active true guta svaki izuzetak u setteru i odlaganje pada na prvi zaštićeni poziv, gde CheckActive diže EPdfError o neaktivnoj komponenti, dok LoadDocument sa TPdfLoadOptions i TPdfLoadReport auditom prolazi header, startxref, xref i marker kraja fajla, pa ponovo diže prvobitni izuzetak sa pravim uzrokom
Gutnja je namerna jer IDE designer deli setter; batch kod treba overload koji diže, izveštava i priča pravu priču fajla
Pdf.FileName := FileName;
try
  Pdf.Active := True;       // SetActive guta svaki izuzetak učitavanja
except
  on E: Exception do
    Log.Add(FileName + ': ' + E.Message);   // nikada se ne izvršava
end;
// Neuspeh se umesto toga pokazuje ovde, kao generički EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));

// Minimalna popravka postojećeg koda: testirajte Active odmah posle dodele;
// od v3.122.1 LastLoadReport čuva tekst progutane greške
Pdf.Active := True;
if not Pdf.Active then
  Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);

Neuspeh se konačno pokazuje na prvom zaštićenom pozivu. TPdf.PageCount, kao i većina svojstava dokumenta, počinje sa CheckActive, koji diže EPdfError koji imenuje komponentu, ali ne fajl i ne uzrok. Testiranje Pdf.Active odmah posle dodele pretvara pogrešno pripisan pad u pošten unos „palo je". Pre PDFiumPas v3.122.1 razlog je u tom trenutku bio izgubljen; od v3.122.1 pala dodela zamenjuje LastLoadReport izveštajem sa plsFailed koji nosi tekst greške, pa uzrok preživi. Sam objekat izuzetka i audit na nivou bajtova i dalje traže drugu tačku ulaza

Zašto ponovna upotreba jednog TPdf-a pada od drugog fajla nadalje?

TPdf.FileName može se dodeliti samo dok je komponenta neaktivna, pa deljena instanca odbija drugi fajl pre nego što ikada pokuša da ga učita. TPdf.SetFileName počinje sa CheckInactive-om, i ista čuvar brani Password i FormFill. Posle prvog uspešnog učitavanja instanca ostaje aktivna, sledeća dodela diže izuzetak, i ako batch petlja taj izuzetak uhvati i krene dalje, greška dospe pod novo ime fajla dok je stari dokument još otvoren. Pomešano s progutanim neuspesima učitavanja, log prestaje da odgovara stvarnosti. U reprodukciji sa 13 fajlova deljena instanca prijavila je 7 neuspeha, dok je svež TPdf.Create(nil) po dokumentu otvorio svih 13. Postavljanje Active := False između fajlova takođe radi, ali jedna instanca po dokumentu drži svaki fajl izolovanim već po konstrukciji

Vremenska linija deljenog PDFium TPdf-a koji pada od drugog fajla nadalje: posle prvog učitavanja instanca ostaje aktivna, sledeća FileName dodela diže izuzetak u CheckInactive pre bilo kog pokušaja učitavanja, i batch petlja beleži grešku pod novim imenom fajla dok je stari dokument još otvoren, zamka iza 7 lažnih neuspeha u batchu od 13 fajlova
SetFileName se brani CheckInactive-om, pa deljena instanca odbija drugi fajl pre nego što ga proba; izolujte svaki dokument sopstvenim TPdf-om i log se opet poklapa sa stvarnošću

Šta vam daje TPdf.LoadDocument sa TPdfLoadReport-om?

TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) diže pravi izuzetak i kaže vam šta se desilo i u strukturiranom obliku. Fajl overload učitava FileName; srodni overloadi uzimaju TBytes ili pokazivač i veličinu, a LoadCustomDocument(AStream, AOwnsStream, Options, Report) pokriva streamove. Svaki validira opcije, proverava da je instanca neaktivna, sprovodi audit na nivou bajtova nad headerom, startxref-om, xref sekcijama i %%EOF markerom, a tek potom radi nativno učitavanje. Audit ograničavaju iste vrste granica o kojima piše budžet resursa parsera za nepoverljive PDF-ove: TPdfLoadOptions.Default postavlja AuditByteLimit na 256 MiB, MaxIssues na 256, MaxXrefSections na 1024 i MaxXrefEntries na 4.000.000. Pri padu metod postavlja Report.Status := plsFailed i ponovo diže; pošto se Report piše na mestu, njegov sadržaj preživi izuzetak, i kopija se čuva u TPdf.LastLoadReport-u

Polja izveštaja odgovaraju na pitanja koja batch log zaista treba. Status je jedan od plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected ili plsFailed. NativeErrorCode drži FPDF_GetLastError, pa FPDF_ERR_PASSWORD (4) razdvaja nedostajuću ili pogrešnu lozinku od oštećenog fajla prijavljenog kao FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid i RecoveryRoute govore da li je PDFium morao da ponovo izgradi xref tabelu, a Issues nabraja svaki nalaz audita sa Code, Severity, Offset, ObjectNumber i MessageText, uz IssuesTruncated postavljen kada je MaxIssues skratio listu

LoadDocument pipeline PDFium Component-a i njegov TPdfLoadReport: validacija opcija i provera neaktivnosti dižu se pre nego što bilo koji izveštaj postoji, bajt audit prelazi preko headera, startxref-a, xref sekcija i markera kraja fajla, nativno učitavanje beleži FPDF_GetLastError, a ishodi se granaju u učitano, učitano sa oporavkom posle ponovne izgradnje xref-a, strogo odbijanje ili pad
Status, NativeErrorCode i lista nalaza odgovaraju na ono što batch log treba; bajt audit dodaje samo options overload, a od v3.122.1 pala Active := True dodela i dalje beleži plsFailed u LastLoadReport-u
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 instanca po dokumentu
    try
      Pdf.FileName := Files[I];
      try
        Pdf.LoadDocument(Options, Report);
      except
        on E: Exception do
        begin
          // Report je popunjen iako je LoadDocument digao izuzetak
          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;

Kada se učitavati sa plmStrict?

Koristite plmStrict kad god je tiho popravljen fajl gori od odbijenog, npr. kod prijema u arhivu, rukovanja dokazima ili pipeline-a za potpisivanje. PDFium tiho rekonstruiše pokvarenu tabelu unakrsnih referenci (ISO 32000-1 §7.5.4) skenirajući fajl za objekte, što je sjajno za pregledač a problem za sve što mora obrađivati baš bajtove koje je dobilo. Posle nativnog učitavanja komponenta pita FPDF_DocumentHasValidCrossReferenceTable. U plmCompatible režimu ponovna izgradnja daje plsLoadedWithRecovery plus upozorenje plicNativeCrossReferenceRebuild. U plmStrict režimu komponenta istovara dokument, postavlja plsRejected, dodaje plicStrictModeRejected i diže EPdfError sa „Strict PDF load rejected the document". Strogi režim odbija i svaku grešku audita, a TPdfLoadOptions.Default(plmStrict) uključuje RequireFinalEndOfFileMarker, koji nedostajući %%EOF ili podatke posle poslednjeg (§7.5.5) unapređuje iz upozorenja u grešku. Xref audit dopunjuje provere na nivou objekata iz teksta o validiranju object i xref streamova uz 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;               // validan xref, bez grešaka audita
    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;

Gde TPdf.LastLoadReport prestaje da govori istinu?

TPdf.LastLoadReport je kompletan tek posle LoadDocument overloada koji uzima opcije, jer samo ti overloadi sprovode bajt audit. Uspešna Active := True dodela piše izveštaj u compatible režimu bez bajt audita, pa AuditAttempted ostaje False. Pre PDFiumPas v3.122.1 pala dodela nije pisala ništa, što je značilo da na deljenoj instanci LastLoadReport još opisuje prethodni fajl, često sa utešnim plsLoaded. Od v3.122.1 svako palo učitavanje zamenjuje izveštaj: pala Active := True dodela, koja i dalje ostavlja komponentu neaktivnom bez dizanja, i pao običan poziv LoadDocument ili LoadCustomDocument beleže plsFailed sa tekstom greške, opet bez audita. Još dve rupe su bitne u praksi. Validacija opcija i CheckInactive rade pre nego što se izveštaj inicijalizuje, pa negativan AuditByteLimit ili već aktivna instanca dižu izuzetak ne proizvevši izveštaj. I NativeErrorCode ima smisla tek kad je PDFium zaista pokušao parsiranje; za fajl koji ne postoji omot diže izuzetak pre nego što PDFium krene, pa logujte ErrorMessage i tekst izuzetka

Praktično pravilo je kratko. Zadržite Active := True za pregledače vezane za dizajnera, gde je neaktivna komponenta prihvatljiv ishod. Svuda drugde, a pre svega u batch i server kodu, pravite jedan TPdf po dokumentu, zovite LoadDocument(Options, Report), uhvatite izuzetak koji diže i logujte Report.Status, NativeErrorCode i Issues na nivou greške zajedno sa imenom fajla. Cena je nekoliko redova po pozivnom mestu, i svaki neuspeh dobije pripisan pravi fajl sa svojim pravim uzrokom

Load report API, strogi režim i audit na nivou bajtova stižu uz PDFium Component za Delphi, C++Builder i Lazarus, uz renderovanje, izvlačenje teksta, popunjavanje formi i PDF/A validaciju