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
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
Č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
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