V PDFium Component pro Delphi a Lazarus nikdy nevyhodí přiřazení TPdf.Active := True výjimku, když se PDF nepodaří načíst: TPdf.SetActive chytá každou výjimku a nechá komponentu neaktivní. Skutečnou chybu uvidíte, když místo toho zavoláte TPdf.LoadDocument(Options, Report). Tenhle overload znovu vyhodí původní výjimku a naplní TPdfLoadReport stavem načtení, nativním chybovým kódem PDFium a tím, zda se musela přestavět tabulka cross-reference
Problém obvykle vyplave v batch kódu. Úloha extrakce tabulek projde složku 13 reálných PDF s jedním sdíleným TPdf a 7 z nich skončí jako selhání. Žádný z těch 7 souborů není ve skutečnosti rozbitý. Bloky except kolem načtení se nespustí nikdy, log obviní špatné názvy souborů a první viditelná chyba je holé EPdfError o neaktivní komponentě, vyhozené z čtení property pár řádků po načtení, které skutečně selhalo. Dvě oddělená chování se nahromadí do tohohle obrazu a obojí funguje, jak bylo navrženo
Proč nevyhodí TPdf.Active := True výjimku, když se PDF nepodaří načíst?
TPdf.SetActive obaluje LoadDocument do try..except, které pohltí každou třídu výjimky a prostě nechá komponentu neaktivní. Pohlcení je záměrné: týž setter běží, když designér formulářů přepíná Active v IDE, a špatná cesta nesmí shodit IDE. Za běhu jen TPdf.Active hlásí, zda existuje nativní handle dokumentu, takže po neúspěšném načtení čte False a nic jiného se neděje. Cokoliv, co se vyhodilo, je ztracené — ať to bylo EPdfError od parseru, chyba streamu nebo EAccessViolation z napůl navázaného pdfium.dll. Detailní DLL zprávy popsané v Nasazení PDFium DLL v Delphi: Řešení chyb při načítání se k vašemu handleru dostanou jen voláním, které je nepohltí
Pdf.FileName := FileName;
try
Pdf.Active := True; // SetActive pohltí jakoukoli výjimku načtení
except
on E: Exception do
Log.Add(FileName + ': ' + E.Message); // nikdy se neprovede
end;
// Selhání se místo toho ukáže tady, jako generické EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));
// Minimální oprava pro existující kód: testujte Active hned po přiřazení;
// od v3.122.1 drží LastLoadReport text pohlcené chyby
Pdf.Active := True;
if not Pdf.Active then
Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);
Selhání se konečně ukáže na prvním střeženém volání. TPdf.PageCount, jako většina properties dokumentu, začíná CheckActive, které vyhodí EPdfError pojmenovávající komponentu, ale ne soubor a ne příčinu. Testování Pdf.Active hned po přiřazení změní chybně připsaný pád na poctivou položku „selhalo". Před PDFiumPas v3.122.1 se důvod v tomhle bodě ztrácel; od v3.122.1 neúspěšné přiřazení nahradí LastLoadReport reportem plsFailed nesoucím text chyby, takže příčina přežije. Samotný objekt výjimky a bajtová auditace pořád vyžadují jiný vstupní bod
Proč sdílený TPdf selhává od druhého souboru dál?
TPdf.FileName jde přiřadit jen tehdy, když je komponenta neaktivní, takže sdílená instance druhý soubor odmítne, ještě než se ho pokusí načíst. TPdf.SetFileName začíná CheckInactive a tutéž pojistku má i Password a FormFill. Po prvním úspěšném načtení instance zůstává aktivní, další přiřazení vyhodí a pokud batch smyčka tu výjimku chytne a jde dál, chyba dopadne pod novým názvem souboru, zatímco starý dokument je pořád otevřený. Smíchané s pohlcenými chybami načtení se log přestane shodovat s realitou. V reprodukci na 13 souborech hlásila sdílená instance 7 selhání, zatímco čerstvý TPdf.Create(nil) na dokument otevřel všech 13. Nastavit Active := False mezi soubory také funguje, ale jedna instance na dokument drží každý soubor izolovaný konstrukcí
Co vám dá TPdf.LoadDocument s TPdfLoadReport?
TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) vyhodí skutečnou výjimku a navíc vám strukturovaně řekne, co se stalo. Souborový overload načítá FileName; sourozenecké overloads berou TBytes nebo ukazatel a velikost a LoadCustomDocument(AStream, AOwnsStream, Options, Report) pokrývá streamy. Každý z nich zvaliduje volby, zkontroluje, že je instance neaktivní, pustí bajtovou auditaci hlavičky, startxref, sekcí xref a značky %%EOF a pak provede nativní načtení. Auditace se ohraničuje stejným druhem limitů, jaké rozebírá Rozpočet zdrojů PDF parseru v Delphi s PDFium Component: TPdfLoadOptions.Default nastavuje AuditByteLimit na 256 MiB, MaxIssues na 256, MaxXrefSections na 1024 a MaxXrefEntries na 4 000 000. Při selhání metoda nastaví Report.Status := plsFailed a znovu vyhodí; protože se Report zapisuje na místě, jeho obsah výjimku přežije a kopie se uloží do TPdf.LastLoadReport
Pole reportu odpovídají na otázky, které batch log doopravdy potřebuje. Status je jedno z plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected nebo plsFailed. NativeErrorCode drží FPDF_GetLastError, takže FPDF_ERR_PASSWORD (4) oddělí chybějící nebo špatné heslo od poškozeného souboru hlášeného jako FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid a RecoveryRoute říkají, zda musel PDFium přestavět tabulku xref, a Issues vyjmenovává každý auditní nález s Code, Severity, Offset, ObjectNumber a MessageText, přičemž IssuesTruncated se nastaví, když MaxIssues seznam usekne
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 instance na dokument
try
Pdf.FileName := Files[I];
try
Pdf.LoadDocument(Options, Report);
except
on E: Exception do
begin
// Report je naplněný, i když LoadDocument vyhodil
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;
Kdy načítat s plmStrict?
plmStrict použijte vždy, když je potichu opravený soubor horší než odmítnutý — například příjem archivu, práce s důkazy nebo podepisovací pipeline. PDFium tiše rekonstruuje rozbitou tabulku cross-reference (ISO 32000-1 §7.5.4) skenováním souboru po objektech, což je pro viewer skvělé a pro cokoliv, co musí zpracovat přesně ty bajty, které dostalo, problém. Po nativním načtení se komponenta zeptá FPDF_DocumentHasValidCrossReferenceTable. V režimu plmCompatible dá rebuild plsLoadedWithRecovery plus varování plicNativeCrossReferenceRebuild. V režimu plmStrict komponenta dokument odloží, nastaví plsRejected, přidá plicStrictModeRejected a vyhodí EPdfError se zprávou „Strict PDF load rejected the document". Striktní režim odmítne i jakoukoli auditní chybu a TPdfLoadOptions.Default(plmStrict) zapne RequireFinalEndOfFileMarker, které povýší chybějící %%EOF nebo data za posledním (§7.5.5) z varování na chybu. Auditace xref doplňuje kontroly na úrovni objektů popsané v Ověřování komprimovaných PDF: Toky objektů a křížových odkazů (Object a XRef streamy)
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; // validní xref, žádné auditní chyby
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 přestává TPdf.LastLoadReport mluvit pravdu?
TPdf.LastLoadReport je kompletní jen po overloadu LoadDocument, který bere volby, protože jen tyhle overloads pustí bajtovou auditaci. Úspěšné Active := True zapíše report v compatible módu bez bajtové auditace, takže AuditAttempted zůstává False. Před PDFiumPas v3.122.1 neúspěšné nezapsalo nic, což znamenalo, že na sdílené instanci pořád LastLoadReport popisoval předchozí soubor, často s uklidňujícím plsLoaded. Od v3.122.1 každé neúspěšné načtení report nahradí: neúspěšné Active := True, které pořád nechá komponentu neaktivní bez vyhození, i neúspěšné volání holého LoadDocument nebo LoadCustomDocument zaznamenají plsFailed s textem chyby, opět bez auditace. V praxi záleží ještě na dvou mezerách. Validace voleb a CheckInactive běží dřív, než se report inicializuje, takže záporný AuditByteLimit nebo už aktivní instance vyhodí bez vyprodukovaného reportu. A NativeErrorCode dává smysl jen tehdy, když PDFium o pokus o parse doopravdy stálo; u chybějícího souboru vyhodí wrapper dřív, než PDFium běží, takže logujte ErrorMessage a text výjimky
Praktické pravidlo je krátké. Nechávejte si Active := True pro viewery vázané na designér, kde je neaktivní komponenta přijatelný výsledek. Všude jinde, a především v batch a serverovém kódu, vytvořte jednu TPdf na dokument, zavolejte LoadDocument(Options, Report), chyťte výjimku, kterou to vyhodí, a logujte Report.Status, NativeErrorCode a error-level Issues spolu s názvem souboru. Náklady jsou pár řádků na místo volání a každé selhání se připíše správnému souboru se svou skutečnou příčinou
API load reportu, striktní režim i bajtová auditace shipují s PDFium Component pro Delphi, C++Builder a Lazarus, po boku renderování, extrakce textu, vyplňování formulářů a validace PDF/A