💡 完全に動作するサンプルは GitHub にあります:
python-linux-container-pdf-signing

はじめに

このスクリプトはローカルで動作します。python:3.11-slim でコンテナ化すると import groupdocs.signature で失敗します。これを修正すると、今度は最初の署名で失敗します。どちらのエラーも実際に何が足りないかを示していません。

Python でのコンテナ署名は、GroupDocs.Signature のワークフローで、1 つではなく 2 つのプロビジョニング層が必要です。バインディングが構築されている .NET ランタイム ライブラリ層と、テキスト署名が描画するために必要なフォント層です。このチュートリアルでは両方を構築し、実行時にフォント ファミリを解決するスクリプトを作成します。これにより、同じコードがコンテナ内でも、作成したマシン上でも動作します。

両方の層が重要な理由

GroupDocs.Signature for Python は .NET バインディングなので、libicu と OpenSSL 1.1 互換のライブラリがインポートが成功する前に存在していなければなりません。これが第 1 層であり、Running in Docker に詳しく記載されています。

2 つの層が混同されがちなのは、どちらもインポート直前のタイミングで失敗し、エラーが原因を示さないためです。libssl1.1 が欠如していると共有オブジェクトに関するローダーエラーが出ます。フォントが欠如していると、プロキシ例外でラップされた署名エラーが出ます。どちらも「ベースイメージが小さすぎる」ことを示しているわけではありません。

第 2 層はフォントで、こちらが人々を驚かせます。python:3.11-slim にはフォントファイルが一切含まれていません。GroupDocs.Signature は欠如したファミリを代替しません。インストールされていないファミリ名を指定すると例外が発生し、何も書き込まれません。また、フォントをクリアしても回避策にはなりません。ライブラリはデフォルトのフォントを要求し、同様に失敗します。フォントが無いイメージでは、テキスト署名は不可能です。

前提条件

Python 3.11(CPython 3.14 未満のホイール)と groupdocs-signature-net==26.1 が必要です。意図的に両方の失敗を確認したい場合は Docker が必要です(約 10 分で確認できます)。

インストール

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 - フォント層の構築

4 つのパッケージを別の層として保持することで、失敗を再現しやすくしています。

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 層、1 つのラテンフォント、起動時の解決チェックが必須です。このチェックにより、デプロイが失敗したコンテナは起動すらせず、失敗した請求書がキューに溜まることを防げます。顧客名を任意の文字体系で受け付ける文書ポータルは、CJK パッケージと検証ステップも必要です。これが、描画されたボックスと署名された名前の間に唯一残る障壁です。

解決チェックの配置場所

プロセス起動時に一度だけ実行できる場所に置きます。たとえばモジュールレベルの呼び出し、FastAPI のライフスパンハンドラ、Django の AppConfig.ready、またはワーカーのメイン冒頭です。ここから得られるのはラテンファミリと CJK ファミリの 2 つの値で、どちらもフォント数とともに起動ログに記録します。

この配置はプローブ時間の節約以上の効果があります。失敗をリクエスト処理時に起こすと、顧客ごとの問題となりスタックトレースは誰も読まないままです。起動時に失敗すれば、デプロイが起動しなかったことがすぐに分かり、監視者が対応できます。たとえば「使用可能なフォントファミリが無い、fonts-dejavu-core をインストールしてください」というメッセージでコンテナが終了すれば、デバッグは不要です。

よくある問題のトラブルシューティング

import groupdocs.signature が失敗する
.NET 層が欠如しているか、ビルド時にスナップショットリポジトリに到達できなかった可能性があります。これは第 1 層の問題で、フォントとは無関係です。署名コードに手を加える前に、apt のステップのビルドログを確認してください。スナップショット取得に失敗してもイメージはビルドされ続けます。

すべての候補フォントが失敗するが fc-list ではフォントが表示される
font.size が int になっていないか確認してください。

署名はあるが CJK テキストがボックスになる
fonts-noto-cjk が欠如しています。署名は存在しますが、該当コードポイントのグリフが無いファミリで書き込まれたためです。検証ステップはこのケースを捕捉するためにあります。署名は成功したように見えても、検証で失敗します。

2 つのイメージが実際に出力する内容

両方のイメージを実行し、最初の 4 行を確認してください。フォントなしイメージは font files on disk: 0、解決行はすべて (none)、欠如フォントエラーが表示され、最小修正を示すメッセージで終了します。プロビジョニング済みイメージはフォント数が 0 でなく、ラテンは DejaVu Sans、CJK は Noto Sans CJK JP、署名が 2 件適用され、両テキストが検証されます。

この出力ペアは重要なアーティファクトです。デプロイノートに貼り付けておけば、次にベースイメージを変更した人が「健康なコンテナはこういうものだ」とすぐに参照できます。fontconfig の内部を理解する必要はありません。

結論

2 つの層と 1 つのプローブです。.NET 依存関係をインストールし、最低でも fontconfig と DejaVu をインストールし、ファミリは推測せずに問い合わせて解決し、ジョブ完了とみなす前に出力を検証します。コード量は少なく、すべてが「後から見れば当然」でも「スタックトレースでは見えない」ものです。サンプルリポジトリには両方の Dockerfile が同梱されているので、動作するイメージと壊れたイメージの違いはビルドが 1 回違うだけです。

追加リソース