💡 Ejemplo completo en funcionamiento disponible en GitHub:
sign-pdf-in-linux-container-fonts-dotnet

La forma antigua era dolorosa

El servicio firma facturas. Se ejecuta en un portátil con trescientos tipos de letra instalados, pasa la revisión y se contenedorizó un viernes. El lunes el primer trabajo en el clúster sale con código distinto de cero con Sign document error: Font Arial was not found, y alguien pasa la mañana leyendo trazas de pila antes de que alguien piense en preguntar qué fuentes contiene realmente una imagen mcr.microsoft.com/dotnet/runtime:8.0.

La respuesta es ninguna. Cero archivos de fuentes, medidos en la imagen en la que se ejecuta la muestra de este artículo.

Vale la pena saber cómo se comparan los demás entornos de ejecución, porque el fallo se ve diferente en cada uno. eclipse-temurin:17-jre incluye 8 archivos DejaVu y node:18-bookworm incluye 6, ambos para AWT, por lo que las imágenes JVM y Node firman texto latino sin problemas y solo fallan cuando llega una cadena japonesa o china. python:3.11-slim no incluye ninguna, al igual que la imagen de tiempo de ejecución .NET, por lo que falla en la primera firma. Nadie obtiene CJK gratis en ninguno de ellos.

El aprovisionamiento de fuentes en el contenedor es el paso que hace que la firma de texto funcione en una imagen Linux con GroupDocs.Signature para .NET. Importa porque la biblioteca no sustituye una familia faltante: nombrar una fuente que no está instalada genera un error y no escribe ningún documento. Este artículo coloca la imagen sin fuentes junto a la corregida, muestra qué cambió y cubre la resolución en tiempo de ejecución que mantiene el mismo código funcionando en una máquina de desarrollo.

Existe una forma mejor

Dos cosas deben ser verdaderas. La imagen necesita al menos una fuente, y el código debe dejar de asumir cuál es.

La primera es una capa en el Dockerfile. La segunda es un paso de resolución: en lugar de codificar Arial, preguntar a la biblioteca cuál de varias familias candidatas puede usar realmente, y quedarse con la primera que funcione. El resultado se ejecuta sin cambios en un contenedor ligero, en Windows y en CI, porque nunca afirma nada sobre el entorno que no haya verificado.

Una cosa que no funciona, y vale la pena decirlo claramente porque es lo primero que la gente intenta: dejar la fuente sin establecer. Sin SignatureFont, GroupDocs.Signature solicita su propio valor predeterminado, Times New Roman, que la imagen sin fuentes también carece. La llamada falla idénticamente.

La nueva forma: dos imágenes, una diferencia

Paso 1 – Ver lo que tiene la imagen

Antes de firmar cualquier cosa, enumere los archivos de fuentes. El recuento convierte una excepción vaga en un diagnóstico, porque cero fuentes y un nombre de familia incorrecto requieren correcciones diferentes:

string[] roots =
{
    "/usr/share/fonts",
    "/usr/local/share/fonts",
    Path.Combine(home, ".fonts"),
    Path.Combine(home, ".local/share/fonts"),
    Environment.GetFolderPath(Environment.SpecialFolder.Fonts),
    "/System/Library/Fonts",
    "/Library/Fonts",
};

Observe lo que falta: System.Drawing. System.Drawing.Common es solo para Windows a partir de .NET 7 y lanza una excepción en Linux, por lo que el código de fuentes construido sobre él falla en el contenedor por una segunda razón no relacionada.

Paso 2 – Añadir la capa de fuentes

Cuatro paquetes, una instrucción RUN, y el fallo desaparece:

RUN apt-get update && apt-get install -y --no-install-recommends \
        fontconfig \
        fonts-dejavu-core \
        fonts-liberation \
        fonts-noto-cjk \
    && fc-cache -f \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

fontconfig es el resolvedor y le brinda fc-list para depuración. fonts-dejavu-core es el mínimo para latín, griego y cirílico. fonts-liberation suministra sustitutos métricamente compatibles para Arial, Times New Roman y Courier New, que es a lo que realmente hacen referencia los documentos creados en Windows. fonts-noto-cjk cubre chino, japonés y coreano.

Paso 3 – Resolver una familia en lugar de nombrar una

La forma portátil de elegir una fuente es intentar una firma desechable por cada candidata y quedarse con la primera que no lance excepción:

foreach (string candidate in candidates)
{
    if (TryFamily(sourcePath, candidate).Ok)
    {
        return candidate;
    }
}

return null;

