💡 Ejemplo completo funcionando disponible en GitHub:
nodejs-docker-signing-with-fonts

Introducción

La resolución de fuentes es la parte de la firma en contenedores que decide si tu servicio Node produce documentos o excepciones. GroupDocs.Signature no sustituye una familia faltante: si el nombre que se pasa no está presente en la imagen, la llamada genera una excepción y no escribe nada. El borrado de la fuente tampoco es una solución alternativa, ya que la biblioteca entonces solicita su propia fuente predeterminada y falla de la misma manera.

Existen tres formas de decidir qué familia pasar, y solo una de ellas sobrevive en un contenedor. Este artículo las compara y luego cubre el aprovisionamiento y el comportamiento del enlace que moldean el código alrededor de ellas, porque Node.js vía Java tiene más de ambos que cualquier otra plataforma en la que esta biblioteca está disponible.

Por qué esto es más importante en Node.js

El paquete es un puente: node-java carga una JVM en el proceso. Por lo tanto, una imagen de firma Node necesita un JDK, la cadena de herramientas node-gyp para compilar el puente y LD_LIBRARY_PATH apuntando a libjvm.so, todo antes de que las fuentes sean relevantes. node:18-bookworm aporta entonces 6 archivos de fuentes DejaVu para AWT — suficientes para latín, nada para CJK.

Esa combinación produce fallos que parecen errores de la aplicación. Una ruta JVM faltante, una fuente ausente y un desajuste de marshaling aparecen todos como Error running instance method, porque eso es lo que node-java informa para cualquier excepción lanzada del lado Java.

Requisitos previos

Node 18 — el puente se construye contra NAN, que no compila contra el V8 en Node 20 o 22 ('AccessorSignature' is not a member of 'v8'). JDK 8 a 17: en JDK 25 la capa de imágenes falla con Cannot open an image. The image size can not be 0!.

Instalación

npm install @groupdocs/groupdocs.signature

En la imagen, esa instalación necesita build-essential y python3 presentes, además de openjdk-17-jdk-headless y la ruta del cargador:

ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ENV PATH="${JAVA_HOME}/bin:${PATH}"
# node-java dlopens libjvm.so at run time; it is not on the default loader path.
ENV LD_LIBRARY_PATH="${JAVA_HOME}/lib/server:${LD_LIBRARY_PATH}"

Método 1 - Codificar el nombre de la familia

La versión que todos escriben primero: elegir Arial, enviarla, seguir adelante. Funciona en la máquina del desarrollador y falla en la primera ejecución del contenedor, porque las imágenes Debian no instalan Arial — instalan Liberation Sans, que es compatible métricamente bajo un nombre de familia diferente.

No hay código que valga la pena mostrar aquí, y ese es el punto. El contenido completo del método es un literal de cadena que resulta verdadero en un entorno.

Método 2 - Detectar fuentes desde el sistema de archivos

La solución natural: escanear los directorios de fuentes, ver qué hay, elegir algo. La mitad es realmente útil — el inventario te dice si la imagen tiene 0 fuentes o 6:

const roots = [
  '/usr/share/fonts',
  '/usr/local/share/fonts',
  path.join(home, '.fonts'),
  path.join(home, '.local', 'share', 'fonts'),
  '/System/Library/Fonts',
  '/Library/Fonts',
];

La otra mitad no funciona. Los archivos de fuentes rara vez llevan la cadena de familia que el llamador debe pasar: fonts-noto-cjk de Debian instala NotoSansCJK-Regular.ttc, cuya familia es Noto Sans CJK JP. Derivar una familia a partir de ese nombre de archivo te da NotoSansCJK-Regular, que no resuelve a nada. La detección por nombre de archivo tanto omite fuentes presentes como informa con confianza familias que fallarán.

Mantén el inventario como diagnóstico. No lo uses para elegir. El recuento responde si la imagen fue aprovisionada en absoluto, lo cual es una pregunta diferente y igualmente útil.

Método 3 - Preguntar a la biblioteca

Intenta una firma desechable por cada familia candidata y conserva la primera que no lance excepción. Cuesta una escritura de PDF por candidata y es el único método cuya respuesta es autoritativa, porque es la misma llamada que hará la firma real.

for (const candidate of candidates) {
  if (tryFamily(sourcePath, candidate) === null) {
    return candidate;
  }
}
return null;

En Node la prueba necesita una pieza extra. node-java colapsa cada excepción Java en Error running instance method, por lo que el mensaje real debe recuperarse del stack trace envuelto:

