💡 Полный рабочий пример доступен на GitHub:
python-linux-container-pdf-signing

Введение

Скрипт работает локально. Вы контейнеризуете его на python:3.11-slim, и он падает на import groupdocs.signature. Вы исправляете это, и он снова падает на первой подписи. Ни одна из ошибок не указывает, чего именно не хватает.

Подписание в контейнере с помощью Python — это workflow GroupDocs.Signature, который требует два уровня подготовки вместо одного: библиотеки среды выполнения .NET, на которой построена привязка, и шрифты, которые нужен каждый текстовой подписи для рендеринга. Этот учебник собирает оба уровня, а затем скрипт, который определяет семейство шрифтов во время выполнения вместо жёстко заданного, так что один и тот же код работает и в контейнере, и на машине, где вы его писали.

Почему важны оба уровня

GroupDocs.Signature для Python — это привязка к .NET, поэтому перед тем как любой импорт завершится успешно, должны быть установлены libicu и библиотека, совместимая с OpenSSL 1.1. Это первый уровень, и он хорошо задокументирован в статье Running in Docker.

Причина, по которой два уровня часто смешивают, в том, что оба дают ошибки рядом с импортом и ни одна из них не указывает причину. Отсутствие libssl1.1 приводит к ошибке загрузчика о недоступном общем объекте; отсутствие шрифта приводит к ошибке подписи, обёрнутой в прокси‑исключение. Ни одна из них не говорит «ваш базовый образ слишком маленький», хотя именно это и происходит.

Второй уровень — шрифты, и именно он удивляет людей. В python:3.11-slim нет ни одного шрифта. GroupDocs.Signature не подставляет недостающее семейство — указание неустановленного семейства вызывает исключение, и ничего не записывается — а очистка шрифта тоже не помогает, потому что библиотека затем запрашивает свой собственный шрифт по умолчанию и падает точно так же. На образе без шрифтов текстовая подпись просто невозможна.

Предварительные требования

Python 3.11 (колёсные файлы ниже CPython 3.14) и groupdocs-signature-net==26.1. Docker, если хотите увидеть обе ошибки намеренно, что займет около десяти минут.

Установка

pip install groupdocs-signature-net==26.1

Шаг 1 — Сборка .NET‑уровня

