💡 Pełny działający przykład dostępny na GitHubie:
sign-pdf-in-linux-container-fonts-dotnet

Stara metoda była bolesna

Usługa podpisuje faktury. Działa na laptopie z trzystoma zainstalowanymi czcionkami, przechodzi przegląd i w piątek zostaje spakowana do kontenera. W poniedziałek pierwszy proces w klastrze kończy się niezerowym kodem wyjścia z komunikatem Sign document error: Font Arial was not found, a ktoś spędza rano na czytaniu stack trace, zanim ktokolwiek pomyśli zapytać, jakie czcionki naprawdę znajdują się w obrazie mcr.microsoft.com/dotnet/runtime:8.0.

Odpowiedź brzmi: żadnych. Zero plików czcionek, zmierzone w obrazie, w którym uruchamiany jest przykład z tego artykułu.

Warto wiedzieć, jak wypadają inne środowiska uruchomieniowe, ponieważ awaria wygląda inaczej w każdym z nich. eclipse-temurin:17-jre zawiera 8 plików DejaVu, a node:18-bookworm – 6, oba dla AWT, co tłumaczy, dlaczego obrazy JVM i Node cicho podpisują tekst łaciński i zawodzą dopiero, gdy pojawi się ciąg japoński lub chiński. python:3.11-slim nie zawiera żadnych czcionek, podobnie jak obraz .NET runtime, więc zawodzi przy pierwszym podpisie. Nikt nie dostaje czcionek CJK „za darmo” w żadnym z tych obrazów.

Dostarczanie czcionek do kontenera to krok, który umożliwia podpisywanie tekstu w obrazie Linux z użyciem GroupDocs.Signature dla .NET. Ma to znaczenie, ponieważ biblioteka nie podmienia brakującej rodziny: podanie nazwy czcionki, której nie ma, powoduje błąd i nie zapisuje dokumentu. Ten artykuł zestawia obraz bez czcionek z obrazem naprawionym, pokazuje, co się zmieniło, i opisuje rozwiązywanie w czasie uruchomienia, które pozwala używać tego samego kodu na maszynie deweloperskiej.

Istnieje lepszy sposób

Muszą być spełnione dwa warunki. Obraz musi zawierać przynajmniej jedną czcionkę, a kod musi przestać zakładać, że to właśnie ona jest dostępna.

Pierwszy warunek realizuje warstwa w Dockerfile. Drugi to krok rozwiązywania: zamiast na stałe kodować Arial, zapytaj bibliotekę, której z kilku kandydatów rodziny faktycznie może użyć, i zachowaj pierwszą działającą. Wynik działa niezmieniony w cienkim kontenerze, w Windows i w CI, ponieważ nigdy nie zakłada nic o środowisku, którego nie sprawdził.

Jedna rzecz, która nie działa, i warto to jasno zaznaczyć, bo jest pierwszą rzeczą, którą ludzie próbują: pozostawienie czcionki nieustawionej. Bez SignatureFont GroupDocs.Signature domyślnie żąda Times New Roman, której również brakuje w obrazie bez czcionek. Wywołanie kończy się identycznym błędem.

Nowy sposób: dwa obrazy, jedna różnica

Krok 1 – sprawdź, co ma obraz

Zanim coś podpiszesz, wypisz pliki czcionek. Liczba zamienia niejasny wyjątek w diagnozę, ponieważ zero czcionek i błędna nazwa rodziny wymagają różnych poprawek:

string[] roots =
{
    "/usr/share/fonts",
    "/usr/local/share/fonts",
    Path.Combine(home, ".fonts"),
    Path.Combine(home, ".local/share/fonts"),
    Environment.GetFolderPath(Environment.SpecialFolder.Fonts),
    "/System/Library/Fonts",
    "/Library/Fonts",
};

Zauważ, co brakuje: System.Drawing. System.Drawing.Common jest dostępny tylko w Windows od .NET 7 i rzuca wyjątek w Linux, więc kod czcionek oparty na tej bibliotece nie działa w kontenerze z innego powodu.

Krok 2 – dodaj warstwę czcionek

Cztery pakiety, jedno polecenie RUN i problem znika:

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 to resolver i udostępnia fc-list do debugowania. fonts-dejavu-core to minimalny zestaw dla łacińskiego, greckiego i cyrylicy. fonts-liberation dostarcza metrically‑compatible zamienniki dla Arial, Times New Roman i Courier New, czyli tego, na co faktycznie odwołują się dokumenty tworzone w Windows. fonts-noto-cjk obejmuje chiński, japoński i koreański.

Krok 3 – rozwiąż rodzinę zamiast podawać nazwę

Przenośny sposób wyboru czcionki to próba tymczasowego podpisu dla każdego kandydata i zachowanie pierwszego, który nie rzuci wyjątku:

foreach (string candidate in candidates)
{
    if (TryFamily(sourcePath, candidate).Ok)
    {
        return candidate;
    }
}

return null;

Wykrywanie po nazwie pliku to kusząca, ale błędna, skrócona metoda. Pakiet fonts-noto-cjk w Debianie instaluje NotoSansCJK-Regular.ttc, którego nazwa rodziny to Noto Sans CJK JP. Dopasowanie po nazwie pliku pomija czcionki, które są obecne, i zgłasza rodziny, które nie zostaną rozpoznane przez SignatureFont.