const stack = err.stack || '';
const match = stack.match(/com\.groupdocs\.signature\.exception\.[^\n]*/);
return match ? match[0].trim() : (err.message || String(err));

Sin esas dos líneas, un contenedor sin fuentes y una ruta JVM rota producen registros idénticos. Pasé más tiempo del que quisiera admitir comparando dos contenedores que imprimían el mismo error por razones totalmente distintas antes de añadir la expresión regular.

Qué cuesta la prueba

La objeción a la prueba es que escribe archivos, y lo hace: un PDF pequeño por candidata, eliminado inmediatamente. La lista latina en el ejemplo tiene cuatro entradas y la lista CJK ocho, así que un arranque en frío escribe como máximo doce documentos de una página en el directorio temporal antes de que el servicio esté listo.

Eso es un costo de inicio, no por solicitud, y brinda una línea de registro nombrando ambas familias resueltas. Comparado con un contenedor que arranca limpiamente y luego falla en el primer documento del cliente con un error de puente, doce archivos temporales no es un intercambio difícil.

Comparación de métodos: cuándo usar cada uno

Método Mejor para Ventajas clave Limitaciones
Familia codificada un entorno controlado único trivial, sin costo de inicio se rompe en cualquier imagen que no tenga esa familia exacta
Detección por nombre de archivo diagnosticar lo que contiene una imagen rápido, sin llamadas de firma los nombres de archivo no son nombres de familia, por lo que las elecciones derivadas fallan
Sondeo de la biblioteca cualquier contenedor o entorno portátil autoritativo, funciona tanto en laptop como en imagen una escritura de PDF por candidata, por lo que se resuelve en el arranque y se almacena en caché

Las dos peculiaridades del enlace que vale la pena conocer

Una vez que una familia se resuelve, la llamada de firma en sí tiene una forma específica de Node. La API Java toma una lista de opciones, pero un array de JavaScript no se marshalea a java.util.List, por lo que pasar uno produce Could not find method "sign(java.lang.String, [Ljava.lang.Object;)". La solución alternativa es encadenar la sobrecarga de una sola opción y pasar por un archivo temporal:

new signatureLib.Signature(sourcePath)
  .sign(firstOutput, buildTextOptions(LATIN_TEXT, latinFamily, 50));

if (stageTwo) {
  new signatureLib.Signature(firstOutput)
    .sign(outputPath, buildTextOptions(CJK_TEXT, cjkFamily, 120));
}

La segunda peculiaridad es la lectura de vuelta. TextVerifyOptions no hace round‑trip a través de este enlace: verify genera el mismo error genérico de puente, por lo que el ejemplo devuelve un sentinel y muestra unavailable en lugar de fingir que la firma falló. El paquete npm está versionado 24.12.0, publicado en diciembre de 2024, y empaqueta un motor 23.6.1 mientras .NET está en 26.6 y Java en 26.5. La firma no se ve afectada; solo falta la ruta de verificación.

¿Debo seguir usando el enlace de Node.js en producción?

Para firmas solo en latín, sí: firma correctamente, y una fuente faltante genera una excepción en lugar de degradarse silenciosamente, por lo que el modo de falla es ruidoso. Para trabajos con scripts mixtos, pondera la ausencia de lectura de vuelta, ya que nada en el proceso puede confirmar entonces que los glifos CJK están incrustados en lugar de renderizarse como cajas. Un verificador pequeño en .NET o Java dentro del mismo pipeline cubre esa brecha.

Mejores prácticas y consejos

  • Aprovisiona en este orden: JDK y cadena de herramientas, ruta del cargador, fuentes, luego la aplicación. Cada capa falla de manera distinta y mezclarlas ralentiza el diagnóstico.
  • Resuelve las familias una sola vez al iniciar y regístralas junto al recuento de fuentes.
  • Fija Node 18 y un JDK entre 8 y 17, y trata ambos como infraestructura fija en lugar de actualizaciones rutinarias.
  • Mantén el Dockerfile sin fuentes en el repositorio, de modo que el fallo quede a una compilación de distancia.

Conclusión

Tres formas de elegir una fuente, una que sobrevive al despliegue. Sondea la biblioteca, almacena la respuesta en caché y deja que el inventario sirva como diagnóstico en lugar de decisión. Luego trabaja con el enlace tal como es: firma una opción a la vez, extrae la excepción Java del stack trace y reporta la falta de verificación honestamente en lugar de ocultarla. El repositorio de ejemplo construye ambas imágenes, por lo que cada afirmación aquí puede verificarse con dos comandos.

Recursos adicionales