En PDFium Component para Delphi, activar BrotliEnabled o IsolatePerDocument en TPdfLibraryConfiguration solía cambiar el build Skia incluido al renderer AGG sin error alguno, porque ambas opciones suben FPDF_LIBRARY_CONFIG a una versión donde PDFium lee m_RendererType al pie de la letra. Desde v3.123.0 el renderer por defecto queda como el default de la propia DLL, y desde v3.125.0 una petición de Skia o Fontations que la DLL no puede honrar lanza un EPdfError atrapable en vez 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 un rasterizador distinto, con anti-aliasing y bordes de texto levemente diferentes a los del build que usted envió y probó. El segundo sí se anunció, y a los gritos, tirando abajo el proceso anfitrión desde dentro de la inicialización nativa. Ambos salen del mismo lugar: 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 elecciones 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 en adelante usa el valor tal cual está escrito. Por debajo de la versión 4 PDFium ignora el campo y escoge el default del build, que es Skia en los builds compilados con PDF_USE_SKIA y AGG en el resto
Cada campo posterior sigue el mismo patrón. La estructura creció de a una capacidad por vez, y cada capacidad llegó junto con un número de versión nuevo. PDFium Component construye la estructura nativa en LoadLibrary a partir de su TPdfLibraryConfiguration y sube la versión solo hasta donde las opciones que usted fija lo requieren
| Versión de la estructura | Campo que agrega | Fijado por |
|---|---|---|
| 2 | m_pIsolate, m_v8EmbedderSlot | Siempre escrito; V8Isolate, V8EmbedderSlot |
| 3 | m_pPlatform | V8Platform distinto de 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 últimas dos 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 haya pedido Brotli. Lo que sea que esté en esos dos campos en ese momento se vuelve el renderer y el font backend, 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 sobre un build Skia. Los runtimes pdfium.dll y pdfium.v8.dll que vienen con el componente son builds 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 jamás 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ó normalmente, renderizó cada página, y no devolvió ningún código de error, porque desde su punto de vista quien llamó había pedido AGG y recibió 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 corrida con Brotli
La corrección de v3.123.0 es la función pública PdfNativeRendererType, que resuelve un TPdfRendererPreference al valor escrito en m_RendererType. prpAgg y prpSkia mapean uno a uno. prpDefault ahora mapea a Skia cuando la DLL cargada exporta FPDF_RenderPageSkia y a AGG en caso contrario. Ese export se compila bajo la misma condición PDF_USE_SKIA que el propio default Skia, lo que lo convierte en la única propiedad del build observable desde fuera de la DLL. Después de la corrección la configuración con Brotli produce el mismo hash que la por defecto
El font backend nunca tuvo el mismo problema. m_FontLibraryType se lee desde la versión 5 en adelante, y su valor cero, FPDF_FONTBACKENDTYPE_FREETYPE, también es el default de PDFium cuando el campo no se lee para nada. Escribir FreeType para pfbpDefault reproduce por lo tanto exactamente el default nativo. Los valores cero no siempre están mal, solo que nunca están automáticamente bien
Con v3.123.0 o posterior, el código de arranque que usted escribiría con naturalidad ahora hace lo que dice:
uses
PDFium;
procedure ConfigurePdfiumAtStartup;
var
Config: TPdfLibraryConfiguration;
begin
// Debe correr antes de que algo cargue la librería nativa
Config := TPdfLibraryConfiguration.Default;
Config.BrotliEnabled := True; // sube FPDF_LIBRARY_CONFIG a versión 6
// Renderer queda prpDefault: resuelve a Skia en builds que exportan
// FPDF_RenderPageSkia y a AGG en builds solo AGG
SetLength(Config.UserFontPaths, 1);
Config.UserFontPaths[0] := 'C:\ProgramData\MyApp\Fonts';
ConfigurePdfLibrary(Config);
end;
Recuerde que BrotliEnabled solo vuelve decodificables los streams /BrotliDecode de PDF 2.0 cuando la propia DLL se compiló con PDF_ENABLE_BROTLI. La bandera es una petición, y en un build sin soporte Brotli no tiene efecto. TPdfLibraryConfiguration.Hardened es igual que Default salvo que AllowMachineTime es False, lo que le bloquea al JavaScript del documento leer el reloj real; es un punto de partida razonable para procesamiento del lado servidor de archivos no confiables
¿Qué pasa cuando pide un backend que la DLL no contiene?
PDFium no devuelve error por un renderer o font backend ausente del build: FPDF_InitLibraryWithConfig hace fallar un CHECK nativo, que en Windows asoma 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, advirtiendo que un valor no soportado "fallará igualmente con un crash inmediato"
Los dos casos concretos son un build solo AGG que recibe FPDF_RENDERERTYPE_SKIA, y un build 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 del lado Delphi. Cuando el depurador o un manejador de excepciones casualmente lo atrapa, la situación sigue siendo irrecuperable:
- PDFium queda medio inicializado
- 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
Este es el fallo opuesto 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 quien llamó escogió deliberadamente y PDFium no acepta 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 PDFium Component prechequea Skia y Fontations
Desde v3.125.0, LoadLibrary valida la configuración después de atar los exports de la DLL y antes de llamar a FPDF_InitLibraryWithConfig, y convierte un renderer o font backend 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 quien llamó 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 Boolean que describen el build y devuelve una cadena vacía cuando la combinación es segura. Como no toca estado nativo, usted puede llamarla desde sus propias pruebas con cualquier combinación de capacidades. Dentro de LoadLibrary los dos Boolean vienen de clases de evidencia distintas, y merecen niveles de confianza distintos:
- Skia se detecta por la presencia del export
FPDF_RenderPageSkia, la misma señal que usaPdfNativeRendererType. El export y el renderer Skia se compilan bajo una sola condición, así que el chequeo es exacto - Fontations no tiene export propio. La única huella que deja son las crates de fuentes Rust que arrastra al binario, así que PDFium Component escanea el archivo de la librería cargada buscando los nombres de crates
skrifayread-fonts(tambiénread_fonts). El escaneo corre solo cuando se pidepfbpFontations, y un archivo que no se pueda leer cuenta como "sin Fontations"
El chequeo de Fontations es una heurística, y puede equivocarse en una dirección: un build Fontations despojado de todas esas cadenas sería rechazado aunque habría funcionado. Esa contrapartida se tomó a propósito. Un rechazo falso le cuesta una excepción atrapable y un fallback a FreeType. Una aceptación falsa le cuesta el proceso
Desellar importa tanto como el chequeo. LoadLibrary sella la configuración al comienzo mismo de la carga, así que sin el reinicio un rechazo de capacidad dejaría a ConfigurePdfLibrary respondiendo cada reintento con EPdfError "la configuración de la librería PDFium ya está sellada". La ruta de rechazo llama primero a UnloadLibrary; su llamada a FPDF_DestroyLibrary es segura en ese punto porque PDFium aún no se ha inicializado y retorna de inmediato. Los demás fallos de carga, como una DLL ausente o un mismatch de arquitectura, conservan el sello, así que un loop de reintentos tiene que distinguirlos:
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 cargó para nada queda sellada: reintentar no ayuda
if PdfLibraryConfigurationSealed then
raise;
Config.Renderer := prpAgg;
ConfigurePdfLibrary(Config);
PDFium.LoadLibrary;
Result := prpAgg;
end;
end;
end;
Note el PDFium.LoadLibrary explícito. En una unidad que también usa Windows o Winapi.Windows, un LoadLibrary sin calificar resuelve a la unidad que aparezca última en la cláusula uses; cuando esa es la función Win32, la llamada sin parámetros no compila con un error de conteo de argumentos que no dice nada sobre PDFium
Validación que ocurre aún antes
ConfigurePdfLibrary rechaza algunas combinaciones antes de que cualquier DLL entre en juego, todas con EPdfError. Un FontBackend explícito, incluido pfbpFreeType, exige Renderer = prpSkia, porque PDFium solo consulta el font backend 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 después del primer intento de carga falla con "la configuración de la librería PDFium ya está sellada"
Esa última regla tiene una consecuencia práctica: no puede sondear la DLL primero y configurarla después. GetSkiaRenderCapabilities, V8FeaturesAvailable, abrir un documento, y la mayoría de los demás puntos de entrada llaman a LoadLibrary internamente, lo que sella la configuración en el acto. Llamar a UnloadLibrary después tampoco la reabre. Configure primero, luego cargue, y luego haga preguntas, que es exactamente el orden que una rutina de diagnóstico debe seguir:
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 arranque es barato, y es lo primero que usted quiere en un ticket de soporte que dice "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 suelto se ata a TComponent.Loaded
Dos maneras en que una struct de configuración C versionada sale mal
Toda estructura de configuración versionada, sea FPDF_LIBRARY_CONFIG, un record Win32 con cbSize, o un ABI de plugin, falla de dos maneras simétricas, y un wrapper tiene que protegerse contra ambas. La primera es llenar un campo dejando la versión demasiado baja; la segunda es subir la versión dejando un campo en un valor cero que la librería lee como una elecció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 quedan indecodificables. La defensa es derivar la versión de los campos realmente en uso, que es lo que haceLoadLibrary, en vez de fijar una a mano - Versión lo bastante alta, un campo en cero significa algo. Suba la versión a 6 y todo campo hasta la versión 6 queda vivo.
FillCharponem_RendererTypeen cero, o seaFPDF_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 intencional, y resolver el "default" contra el build real en vez de asumirlo
Una tercera regla se sigue para los valores que pueden hacer crashear a quien recibe la llamada: valídelos contra lo que el binario puede 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 librería de PDFium Component
- Llame a
ConfigurePdfLibraryuna vez, antes de que algo cargue la DLL; cualquier consulta de capacidad 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 específico; ahora resuelve al default del build en cada versión de la estructura - Use
PdfNativeRendererTypeconGetSkiaRenderCapabilities.PageRenderpara registrar qué renderer está realmente activo - Espere
EPdfError, no un crash, porprpSkiasobre una DLL solo AGG opfbpFontationssobre 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 queda en True - Trate la detección de Fontations como heurística y conserve un fallback a FreeType
- Escriba
PDFium.LoadLibraryyPDFium.Loadedcon el nombre de la unidad para evitar choques de nombre con Win32 yTComponent
Si la DLL falla antes de que la configuración siquiera importe, empiece por diagnosticar fallos de carga de la DLL PDFium en Delphi, y para cómo el componente encuentra el binario correcto en cada plataforma vea cargar la librería nativa PDFium en cualquier objetivo. Una vez resuelto el renderer, tácticas de caché de render y zoom suave cubre cómo mantener rápido el renderizado de páginas en un visor
PDFium Component envuelve el engine PDFium para Delphi y C++Builder con chequeos de configuración como estos, así que la inicialización nativa falla como una excepción Pascal que usted puede manejar y no como la salida del proceso. Detalles del producto y descargas están en la página de producto de PDFium Component para Delphi