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 mapeo de fuente que hace PDFium, completa con nombre de familia, peso, indicador de cursiva, conjunto de caracteres y familia de trazo, y responde con los bytes TrueType, TrueType Collection u OpenType a 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 sea que encuentre el mapeador del host en una imagen de contenedor restringida. La misma factura se ve distinta en cada uno, los saltos de línea se mueven, y un cliente recibe un documento que no coincide con la copia archivada
¿Por qué no simplemente instalar las fuentes en el servidor?
A veces esa es la respuesta, y cuando lo es, tómala. Pero falla en tres situaciones comunes. 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, algo que una instalación de fuente a nivel de máquina no es
Un proveedor resuelve las tres al trasladar la decisión hacia tu aplicación. Las fuentes se distribuyen como recursos que controlas, la política de mapeo es código que puedes revisar, y el mismo binario renderiza de forma idéntica en todas partes porque nada depende de lo que esté instalado
Instalar un proveedor
La configuración debe ocurrir antes de que se cargue la biblioteca. PDFium acepta una estructura de información de fuente del sistema al inicializarse y conserva los descriptores que entrega después, así que cambiar de proveedor mientras hay documentos abiertos invalidaría descriptores de fuente que PDFium todavía retiene; el componente rechaza eso de plano 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
// Mapeo determinista: el nombre de familia más el peso y la cursiva
// deciden qué archivo distribuimos 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 host 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 cargar la biblioteca y abrir documentos
end;
El desmontaje se ejecuta en el orden inverso: primero se desconecta el proveedor de PDFium, luego se descarga la biblioteca. Omitir la desconexión deja descriptores de fuente nativos apuntando a objetos Pascal que están 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 operación. Con él desactivado, una solicitud que el proveedor rechaza simplemente falla, que es lo que se quiere mientras se demuestra que cada fuente de un corpus está contabilizada: cualquier vacío se vuelve visible de inmediato en lugar de disimularse. Con él activado, las solicitudes no resueltas se delegan al mapeador que devuelve FPDF_GetDefaultSystemFontInfo, mientras el mundo exterior sigue viendo un envoltorio de descriptor uniforme, con el nombre de familia, el conjunto de caracteres, los datos de tabla y la eliminación de fuente enrutados correctamente según su origen
AllowFaceSubstitution gobierna si un proveedor puede responder con un nombre de familia distinto al solicitado. Desactivarlo convierte la sustitución en una decisión explícita en lugar de un accidente, lo cual 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 contra MaxFontBytes, se verifica 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 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 mapeo 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 orden acotado de uso menos reciente. La sutileza está en el ciclo de vida: PDFium puede seguir leyendo los bytes de una fuente cuya entrada de caché acaba de descartarse
La caché almacena arreglos dinámicos con conteo de referencias, y cada descriptor nativo conserva su propia instantánea, así que descartar una entrada libera una referencia en lugar de liberar memoria en uso. La retrollamada de eliminación libera el descriptor y mantiene un conteo activo. En la práctica, esto significa que MaxCacheEntries puede ajustarse por memoria sin ningún riesgo de retirar datos de debajo de un renderizado en curso
¿Se llama al proveedor en mi hilo?
No, no necesariamente. PDFium puede llamar al mapeador 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 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 tuya hacerlo seguro
La forma más segura es un proveedor que no toque estado compartido mutable: leer de una tabla construida al inicio, cargar bytes de un archivo o un recurso, devolver. Si una búsqueda necesita una caché compartida propia, protégela. Y mantén las excepciones dentro de tu implementación, ya que una excepción de 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 a un fallo o a un respaldo predeterminado opcional, pero confiar en eso como flujo de control normal cuesta rendimiento y oculta errores. Las reglas de hilos para el resto del componente siguen los mismos principios que las de la disciplina del bloqueo de renderizado
Demostrar el mapeo en producción
Las estadísticas convierten la sustitución de fuentes de una conjetura en algo que se puede verificar. GetSystemFontProviderStatistics reporta si hay un proveedor configurado e instalado, cuántas solicitudes de mapeo 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, descriptores vivos 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
// hizo referencia a 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 conteo creciente de RejectedProviderResponses es la señal de que un proveedor está respondiendo con datos que la política rechaza, usualmente un archivo de tamaño excesivo o una familia sustituida, y vale la pena generar una alerta al respecto porque esas solicitudes degradan en silencio hacia el respaldo o el fallo. Para diagnosticar qué fuentes necesita realmente un documento antes de construir la tabla de mapeo, la ruta de inspección en análisis de propiedades de fuente 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