В PDFium Component для Delphi включение BrotliEnabled или IsolatePerDocument в TPdfLibraryConfiguration раньше безо всякой ошибки переключало поставляемую сборку Skia на рендерер AGG, потому что обе опции поднимают FPDF_LIBRARY_CONFIG до версии, где PDFium читает m_RendererType буквально. С v3.123.0 рендерер по умолчанию остаётся собственным дефолтом DLL, а с v3.125.0 запрос Skia или Fontations, которого DLL не может исполнить, поднимает ловимый EPdfError вместо убийства процесса
Ни один из багов себя не объявил. Первый выдавал страницы, выглядевшие нормально, — просто отрендеренные другим растеризатором, с чуть иным анти-алиасингом и краями текста, чем в сборке, которую вы выпускали и тестировали. Второй объявился — громко, укладывая процесс хоста изнутри нативной инициализации. Оба из одного места: версионированная C-структура, чьи поля засчитываются, лишь когда скажет номер версии, и чьи нулевые значения — не «не задано», а настоящий выбор
Как FPDF_LIBRARY_CONFIG решает, какой рендерер использует PDFium?
FPDF_InitLibraryWithConfig советуется с m_RendererType, только когда поле Version структуры — 4 или выше, и с той версии использует значение ровно как записано. Ниже версии 4 PDFium игнорирует поле и берёт дефолт сборки — Skia в сборках, скомпилированных с PDF_USE_SKIA, и AGG во всех прочих
Каждое следующее поле следует тому же образцу. Структура росла по одной возможности за раз, и каждая возможность приходила вместе с новым номером версии. PDFium Component строит нативную структуру в 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. Что бы ни сидело в тех двух полях в тот момент, то и станет рендерером и шрифтовым бэкендом — задумывали вы их выбрать или нет
Почему включение Brotli переключало рендерер на AGG?
До v3.123.0 PDFium Component писал FPDF_RENDERERTYPE_AGG в m_RendererType для prpDefault, так что любая конфигурация, дотолкавшая структуру до версии 6 или 7, навязывала AGG сборке со Skia. Рантаймы pdfium.dll и pdfium.v8.dll, поставляемые с компонентом, — сборки Skia, так что доставалось дефолтному развёртыванию, а не какому-то экзотическому
Отображение выглядело безобидно, когда писалось. На версии 2 или 3 поле не читается никогда, так что prpDefault и вправду значил «как решит DLL». В момент, когда в картину входили BrotliEnabled (версия 6) или IsolatePerDocument (версия 7), тот же код превращал «без предпочтений» в явный запрос AGG. Ничего не падало. PDFium инициализировался нормально, рендерил каждую страницу и не возвращал кода ошибки, потому что с его точки зрения звонящий просил AGG и получал AGG
Пиксельный хэш делает подмену видимой там, где скриншоты не видят. Рендер первой страницы одного и того же образца в трёх конфигурациях дал:
- Конфигурация по умолчанию: хэш
502D77C3711B4ACF BrotliEnabled= True приRenderer, оставленном наprpDefault: хэшF75B5EB4728ADE87- Явный
prpAgg: хэшF75B5EB4728ADE87, идентичен прогону с Brotli
Фикс в v3.123.0 — публичная функция PdfNativeRendererType, разрешающая TPdfRendererPreference в значение, записываемое в m_RendererType. prpAgg и prpSkia отображаются один в один. prpDefault теперь отображается в Skia, когда загруженная DLL экспортирует FPDF_RenderPageSkia, и в AGG в противном случае. Тот экспорт компилируется под тем же условием PDF_USE_SKIA, что и сам дефолт Skia, что делает его единственным свойством сборки, наблюдаемым снаружи DLL. После фикса конфигурация с Brotli даёт тот же хэш, что и дефолтная
Шрифтовой бэкенд той же беды не знал. m_FontLibraryType читается с версии 5, а его нулевое значение, FPDF_FONTBACKENDTYPE_FREETYPE, — заодно и дефолт PDFium, когда поле не читается вовсе. Запись FreeType для pfbpDefault потому воспроизводит нативный дефолт в точности. Нулевые значения не всегда неверны — они просто никогда не бывают верны автоматически
С v3.123.0 и новее стартовый код, который вы и так написали бы, теперь делает то, что говорит:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// Должно бежать раньше, чем что-либо загрузит нативную библиотеку
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 делает потоки /BrotliDecode из PDF 2.0 декодируемыми, лишь когда сама DLL собрана с PDF_ENABLE_BROTLI. Флаг — это просьба, и на сборке без поддержки Brotli он ничего не делает. TPdfLibraryConfiguration.Hardened — то же, что Default, кроме AllowMachineTime = False, что отрезает документному JavaScript доступ к настоящим часам; разумная стартовая точка для серверной обработки недоверенных файлов
Что происходит, когда вы просите бэкенд, которого в DLL нет?
PDFium не возвращает ошибки для отсутствующего в сборке рендерера или шрифтового бэкенда: FPDF_InitLibraryWithConfig проваливает нативный CHECK, что на Windows всплывает исключением-точкой останова и без структурного обработчика исключений вокруг вызова терминирует процесс. Заголовок прямо предупреждает, что неподдерживаемое значение «will similarly fail with an immediate crash»
Два конкретных случая — сборка только с AGG, получающая FPDF_RENDERERTYPE_SKIA, и сборка без Fontations, получающая FPDF_FONTBACKENDTYPE_FONTATIONS. Поставляемый Skia-рантайм из второй группы: рендерит через Skia, но шрифты берёт из FreeType. Запрос prpSkia вместе с pfbpFontations против него давал External exception 80000003 на стороне Delphi. Когда отладчик или обработчик исключений случается это поймать, положение всё равно невосполнимо:
- PDFium остаётся наполовину инициализированным
- Конфигурация процесса уже опечатана, так что
ConfigurePdfLibraryотказывает исправленной конфигурации - Повтор с другой конфигурацией в том же процессе уже невозможен
Это зеркальный отказ по отношению к багу с Brotli. Там поле держало значение, которое никто не выбирал, и PDFium беззвучно его принимал. Здесь поле держит значение, выбранное звонящим нарочно, и PDFium не приемлет о нём никаких обсуждений. Оба — проблемы, которые обёртка обязана решить до нативного вызова, потому что после него ловить уже нечего
Как PDFium Component предварительно проверяет Skia и Fontations
С v3.125.0 LoadLibrary валидирует конфигурацию после привязки экспортов DLL и до вызова FPDF_InitLibraryWithConfig и превращает неподдерживаемый рендерер или шрифтовой бэкенд в EPdfError с сообщением, называющим провинившуюся настройку и альтернативы. DLL выгружается, конфигурация распечатывается, так что звонящий может выбрать другие настройки и загрузиться снова
Само решение живёт в чистой функции PdfLibraryConfigurationSupportError, берущей конфигурацию плюс два Boolean, описывающих сборку, и возвращающей пустую строку, когда комбинация безопасна. Поскольку она не трогает нативное состояние, звать её из собственных тестов можно с любой комбинацией возможностей. Внутри LoadLibrary два Boolean приходят из разных сортов улик, и доверия они заслуживают разного:
- Skia детектируется по наличию экспорта
FPDF_RenderPageSkia— той же улики, которой пользуетсяPdfNativeRendererType. Экспорт и Skia-рендерер компилируются под одним условием, так что проверка точна - Fontations собственного экспорта не имеет. Единственный след, который он оставляет, — Rust-крейты шрифтов, втянутые в бинарник, так что PDFium Component сканирует файл загруженной библиотеки на имена крейтов
skrifaиread-fonts(такжеread_fonts). Сканирование идёт лишь при запросеpfbpFontations, а нечитаемый файл считается «Fontations нет»
Проверка Fontations — эвристика, и ошибиться она может в одну сторону: сборка с Fontations, ободранная от всех этих строк, была бы отвергнута, хотя могла бы сработать. Тот размен сделан нарочно. Ложное отвержение стоит вам ловимого исключения и фоллбэка на FreeType. Ложное принятие стоит процесса
Распечатывание важно не меньше проверки. LoadLibrary опечатывает конфигурацию в самом начале загрузки, так что без сброса отказ в возможности оставлял бы ConfigurePdfLibrary отвечающим на каждый повтор EPdfError «PDFium library configuration is already sealed». Путь отказа сперва зовёт UnloadLibrary; его вызов FPDF_DestroyLibrary в тот момент безопасен, потому что PDFium ещё не инициализирован и возвращается немедленно. Прочие отказы загрузки — отсутствующая DLL, несовпадение архитектуры — печать хранят, так что циклу повторов придётся различать то и другое:
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, вовсе не загрузившаяся, остаётся запечатанной: повтор не поможет
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Заметьте явный PDFium.LoadLibrary. В юните, пользующем также Windows или Winapi.Windows, неквалифицированный LoadLibrary разрешается в тот юнит, что стоит последним в uses; когда это Win32-функция, беспараметровый вызов не компилируется с ошибкой счёта аргументов, не говорящей о PDFium ничего
Валидация, что случается ещё раньше
ConfigurePdfLibrary отвергает некоторые комбинации ещё до всякой DLL — все с EPdfError. Явный FontBackend, включая pfbpFreeType, требует Renderer = prpSkia, потому что шрифтовым бэкендом PDFium советуется только при рендерере Skia. IsolatePerDocument требует nil в V8Isolate, поскольку PDFium сам создаёт изолят на документ и проваливает нативный 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;
Залогировать ту строку раз при старте дёшево, и это первое, что вам понадобится в тикете поддержки со словами «на сервере текст выглядит иначе». PDFium.Loaded квалифицирован по той же причине, что и LoadLibrary: внутри метода формы или компонента голый Loaded привяжется к TComponent.Loaded
Два способа, какими ломается версионированная C-структура конфигурации
Всякая версионированная структура конфигурации — FPDF_LIBRARY_CONFIG, Win32-запись с cbSize или ABI плагина — ломается двумя симметричными способами, и обёртка обязана огораживаться от обоих. Первый — заполнить поле, оставив версию слишком низкой; второй — поднять версию, оставив поле на нулевом значении, которое библиотека прочтёт как нарочитый выбор
- Поле задано, версия занижена. Впишите
m_BrotliEnabled= 1 в структуру версии 2 — PDFium на неё и не взглянет. Вызов успехен, потоки Brotli остаются недекодируемыми. Защита — выводить версию из реально используемых полей, что и делаетLoadLibrary, а не зашивать её константой - Версия достаточна, нулевое поле значит нечто. Поднимите версию до 6 — и всякое поле вплоть до версии 6 теперь живо.
FillCharобнуляетm_RendererTypeвFPDF_RENDERERTYPE_AGG, а это настоящий рендерер, не «не задано». Защита — писать всякое поле, покрываемое выбранной версией, нарочитым значением и разрешать «дефолт» против фактической сборки, а не предполагать его
Третье правило следует для значений, способных уронить вызываемого: валидируйте их против способностей бинарника до вызова, по самым сильным доступным уликам, и будьте честны в коде и документации, когда улика — эвристика. Экспортированный символ — доказательство. Имя крейта в таблице строк — добрая догадка
Шпаргалка: конфигурация библиотеки PDFium Component
- Зовите
ConfigurePdfLibraryоднажды, раньше, чем что-либо загрузит DLL; любой запрос возможностей или загрузка документа запечатывают её - Обновляйтесь до v3.123.0 и новее, если выставляете
BrotliEnabledилиIsolatePerDocumentи ждёте вывода Skia от поставляемых рантаймов - Оставляйте
RendererнаprpDefault, если не нужен конкретный растеризатор; он теперь разрешается в дефолт сборки на всякой версии структуры - Пользуйтесь
PdfNativeRendererTypeсGetSkiaRenderCapabilities.PageRender, чтобы логировать активный рендерер - Ждите
EPdfError, а не краха, заprpSkiaна DLL только с AGG илиpfbpFontationsна DLL без Fontations в v3.125.0 и новее - После отказа в возможности
PdfLibraryConfigurationSealed— False, и можно перенастроить; после сорванной загрузки DLL флаг хранит True - Считайте детекцию Fontations эвристикой и держите фоллбэк на FreeType
- Пишите
PDFium.LoadLibraryиPDFium.Loadedс именем юнита, чтобы избежать коллизий имён с Win32 иTComponent
Если DLL падает раньше, чем конфигурация вообще вступает в игру, начните со статьи о диагностике отказов загрузки PDFium DLL в Delphi, а о том, как компонент находит правильный бинарник на каждой платформе, — загрузка нативной библиотеки PDFium на любой цели. Когда рендерер улажен, тактики рендер-кэша и плавного зума расскажут, как держать рендеринг страниц быстрым во вьюере
PDFium Component оборачивает движок PDFium для Delphi и C++Builder с такими вот проверками конфигурации, чтобы нативная инициализация падала Pascal-исключением, которое можно обработать, а не выходом из процесса. Детали продукта и загрузки — на странице PDFium Component for Delphi product page