Ir al contenido
getnextpdf.com

Aprovisionar fuentes en producción

Tu PDF se representa correctamente en tu portátil, luego se despliega en un contenedor y sale como una fila de cuadros vacíos (el glifo «tofu») o con acentos y caracteres no latinos ausentes. La causa es casi siempre la misma: la fuente que seleccionaste no está presente en la imagen desplegada.

El motor nativo, en proceso, de NextPDF resuelve las fuentes a partir de archivos de fuente que el registro de fuentes puede leer. No descubre automáticamente las fuentes del sistema operativo ni de fontconfig: los archivos de fuente instalados por el sistema operativo solo ayudan si registras explícitamente esos archivos o añades su directorio contenedor a la ruta de búsqueda de FontRegistry. Un contenedor construido a partir de una imagen base mínima no tiene fuentes instaladas por apt/apk, e incluso cuando las tiene, el motor nativo las ignora a menos que apuntes el registro a sus archivos. La solución es empaquetar los archivos de fuente reales dentro de tu aplicación o imagen y registrarlos en el motor. El registro lee archivos TrueType (.ttf), OpenType (.otf) y TrueType Collection (.ttc); también se acepta el Type1 antiguo (.pfb), pero rara vez es necesario para trabajo nuevo.

Antes de empezar, confirma que estas piezas están en su sitio:

  • El Core de NextPDF está instalado.
  • Tienes los archivos de fuente reales que pretendes usar y tienes licencia para incrustarlos. Los derechos de incrustación son tu responsabilidad; consulta Incrustar y subconjuntar una fuente TrueType.
  • Tu compilación puede copiar esos archivos en el artefacto desplegado.

Esta es una guía práctica de operaciones. El código es mínimo; el trabajo está en la compilación y en la disposición del sistema de archivos. Para la mecánica a nivel de API de registrar y subconjuntar una sola tipografía, lee la receta de incrustación y subconjuntado enlazada arriba. Esta página cubre cómo llevar los archivos al servidor y apuntar el motor hacia ellos.

Por qué el motor nativo no encuentra automáticamente las fuentes del sistema operativo

Sección titulada «Por qué el motor nativo no encuentra automáticamente las fuentes del sistema operativo»

Hay dos rutas de representación distintas, y la historia de las fuentes difiere entre ambas.

  • Motor nativo en proceso (el predeterminado, Document / writeHtml): el motor no llama al sistema de fuentes del sistema operativo ni a fontconfig para el descubrimiento. Resuelve una tipografía a través del registro de fuentes, que lee un archivo de fuente específico que registraste o encuentra uno dentro de un directorio que configuraste como ruta de búsqueda. Instalar una fuente con apt-get install fonts-noto o ejecutar fc-cache no hace nada por sí mismo: el motor nativo ve esos archivos solo si los registras o añades su directorio a la ruta de búsqueda del registro.
  • Puente Chrome (el representador de HTML a PDF que controla un navegador headless): esta ruta usa las fuentes instaladas del host mediante el descubrimiento de fuentes normal del navegador, así que ahí sí importan los paquetes de fuentes de apt/apk y fontconfig.

Si lees orientaciones genéricas del tipo «instala estos paquetes de fuentes del sistema en tu Dockerfile», se aplican al puente Chrome, no al motor nativo que cubre esta página. Para la generación nativa, empaqueta los archivos y regístralos.

Paso 1 — Empaquetar los archivos de fuente reales

Sección titulada «Paso 1 — Empaquetar los archivos de fuente reales»

Coloca los archivos de fuente dentro del árbol de tu aplicación para que estén versionados y se desplieguen con cada compilación. Una ubicación convencional es un directorio resources/fonts/.

your-app/
├── resources/
│ └── fonts/
│ ├── DejaVuSans.ttf
│ ├── DejaVuSans-B.ttf
│ └── NotoSansCJK-Regular.ttc
└── src/

