💡 Повний робочий приклад доступний на GitHub:
python-linux-container-pdf-signing
Вступ
Скрипт працює локально. Ви контейнеризуєте його на python:3.11-slim, і він падає на import groupdocs.signature. Ви виправляєте це, і він знову падає на першому підписі. Жодна з помилок не вказує, чого саме не вистачає.
Підписання в контейнері за допомогою Python — це робочий процес GroupDocs.Signature, який потребує двох шарів підготовки, а не одного: бібліотек .NET‑runtime, на яких побудовано прив’язку, і шрифтів, якими має рендеритися кожний текстовий підпис. У цьому посібнику ми створюємо обидва шари, а потім скрипт, який визначає сімейство шрифту під час виконання замість жорсткого кодування, тож той самий код працює і в контейнері, і на машині, де його написано.
Чому важливі обидва шари
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, тому різниця між робочим і поламаним образом — це один крок збірки.