В PDFium Component за Delphi включването на BrotliEnabled или IsolatePerDocument в TPdfLibraryConfiguration сменяше вградения Skia билд с AGG рендерера без никаква грешка, защото и двете опции вдигат FPDF_LIBRARY_CONFIG до версия, в която PDFium чете m_RendererType буквално. От v3.123.0 насам default рендерерът си остава собственият default на DLL-а, а от v3.125.0 насам заявка за Skia или Fontations, която DLL-ът не може да уважи, вдига прихващаем EPdfError вместо да убие процеса
Нито единият бъг не се обяви. Първият даваше страници, които изглеждаха наред — просто рендерирани от друг растеризатор, с малко по-различно anti-aliasing и ръбове на текста от билда, който сте доставили и тествали. Вторият се обяви, и то гръмко, като сваля host процеса отвътре, от native инициализацията. И двамата идват от едно място: версионирана C структура, чиито полета се броят чак когато номерът на версия каже, че се броят, и чиито нулеви стойности не са „неизпаднати“, а истински избори
Как FPDF_LIBRARY_CONFIG решава кой рендерер ползва PDFium?
FPDF_InitLibraryWithConfig поглежда m_RendererType само когато полето Version на структурата е 4 или по-високо, и от тази версия нататък ползва стойността точно както е написана. Под версия 4 PDFium игнорира полето и взима default-а на билда — Skia в билдове, компилирани с PDF_USE_SKIA, и AGG навсякъде другаде
Всяко следващо поле следва същия модел. Структурата растеше по една възможност наведнъж, и всяка възможност идваше заедно с нов номер на версия. PDFium Component строи native структурата в LoadLibrary от вашата TPdfLibraryConfiguration и вдига версията само дотолкова, доколкото изискват зададените опции
| Версия на структурата | Поле, което добавя | Задава се чрез |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Винаги се записва; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform не е nil |
| 4 | m_RendererType | Renderer различно от prpDefault |
| 5 | m_FontLibraryType | FontBackend различно от pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
Капанът е в последните два реда. Версиите са кумулативни: структура от версия 6 е и структура от версия 4 и от версия 5, така че PDFium чете m_RendererType и m_FontLibraryType, макар вие да сте поискали само Brotli. Това, което стои в тези две полета в момента, става рендерерът и font backend-ът — независимо дали сте искали да ги изберете
Защо включването на Brotli смени рендерера с AGG?
Преди v3.123.0 PDFium Component записваше FPDF_RENDERERTYPE_AGG в m_RendererType за prpDefault, така че всяка конфигурация, бутаща структурата до версия 6 или 7, принуждаваше AGG върху Skia билд. Runtime-ите pdfium.dll и pdfium.v8.dll, които се доставят с компонента, са Skia билдове, така че това улучваше default deployment-а, а не някой екзотичен
Мапването изглеждаше безобидно, когато го е писано. При версия 2 или 3 полето никога не се чете, така че prpDefault реално значеше „каквото прави DLL-ът“. В момента, в който BrotliEnabled (версия 6) или IsolatePerDocument (версия 7) влязоха в картината, същият код превръщаше „нямам предпочитание“ в изрична AGG заявка. Нищо не се провали. PDFium се инициализира нормално, рендерира всяка страница и не върна никакъв код за грешка, защото от негова гледна точка извикващият е поискал AGG и е получил AGG
Pixel hash прави смяната видима там, където екранните снимки не. Рендерирането на първата страница на един и същ примерен документ под три конфигурации даде:
- Default конфигурация: hash
502D77C3711B4ACF BrotliEnabled= True сRendererоставен наprpDefault: hashF75B5EB4728ADE87- Изричен
prpAgg: hashF75B5EB4728ADE87, идентичен с Brotli прогона
Поправката в v3.123.0 е публичната функция PdfNativeRendererType, която разрешава TPdfRendererPreference до стойността, записвана в m_RendererType. prpAgg и prpSkia мапват един към един. prpDefault вече мапва към Skia, когато заредения DLL експортира FPDF_RenderPageSkia, и към AGG в останалите случаи. Този export се компилира под същото условие PDF_USE_SKIA като самия Skia default, което го прави единственото свойство на билда, наблюдаемо отвън на DLL-а. След поправката Brotli конфигурацията дава същия hash като default-ната
Font backend-ът никога не имаше същия проблем. m_FontLibraryType се чете от версия 5 нататък, а нулевата му стойност, FPDF_FONTBACKENDTYPE_FREETYPE, е същевременно и default-ът на PDFium, когато полето изобщо не се чете. Записването на FreeType за pfbpDefault затова възпроизвежда native default-а точно. Нулевите стойности не са винаги грешни — те просто никога не са автоматично верни
С v3.123.0 или по-нов startup кодът, който бихте написали естествено, вече прави това, което казва:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// Трябва да тръгне преди нещо да зареди native библиотеката
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // вдига FPDF_LIBRARY_CONFIG до версия 6
// Renderer остава prpDefault: разрешава се на Skia на билдове, които експортират
// FPDF_RenderPageSkia и на AGG на билдове само с AGG
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
Помнете, че BrotliEnabled прави PDF 2.0 stream-овете /BrotliDecode декодируеми само когато самият DLL е билднат с PDF_ENABLE_BROTLI. Флагът е заявка, а върху билд без Brotli поддръжка няма никакъв ефект. TPdfLibraryConfiguration.Hardened е същото като Default, с това, че AllowMachineTime е False, което спира document JavaScript да чете истинския часовник; разумен начален пункт за сървърна обработка на недоверени файлове
Какво става, когато поискате backend, когото DLL-ът не съдържа?
PDFium не връща грешка за липсващ в билда рендерер или font backend: FPDF_InitLibraryWithConfig проваля native CHECK, който на Windows излиза като breakpoint exception и, без structured exception handler около извикването, прекратява процеса. Header-ът го казва прямо, с предупреждение, че неподдържана стойност „will similarly fail with an immediate crash“
Двата конкретни случая са AGG-само билд, получил FPDF_RENDERERTYPE_SKIA, и билд без Fontations, получил FPDF_FONTBACKENDTYPE_FONTATIONS. Вграденият Skia runtime е във втората група: рендерира със Skia, но ползва FreeType за шрифтове. Заявка за prpSkia заедно с pfbpFontations срещу него даваше External exception 80000003 от Delphi страната. Когато дебъгерът или exception handler случайно го хванат, ситуацията пак е невъзстановима:
- PDFium остава полуинициализиран
- Конфигурацията за целия процес вече е запечатана, така че
ConfigurePdfLibraryотказва коригирана конфигурация - Повторен опит с друга конфигурация в същия процес вече не е възможен
Това е противоположният провал на бъга с Brotli. Там полето държеше стойност, която никой не е избрал, и PDFium я прие тихо. Тук полето държи стойност, която извикващият е избрал съзнателно, и PDFium не приема никакъв разговор по въпроса. И двата са проблеми, които wrapper трябва да разреши преди native извикването, защото след него няма какво да прихване
Как PDFium Component предпроверява Skia и Fontations
От v3.125.0 насам LoadLibrary валидира конфигурацията след закачането на DLL export-ите и преди извикването на FPDF_InitLibraryWithConfig и превръща неподдържан рендерер или font backend в EPdfError със съобщение, което назовава виновната настройка и алтернативите. DLL-ът се разтоварва и конфигурацията се отпечатва, така че извикващият може да избере други настройки и да зареди пак
Самото решение живее в чистата функция PdfLibraryConfigurationSupportError, която приема конфигурацията плюс две булеви стойности, описващи билда, и връща празен низ, когато комбинацията е безопасна. Тъй като не пипа native състояние, можете да я викате от собствени тестове с всякаква комбинация от възможности. Вътре в LoadLibrary двете булеви идват от различни видове доказателства, и заслужават различно ниво на доверие:
- Skia се разпознава от наличието на export-а
FPDF_RenderPageSkia— същият сигнал, който ползваPdfNativeRendererType. Export-ът и Skia рендерерът се компилират под едно условие, така че проверката е точна - Fontations няма собствен export. Единствената следа, която оставя, са Rust font crate-овете, които дърпа в бинария, затова PDFium Component сканира заредения библиотечен файл за имената на crate-овете
skrifaиread-fonts(същоread_fonts). Сканирането тръгва само когато е поисканpfbpFontations, а файл, който не може да се прочете, се брои за „без Fontations“
Проверката за Fontations е евристика и може да сгреши в една посока: Fontations билд, оголен от всеки един от тези низове, би бил отхвърлян, макар да би могъл да работи. Този компромис е направен нарочно. Фалшив отказ струва едно exception, което можете да прихванете, и fallback към FreeType. Фалшиво приемане струва процеса
Отпечатването е толкова важно, колкото и проверката. LoadLibrary запечатва конфигурацията в самото начало на зареждането, така че без нулирането отказ за възможност би оставил ConfigurePdfLibrary да отговаря на всеки повторен опит с EPdfError „PDFium library configuration is already sealed“. Пътят на отказа първо вика UnloadLibrary; извикването му FPDF_DestroyLibrary е безопасно в този момент, защото PDFium все още не е инициализиран и връща веднага. Други провали при зареждане — липсваща DLL или несъответствие на архитектура — пазят печата, така че retry цикъл трябва да различава двете:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // с име на unit: Windows.LoadLibrary се казва същото
Result := prpSkia;
except
on E: EPdfError do
begin
// Отказ за възможност разтоварва DLL-а и отпечатва конфигурацията.
// DLL, който изобщо не се е заредил, си остава запечатан: повторението не помага
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Обърнете внимание на изричното PDFium.LoadLibrary. В unit, който ползва и Windows или Winapi.Windows, неквалифицирано LoadLibrary се разрешава към unit-а, стоящ последен в uses клаузата; когато това е Win32 функцията, извикването без параметри не се компилира с грешка за брой аргументи, която не казва нищо за PDFium
Валидация, която става още по-рано
ConfigurePdfLibrary отказва някои комбинации, преди изобщо да се намеси DLL, всички с EPdfError. Изричен FontBackend, включително pfbpFreeType, изисква Renderer = prpSkia, защото PDFium поглежда font backend-а само за Skia рендерера. IsolatePerDocument изисква V8Isolate да е nil, понеже PDFium създава свой isolate на документ и проваля native CHECK, ако му подадете и вие такъв. Празни низове в UserFontPaths се отхвърлят. И всяко извикване след първия опит за зареждане проваля с „PDFium library configuration is already sealed“
Последното правило има практическа последица: не можете първо да надникнете в DLL-а и после да го конфигурирате. GetSkiaRenderCapabilities, V8FeaturesAvailable, отварянето на документ и повечето други входни точки викат LoadLibrary вътрешно, което запечатва конфигурацията на място. И по-късно извикване на UnloadLibrary не я отваря пак. Първо конфигурирайте, после заредете, после питайте — точно този ред трябва да следва и диагностична рутина:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // копие, безопасно за оглед
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;
// Същото разрешаване, което LoadLibrary е приложило, когато е строило 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;
Логването на този ред веднъж при стартиране е евтино, и е първото нещо, което искате в support тикет, казващ „текстът изглежда различно на сървъра“. PDFium.Loaded е квалифицирано по същата причина като LoadLibrary: вътре в метод на форма или компонента голо Loaded се вързва за TComponent.Loaded
Два начина, по които версионирана C конфигурационна структура се чупи
Всяка версионирана конфигурационна структура — дали FPDF_LIBRARY_CONFIG, Win32 запис с cbSize или plugin ABI — проваля по два симетрични начина, и wrapper трябва да се пази и от двата. Първият е попълване на поле при оставена прекалено ниска версия; вторият е вдигане на версия при оставено поле на нулева стойност, която библиотеката чете като съзнателен избор
- Поле зададено, версия прекалено ниска. Запишете
m_BrotliEnabled= 1 в структура от версия 2 и PDFium изобщо не я поглежда. Извикването успява, а Brotli stream-овете остават недекодируеми. Защитата е версията да се извежда от полетата, реално ползвани — което правиLoadLibrary— вместо да се закова една - Версия достатъчно висока, нулевото поле значи нещо. Вдигнете версията на 6 и всяко поле до версия 6 вече е живо.
FillCharнулираm_RendererTypeдоFPDF_RENDERERTYPE_AGG, което е истински рендерер, не „незададено“. Защитата е всеки член, покрит от избраната версия, да получи съзнателна стойност, а „default“ да се разрешава спрямо реалния билд, вместо да се предполага
За стойности, които могат да катастрофират извиквания, следва и трето правило: валидирайте ги спрямо това, което бинарият може, преди извикването, с най-силното налично доказателство, и бъдете честни в кода и документацията, когато това доказателство е евристика. Експортиран символ е доказателство. Име на crate в string таблица е добра догадка
Бърза справка: конфигурация на библиотеката в PDFium Component
- Викайте
ConfigurePdfLibraryведнъж, преди нещо да зареди DLL-а; всяко запитване за възможности или зареждане на документ го запечатва - Обновете до v3.123.0 или по-нов, ако задавате
BrotliEnabledилиIsolatePerDocumentи очаквате Skia изход от вградените runtime-и - Оставете
RendererнаprpDefault, освен ако не ви трябва конкретен растеризатор; той вече се разрешава до default-а на билда при всяка версия на структурата - Ползвайте
PdfNativeRendererTypeсGetSkiaRenderCapabilities.PageRender, за да логвате кой рендерер реално е активен - Очаквайте
EPdfError, а не краш, заprpSkiaвърху AGG-само DLL илиpfbpFontationsвърху DLL без Fontations в v3.125.0 или по-нов - След отказ за възможности
PdfLibraryConfigurationSealedе False и можете да преконфигурирате; след провалено зареждане на DLL той остава True - Третирайте разпознаването на Fontations като евристика и дръжте fallback към FreeType
- Пишете
PDFium.LoadLibraryиPDFium.Loadedс името на unit-а, за да избегнете сблъсъци с имената на Win32 иTComponent
Ако DLL-ът се провали преди конфигурацията изобщо да има значение, започнете с диагностицирането на провалени зареждания на PDFium DLL в Delphi, а за това как компонентът намира правилния бинарен файл на всяка платформа вижте зареждането на PDFium native библиотеката на произволна цел. Щом рендерерът е уреден, render cache и тактиките за плавно zoom покрива как да държите рендерирането на страниците бързо в viewer
PDFium Component обвива PDFium двигателя за Delphi и C++Builder с конфигурационни проверки като тези, така че native инициализацията проваля като Pascal exception, който можете да обработите, а не като изход от процеса. Детайли за продукта и изтеглянията са на страницата на PDFium Component за Delphi