У PDFium Component для Delphi увімкнення BrotliEnabled чи IsolatePerDocument у TPdfLibraryConfiguration колись безшумно перемикало bundled Skia build на AGG renderer, бо обидві опції підіймають FPDF_LIBRARY_CONFIG до версії, де PDFium читає m_RendererType дослівно. Від v3.123.0 усталений renderer лишається власним усталеним DLL, а від v3.125.0 запит Skia чи Fontations, який DLL не може вшанувати, підіймає catchable EPdfError замість убивства процесу
Жоден із двох bug-ів не анонсував себе. Перший продукував сторінки, що виглядали гаразд — просто відрендерені іншим растеризатором, з трохи іншим anti-aliasing та краями тексту, ніж у збірці, яку ви випустили й тестували. Другий анонсував себе, і гучно — поклавши host-процес ізсередини native ініціалізації. Обидва походять з одного місця: версіонована C-структура, чиї поля рахуються лише коли номер версії каже, що рахуються, і чиї нульові значення — не «не задано», а справжній вибір
Як FPDF_LIBRARY_CONFIG вирішує, який renderer уживає PDFium?
FPDF_InitLibraryWithConfig консультується з m_RendererType лише коли поле Version структури — 4 чи вище, і від тієї версії вживає значення дослівно, як написано. Нижче версії 4 PDFium ігнорує поле і бере build-усталену, яка — 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. Те, що сидить у тих двох полях у той момент, стає renderer-ом і font backend-ом — задумали ви той вибір чи ні
Чому увімкнення Brotli перемикало renderer на AGG?
До v3.123.0 PDFium Component писав FPDF_RENDERERTYPE_AGG у m_RendererType для prpDefault, тож будь-яка конфігурація, що підіймала структуру до версії 6 чи 7, нав’язувала AGG на Skia build. Runtime pdfium.dll і pdfium.v8.dll, що йдуть із компонентом, — Skia збірки, тож це било по усталеному deployment, а не по якійсь екзотиці
Маплення виглядало нешкідливим, коли його писали. На версії 2 чи 3 поле ніколи не читається, тож prpDefault справді означав «як зробить DLL». Той момент, коли BrotliEnabled (версія 6) чи IsolatePerDocument (версія 7) входили в гру, той самий код перетворював «без переваги» на явний запит AGG. Нічого не провалилося. PDFium ініціалізувався нормально, відрендерив кожну сторінку і повернув жодного error-коду, бо з його погляду caller попросив AGG і отримав AGG
Pixel hash робить підміну видимою там, де скріншоти не бачать. Рендеринг першої сторінки того самого зразкового документа під трьома конфігураціями дав:
- Усталена конфігурація: hash
502D77C3711B4ACF BrotliEnabled= True зRendererнаprpDefault: hashF75B5EB4728ADE87- Явний
prpAgg: hashF75B5EB4728ADE87, ідентично прогону Brotli
Fix у v3.123.0 — публічна функція PdfNativeRendererType, яка резолвить TPdfRendererPreference у значення, що пишеться в m_RendererType. prpAgg і prpSkia мапляться один до одного. prpDefault тепер мапиться на Skia, коли завантажений DLL експортує FPDF_RenderPageSkia, і на AGG інакше. Той export компілюється під тим самим PDF_USE_SKIA показником, що й сама Skia-усталена, що робить його єдиною властивістю build-а, яку можна спостерегти ззовні DLL. Після fix конфігурація Brotli продукує той самий hash, що й усталена
Font backend ніколи не мав тієї проблеми. m_FontLibraryType читається від версії 5, і його нульове значення, FPDF_FONTBACKENDTYPE_FREETYPE, — це також усталена PDFium, коли поле взагалі не читається. Запис FreeType для pfbpDefault відтворює native усталену дослівно. Нульові значення не завжди неправильні — вони просто ніколи не правильні автоматично
З v3.123.0 чи новішою стартовий код, який ви й так написали б природно, тепер робить те, що каже:
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 /BrotliDecode декодовними лише коли сам DLL був збудований з PDF_ENABLE_BROTLI. Прапорець — це запит, і на збірці без підтримки Brotli він не має жодного ефекту. TPdfLibraryConfiguration.Hardened — те саме, що Default, за винятком AllowMachineTime = False, що блокує документному JavaScript читання справжнього годинника; це розумна стартова точка для server-side опрацювання недовірених файлів
Що стається, коли ви просите backend, якого DLL не містить?
PDFium не повертає помилку за renderer чи font backend, відсутній у збірці: FPDF_InitLibraryWithConfig провалює native CHECK, який на Windows виходить на поверхню як breakpoint exception і, без structured exception handler навколо виклику, завершує процес. Заголовок каже саме це, попереджаючи, що непідтримуване значення «так само провалиться з негайним crash»
Два конкретні випадки — AGG-only збірка, що отримує FPDF_RENDERERTYPE_SKIA, і збірка без Fontations, що отримує FPDF_FONTBACKENDTYPE_FONTATIONS. Bundled Skia runtime — у другій групі: він рендерить Skia-ом, але уживає FreeType для шрифтів. Запит prpSkia разом із pfbpFontations проти нього продукував External exception 80000003 на Delphi боці. Коли debugger чи exception handler трапом його ловить, ситуація все одно невиправна:
- PDFium лишається напівініціалізованим
- Загальнопроцесна конфігурація вже запечатана, тож
ConfigurePdfLibraryвідмовляє виправленій конфігурації - Повторна спроба з іншою конфігурацією в тому самому процесі більше неможлива
Це протилежний провал до Brotli bug-а. Там поле тримало значення, якого ніхто не обирав, і PDFium тихо його приймав. Тут поле тримає значення, яке caller обрав намірено, і PDFium не приймає жодного обговорення. Обидва — проблеми, які wrapper мусить розв’язати до native виклику, бо після нього ловити вже нічого
Як PDFium Component пре-перевіряє Skia і Fontations
Від v3.125.0 LoadLibrary валідує конфігурацію після біндінгу export-ів DLL і перед викликом FPDF_InitLibraryWithConfig і перетворює непідтримуваний renderer чи font backend на EPdfError з повідомленням, що називає провинне налаштування та альтернативи. DLL вивантажується, конфігурація розпечатується, тож caller може обрати інші налаштування і завантажити знову
Саме рішення живе в чистій функції PdfLibraryConfigurationSupportError, яка приймає конфігурацію плюс два Boolean, що описують збірку, і повертає порожній рядок, коли комбінація безпечна. Оскільки вона не торкається жодного native стану, можна викликати її з власних тестів з будь-якою комбінацією можливостей. Усередині LoadLibrary два Boolean походять з різних видів свідчень, і вони заслуговують різних рівнів довіри:
- Skia детектиться з наявності export
FPDF_RenderPageSkia— того самого сигналу, що вживаєPdfNativeRendererType. Export і Skia renderer компілюються під однією умовою, тож перевірка точна - Fontations не має власного export. Єдиний слід, який він лишає, — Rust font crates, що він їх тягне в бінарник, тож PDFium Component сканує завантажений файл бібліотеки за іменами crate
skrifaіread-fonts(такожread_fonts). Скан працює лише коли запитаноpfbpFontations, а файл, який неможливо прочитати, рахується «без Fontations»
Перевірка Fontations — евристика, і помилитися вона може в один бік: Fontations-збірка, позбавлена кожного з тих рядків, була б відхилена, хоч могла б працювати. Той трейд зроблено намірено. Хибна відмова коштує вам exception, який можна зловити, і fallback на FreeType. Хибне прийняття коштує вам процес
Розпечатування важить не менше за перевірку. LoadLibrary запечатує конфігурацію на самому початку завантаження, тож без reset відмова можливості лишила б ConfigurePdfLibrary відповідати на кожен retry 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; // з юнітом: Windows.LoadLibrary має те саме ім’я
Result := prpSkia;
except
on E: EPdfError do
begin
// Відмова можливості вивантажує DLL і розпечатує конфігурацію.
// DLL, що взагалі не завантажилася, лишається запечатаною: retry не допоможе
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Зауважте явне PDFium.LoadLibrary. В юніті, що також уживає Windows чи Winapi.Windows, некваліфіковане LoadLibrary резолвиться в той юніт, який стоїть останнім у uses; коли це Win32 функція, виклик без параметрів не компілюється з error-ом про кількість аргументів, який не каже нічого про PDFium
Валідація, яка стається ще раніше
ConfigurePdfLibrary відмовляє деякі комбінації ще до того, як якась DLL взагалі з’явиться, — усі з EPdfError. Явний FontBackend, включно з pfbpFreeType, вимагає Renderer = prpSkia, бо PDFium консультується з font backend-ом лише для Skia renderer-а. IsolatePerDocument вимагає nil у V8Isolate, бо 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 потоки лишаються недекодовними. Захист — виводити версію з полів, які реально вжито, що й робитьLoadLibrary, замість хардкоду однієї - Версія достатня, нульове поле щось означає. Підніміть версію до 6 — і кожне поле аж до версії 6 тепер живе.
FillCharзануляєm_RendererTypeуFPDF_RENDERERTYPE_AGG, а це справжній renderer, не «не задано». Захист — писати кожне поле, яке покриває обрана версія, умисним значенням, і резолвити «усталене» проти реальної збірки замість припускання
Третє правило випливає для значень, що можуть упасти callee: валідуйте їх проти того, що бінарник уміє, до виклику, найсильнішими доступними свідченнями, і будьте чесні в коді та документації, коли те свідчення — евристика. Експортований символ — доказ. Ім’я crate у стрінговій таблиці — добра здогадка
Швидка довідка: конфігурація бібліотеки PDFium Component
- Викличте
ConfigurePdfLibraryраз, до того, як що-небудь завантажить DLL; будь-який capability-запит чи завантаження документа її запечатують - Оновіться до v3.123.0 чи новішої, якщо ставите
BrotliEnabledчиIsolatePerDocumentі очікуєте Skia виводу від bundled runtime-ів - Лишайте
RendererнаprpDefault, якщо не потрібен конкретний растеризатор; тепер він резолвиться в build-усталену на кожній версії структури - Уживайте
PdfNativeRendererTypeзGetSkiaRenderCapabilities.PageRender, щоб залогувати, який renderer реально активний - Очікуйте
EPdfError, а не crash, заprpSkiaна AGG-only DLL чиpfbpFontationsна не-Fontations DLL у v3.125.0 чи новішій - Після відмови можливості
PdfLibraryConfigurationSealed— False, і можна переконфігурувати; після проваленого завантаження DLL вона лишається True - Трактуйте детект Fontations як евристику і тримайте fallback на FreeType
- Пишіть
PDFium.LoadLibraryіPDFium.Loadedз ім’ям юніта, щоб уникнути колізій імен з Win32 таTComponent
Якщо DLL провалюється до того, як конфігурація взагалі стає важливою, почніть із діагностики провалів завантаження PDFium DLL у Delphi, а про те, як компонент знаходить правильний бінарник на кожній платформі, — завантаження PDFium native бібліотеки на будь-якій цілі. Щойно renderer вирішено, render cache і тактики плавного зуму покриває, як тримати рендеринг сторінок швидким у viewer
PDFium Component обгортає PDFium engine для Delphi та C++Builder з перевірками конфігурації, як ці, тож native ініціалізація провалюється як Pascal exception, який можна опрацювати, а не як вихід процесу. Деталі продукту та завантаження — на сторінці продукту PDFium Component for Delphi