W PDFium Component dla Delphi włączenie BrotliEnabled albo IsolatePerDocument w TPdfLibraryConfiguration potrafiło przełączyć dołączony build Skia na renderer AGG bez żadnego błędu, bo obie opcje podnoszą FPDF_LIBRARY_CONFIG do wersji, w której PDFium czyta m_RendererType dosłownie. Od v3.123.0 domyślny renderer pozostaje domyślnym biblioteki DLL, a od v3.125.0 żądanie Skia albo Fontations, którego DLL nie zdoła spełnić, podnosi łapywalny EPdfError zamiast zabijać proces
Żaden z bugów się nie zasygnalizował. Pierwszy produkował strony wyglądające dobrze, po prostu wyrenderowane przez inny rasteryzer, z minimalnie innym antyaliasingiem i krawędziami tekstu niż build, który wysłałeś i przetestowałeś. Drugi się zasygnalizował — głośno, ściągnąwszy proces gospodarza od środka natywnej inicjalizacji. Oba biorą się z tego samego miejsca: wersjonowanej struktury C, której pola liczą się dopiero, gdy numer wersji tak powie, a której zera nie znaczą „nieustawione”, tylko są prawdziwymi wyborami
Jak FPDF_LIBRARY_CONFIG rozstrzyga, którego renderera używa PDFium?
FPDF_InitLibraryWithConfig zagląda do m_RendererType tylko wtedy, gdy pole Version struktury wynosi 4 albo więcej, i od tej wersji używa wartości dokładnie tak, jak zapisano. Poniżej wersji 4 PDFium ignoruje pole i bierze domyślny builda — Skia w buildach skompilowanych z PDF_USE_SKIA i AGG wszędzie indziej
Każde dalsze pole idzie tym samym schematem. Struktura rosła o jedną możliwość na raz, i każda możliwość przychodziła razem z nowym numerem wersji. PDFium Component buduje strukturę natywną w LoadLibrary z twojego TPdfLibraryConfiguration i podnosi wersję tylko tak wysoko, jak wymagają ustawione opcje
| Wersja struktury | Pole, które dodaje | Ustawiane przez |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Zawsze zapisywane; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform różne od nil |
| 4 | m_RendererType | Renderer inne niż prpDefault |
| 5 | m_FontLibraryType | FontBackend inne niż pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
Pułapka siedzi w dwóch ostatnich wierszach. Wersje są skumulowane: struktura wersji 6 jest też strukturą wersji 4 i wersji 5, więc PDFium czyta m_RendererType i m_FontLibraryType, choć poprosiłeś wyłącznie o Brotli. Cokolwiek siedzi w tych dwóch polach w tej chwili, staje się rendererem i backendem czcionek — czy chciałeś je wybrać, czy nie
Dlaczego włączenie Brotli przełączało renderer na AGG?
Przed v3.123.0 PDFium Component zapisywał FPDF_RENDERERTYPE_AGG do m_RendererType dla prpDefault, więc każda konfiguracja pchająca strukturę do wersji 6 albo 7 wymuszała AGG na buildzie Skia. Runtimy pdfium.dll i pdfium.v8.dll dołączane do komponentu to buildy Skia, więc trafiał to domyślne wdrożenie, a nie jakieś egzotyczne
Mapowanie wyglądało niewinnie, gdy je napisano. Przy wersji 2 albo 3 pole nie jest nigdy czytane, więc prpDefault naprawdę znaczyło „cokolwiek zrobi biblioteka DLL”. W momencie, gdy do gry weszły BrotliEnabled (wersja 6) albo IsolatePerDocument (wersja 7), ten sam kod zamienił „bez preferencji” w jawne żądanie AGG. Nic nie zawiodło. PDFium zainicjalizował się normalnie, wyrenderował każdą stronę i nie zwrócił żadnego kodu błędu, bo z jego punktu widzenia wołający poprosił o AGG i dostał AGG
Hash pikseli czyni podmianę widoczną tam, gdzie zrzuty ekranu nie. Wyrenderowanie pierwszej strony tego samego dokumentu próbnego w trzech konfiguracjach dało:
- konfiguracja domyślna: hash
502D77C3711B4ACF BrotliEnabled= True przyRendererpozostawionym naprpDefault: hashF75B5EB4728ADE87- jawne
prpAgg: hashF75B5EB4728ADE87, identyczny z przebiegiem Brotli
Poprawka z v3.123.0 to publiczna funkcja PdfNativeRendererType, która rozwiązuje TPdfRendererPreference na wartość wpisywaną do m_RendererType. prpAgg i prpSkia mapują się jeden do jednego. prpDefault mapuje się teraz na Skia, gdy wczytana biblioteka DLL eksportuje FPDF_RenderPageSkia, i na AGG w przeciwnym razie. Ten eksport jest kompilowany pod tym samym warunkiem PDF_USE_SKIA co sam domyślny Skia, co czyni go jedyną własnością builda obserwowalną z zewnątrz biblioteki DLL. Po poprawce konfiguracja Brotli daje ten sam hash co domyślna
Backend czcionek nigdy nie miał tego problemu. m_FontLibraryType jest czytane od wersji 5, a jego wartość zero, FPDF_FONTBACKENDTYPE_FREETYPE, to zarazem domyślny PDFium, gdy pole nie jest czytane wcale. Zapisanie FreeType dla pfbpDefault odtwarza więc natywny domyślny dokładnie. Wartości zero nie zawsze są złe — po prostu nigdy nie są automatycznie dobre
Z v3.123.0 albo nowszym kod startowy, który i tak napisałbyś naturalnie, robi teraz to, co mówi:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// Musi się wykonać, zanim cokolwiek wczyta bibliotekę natywną
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // podnosi FPDF_LIBRARY_CONFIG do wersji 6
// Renderer zostaje prpDefault: rozwiązywany na Skia w buildach eksportujących
// FPDF_RenderPageSkia i na AGG w buildach wyłącznie AGG
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
Pamiętaj, że BrotliEnabled czyni strumienie /BrotliDecode z PDF 2.0 dekodowalne tylko wtedy, gdy sama biblioteka DLL była zbudowana z PDF_ENABLE_BROTLI. Flaga to prośba, a na buildzie bez wsparcia Brotli nie daje żadnego efektu. TPdfLibraryConfiguration.Hardened to to samo co Default, z tą różnicą, że AllowMachineTime jest False, co blokuje dokumentowemu JavaScriptowi czytanie prawdziwego zegara; to rozsądny punkt wyjścia do przetwarzania niezaufanych plików po stronie serwera
Co się dzieje, gdy zażądasz backendu, którego biblioteka DLL nie zawiera?
PDFium nie zwraca błędu dla renderera albo backendu czcionek nieobecnego w buildzie: FPDF_InitLibraryWithConfig wykłada natywny CHECK, który na Windows wychodzi na wierzch jako wyjątek breakpointa i, bez strukturalnego handlera wyjątków wokół wywołania, terminuje proces. Nagłówek mówi to wprost, ostrzegając, że niewspierana wartość „zawodzi podobnie, natychmiastowym crashem”
Dwa konkretne przypadki to build wyłącznie AGG dostający FPDF_RENDERERTYPE_SKIA i build bez Fontations dostający FPDF_FONTBACKENDTYPE_FONTATIONS. Dołączany runtime Skia należy do drugiej grupy: renderuje przez Skia, ale używa FreeType do czcionek. Żądanie prpSkia razem z pfbpFontations wobec niego produkowało External exception 80000003 po stronie Delphi. Gdy debugger albo handler wyjątków akurat to złapie, sytuacja jest wciąż nieodwracalna:
- PDFium zostaje na wpół zainicjalizowany
- konfiguracja całego procesu jest już zapieczętowana, więc
ConfigurePdfLibraryodmawia skorygowanej konfiguracji - ponawianie z inną konfiguracją w tym samym procesie nie jest już możliwe
To porażka przeciwna do buga Brotli. Tam pole trzymało wartość, której nikt nie wybrał, a PDFium przyjmował ją po cichu. Tutaj pole trzyma wartość, którą wołający wybrał świadomie, i PDFium nie przyjmuje o niej żadnej dyskusji. Oba to problemy, które wrapper musi rozwiązać przed wywołaniem natywnym, bo po nim nie ma już czego łapać
Jak PDFium Component precheckuje Skia i Fontations
Od v3.125.0 LoadLibrary waliduje konfigurację po dowiązaniu eksportów biblioteki DLL, a przed wywołaniem FPDF_InitLibraryWithConfig, i zamienia niewspierany renderer albo backend czcionek w EPdfError z komunikatem nazywającym winne ustawienie i alternatywy. Biblioteka DLL jest wyładowywana, a konfiguracja odpieczętowuje się, więc wołający może wybrać inne ustawienia i wczytać ponownie
Decyzja sama mieszka w czystej funkcji PdfLibraryConfigurationSupportError, która bierze konfigurację plus dwa Boole'e opisujące build i zwraca pusty tekst, gdy kombinacja jest bezpieczna. Bo nie dotyka żadnego stanu natywnego, możesz wołać ją z własnych testów z dowolną kombinacją możliwości. Wewnątrz LoadLibrary oba Boole'e pochodzą z różnych rodzajów dowodów i zasługują na różny poziom zaufania:
- Skia jest wykrywana z obecności eksportu
FPDF_RenderPageSkia— tego samego sygnału, którego używaPdfNativeRendererType. Eksport i renderer Skia są kompilowane pod jednym warunkiem, więc sprawdzenie jest ścisłe - Fontations nie ma własnego eksportu. Jedyny ślad, jaki zostawia, to skrzynie czcionek Rust, które wciąga do binarki, więc PDFium Component skanuje wczytany plik biblioteki pod kątem nazw skrzyń
skrifairead-fonts(takżeread_fonts). Skan biegnie tylko, gdy zażądanopfbpFontations, a plik, którego nie da się przeczytać, liczy się jako „brak Fontations”
Sprawdzenie Fontations to heurystyka i może się mylić w jedną stronę: build Fontations obdarty ze wszystkich tych tekstów zostałby odrzucony, choć mógłby zadziałać. Ten kompromis zawarto celowo. Fałszywe odrzucenie kosztuje cię wyjątek, który złapiesz, i fallback na FreeType. Fałszywe przyjęcie kosztuje cię proces
Odpieczętowanie znaczy tyle, co samo sprawdzenie. LoadLibrary pieczętuje konfigurację na samym początku wczytywania, więc bez resetu odrzucenie możliwości zostawiłoby ConfigurePdfLibrary odpowiadające na każdą ponowną próbę EPdfError „PDFium library configuration is already sealed”. Ścieżka odrzucenia woła najpierw UnloadLibrary; jej wywołanie FPDF_DestroyLibrary jest w tym punkcie bezpieczne, bo PDFium nie został jeszcze zainicjalizowany i wraca natychmiast. Inne porażki wczytania, jak brakująca biblioteka DLL albo niedopasowanie architektury, trzymają pieczęć, więc pętla ponawiania musi umieć je odróżnić:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // kwalifikowane modułem: Windows.LoadLibrary ma tę samą nazwę
Result := prpSkia;
except
on E: EPdfError do
begin
// Odrzucenie możliwości wyładowuje DLL i odpieczętowuje konfigurację.
// DLL, który w ogóle się nie wczytał, zostaje zapieczętowany: ponawianie nie pomoże
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Zauważ jawne PDFium.LoadLibrary. W module, który używa też Windows albo Winapi.Windows, niewykwalifikowane LoadLibrary rozwiązuje się do tej jednostki, która jest ostatnia w uses; gdy to funkcja Win32, wywołanie bez parametrów nie skompiluje się z błędem liczby argumentów, który nic nie mówi o PDFium
Walidacja, która dzieje się jeszcze wcześniej
ConfigurePdfLibrary odrzuca niektóre kombinacje, zanim w ogóle wchodzi do gry jakakolwiek biblioteka DLL — wszystkie przez EPdfError. Jawny FontBackend, łącznie z pfbpFreeType, wymaga Renderer = prpSkia, bo PDFium konsultuje backend czcionek wyłącznie dla renderera Skia. IsolatePerDocument wymaga V8Isolate = nil, bo PDFium tworzy własny izolat per dokument i wykłada natywny CHECK, jeśli wręczysz mu też swój. Puste teksty w UserFontPaths są odrzucane. I każde wywołanie po pierwszej próbie wczytania zawodzi z „PDFium library configuration is already sealed”
Ostatnia reguła ma praktyczną konsekwencję: nie da się najpierw rozeznać w bibliotece DLL, a skonfigurować ją potem. GetSkiaRenderCapabilities, V8FeaturesAvailable, otwarcie dokumentu i większość pozostałych punktów wejścia woła w środku LoadLibrary, który pieczętuje konfigurację na miejscu. Późniejsze UnloadLibrary też jej nie otwiera. Najpierw skonfiguruj, potem wczytaj, potem zadawaj pytania — dokładnie ta kolejność, jakiej powinna trzymać się procedura diagnostyczna:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // kopia, można bezpiecznie oglądać
if not PDFium.Loaded then
begin
if PdfLibraryConfigurationSealed then
Exit('PDFium failed to load; configuration is sealed');
Exit('PDFium not loaded; configuration can still change');
end;
// Ta sama resolucja, którą LoadLibrary zastosował, budując FPDF_LIBRARY_CONFIG
if PdfNativeRendererType(Config.Renderer,
GetSkiaRenderCapabilities.PageRender) = FPDF_RENDERERTYPE_SKIA then
Renderer := 'Skia'
else
Renderer := 'AGG';
Result := Format('Renderer=%s Brotli=%s IsolatePerDocument=%s',
[Renderer, BoolToStr(Config.BrotliEnabled, True),
BoolToStr(Config.IsolatePerDocument, True)]);
end;
Zalogowanie tej linijki raz na starcie jest tanie, i to pierwsza rzecz, jaką chcesz mieć w zgłoszeniu supportu mówiącym „tekst wygląda inaczej na serwerze”. PDFium.Loaded jest kwalifikowane z tego samego powodu co LoadLibrary: wewnątrz metody formularza albo komponentu gołe Loaded wiąże się do TComponent.Loaded
Dwa sposoby, na jakie wersjonowana struktura konfiguracyjna C idzie na opak
Każda wersjonowana struktura konfiguracyjna — czy to FPDF_LIBRARY_CONFIG, rekord Win32 z cbSize, czy ABI wtyczki — zawodzi na dwa symetryczne sposoby, i wrapper musi strzec się obu. Pierwszy to wypełnienie pola przy zostawieniu wersji zbyt niskiej; drugi to podniesienie wersji przy zostawieniu pola na wartości zero, którą biblioteka czyta jako świadomy wybór
- Pole ustawione, wersja zbyt niska. Wpisz
m_BrotliEnabled= 1 do struktury wersji 2, a PDFium nigdy na to nie spojrzy. Wywołanie się udaje, a strumienie Brotli pozostają niedekodowalne. Obroną jest wyprowadzanie wersji z pól faktycznie używanych — to robiLoadLibrary— zamiast wbijania jednej na sztywno - Wersja wystarczająca, zero w polu coś znaczy. Podnieś wersję do 6, a każde pole do wersji 6 włącznie staje się żywe.
FillCharzerujem_RendererTypedoFPDF_RENDERERTYPE_AGG, czyli prawdziwego renderera, nie „nieustawione”. Obroną jest wpisanie każdego pola objętego wybraną wersją wartością intencjonalną i rozwiązywanie „domyślnego” względem faktycznego builda, zamiast go zakładać
Trzecia reguła dotyczy wartości, które potrafią wywalić wołanego: waliduj je względem tego, co binarka umie, przed wywołaniem, używając najmocniejszego dostępnego dowodu, i bądź szczery w kodzie i w dokumentacji, kiedy ten dowód jest heurystyką. Wyeksportowany symbol to dowód. Nazwa skrzyni w tablicy tekstów to dobre zgadywanie
Ściąga: konfiguracja biblioteki w PDFium Component
- Wołaj
ConfigurePdfLibraryraz, zanim cokolwiek wczyta bibliotekę DLL; każde zapytanie o możliwości albo wczytanie dokumentu ją pieczętuje - Zaktualizuj do v3.123.0 albo nowszej, jeśli ustawiasz
BrotliEnabledalboIsolatePerDocumenti oczekujesz wyjścia Skia z dołączonych runtime'ów - Zostaw
RenderernaprpDefault, chyba że potrzebujesz konkretnego rasteryzera; rozwiązuje się teraz do domyślnego builda przy każdej wersji struktury - Używaj
PdfNativeRendererTypezGetSkiaRenderCapabilities.PageRender, żeby logować, który renderer faktycznie działa - Oczekuj
EPdfError, nie crasha, dlaprpSkiana bibliotece DLL wyłącznie AGG albopfbpFontationsna bibliotece bez Fontations w v3.125.0 albo nowszej - Po odrzuceniu możliwości
PdfLibraryConfigurationSealedjest False i możesz przekonfigurować; po nieudanym wczytaniu biblioteki DLL zostaje True - Traktuj wykrywanie Fontations jako heurystykę i trzymaj fallback na FreeType
- Pisz
PDFium.LoadLibraryiPDFium.Loadedz nazwą modułu, żeby uniknąć kolizji nazw z Win32 iTComponent
Jeśli biblioteka DLL zawodzi, zanim konfiguracja w ogóle zacznie mieć znaczenie, zacznij od diagnostyki niepowodzeń wczytania PDFium DLL w Delphi, a o tym, jak komponent znajduje właściwą binarkę na każdej platformie, pisze wczytywanie natywnej biblioteki PDFium na dowolnym celu. Gdy renderer jest już ustalony, cache renderowania i taktyki płynnego zoomu pokazują, jak trzymać renderowanie stron szybkie w viewerze
PDFium Component opakowuje silnik PDFium dla Delphi i C++Buildera ze sprawdzeniami konfiguracji takimi jak te, więc natywna inicjalizacja zawodzi jako wyjątek Pascal, który obsłużysz, a nie jako śmierć procesu. Szczegóły produktu i pobieranie są na stronie produktu PDFium Component for Delphi