💡 Полный рабочий пример доступен на 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, так что разница между рабочим и сломанным образом — один билд.