W PDFium Component dla Delphi i Lazarusa przypisanie TPdf.Active := True nigdy nie podnosi wyjątku, gdy PDF nie chce się załadować: TPdf.SetActive łapie każdy wyjątek i zostawia komponent nieaktywny. Żeby zobaczyć prawdziwy błąd, wołaj zamiast tego TPdf.LoadDocument(Options, Report). To przeciążenie ponownie wyrzuca oryginalny wyjątek i wypełnia TPdfLoadReport statusem ładowania, natywnym kodem błędu PDFium oraz informacją, czy tabelę odwołań krzyżowych trzeba było przebudować
Problem zwykle wypływa w kodzie wsadowym. Zadanie ekstrakcji tabel przechodzi przez folder 13 prawdziwych PDF-ów z jednym współdzielonym TPdf, a 7 z nich wraca jako porażki. Żaden z tych 7 plików nie jest faktycznie zepsuty. Bloki except wokół ładowania nigdy nie odpalają, log wini złe nazwy plików, a pierwszy widoczny błąd to goły EPdfError o nieaktywnym komponencie, wyrzucony z odczytu właściwości kilka linii po ładowaniu, które faktycznie padło. Dwa osobne zachowania składają się na ten obraz i oba działają zgodnie z projektem
Dlaczego TPdf.Active := True nie podnosi wyjątku, gdy PDF nie chce się załadować?
TPdf.SetActive owija LoadDocument w try..except, które połyka każdą klasę wyjątku i po prostu zostawia komponent nieaktywny. Połknięcie jest celowe: ten sam setter biegnie, gdy projektant formularza przełącza Active w IDE, a zła ścieżka nie może rozwalić IDE. W czasie biegu TPdf.Active po prostu raportuje, czy istnieje natywny uchwyt dokumentu, więc po nieudanym ładowaniu czyta się jako False i nic więcej się nie dzieje. Cokolwiek zostało wyrzucone, przepadło — czy to EPdfError od parsera, błąd strumienia, czy EAccessViolation od półpodpiętego pdfium.dll. Szczegółowe komunikaty DLL opisane w diagnozowaniu awarii ładowania pdfium.dll w Delphi docierają do twojej obsługi tylko przez wywołanie, które ich nie połyka
Pdf.FileName := FileName;
try
Pdf.Active := True; // SetActive połyka każdy wyjątek ładowania
except
on E: Exception do
Log.Add(FileName + ': ' + E.Message); // nigdy się nie wykonuje
end;
// Porażka wypływa zamiast tego tutaj, jako ogólny EPdfError:
// 'Cannot perform this operation on an inactive Pdf1 component'
Log.Add(Format('%s: %d pages', [FileName, Pdf.PageCount]));
// Minimalna poprawka istniejącego kodu: testuj Active zaraz po przypisaniu;
// od v3.122.1 LastLoadReport trzyma tekst połkniętego błędu
Pdf.Active := True;
if not Pdf.Active then
Log.Add(FileName + ': load failed: ' + Pdf.LastLoadReport.ErrorMessage);
Porażka w końcu wychodzi na pierwszym strzeżonym wywołaniu. TPdf.PageCount, jak większość właściwości dokumentu, zaczyna od CheckActive, które wyrzuca EPdfError wymieniający komponent, ale nie plik i nie przyczynę. Testowanie Pdf.Active zaraz po przypisaniu zamienia źle przypisany crash w uczciwy wpis "padło". Przed PDFiumPas v3.122.1 przyczyna ginęła w tym punkcie; od v3.122.1 nieudane przypisanie zastępuje LastLoadReport raportem plsFailed niosącym tekst błędu, więc przyczyna przeżywa. Sam obiekt wyjątku i audyt na poziomie bajtów nadal wymagają innego wejścia
Dlaczego ponowne użycie jednego TPdf pada od drugiego pliku?
TPdf.FileName można przypisać tylko, gdy komponent jest nieaktywny, więc współdzielona instancja odrzuca drugi plik, zanim w ogóle spróbuje go załadować. TPdf.SetFileName zaczyna od CheckInactive, a ta sama straż chroni Password i FormFill. Po pierwszym udanym ładowaniu instancja zostaje aktywna, następne przypisanie wyrzuca, a jeśli pętla wsadowa złapie ten wyjątek i idzie dalej, błąd ląduje pod nową nazwą pliku, podczas gdy stary dokument wciąż jest otwarty. Zmieszane z połkniętymi porażkami ładowania, log przestaje pasować do rzeczywistości. W reprodukcji na 13 plikach współdzielona instancja zgłaszała 7 porażek, podczas gdy świeży TPdf.Create(nil) na dokument otwierał wszystkie 13. Ustawianie Active := False między plikami też działa, ale jedna instancja na dokument trzyma każdy plik odizolowany z samej konstrukcji
Co daje TPdf.LoadDocument z TPdfLoadReport?
TPdf.LoadDocument(const Options: TPdfLoadOptions; out Report: TPdfLoadReport) wyrzuca prawdziwy wyjątek i mówi ci też, co się stało, w postaci ustrukturyzowanej. Przeciążenie plikowe ładuje FileName; przeciążenia siostrzane biorą TBytes albo wskaźnik i rozmiar, a LoadCustomDocument(AStream, AOwnsStream, Options, Report) pokrywa strumienie. Każde waliduje opcje, sprawdza, że instancja jest nieaktywna, biegnie audyt bajtowy nagłówka, startxref, sekcji xref i znacznika %%EOF, po czym wykonuje ładowanie natywne. Audyt jest ograniczony tym samym rodzajem limitów, o których mowa w budżetach zasobów parsera dla niezaufanych PDF-ów: TPdfLoadOptions.Default ustawia AuditByteLimit na 256 MiB, MaxIssues na 256, MaxXrefSections na 1024 i MaxXrefEntries na 4 000 000. Przy porażce metoda ustawia Report.Status := plsFailed i wyrzuca ponownie; ponieważ Report jest pisany w miejscu, jego treść przeżywa wyjątek, a kopia ląduje w TPdf.LastLoadReport
Pola raportu odpowiadają na pytania, których faktycznie potrzebuje log wsadowy. Status to jedno z plsNotAttempted, plsLoaded, plsLoadedWithRecovery, plsRejected albo plsFailed. NativeErrorCode trzyma FPDF_GetLastError, więc FPDF_ERR_PASSWORD (4) oddziela brakujące albo złe hasło od uszkodzonego pliku zgłaszanego jako FPDF_ERR_FORMAT (3). UsedRecovery, CrossReferenceTableValid i RecoveryRoute mówią, czy PDFium musiał przebudować tabelę xref, a Issues wypisuje każde ustalenie audytu z Code, Severity, Offset, ObjectNumber i MessageText, z ustawionym IssuesTruncated, gdy MaxIssues skróciło listę
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 instancja na dokument
try
Pdf.FileName := Files[I];
try
Pdf.LoadDocument(Options, Report);
except
on E: Exception do
begin
// Report jest wypełniony, choć LoadDocument wyrzucił
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;
Kiedy ładować z plmStrict?
Używaj plmStrict, kiedy po cichu naprawiony plik jest gorszy niż odrzucony — na przykład w przyjmowaniu archiwów, obrobie dowodów albo pipeline podpisującym. PDFium po cichu odtwarza zepsutą tabelę odwołań krzyżowych (ISO 32000-1 §7.5.4), skanując plik w poszukiwaniu obiektów, co jest świetne dla przeglądarki i problemem dla wszystkiego, co musi przetworzyć dokładnie te bajty, które dostało. Po ładowaniu natywnym komponent pyta FPDF_DocumentHasValidCrossReferenceTable. W trybie plmCompatible przebudowa daje plsLoadedWithRecovery plus ostrzeżenie plicNativeCrossReferenceRebuild. W trybie plmStrict komponent zrzuca dokument, ustawia plsRejected, dodaje plicStrictModeRejected i wyrzuca EPdfError z komunikatem "Strict PDF load rejected the document". Tryb strict odrzuca też każdy błąd audytu, a TPdfLoadOptions.Default(plmStrict) włącza RequireFinalEndOfFileMarker, które awansuje brakujący %%EOF albo dane po ostatnim (§7.5.5) z ostrzeżenia na błąd. Audyt xref uzupełnia kontrole na poziomie obiektów opisane w walidacji strumieni obiektów i xref z 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; // poprawny xref, zero błędów audytu
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;
Gdzie TPdf.LastLoadReport przestaje mówić prawdę?
TPdf.LastLoadReport jest pełny tylko po przeciążeniu LoadDocument przyjmującym opcje, bo tylko te przeciążenia biegną audyt bajtowy. Udane Active := True zapisuje raport w trybie zgodnym bez audytu bajtowego, więc AuditAttempted zostaje False. Przed PDFiumPas v3.122.1 nieudane nie zapisywało nic, co znaczyło, że na współdzielonej instancji LastLoadReport wciąż opisywał poprzedni plik, często z uspokajającym plsLoaded. Od v3.122.1 każde nieudane ładowanie zastępuje raport: nieudane Active := True, które nadal zostawia komponent nieaktywny bez wyrzucania, i nieudane gołe wołanie LoadDocument albo LoadCustomDocument zapisują plsFailed z tekstem błędu, znów bez audytu. Dwie kolejne luki mają znaczenie w praktyce. Walidacja opcji i CheckInactive biegną, zanim raport zostanie zainicjowany, więc ujemny AuditByteLimit albo już aktywna instancja wyrzucają bez produkowania raportu. A NativeErrorCode ma znaczenie tylko, gdy PDFium faktycznie spróbował parsowania; dla brakującego pliku wrapper wyrzuca, zanim PDFium ruszy, więc loguj zamiast tego ErrorMessage i tekst wyjątku
Praktyczna reguła jest krótka. Trzymaj Active := True dla przeglądarek związanych z projektantem, gdzie nieaktywny komponent to akceptowalny wynik. Wszędzie indziej, a przede wszystkim w kodzie wsadowym i serwerowym, stwórz jeden TPdf na dokument, wołaj LoadDocument(Options, Report), łap wyrzucany wyjątek i loguj Report.Status, NativeErrorCode i ustalenia Issues na poziomie błędu razem z nazwą pliku. Koszt to kilka linii na miejsce wywołania, a każda porażka zostaje przypisana do właściwego pliku z prawdziwą przyczyną
API raportu ładowania, tryb strict i audyt na poziomie bajtów płyną z PDFium Component dla Delphi, C++Buildera i Lazarusa, razem z renderowaniem, ekstrakcją tekstu, wypełnianiem formularzy i walidacją PDF/A