Detectar por nombre de archivo es el atajo tentador y está equivocado. El paquete fonts-noto-cjk de Debian instala NotoSansCJK-Regular.ttc, cuyo nombre de familia es Noto Sans CJK JP. Una coincidencia por nombre de archivo pasa por alto fuentes que están presentes y reclama familias que no se resolverán cuando se pasen a SignatureFont.

Paso 4 – Firmar lo que se resolvió, verificar lo que firmó

Se requiere una familia latina resuelta; una familia CJK resuelta es opcional y su ausencia implica omitirla, no provocar un bloqueo:

var options = new List<SignOptions>
{
    BuildTextOptions(LatinText, latinFamily, top: 50),
};

if (cjkFamily is not null)
{
    options.Add(BuildTextOptions(CjkText, cjkFamily, top: 120));
}

SignResult result = signature.Sign(outputPath, options);

Luego lea el archivo de nuevo, porque CJK sin una fuente CJK puede renderizarse como cajas vacías sin generar ningún error:

var options = new TextSearchOptions { AllPages = true };
List<TextSignature> found = signature.Search<TextSignature>(options);

Comparación lado a lado: antes vs. después

Dockerfile.nofonts Dockerfile
Archivos de fuentes en la imagen 0 DejaVu, Liberation, Noto CJK
Firma de texto latino falla, salida 3 escrita y recuperada al leer de nuevo
Firma de texto CJK falla escrita y recuperada
Error mostrado Font <name> was not found ninguno
Diferencia en el código ninguna – mismo binario ninguna – mismo binario

La última fila es el punto clave. Nada cambió en la aplicación entre las dos ejecuciones. El repositorio de ejemplo incluye ambos archivos para que la comparación requiera dos comandos docker build en lugar de confiar ciegamente. Mantenga también la variante sin fuentes en el repositorio después: es la forma más rápida de reproducir el fallo cuando alguien cambia la imagen base seis meses después y las firmas dejan de aparecer silenciosamente.

¿Por qué no instalar todas las fuentes?

Porque el tamaño de la imagen es una restricción real y los cuatro paquetes anteriores ya cubren los scripts que la mayoría de los documentos utilizan. fonts-dejavu-core por sí solo basta para firmas en latín, griego y cirílico; Liberation es importante cuando los documentos hacen referencia a las familias de Windows por nombre; Noto CJK es el que realmente ocupa mucho espacio y solo justifica su peso si firma texto de Asia Oriental. Instale lo que necesiten sus documentos y luego verifique con una lectura posterior.

Ejemplo del mundo real: el trabajador de firma por lotes

Un trabajador de cola firma varios miles de PDFs por noche. Con la resolución al iniciar, registra una línea con los nombres de las familias que usará, y si nada se resuelve sale antes de tocar la cola en lugar de fallar por mensaje. Esa comprobación al iniciar es lo que convierte un problema de fuentes de una corriente de trabajos fallidos en un contenedor que se niega a arrancar con una razón de una sola línea.

El coste de la prueba es lo suficientemente pequeño como para ignorarlo al iniciar y demasiado grande para repetirlo por documento. Cada prueba es una firma real escrita en un archivo temporal, por lo que la lista latina cuesta hasta cuatro de ellas y la lista CJK hasta ocho, todo contra un PDF de una página. Resuelva una vez, almacene en caché los dos nombres de familia, y la ruta por documento es exactamente la que era antes: construya las opciones, llame a Sign, lea el recuento de resultados.

Perdí una tarde con la versión que adivinaba. Escaneó el directorio de fuentes, encontró NotoSansCJK-Regular.ttc, informó que CJK estaba disponible y luego falló con cada nombre de familia que derivé de ese nombre de archivo. Probar con una firma real fue tanto más simple como correcto.

¿Qué más muerde en un contenedor?

Una cosa más, y no está relacionada con las fuentes: InvariantGlobalization=true. Es un consejo estándar para eliminar ICU de una imagen .NET, y con GroupDocs.Signature hace que el primer new Signature(...) lance CultureNotFoundException: ... en-US is an invalid culture identifier, porque SignatureSettings construye un CultureInfo("en-US"). Mantenga la globalización habilitada y deje que ICU permanezca en la imagen. La página de requisitos del sistema es el lugar para comprobar la compatibilidad de la plataforma antes de comprometerse con una imagen base.

Conclusión

Un servicio de firma que funciona localmente y falla en Docker casi siempre carece de fuentes, y la solución es una capa de cuatro paquetes más código que resuelva una familia en lugar de asumir una. Construya ambas imágenes a partir del ejemplo, ejecútelas lado a lado y lea las líneas [fonts]: todo el argumento cabe en esa única comparación.

Recursos adicionales