En PDFium Component para Delphi, activar BrotliEnabled o IsolatePerDocument en TPdfLibraryConfiguration solía cambiar la compilación Skia incluida al renderer AGG sin error alguno, porque ambas opciones elevan FPDF_LIBRARY_CONFIG a una versión donde PDFium lee m_RendererType literalmente. Desde v3.123.0 el renderer por defecto se queda en el valor por defecto de la propia DLL, y desde v3.125.0 una petición Skia o Fontations que la DLL no pueda honrar lanza un EPdfError capturable en lugar de matar el proceso
Ninguno de los dos bugs se anunció. El primero producía páginas que se veían bien, solo que renderizadas por otro rasterizador, con un antialiasing y bordes de texto ligeramente distintos a los de la compilación que usted envió y probó. El segundo sí se anunció, y a gritos, tirando el proceso anfitrión desde dentro de la inicialización nativa. Ambos vienen del mismo sitio: una estructura C versionada cuyos campos solo cuentan cuando el número de versión lo dice, y cuyos valores cero no son «sin fijar» sino decisiones reales
¿Cómo decide FPDF_LIBRARY_CONFIG qué renderer usa PDFium?
FPDF_InitLibraryWithConfig consulta m_RendererType solo cuando el campo Version de la estructura es 4 o superior, y desde esa versión usa el valor exactamente como está escrito. Por debajo de la versión 4 PDFium ignora el campo y toma el valor por defecto de la compilación, que es Skia en las compilaciones con PDF_USE_SKIA y AGG en las demás
Cada campo posterior sigue el mismo patrón. La estructura creció una capacidad cada vez, y cada capacidad llegó acompañada de un número de versión nuevo. PDFium Component construye la estructura nativa en LoadLibrary a partir de su TPdfLibraryConfiguration y eleva la versión solo hasta donde exijan las opciones que usted fija
| Versión de estructura | Campo que añade | Fijado por |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Siempre escrito; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform no nil |
| 4 | m_RendererType | Renderer distinto de prpDefault |
| 5 | m_FontLibraryType | FontBackend distinto de pfbpDefault |
| 6 | m_BrotliEnabled | BrotliEnabled = True |
| 7 | m_IsolatePerDocument | IsolatePerDocument = True |
La trampa está en las dos últimas filas. Las versiones son acumulativas: una estructura de versión 6 es también una de versión 4 y de versión 5, así que PDFium lee m_RendererType y m_FontLibraryType aunque usted solo pidiera Brotli. Lo que quede en esos dos campos en ese momento se convierte en el renderer y el backend de fuentes, quisiera usted elegirlos o no
¿Por qué activar Brotli cambiaba el renderer a AGG?
Antes de v3.123.0, PDFium Component escribía FPDF_RENDERERTYPE_AGG en m_RendererType para prpDefault, así que cualquier configuración que empujara la estructura a versión 6 o 7 forzaba AGG en una compilación Skia. Los runtimes pdfium.dll y pdfium.v8.dll que vienen con el componente son compilaciones Skia, así que esto golpeaba al despliegue por defecto, no a uno exótico
El mapeo parecía inofensivo cuando se escribió. En versión 2 o 3 el campo nunca se lee, así que prpDefault de verdad significaba «lo que haga la DLL». En el momento en que BrotliEnabled (versión 6) o IsolatePerDocument (versión 7) entraron en escena, el mismo código convirtió «sin preferencia» en una petición AGG explícita. Nada falló. PDFium se inicializó con normalidad, renderizó cada página, y no devolvió código de error alguno, porque desde su punto de vista el llamador había pedido AGG y había recibido AGG
Un hash de píxeles hace visible el cambio donde las capturas de pantalla no. Renderizar la primera página del mismo documento de muestra bajo tres configuraciones dio:
- Configuración por defecto: hash
502D77C3711B4ACF BrotliEnabled= True conRendererenprpDefault: hashF75B5EB4728ADE87prpAggexplícito: hashF75B5EB4728ADE87, idéntico a la ejecución con Brotli
El arreglo de v3.123.0 es la función pública PdfNativeRendererType, que resuelve un TPdfRendererPreference al valor escrito en m_RendererType. prpAgg y prpSkia se mapean uno a uno. prpDefault ahora mapea a Skia cuando la DLL cargada exporta FPDF_RenderPageSkia y a AGG en caso contrario. Esa exportación se compila bajo la misma condición PDF_USE_SKIA que el valor por defecto Skia en sí, lo que la convierte en la única propiedad de compilación observable desde fuera de la DLL. Tras el arreglo la configuración con Brotli produce el mismo hash que la por defecto
El backend de fuentes nunca tuvo el mismo problema. m_FontLibraryType se lee desde la versión 5, y su valor cero, FPDF_FONTBACKENDTYPE_FREETYPE, es también el valor por defecto de PDFium cuando el campo no se lee en absoluto. Escribir FreeType para pfbpDefault reproduce por tanto el valor por defecto nativo exactamente. Los valores cero no siempre están mal, simplemente nunca están automáticamente bien
Con v3.123.0 o posterior, el código de arranque que usted escribiría con naturalidad hace ahora lo que dice:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// Debe correr antes de que algo cargue la biblioteca nativa
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // eleva FPDF_LIBRARY_CONFIG a versión 6
// Renderer se queda en prpDefault: resuelve a Skia en compilaciones que exportan
// FPDF_RenderPageSkia y a AGG en compilaciones solo AGG
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
Recuerde que BrotliEnabled solo hace decodificables los streams /BrotliDecode de PDF 2.0 cuando la propia DLL se compiló con PDF_ENABLE_BROTLI. El flag es una petición, y en una compilación sin soporte Brotli no tiene efecto. TPdfLibraryConfiguration.Hardened es igual que Default salvo que AllowMachineTime es False, lo que bloquea que el JavaScript del documento lea el reloj real; es un punto de partida razonable para el procesado del lado servidor de archivos no confiables
¿Qué pasa si pide un backend que la DLL no contiene?
PDFium no devuelve error por un renderer o backend de fuentes ausente de la compilación: FPDF_InitLibraryWithConfig hace fallar un CHECK nativo, que en Windows aflora como una excepción de breakpoint y, sin un manejador de excepciones estructuradas alrededor de la llamada, termina el proceso. La cabecera lo dice textualmente, avisando de que un valor no soportado «fallará igualmente con un crash inmediato»
Los dos casos concretos son una compilación solo AGG que recibe FPDF_RENDERERTYPE_SKIA, y una compilación sin Fontations que recibe FPDF_FONTBACKENDTYPE_FONTATIONS. El runtime Skia incluido está en el segundo grupo: renderiza con Skia pero usa FreeType para las fuentes. Pedir prpSkia junto con pfbpFontations contra él producía External exception 80000003 por el lado Delphi. Cuando el debugger o un manejador de excepciones casualmente la atrapaba, la situación seguía siendo irrecuperable:
- PDFium queda a medio inicializar
- La configuración de todo el proceso ya está sellada, así que
ConfigurePdfLibraryrechaza una configuración corregida - Reintentar con otra configuración en el mismo proceso ya no es posible
Es el fallo contrario al bug de Brotli. Allí el campo guardaba un valor que nadie escogió y PDFium lo aceptaba en silencio. Aquí el campo guarda un valor que el llamador escogió deliberadamente y PDFium no admite discusión alguna al respecto. Ambos son problemas que un wrapper tiene que resolver antes de la llamada nativa, porque después ya no queda nada que atrapar
Cómo precomprueba PDFium Component Skia y Fontations
Desde v3.125.0, LoadLibrary valida la configuración tras vincular las exportaciones de la DLL y antes de llamar a FPDF_InitLibraryWithConfig, y convierte un renderer o backend de fuentes no soportado en un EPdfError con un mensaje que nombra el ajuste ofensor y las alternativas. La DLL se descarga y la configuración se desella, así que el llamador puede escoger otros ajustes y cargar de nuevo
La decisión en sí vive en la función pura PdfLibraryConfigurationSupportError, que toma la configuración más dos booleanos que describen la compilación y devuelve una cadena vacía cuando la combinación es segura. Como no toca estado nativo, puede llamarla desde sus propias pruebas con cualquier combinación de capacidades. Dentro de LoadLibrary los dos booleanos vienen de tipos de evidencia distintos, y merecen niveles de confianza distintos:
- Skia se detecta por la presencia de la exportación
FPDF_RenderPageSkia, la misma señal que usaPdfNativeRendererType. La exportación y el renderer Skia se compilan bajo una sola condición, así que la comprobación es exacta - Fontations no tiene exportación propia. La única huella que deja son los crates de fuentes Rust que arrastra al binario, así que PDFium Component escanea el archivo de la biblioteca cargada en busca de los nombres de crate
skrifayread-fonts(tambiénread_fonts). El escaneo corre solo cuando se pidepfbpFontations, y un archivo que no pueda leerse cuenta como «sin Fontations»
La comprobación de Fontations es una heurística, y puede equivocarse en una dirección: una compilación Fontations despojada de todas esas cadenas sería rechazada aunque podría haber funcionado. Ese compromiso se hizo a propósito. Un rechazo falso le cuesta una excepción que puede atrapar y un fallback a FreeType. Una aceptación falsa le cuesta el proceso
Desellar importa tanto como comprobar. LoadLibrary sella la configuración al mismísimo principio de la carga, así que sin el reset un rechazo de capacidad dejaría a ConfigurePdfLibrary respondiendo cada reintento con EPdfError «la configuración de la biblioteca PDFium ya está sellada». El camino de rechazo llama primero a UnloadLibrary; su llamada a FPDF_DestroyLibrary es segura en ese punto porque PDFium todavía no se ha inicializado y devuelve inmediatamente. Otros fallos de carga, como una DLL ausente o un desajuste de arquitectura, conservan el sello, así que un bucle de reintentos tiene que distinguir ambos casos:
uses
SysUtils, PDFium;
function StartPdfiumPreferringSkia: TPdfRendererPreference;
var
Config: TPdfLibraryConfiguration;
begin
Config := TPdfLibraryConfiguration.Default;
Config.Renderer := prpSkia;
ConfigurePdfLibrary(Config);
try
PDFium.LoadLibrary; // calificado por unidad: Windows.LoadLibrary tiene el mismo nombre
Result := prpSkia;
except
on E: EPdfError do
begin
// Un rechazo de capacidad descarga la DLL y desella la configuración.
// Una DLL que no llegó a cargar sigue sellada: reintentar no ayuda
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Fíjese en el PDFium.LoadLibrary explícito. En una unidad que también use Windows o Winapi.Windows, un LoadLibrary sin calificar resuelve a la unidad que aparezca la última en la cláusula uses; cuando esa es la función Win32, la llamada sin parámetros no compila con un error de recuento de argumentos que no dice nada de PDFium
Validación que ocurre incluso antes
ConfigurePdfLibrary rechaza algunas combinaciones antes de que entre en juego cualquier DLL, todas con EPdfError. Un FontBackend explícito, incluido pfbpFreeType, exige Renderer = prpSkia, porque PDFium solo consulta el backend de fuentes para el renderer Skia. IsolatePerDocument exige que V8Isolate sea nil, ya que PDFium crea su propio isolate por documento y hace fallar un CHECK nativo si usted además le entrega uno. Las cadenas vacías en UserFontPaths se rechazan. Y cualquier llamada tras el primer intento de carga falla con «la configuración de la biblioteca PDFium ya está sellada»
Esa última regla tiene una consecuencia práctica: no puede sondear primero la DLL y configurarla después. GetSkiaRenderCapabilities, V8FeaturesAvailable, abrir un documento y casi todos los demás puntos de entrada llaman a LoadLibrary internamente, lo que sella la configuración en el acto. Llamar después a UnloadLibrary tampoco la reabre. Configure primero, luego cargue, luego pregunte, que es exactamente el orden que debe seguir una rutina de diagnóstico:
uses
SysUtils, PDFium, FPdfView;
function DescribePdfiumState: string;
var
Config: TPdfLibraryConfiguration;
Renderer: string;
begin
Config := GetPdfLibraryConfiguration; // una copia, segura de inspeccionar
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;
// La misma resolución que LoadLibrary aplicó al construir 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;
Registrar esa línea una vez al arrancar es barato, y es lo primero que usted quiere en un ticket de soporte que diga «el texto se ve distinto en el servidor». PDFium.Loaded va calificado por la misma razón que LoadLibrary: dentro de un método de un formulario o componente, un Loaded desnudo se vincula a TComponent.Loaded
Dos maneras de que una estructura de configuración C versionada salga mal
Toda estructura de configuración versionada, sea FPDF_LIBRARY_CONFIG, un registro Win32 con cbSize o un ABI de plugin, falla de dos maneras simétricas, y un wrapper tiene que cubrirse contra ambas. La primera es rellenar un campo dejando la versión demasiado baja; la segunda es elevar la versión dejando un campo en un valor cero que la biblioteca lee como una decisión deliberada
- Campo fijado, versión demasiado baja. Escriba
m_BrotliEnabled= 1 en una estructura de versión 2 y PDFium jamás lo mira. La llamada tiene éxito y los streams Brotli siguen indecodificables. La defensa es deducir la versión de los campos realmente en uso, que es lo que haceLoadLibrary, en lugar de empotrar una - Versión suficiente, cero significa algo. Eleve la versión a 6 y todo campo hasta la versión 6 queda vivo.
FillCharpone a cerom_RendererTypeenFPDF_RENDERERTYPE_AGG, que es un renderer real, no «sin fijar». La defensa es escribir cada campo que cubre la versión escogida con un valor intencionado, y resolver «por defecto» contra la compilación real en lugar de asumirlo
Una tercera regla se sigue para los valores que pueden tumbar al que recibe la llamada: valídelos contra lo que el binario sabe hacer antes de la llamada, usando la evidencia más fuerte disponible, y sea honesto en el código y la documentación cuando esa evidencia sea una heurística. Un símbolo exportado es prueba. Un nombre de crate en una tabla de cadenas es una buena conjetura
Referencia rápida: configuración de biblioteca de PDFium Component
- Llame a
ConfigurePdfLibraryuna vez, antes de que algo cargue la DLL; cualquier consulta de capacidades o carga de documento la sella - Actualice a v3.123.0 o posterior si fija
BrotliEnabledoIsolatePerDocumenty espera salida Skia de los runtimes incluidos - Deje
RendererenprpDefaultsalvo que necesite un rasterizador concreto; ahora resuelve al valor por defecto de la compilación en cada versión de estructura - Use
PdfNativeRendererTypeconGetSkiaRenderCapabilities.PageRenderpara registrar qué renderer está realmente activo - Espere
EPdfError, no un crash, paraprpSkiaen una DLL solo AGG opfbpFontationsen una DLL sin Fontations en v3.125.0 o posterior - Tras un rechazo de capacidad,
PdfLibraryConfigurationSealedes False y puede reconfigurar; tras una carga de DLL fallida se queda True - Trate la detección de Fontations como heurística y conserve un fallback a FreeType
- Escriba
PDFium.LoadLibraryyPDFium.Loadedcon el nombre de unidad para evitar choques de nombres con Win32 yTComponent
Si la DLL falla antes de que la configuración importe siquiera, empiece por diagnosticar fallos de carga de la DLL de PDFium en Delphi, y para cómo encuentra el componente el binario correcto en cada plataforma vea cargar la biblioteca nativa de PDFium en cualquier objetivo. Una vez asentado el renderer, caché de render y tácticas de zoom suave cubre cómo mantener rápido el renderizado de páginas en un visor
PDFium Component envuelve el motor PDFium para Delphi y C++Builder con comprobaciones de configuración como estas, así que la inicialización nativa falla como una excepción Pascal que puede manejar en lugar de una salida del proceso. Detalles del producto y descargas están en la página de producto de PDFium Component para Delphi