Nombra los archivos de modo que la búsqueda por directorio del motor pueda encontrarlos por familia y estilo. Cuando registras un directorio (en lugar de un archivo específico) y luego llamas a setFont('DejaVuSans', 'B', 12), el motor busca archivos como DejaVuSans-B.ttf, DejaVuSansB.ttf o DejaVuSans.ttf en cada directorio configurado. La búsqueda por directorio construye esos nombres candidatos a partir del mismo código de estilo de una sola letra que pasas a setFont (B para negrita, I para cursiva, BI para negrita-cursiva), no de una palabra completa, de modo que la forma fiable es Family-<StyleCode>.ttf (por ejemplo DejaVuSans-B.ttf o DejaVuSans-BI.ttf), no Family-Bold.ttf. Un archivo llamado DejaVuSans-Bold.ttf nunca lo encuentra la búsqueda por directorio; para usar tal archivo, regístralo explícitamente con register(), que analiza la fuente y la indexa por la familia y el estilo leídos de las propias tablas de nombres del archivo, de modo que el nombre de archivo completo deja de importar (consulta el Paso 2).

Paso 2 — Registrar las fuentes en el motor

Sección titulada «Paso 2 — Registrar las fuentes en el motor»

Tienes dos formas equivalentes de hacer visibles los archivos. Ambas pasan por NextPDF\Typography\FontRegistry, que implementa NextPDF\Contracts\FontRegistryInterface.

Registra un archivo específico bajo un alias cuando controlas la tipografía exacta:

use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$registry->register(__DIR__ . '/../resources/fonts/DejaVuSans.ttf', alias: 'DejaVuSans');

register(string $fontFile, string $alias = '', int $fontIndex = 0) acepta archivos .ttf, .otf y .ttc, además del Type1 antiguo .pfb (que carga sus métricas .afm complementarias de la misma ruta); $fontIndex selecciona una subfuente dentro de un TrueType Collection (.ttc). register() analiza el archivo e indexa la tipografía por la familia y el estilo leídos de sus propias tablas de nombres, de modo que el nombre de archivo físico es irrelevante una vez registrado. El $alias opcional es solo un nombre de búsqueda adicional para la tipografía: no es un código de estilo y no cambia qué estilo proporciona el archivo; pásalo cuando quieras llamar a setFont() con un nombre distinto del nombre de familia incrustado de la fuente. Devuelve el FontInfo analizado.

Registra un directorio cuando quieres que el motor resuelva tipografías por nombre desde una carpeta que controlas:

$registry = new FontRegistry('/var/www/app/resources/fonts');
// or, equivalently, after construction:
$registry->addFontDirectory('/var/www/app/resources/fonts');

El constructor de FontRegistry toma ese directorio como su primer argumento, y addFontDirectory() añade más rutas de búsqueda. Un Document simple también expone addFontDirectory() para el caso autónomo.

Para usar un registro que poblaste tú mismo, construye los documentos a través de DocumentFactory, que conecta ese registro exacto en cada documento que crea:

use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'Réndéred wîth a bundled face — no tofu.', newLine: true);
$doc->save('/tmp/out.pdf');

Document::createStandalone() construye su propio registro interno, de modo que una tipografía que registraste en un FontRegistry separado es invisible para él. En producción, pasa por DocumentFactory (o la factoría de tu framework) para que el registro poblado sea el que está en uso.

Cada integración de framework expone los mismos dos conceptos como configuración, así que rara vez tocas el registro directamente. En el nextpdf.php del paquete de Laravel, fonts_path (predeterminado NEXTPDF_FONTS_PATH, con resource_path('fonts') como reserva) es el directorio de búsqueda, y preload_fonts es una lista de rutas absolutas de archivos de fuente que se analizan al arrancar el worker. Apunta fonts_path al directorio que empaquetaste y tus tipografías registradas se resuelven automáticamente.

Paso 3 — Aprovisionar fuentes en una imagen de Docker

Sección titulada «Paso 3 — Aprovisionar fuentes en una imagen de Docker»

En un contenedor, los archivos de fuente deben formar parte de la capa de imagen, copiados en tiempo de compilación. Como el código de la aplicación y las fuentes se despliegan juntos cuando los empaquetas bajo resources/fonts/, un COPY . . normal ya los lleva. Si mantienes las fuentes fuera del contexto de compilación, cópialas explícitamente y asegúrate de que la ruta que registras coincide con la ruta dentro de la imagen.

# Native engine: NO system font packages are required.
# The native engine does not discover OS-installed fonts automatically; install OS
# font packages (`apt-get install fonts-*`) only if you also register them or point
# the font registry's search directory at their files.
FROM php:8.4-cli
WORKDIR /var/www/app
# Bundle the application, including resources/fonts/, into the image.
COPY . /var/www/app
# Make the bundled directory the engine's font search path.
ENV NEXTPDF_FONTS_PATH=/var/www/app/resources/fonts
CMD ["php", "bin/generate.php"]

