PDFium Component permite que una aplicación Delphi decida qué bytes de fuente se usan cuando un PDF hace referencia a una fuente que no incrusta. ConfigureSystemFontProvider instala una implementación de IPdfSystemFontProvider que recibe cada solicitud de correspondencia de fuente que hace PDFium, completa con nombre de familia, grosor, indicador de cursiva, juego de caracteres y familia de paso, y responde con los bytes TrueType, TrueType Collection u OpenType que hay que usar
Esto existe porque las fuentes no incrustadas son una lotería de renderizado. Un PDF que nombra Arial y no incrusta nada se renderiza con Arial en una estación de trabajo, con un sustituto compatible en métricas en un servidor Linux, y con lo que encuentre el asignador del sistema anfitrión en una imagen de contenedor restringida. La misma factura tiene un aspecto distinto en cada caso, los saltos de línea se desplazan, y un cliente recibe un documento que no coincide con la copia archivada
¿Por qué no instalar simplemente las fuentes en el servidor?
A veces esa es la respuesta, y cuando lo es, adóptela. Pero falla en tres situaciones habituales. La licencia puede prohibir instalar una fuente en un servidor para renderizado automatizado. Las imágenes de contenedor se reconstruyen con frecuencia y una fuente instalada a mano desaparece con el siguiente despliegue. Y los flujos de trabajo regulados necesitan que la pila de renderizado sea reproducible a partir de artefactos bajo control de versiones, cosa que una instalación de fuente a nivel de máquina no es
Un proveedor resuelve las tres cosas trasladando la decisión a su aplicación. Las fuentes se distribuyen como recursos que usted controla, la política de correspondencia es código que puede revisar, y el mismo binario renderiza de forma idéntica en todas partes porque nada depende de lo que casualmente esté instalado
Instalar un proveedor
La configuración debe realizarse antes de cargar la biblioteca. PDFium acepta una estructura de información de fuentes del sistema en la inicialización y conserva los identificadores que entrega después, así que cambiar de proveedor mientras hay documentos abiertos invalidaría identificadores de fuente que PDFium aún conserva; el componente rechaza eso directamente en lugar de dejar que corrompa un renderizado:
uses
PDFium;
type
TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
public
function ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
end;
function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
var
Path: string;
begin
// Correspondencia determinista: el nombre de familia más el grosor y la cursiva deciden
// qué archivo se entrega para esta solicitud
Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
Request.Italic, Request.Charset);
Result := Path <> '';
if not Result then
Exit;
Font.FaceName := Request.FaceName;
Font.FontData := LoadFileBytes(Path); // bytes sfnt o TTC completos
Font.Charset := Request.Charset;
Font.TTCIndex := 0; // índice dentro de una colección
end;
var
Policy: TPdfSystemFontPolicy;
begin
Policy := TPdfSystemFontPolicy.Default;
Policy.AllowDefaultFallback := False; // el sistema anfitrión decide todo
Policy.AllowFaceSubstitution := False; // rechazar un nombre de familia distinto
Policy.MaxFontBytes := 32 * 1024 * 1024;
Policy.MaxCacheEntries := 64;
ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
// Solo ahora se carga la biblioteca y se abren documentos
end;
El desmontaje se ejecuta en el orden opuesto: primero se desconecta el proveedor de PDFium, y después se descarga la biblioteca. Omitir la desconexión deja identificadores de fuente nativos apuntando a objetos Pascal a punto de liberarse, que es la clásica violación de acceso al cierre en código que mezcla interfaces con conteo de referencias con una biblioteca en C
Qué deciden realmente los indicadores de política
AllowDefaultFallback es el interruptor entre dos modos de funcionamiento. Con él desactivado, una solicitud que el proveedor rechaza simplemente falla, que es lo que se quiere mientras se demuestra que todas las fuentes de un corpus están contempladas: cualquier hueco se hace visible de inmediato en lugar de quedar disimulado. Con él activado, las solicitudes no resueltas se delegan al asignador que devuelve FPDF_GetDefaultSystemFontInfo, mientras que el mundo exterior sigue viendo un único envoltorio de identificador uniforme, con el nombre de familia, el juego de caracteres, los datos de tabla y la eliminación de fuente correctamente encaminados según su origen
AllowFaceSubstitution rige si un proveedor puede responder con un nombre de familia distinto del solicitado. Desactivarlo convierte la sustitución en una decisión explícita en lugar de un accidente, algo que importa cuando un documento nombra una fuente cuyas métricas difieren lo suficiente como para cambiar la paginación
El componente valida cada respuesta del proveedor antes de que llegue a PDFium: los datos vacíos se rechazan, las fuentes de tamaño excesivo se rechazan frente a MaxFontBytes, se comprueba el índice TTC, y las tablas sfnt individuales se sirven desde el directorio de fuentes cuando PDFium pide una tabla en lugar del archivo completo. Esa última capacidad significa que un proveedor puede entregar un archivo de fuente completo y dejar que el componente responda a las consultas a nivel de tabla, en lugar de exponer objetos Pascal sin procesar a través del ABI de C
Caché sin datos de fuente colgantes
Las solicitudes de correspondencia de fuente se repiten constantemente durante el renderizado, así que las respuestas se almacenan en caché con una clave que cubre cada parámetro de selección de fuente, y se descartan por un orden de uso menos reciente acotado. La sutileza está en el ciclo de vida: PDFium puede seguir leyendo los bytes de una fuente cuya entrada de caché acaba de ser descartada
La caché almacena arrays dinámicos con conteo de referencias y cada identificador nativo mantiene su propia instantánea, así que el descarte libera una referencia en lugar de liberar memoria en uso. La retrollamada de eliminación libera el identificador y mantiene un recuento activo. En la práctica, esto significa que MaxCacheEntries se puede ajustar por memoria sin ningún riesgo de retirar datos por debajo de un renderizado en curso
¿Se llama al proveedor en mi hilo?
No, no necesariamente. PDFium puede llamar al asignador desde sus propios hilos de trabajo, así que una implementación debe ser segura para hilos. Los contadores compartidos, la caché y la observación de la configuración están cada uno protegidos dentro del componente por su propia sección crítica, pero el código dentro de ResolveFont es responsabilidad suya hacerlo seguro
La forma más segura es un proveedor que no toca ningún estado compartido mutable: leer de una tabla construida al arrancar, cargar bytes de un archivo o un recurso, devolver. Si una búsqueda necesita una caché compartida propia, protéjala. Y mantenga las excepciones dentro de su implementación, ya que una excepción Pascal nunca debe desenrollarse a través de la pila de PDFium; el componente captura en el límite del ABI de C y convierte en un fallo o un respaldo predeterminado opcional, pero confiar en eso como flujo de control normal cuesta rendimiento y oculta errores. Las reglas de hilos del resto del componente siguen los mismos principios que las de la disciplina de bloqueo de renderizado
Demostrar la correspondencia en producción
Las estadísticas convierten la sustitución de fuentes de una conjetura en algo sobre lo que se puede afirmar. GetSystemFontProviderStatistics informa de si hay un proveedor configurado e instalado, cuántas solicitudes de correspondencia se hicieron y cómo se satisficieron, divididas en aciertos de caché, aciertos de proveedor y aciertos de respaldo predeterminado, junto con respuestas rechazadas, solicitudes fallidas, identificadores activos y fuentes en caché:
var
Stats: TPdfSystemFontStatistics;
begin
Stats := GetSystemFontProviderStatistics;
Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
[Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
Stats.DefaultFallbackHits]));
Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
[Stats.RejectedProviderResponses, Stats.FailedRequests,
Stats.ActiveHandles, Stats.CachedFonts]));
// En una ejecución de conformidad con el respaldo desactivado, cualquier acierto de respaldo o
// solicitud fallida significa que un documento referenció una fuente que no distribuimos
if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
raise Exception.Create('unmapped font encountered - update the font set');
end;
Un recuento creciente de RejectedProviderResponses es la señal de que un proveedor está respondiendo con datos que la política rechaza, normalmente un archivo de tamaño excesivo o una familia sustituida, y merece la pena generar alertas al respecto porque esas solicitudes degradan silenciosamente a respaldo o fallo. Para diagnosticar qué fuentes necesita realmente un documento antes de construir la tabla de correspondencia, la vía de inspección de analizar las propiedades de fuente de un PDF enumera las fuentes incrustadas y no incrustadas por documento
El aprovisionamiento de fuentes, el renderizado y la extracción de texto comparten la misma instancia de biblioteca en Delphi, C++Builder y Lazarus; los detalles de despliegue se describen en la página de PDFium Component para Delphi