libssl1.1 отсутствует в bookworm, поэтому берём его из зафиксированного снимка Debian:

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/*

Ключевые моменты:

  • Этот слой лишь делает импорт рабочим; он ничего не говорит о шрифтах.
  • Фиксация даты снимка делает сборку воспроизводимой, когда архив меняется.

Шаг 2 — Сборка слоя шрифтов

Четыре пакета, оставленные в отдельном слое, чтобы их можно было закомментировать и воспроизвести ошибку:

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 — это резольвер, он предоставляет fc-list. fonts-dejavu-core — минимум для латиницы, греческого и кириллицы. fonts-liberation покрывает документы, в которых по имени указываются Arial или Times New Roman. fonts-noto-cjk покрывает китайский, японский и корейский.

Шаг 3 — Запрос к библиотеке, какое семейство она может использовать

Сканирование /usr/share/fonts на наличие файла выглядит эквивалентным, но не является таковым: fonts-noto-cjk устанавливает NotoSansCJK-Regular.ttc, семейство которого — Noto Sans CJK JP. Портативный ответ — проба — реальная подпись в временный файл — с преобразованием ошибки в значение:

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

Обратите внимание на font.size = 10.0. Привязка преобразует размер в .NET float и отклоняет int с сообщением numeric argument expected, got 'int'. Поскольку это происходит внутри пробы, каждая кандидатная семья падает, и вывод выглядит точно так же, как на образе без шрифтов. Я добавил три пакета шрифтов в образ, в котором они уже были, прежде чем заметил эту букву.

Дальше решение реализуется в виде цикла:

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

Шаг 4 — Подписываем то, что удалось определить, проверяем результат

Латинское семейство обязательно, CJK — опционально:

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)

Затем проверяем, потому что CJK, отрисованный пустыми квадратами, не вызывает исключения:

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

CONTAINS выбран намеренно: в режиме оценки библиотека добавляет пробный текст на страницу, и точное совпадение объявило бы полностью корректный документ как проваленный.

Что насчёт документации, где говорится, что у Python ограниченная поддержка Linux?

Страница Running in Docker перечисляет готовые к использованию Linux‑пакеты Python и не упоминает Signature. На groupdocs-signature-net==26.1 этот пример подписывал и проверялся внутри python:3.11-slim, включая CJK, при установленном обоих уровнях. Считайте список устаревшим, а не препятствием, и проверьте свою версию перед тем, как фиксировать её в развертывании.

Реальные сценарии применения

Сервис выставления счетов, который ставит строку одобрения на генерируемые PDF, нуждается именно в этом: .NET‑слой, один латинский шрифт и проверку разрешения при старте. Эта проверка превращает плохое развертывание в контейнер, который отказывается запускаться, а не в очередь счетов, тихо падающих один за другим. Портал документов, принимающий имена клиентов на любом письме, также требует пакет CJK и шаг проверки, потому что только они отделяют отрисованный квадрат от подписанного имени.

Где разместить проверку разрешения

Поместите её туда, где код исполняется один раз за процесс: вызов на уровне модуля, обработчик lifespan в FastAPI, AppConfig.ready в Django или первые строки main воркера. Функция возвращает два значения — латинское семейство и CJK‑семейство, — и оба должны быть записаны в журнал старта рядом с количеством шрифтов.

Такой подход экономит время пробы и перемещает ошибку из обработки запросов (проблема одного клиента, стек‑трейс, который никто не читает) в стартовый этап, когда это уже развертывание, которое не поднялось, и кто‑то уже наблюдает. Контейнер, завершающийся с сообщением «no usable font family, install fonts-dejavu-core», не требует отладки.

Устранение распространённых проблем

import groupdocs.signature не проходит
Отсутствует .NET‑слой или репозиторий снимка был недоступен во время сборки. Это первый уровень, он не связан с шрифтами. Проверьте журнал сборки на этапе apt до того, как будете трогать код подписи, потому что неудачная загрузка снимка не останавливает построение образа.

Каждый кандидат шрифта падает, но fc-list показывает шрифты
Убедитесь, что font.size не является int перед добавлением новых пакетов.

Подпись есть, но CJK‑текст отображается квадратами
Отсутствует fonts-noto-cjk. Подпись была записана семейством, у которого нет глифов для этих кодовых точек, поэтому шаг проверки существует именно для такого случая, когда подпись сообщает об успехе, а визуально ничего нет.

Что действительно выводят два образа

Запустите оба и посмотрите первые четыре строки. Образ без шрифтов выводит font files on disk: 0, обе строки разрешения как (none), ошибку о недостающем шрифте и завершает работу с кодом 3, напечатав минимальное исправление. Образ с установленными зависимостями выводит ненулевое количество шрифтов, DejaVu Sans для латиницы и Noto Sans CJK JP для CJK, две применённые подписи и подтверждённые оба текста.

Эти два вывода — артефакт, который стоит сохранить. Вставьте их в свои заметки о развертывании, и у следующего человека, меняющего базовый образ, будет ссылка на то, как выглядит здоровый контейнер, без необходимости разбираться в fontconfig.

Заключение

Два уровня и одна проба. Установите .NET‑зависимости, установите хотя бы fontconfig и DejaVu, определите семейство, запрашивая его, а не предполагая, и проверьте результат перед тем, как объявить задачу выполненной. Всё это — небольшое количество кода, но именно такие вещи очевидны задним числом и незаметны в трассировке стека. Репозиторий‑пример поставляет оба Dockerfile, так что разница между рабочим и сломанным образом — один билд.

Дополнительные ресурсы