En un sistema de archivos inmutable o de solo lectura (un contenedor con readOnlyRootFilesystem, una imagen serverless o un host endurecido), los archivos de fuente se leen en tiempo de generación y nunca se escriben, de modo que un montaje de solo lectura es correcto. La única escritura que el motor podría querer es su caché de fuentes analizadas: o bien das a ese directorio un pequeño volumen escribible, o calientas y bloqueas el registro al arrancar (siguiente sección) para que no se intente ninguna escritura ni registro en tiempo de ejecución.

En un worker de larga duración, analiza cada tipografía una vez al arrancar, luego bloquea el registro para que no se produzca ningún registro por solicitud y una mala configuración falle de forma ruidosa en lugar de recurrir silenciosamente a una alternativa:

$registry = new FontRegistry('/var/www/app/resources/fonts');
$registry->warmup([
'/var/www/app/resources/fonts/DejaVuSans.ttf',
'/var/www/app/resources/fonts/DejaVuSans-B.ttf',
]);
$registry->lock();

Tras lock(), register(), addFontDirectory() y warmup() lanzan, lo que convierte un error de «ruta equivocada en la imagen» en un fallo de arranque contundente en lugar de una página con tofu en producción.

Añade una comprobación de humo del despliegue que represente una página con cada tipografía requerida. La comprobación de cabecera de abajo solo verifica que el documento produjo salida: no demuestra que la fuente se analizara, se incrustara ni siquiera se resolviera. Una tipografía que el motor no puede encontrar puede recurrir a una fuente base estándar (y, bajo el comportamiento no estricto actual, un perfil de conformidad puede en su lugar proporcionar un sustituto incluido) sin dejar de emitir un PDF válido y no vacío, de modo que, incluso donde ocurre esa reserva, esta comprobación por sí sola no detectará la degradación silenciosa. No confíes en que la reserva esté garantizada ni sea silenciosa en todas las rutas; verifica directamente el programa incrustado, como se muestra abajo:

$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'warmup check', newLine: true);
$pdf = $doc->getPdfData();
// `getPdfData()` would normally throw on a real failure; this header check only
// confirms serialization returned PDF bytes, not that any specific font resolved.
if (!str_starts_with($pdf, '%PDF')) {
throw new RuntimeException('Font warmup smoke check produced no PDF output.');
}

Para que el despliegue falle de verdad cuando falta una tipografía, comprueba en el PDF emitido el programa de fuente incrustado. Una tipografía registrada que se resuelve lleva su propio diccionario de fuente con un programa incrustado, así que afirmar su presencia detecta el caso en que la tipografía solicitada nunca se resolvió (sea cual sea la alternativa a la que recurrió el motor) que la comprobación de cabecera no detecta. La clave que contiene el programa depende del formato de contornos: los contornos TrueType (.ttf, .ttc) usan /FontFile2, los contornos CFF/OpenType (.otf con contornos PostScript) usan /FontFile3, y el Type1 antiguo (.pfb) usa /FontFile.

Si todo lo que necesitas es una señal de «algún programa de fuente incrustado» agnóstica del formato, comprueba /FontFile a secas: como /FontFile es una subcadena tanto de /FontFile2 como de /FontFile3, una comprobación de subcadena simple ya coincide con todos los tipos de contorno, y añadir /FontFile2//FontFile3 como ramas || adicionales es redundante:

if (!str_contains($pdf, '/FontFile')) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

Sin embargo, una subcadena /FontFile a secas no puede distinguir los tipos de contorno. Para diferenciarlos, busca el token exacto con un límite de palabra para que /FontFile no se dispare también con /FontFile2 o /FontFile3:

$isTrueType = preg_match('~/FontFile2\b~', $pdf) === 1; // TrueType (.ttf/.ttc)
$isCffOtf = preg_match('~/FontFile3\b~', $pdf) === 1; // CFF/OpenType (.otf)
$isType1 = preg_match('~/FontFile(?![23])\b~', $pdf) === 1; // Type1 (.pfb)
if (!$isTrueType && !$isCffOtf && !$isType1) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

