💡 完整的工作範例可在 GitHub 上取得:
python-linux-container-pdf-signing

介紹

此腳本在本機可以正常執行。將它容器化於 python:3.11-slim 時,會在 import groupdocs.signature 失敗。修正後,第一個簽章仍會失敗。兩個錯誤都沒有說明實際缺少了什麼。

使用 Python 進行容器簽章是 GroupDocs.Signature 的工作流程,需要兩個佈建層而非一層:綁定所依賴的 .NET 執行時函式庫,以及每個文字簽章必須使用的字型。此教學同時建構這兩個層,並提供在執行時解析字型家族的腳本(而非硬編碼),讓相同程式碼能在容器與開發機上皆能運作。

為何兩層都很重要

GroupDocs.Signature for Python 是 .NET 綁定,因此在任何匯入成功之前,必須先有 libicu 與相容 OpenSSL 1.1 的函式庫。這是第一層,相關說明可見於 Running in Docker。

兩層常被混為一談的原因是,兩者都會在匯入相關時失敗,且錯誤訊息都未指明原因。缺少 libssl1.1 會出現共享物件的載入錯誤;缺少字型則會出現被代理例外包住的簽章錯誤。兩者都不會說「你的基礎映像太小」,而這正是實際情況。

第二層是字型,這層最常讓人感到意外。python:3.11-slim 完全不含任何字型檔案。GroupDocs.Signature 不會自動替代缺失的字型家族——指定未安裝的字型會拋出例外,且不會寫入任何內容——而清除字型也不是解法,因為函式庫會再度要求預設字型,結果仍然失敗。在沒有字型的映像中,文字簽章根本無法完成。

前置需求

  • Python 3.11(wheel 最高支援 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。綁定會將 size 轉為 .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 套件與驗證步驟,因為這是防止渲染成方塊卻仍被視為簽章成功的唯一機制。

解析檢查的放置位置

將檢查放在每個程序只會執行一次的地方:模組層級呼叫、FastAPI 的 lifespan handler、Django 的 AppConfig.ready,或是 worker 主程式的前幾行。檢查會回傳兩個值:拉丁字型家族與 CJK 字型家族,並將它們寫入啟動日誌,與字型數量一起列出。

這樣的放置方式不僅節省探測時間,還能把失敗從請求處理(客戶問題、沒人看的堆疊追蹤)移到啟動階段(部署未成功、有人在監控)。若容器因「找不到可用的字型家族,請安裝 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、CJK 為 Noto Sans CJK JP,兩個簽章皆已套用,且兩段文字皆驗證通過。

將這對輸出貼到部署說明中,之後有人更換基礎映像時,只要比對這個參考,即可快速判斷容器是否健康,無需深入了解 fontconfig。

結論

兩層加一次探測。先安裝 .NET 相依套件,再安裝至少 fontconfig 與 DejaVu,透過詢問而非假設取得可用字型家族,最後在完成工作前驗證輸出。程式碼量不多,但這些步驟在事後回顧時顯而易見,卻在追蹤錯誤時幾乎不可見。範例倉庫同時提供兩個 Dockerfile,讓「可用映像」與「壞的映像」只差一步建置。

其他資源