Krok 4 – podpisz to, co zostało rozpoznane, i zweryfikuj wynik

Rozpoznana rodzina łacińska jest wymagana; rozpoznana rodzina CJK jest opcjonalna – jej brak oznacza pominięcie, a nie awarię:

var options = new List<SignOptions>
{
    BuildTextOptions(LatinText, latinFamily, top: 50),
};

if (cjkFamily is not null)
{
    options.Add(BuildTextOptions(CjkText, cjkFamily, top: 120));
}

SignResult result = signature.Sign(outputPath, options);

Następnie odczytaj plik, ponieważ CJK bez czcionki CJK może wyświetlać się jako puste kwadraty, nie generując żadnego błędu:

var options = new TextSearchOptions { AllPages = true };
List<TextSignature> found = signature.Search<TextSignature>(options);

Rzut oka: przed i po

Dockerfile.nofonts Dockerfile
Pliki czcionek w obrazie 0 DejaVu, Liberation, Noto CJK
Podpis tekstu łacińskiego niepowodzenie, exit 3 zapisany i odczytany przy weryfikacji
Podpis tekstu CJK niepowodzenie zapisany i odczytany
Wyświetlony błąd Font <name> was not found brak
Różnica w kodzie brak – ten sam binarny brak – ten sam binarny

Ostatni wiersz jest sednem. Nic w aplikacji nie zmieniło się między dwoma uruchomieniami. Repozytorium przykładu zawiera oba pliki, więc porównanie wymaga dwóch poleceń docker build zamiast domysłów. Zachowaj także wariant bez czcionek w repozytorium: to najszybszy sposób odtworzenia awarii, gdy ktoś zmieni bazowy obraz po sześciu miesiącach i podpisy przestaną się pojawiać.

Dlaczego nie zainstalować każdej czcionki?

Ponieważ rozmiar obrazu jest realnym ograniczeniem, a cztery wymienione pakiety już pokrywają najczęstsze scenariusze. fonts-dejavu-core wystarcza do podpisów łacińskich, greckich i cyrylicy; Liberation jest potrzebny, gdy dokumenty odwołują się do rodzin Windows po nazwie; Noto CJK jest jedynym naprawdę dużym pakietem i płaci się za niego tylko wtedy, gdy podpisuje się tekst wschodnioazjatycki. Zainstaluj to, czego potrzebują Twoje dokumenty, a potem zweryfikuj przy odczycie.

Przykład z życia: pracownik wsadowego podpisywania

Pracownik kolejki podpisuje kilka tysięcy PDF‑ów nocą. Dzięki rozwiązywaniu przy starcie, loguje jedną linię z nazwami rodzin, które zamierza używać, i jeśli nic nie zostanie rozpoznane, kończy działanie przed dotknięciem kolejki, zamiast zawodzić przy każdym dokumencie. Ten kontrolny test przy starcie zamienia problem czcionek z ciągu nieudanych zadań w kontener, który odmawia uruchomienia z jednozdaniowym powodem.

Koszt sondowania jest na tyle mały, że można go zignorować przy starcie, a jednocześnie na tyle duży, by nie powtarzać go przy każdym dokumencie. Każde sondowanie to prawdziwy podpis zapisany do pliku tymczasowego, więc lista łacińska kosztuje do czterech takich operacji, a lista CJK do ośmiu, wszystko na jednopaginowym PDF. Rozwiąż raz, zapamiętaj dwie nazwy rodzin i ścieżka per dokument pozostaje taka sama jak wcześniej: buduj opcje, wywołaj Sign, odczytaj liczbę wyników.

Straciłem po południu na wersji, która zgadywała. Przeszukiwała katalog czcionek, znalazła NotoSansCJK-Regular.ttc, zgłosiła CJK jako dostępne i potem zawiodła przy każdej nazwie rodziny wyprowadzonej z tej nazwy pliku. Sondowanie rzeczywistym podpisem było zarówno prostsze, jak i poprawne.

Co jeszcze gryzie w kontenerze?

Jeszcze jedna rzecz, niezwiązana z czcionkami: InvariantGlobalization=true. To standardowa rada przy usuwaniu ICU z obrazu .NET, a w połączeniu z GroupDocs.Signature powoduje, że pierwsze new Signature(...) rzuca CultureNotFoundException: ... en-US is an invalid culture identifier, ponieważ SignatureSettings tworzy CultureInfo("en-US"). Pozostaw globalizację włączoną i pozwól ICU pozostać w obrazie. Strona system requirements to miejsce, gdzie sprawdzić wsparcie platformy przed wyborem obrazu bazowego.

Wnioski

Usługa podpisująca, która działa lokalnie, a w Dockerze nie, prawie zawsze brak jej czcionek, a naprawa polega na warstwie czterech pakietów oraz kodzie, który rozwiązuje rodzinę zamiast zakładać jedną. Zbuduj oba obrazy z przykładu, uruchom je obok siebie i przeczytaj linie [fonts]: cała argumentacja mieści się w tym jednym porównaniu.

Dodatkowe zasoby