En cualquier caso, trata esto como una heurística aproximada únicamente, no como una compuerta de despliegue fiable. Una búsqueda de bytes en bruto sobre el PDF serializado es inexacta por varias razones: los programas de fuente pueden residir dentro de flujos de objetos comprimidos (donde /FontFile* nunca aparece como bytes en claro), las actualizaciones incrementales pueden anexar o sustituir objetos, las fuentes no incrustadas o de los 14 estándar no llevan legítimamente ningún programa de fuente, y las diferencias de serialización (orden de objetos, espacios en blanco, codificación de nombres) pueden mover u ocultar el token. En el mejor de los casos confirma que alguna tipografía incrustó un programa, nunca que la tipografía específica que querías se resolvió.

Para una compuerta de despliegue real, no confíes en la búsqueda de bytes. Analiza el PDF emitido con un analizador de PDF o un inspector de objetos adecuado y afirma que el objeto de fuente de tu tipografía objetivo lleva un programa incrustado /FontFile//FontFile2//FontFile3, o usa una aserción de resolución de fuentes proporcionada por el producto si está disponible para tu integración. Las expresiones regulares conscientes del token de arriba son útiles para una comprobación rápida de coherencia local, pero una inspección estructural es lo que debería hacer fallar el despliegue. La incrustación y la estructura del diccionario de fuente se describen en Incrustar y subconjuntar una fuente TrueType.

  • createStandalone() tiene su propio registro. Una tipografía registrada en un FontRegistry separado no es visible para un documento autónomo. Usa DocumentFactory (o la factoría del framework) para que tu registro sea el activo.
  • Los archivos de estilo deben existir como archivos. El motor no sintetiza negrita ni cursiva a partir de una tipografía regular. Si llamas a setFont('DejaVuSans', 'B'), la búsqueda por directorio busca DejaVuSans-B.ttf, DejaVuSansB.ttf o DejaVuSans.ttf (también las variantes en minúscula y .otf): forma el candidato a partir del código de estilo literal B, así que nunca busca DejaVuSans-Bold.ttf. Un archivo con un nombre completo como DejaVuSans-Bold.ttf solo se resuelve cuando lo registras explícitamente con register(), que lo indexa por la familia y el estilo leídos de las propias tablas de nombres del archivo independientemente del nombre de archivo; confiar en la búsqueda por directorio para encontrarlo produce un fallo, tras el cual el motor puede recurrir a una fuente base (no una ruta garantizada ni siempre silenciosa): la degradación de la que advierte esta página.
  • Las rutas de envoltorio de flujo y remotas se rechazan. El registro rechaza rutas que contengan un esquema de URI o un byte nulo. Registra solo archivos locales; para fuentes obtenidas en tiempo de ejecución usa registerFromBinary() con los bytes en bruto.
  • Un registro bloqueado es inmutable. Una vez que llamas a lock(), cualquier register(), addFontDirectory() o warmup() posterior lanza. Los métodos de búsqueda siguen disponibles. Registra y calienta todo antes de bloquear.
  • Las colecciones CJK son grandes. Registra la subfuente correcta de un .ttc con $fontIndex y presupuesta un subconjunto incrustado mayor. Consulta las notas sobre CJK en la receta de incrustación y subconjuntado.
  • Un archivo de fuente es entrada binaria no confiable. Empaqueta solo fuentes de fuentes de procedencia que confíes, y valida la procedencia de cualquier tipografía aceptada de usuarios finales.
  • Bloquear el registro tras el calentamiento elimina una superficie de mutación en tiempo de ejecución y hace que un error de ruta falle en el arranque en lugar de degradar silenciosamente la salida.
  • No interpoles entrada de usuario en una ruta de archivo registrada. Registra un conjunto fijo de tipografías empaquetadas; no dejes que una solicitud elija una ruta arbitraria del sistema de archivos.

Esta guía no hace ninguna afirmación normativa de estándares. Cada símbolo mostrado es superficie pública verificada: NextPDF\Typography\FontRegistry (register(), addFontDirectory(), warmup(), lock(), el argumento de directorio del constructor), su contrato NextPDF\Contracts\FontRegistryInterface, NextPDF\Core\DocumentFactory::create() y NextPDF\Core\Document::setFont() / addFontDirectory(). Las claves fonts_path y preload_fonts de Laravel son la configuración documentada del paquete nextpdf/laravel. El comportamiento de incrustación y de etiqueta de subconjunto, con sus citas de ISO 32000-2, está documentado en la receta de incrustación y subconjuntado enlazada en Véase también.