Artículo técnico

Config de biblioteca PDFium: Brotli cambia Skia por AGG

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

Escalera de versiones FPDF_LIBRARY_CONFIG de PDFium Component de la versión 2 a la 7 mostrando qué opción de TPdfLibraryConfiguration añade m_RendererType, m_FontLibraryType, m_BrotliEnabled y m_IsolatePerDocument, y por qué las versiones acumulativas convierten un campo renderer a cero en una decisión AGG deliberada en vez de un valor sin fijar en cualquier compilación
Cada opción eleva la versión de la estructura y todo campo anterior sigue vivo, así que el cero de 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 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 con Renderer en prpDefault: hash F75B5EB4728ADE87
  • prpAgg explícito: hash F75B5EB4728ADE87, 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

Comparación de hashes de píxeles de PDFium Component mostrando el hash de render Skia por defecto 502D77C3711B4ACF, la configuración BrotliEnabled anterior a v3.123.0 coincidiendo con una ejecución prpAgg explícita con hash F75B5EB4728ADE87, y el wrapper arreglado resolviendo prpDefault por la exportación 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 valor por defecto arreglado ahora coincide con la configuración sin tocar

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

Flujo de precomprobación de LoadLibrary de PDFium Component donde ConfigurePdfLibrary sella la configuración, la comprobación de capacidad prueba la exportación FPDF_RenderPageSkia y la evidencia de la cadena skrifa, una petición no soportada lanza un EPdfError capturable y desella para reintentar, mientras una DLL que nunca carga mantiene PdfLibraryConfigurationSealed true
La validación corre tras vincularse las exportaciones y antes de la inicialización, así que un backend ausente falla como un EPdfError que puede atrapar en lugar de un CHECK nativo que mata el proceso

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

  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 siguen indecodificables. La defensa es deducir la versión de los campos realmente en uso, que es lo que hace LoadLibrary, en lugar de empotrar una
  2. Versión suficiente, cero significa algo. Eleve la versión a 6 y todo campo hasta la versión 6 queda vivo. FillChar pone a cero m_RendererType en 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 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 ConfigurePdfLibrary una 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 BrotliEnabled o IsolatePerDocument y espera salida Skia de los runtimes incluidos
  • Deje Renderer en prpDefault salvo que necesite un rasterizador concreto; ahora resuelve al valor por defecto de la compilación en cada versión de estructura
  • Use PdfNativeRendererType con GetSkiaRenderCapabilities.PageRender para registrar qué renderer está realmente activo
  • Espere EPdfError, no un crash, para prpSkia en una DLL solo AGG o pfbpFontations en 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 se queda 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 unidad para evitar choques de nombres con Win32 y TComponent

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