Artículo técnico

Config PDFium: cuando Brotli cambia Skia en silencio

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 estructuraCampo que agregaFijado por
2m_pIsolate, m_v8EmbedderSlotSiempre escrito; V8Isolate, V8EmbedderSlot
3m_pPlatformV8Platform distinto de nil
4m_RendererTypeRenderer distinto de prpDefault
5m_FontLibraryTypeFontBackend distinto de pfbpDefault
6m_BrotliEnabledBrotliEnabled = True
7m_IsolatePerDocumentIsolatePerDocument = 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

Escalera de versiones de FPDF_LIBRARY_CONFIG de PDFium Component de la versión 2 a la 7 mostrando qué opción de TPdfLibraryConfiguration agrega m_RendererType, m_FontLibraryType, m_BrotliEnabled y m_IsolatePerDocument, y por qué las versiones acumulativas vuelven un campo de renderer en cero una elección AGG deliberada y no un valor sin fijar en cualquier build
Cada opción sube la versión de la estructura y todo campo anterior queda vivo, así que el cero en m_RendererType llega a PDFium como una petición AGG explícita

¿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 con Renderer en prpDefault: hash F75B5EB4728ADE87
  • prpAgg explícito: hash F75B5EB4728ADE87, 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

Comparación de hash de píxeles de PDFium Component mostrando el hash de render Skia por defecto 502D77C3711B4ACF, la configuración BrotliEnabled previa a v3.123.0 coincidiendo con una corrida prpAgg explícita con hash F75B5EB4728ADE87, y el wrapper corregido resolviendo prpDefault por medio del export FPDF_RenderPageSkia de vuelta al hash Skia original
Un hash de píxeles caza lo que las capturas esconden: activar Brotli renderizaba cada página con AGG, y el default corregido ahora coincide con la configuración intacta

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 ConfigurePdfLibrary rechaza 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 usa PdfNativeRendererType. 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 skrifa y read-fonts (también read_fonts). El escaneo corre solo cuando se pide pfbpFontations, 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

Flujo de prechequeo de LoadLibrary de PDFium Component donde ConfigurePdfLibrary sella la configuración, el chequeo de capacidad prueba el export FPDF_RenderPageSkia y la evidencia de cadenas skrifa, una petición no soportada lanza un EPdfError atrapable y desella para reintentar, mientras que una DLL que jamás carga mantiene PdfLibraryConfigurationSealed en true
La validación corre después de que los exports se atan y antes de la inicialización, así que un backend ausente falla como un EPdfError atrapable en vez de un CHECK nativo que mata el proceso

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

  1. 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 hace LoadLibrary, en vez de fijar una a mano
  2. 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. FillChar pone m_RendererType en cero, o sea FPDF_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 ConfigurePdfLibrary una 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 BrotliEnabled o IsolatePerDocument y espera salida Skia de los runtimes incluidos
  • Deje Renderer en prpDefault salvo que necesite un rasterizador específico; ahora resuelve al default del build en cada versión de la estructura
  • Use PdfNativeRendererType con GetSkiaRenderCapabilities.PageRender para registrar qué renderer está realmente activo
  • Espere EPdfError, no un crash, por prpSkia sobre una DLL solo AGG o pfbpFontations sobre una DLL sin Fontations en v3.125.0 o posterior
  • Tras un rechazo de capacidad, PdfLibraryConfigurationSealed es 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.LoadLibrary y PDFium.Loaded con el nombre de la unidad para evitar choques de nombre con Win32 y TComponent

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