💡 Ejemplo completo disponible en GitHub:
python-linux-container-pdf-signing

Introducción

El script funciona localmente. Lo contenedorizas en python:3.11-slim y falla en import groupdocs.signature. Lo corriges y vuelve a fallar en la primera firma. Ningún error menciona lo que realmente falta.

La firma en contenedores con Python es un flujo de trabajo de GroupDocs.Signature que necesita dos capas de aprovisionamiento en lugar de una: las bibliotecas de tiempo de ejecución de .NET sobre las que se construye el enlace, y las fuentes que toda firma de texto necesita para renderizarse. Este tutorial construye ambas, luego el script que resuelve una familia de fuentes en tiempo de ejecución en lugar de codificar una de forma estática, de modo que el mismo código funciona tanto en el contenedor como en la máquina donde lo escribiste.

Por qué ambas capas importan

GroupDocs.Signature para Python es un enlace de .NET, por lo que libicu y una biblioteca compatible con OpenSSL 1.1 deben existir antes de que cualquier importación tenga éxito. Esa es la capa uno, y está bien documentada en Running in Docker.

La razón por la que se confunden ambas capas es que ambas fallan en momentos cercanos a la importación y ninguno de los errores nombra su causa. Una libssl1.1 ausente genera un error de cargador sobre un objeto compartido; una fuente faltante genera un error de firma envuelto en una excepción proxy. Ninguno dice “tu imagen base es demasiado pequeña”, que es lo que realmente significan ambos.

La capa dos son las fuentes, y es la que sorprende a la gente. python:3.11-slim no contiene archivos de fuentes. GroupDocs.Signature no sustituye una familia faltante: nombrar una que no está instalada genera una excepción, y nada se escribe; y eliminar la fuente no es una solución alternativa, porque la biblioteca entonces solicita su propia fuente predeterminada y falla de la misma manera. En una imagen sin fuentes, una firma de texto es simplemente imposible.

Requisitos previos

Python 3.11 (la rueda está limitada a CPython 3.14) y groupdocs-signature-net==26.1. Docker si deseas ver ambos fallos a propósito, lo cual vale diez minutos.

Instalación

pip install groupdocs-signature-net==26.1

Paso 1 – Construir la capa .NET

libssl1.1 no está en bookworm, por lo que se obtiene de una instantánea de Debian fijada:

ENV SNAPSHOT_DATE=20220328T000000Z
RUN echo "deb [trusted=yes] http://snapshot.debian.org/archive/debian/${SNAPSHOT_DATE} bullseye main" \
        > /etc/apt/sources.list.d/debian-archive.list \
    && apt-get -o Acquire::Check-Valid-Until=false update \
    && apt-get install -y --no-install-recommends \
        libicu67 \
        libssl1.1 \
    && apt-get clean && rm -rf /var/lib/apt/lists/*

Puntos clave:

  • Esta capa solo hace que la importación funcione; no dice nada sobre fuentes.
  • Fijar la fecha de la instantánea mantiene la compilación reproducible cuando el archivo cambia.

Paso 2 – Construir la capa de fuentes

Cuatro paquetes, mantenidos como su propia capa para que puedan comentarse y reproducir el fallo:

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 te brinda fc-list. fonts-dejavu-core cubre el mínimo latino, griego y cirílico. fonts-liberation cubre documentos que hacen referencia a Arial o Times New Roman por nombre. fonts-noto-cjk cubre chino, japonés y coreano.

Paso 3 – Preguntar a la biblioteca qué familia puede usar

Escanear /usr/share/fonts en busca de un nombre de archivo parece equivalente y no lo es: fonts-noto-cjk instala NotoSansCJK-Regular.ttc, cuyo nombre de familia es Noto Sans CJK JP. La respuesta portátil es una prueba – una firma real en un archivo temporal – con el fallo convertido en un valor:

with signature.Signature(source_path) as sign:
    options = TextSignOptions()
    options.text = "probe"
    options.left = 10
    options.top = 10
    options.width = 60
    options.height = 20
    font = SignatureFont()
    font.family_name = family_name
    font.size = 10.0
    options.font = font
    sign.sign(scratch, [options])
return None

Observa detenidamente font.size = 10.0. El enlace mapea el tamaño a un float de .NET y rechaza un entero con numeric argument expected, got 'int'. Como eso ocurre dentro de la prueba, cada familia candidata falla y la salida se ve exactamente como una imagen sin fuentes. Añadí tres paquetes de fuentes a una imagen que ya los tenía antes de notar el literal.

La resolución entonces es un bucle:

for candidate in candidates:
    if try_family(source_path, candidate) is None:
        return candidate
return None

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

La familia latina es obligatoria, la CJK opcional:

with signature.Signature(source_path) as sign:
    options = [build_text_options(LATIN_TEXT, latin_family, 50)]
    if cjk_family:
        options.append(build_text_options(CJK_TEXT, cjk_family, 120))
    result = sign.sign(output_path, options)
    return len(result.succeeded)

Luego verifica, porque CJK renderizado como cajas vacías no genera ninguna excepción:

options = TextVerifyOptions()
options.text = expected_text
options.match_type = gsd.TextMatchType.CONTAINS
options.all_pages = True
result = sign.verify(options)

CONTAINS es deliberado: en modo de evaluación la biblioteca agrega texto de prueba a la página, y una coincidencia exacta reportaría un documento perfectamente válido como fallido.

¿Qué pasa con la documentación que dice que Python tiene soporte limitado en Linux?

La página Running in Docker enumera paquetes de Python listos para Linux y deja fuera Signature. Con groupdocs-signature-net==26.1 este ejemplo firmó y verificó dentro de python:3.11-slim, incluyendo CJK, con ambas capas instaladas. Considera la lista como obsoleta más que como un obstáculo, y confirma con tu propia versión antes de comprometerte a un despliegue.

Aplicaciones del mundo real

Un servicio de facturación que estampa una línea de aprobación sobre PDFs generados necesita exactamente esto: la capa .NET, una fuente latina y una comprobación de resolución al iniciar. Esa comprobación es lo que convierte un despliegue defectuoso en un contenedor que se niega a arrancar, en lugar de una cola de facturas que fallan silenciosamente una a una. Un portal de documentos que acepta nombres de clientes en cualquier escritura necesita también el paquete CJK, más el paso de verificación, porque es lo único que separa una caja renderizada de un nombre firmado.

Dónde pertenece la comprobación de resolución

Colócala donde se ejecute una sola vez por proceso: una llamada a nivel de módulo, un manejador de ciclo de vida de FastAPI, un AppConfig.ready de Django, o las primeras líneas del main de un trabajador. De ella salen dos valores, la familia latina y la familia CJK, y ambos deben aparecer en el registro de inicio junto al recuento de fuentes.

Esa ubicación hace más que ahorrar tiempo de prueba. Mueve el fallo del manejo de la solicitud, donde es problema de un cliente y una traza que nadie lee, al inicio, donde se trata de un despliegue que no arrancó y alguien ya está observando. Un contenedor que finaliza con “no usable font family, install fonts-dejavu-core” no necesita depuración alguna.

Solución de problemas de problemas comunes

import groupdocs.signature falla
La capa .NET falta o el repositorio de instantáneas no estuvo disponible durante la compilación. Esta es la capa uno y no tiene nada que ver con fuentes. Revisa el registro de compilación para el paso apt antes de tocar cualquier código de firma, porque una descarga de instantánea fallida no impide que la imagen se construya.

Cada fuente candidata falla, pero fc-list muestra fuentes
Verifica que font.size no sea un entero antes de añadir más paquetes.

La firma está, pero el texto CJK aparecen como cajas
Falta fonts-noto-cjk. La firma se escribió con una familia que no tiene glifos para esos puntos de código, por eso existe el paso de verificación: falla exactamente en este caso, donde la firma reportó éxito.

Qué imprimen realmente las dos imágenes

Ejecuta ambas y lee las primeras cuatro líneas. La imagen sin fuentes informa font files on disk: 0, ambas líneas de resolución como (none), el error deliberado de fuente faltante y luego sale con código 3 mostrando la corrección mínima. La imagen aprovisionada informa un recuento de fuentes distinto de cero, DejaVu Sans para latino y Noto Sans CJK JP para CJK, dos firmas aplicadas y ambos textos verificados.

Ese par de salidas es el artefacto que vale la pena conservar. Pégalo en tus notas de despliegue y la próxima persona que cambie la imagen base tendrá una referencia de cómo se ve un contenedor saludable, sin necesidad de entender fontconfig.

Conclusión

Dos capas y una prueba. Instala las dependencias de .NET, instala al menos fontconfig y DejaVu, resuelve la familia preguntando en lugar de asumir, y verifica la salida antes de dar por terminado el trabajo. No es mucho código, y todo es del tipo de cosas que parecen obvias en retrospectiva e invisibles en una traza. El repositorio de ejemplo incluye ambos Dockerfiles, de modo que la diferencia entre una imagen funcional y una rota está a un build de distancia.

Recursos adicionales