💡 Voll funktionsfähiges Beispiel auf GitHub verfügbar:
sign-pdf-in-linux-container-fonts-dotnet
Der alte Weg war schmerzhaft
Der Service signiert Rechnungen. Er läuft auf einem Laptop mit dreihundert installierten Schriftarten, besteht die Prüfung und wird an einem Freitag containerisiert. Am Montag beendet der erste Job im Cluster mit einem Nicht‑Null‑Exit‑Code und der Meldung Sign document error: Font Arial was not found, und jemand verbringt den Morgen damit, Stack‑Traces zu lesen, bevor jemand fragt, welche Schriftarten ein mcr.microsoft.com/dotnet/runtime:8.0‑Image tatsächlich enthält.
Die Antwort lautet: keine. Null Schriftdateien, gemessen im Image, in dem das Beispiel dieses Artikels läuft.
Es ist nützlich zu wissen, wie die anderen Laufzeiten im Vergleich abschneiden, weil das Versagen in jedem Fall anders aussieht. eclipse-temurin:17-jre enthält 8 DejaVu‑Dateien und node:18-bookworm enthält 6, beide für AWT, weshalb JVM‑ und Node‑Images lateinischen Text still signieren und nur scheitern, wenn ein japanischer oder chinesischer String auftaucht. python:3.11-slim liefert null, genau wie das .NET‑Runtime‑Image, sodass es beim ersten Signaturversuch fehlschlägt. Niemand bekommt CJK kostenlos in einem dieser Images.
Die Bereitstellung von Schriftarten im Container ist der Schritt, der das Signieren von Text in einem Linux‑Image mit GroupDocs.Signature für .NET ermöglicht. Es ist wichtig, weil die Bibliothek keine fehlende Familie substituiert: Das Nennen einer nicht installierten Schriftart löst einen Fehler aus und erzeugt kein Dokument. Dieser Artikel stellt das schrifflos‑Image dem korrigierten gegenüber, zeigt, was sich geändert hat, und behandelt die Laufzeit‑Auflösung, die denselben Code auf einer Entwickler‑Maschine funktionieren lässt.
Es gibt einen besseren Weg
Zwei Dinge müssen wahr sein. Das Image benötigt mindestens eine Schriftart, und der Code muss aufhören, eine bestimmte anzunehmen.
Das erste ist eine Dockerfile‑Ebene. Das zweite ist ein Auflösungsschritt: Anstatt Arial fest zu codieren, fragt man die Bibliothek, welche der mehreren Kandidatenfamilien sie tatsächlich verwenden kann, und behält die erste, die funktioniert. Das Ergebnis läuft unverändert in einem schlanken Container, unter Windows und in CI, weil es nie Annahmen über die Umgebung trifft, die nicht geprüft wurden.
Eine Sache, die nicht funktioniert – und die man klar benennen sollte, weil sie das Erste ist, was Leute versuchen – ist das Weglassen der Schriftart. Ohne SignatureFont fragt GroupDocs.Signature nach seiner eigenen Vorgabe, Times New Roman, die im schrifflosen Image ebenfalls fehlt. Der Aufruf schlägt identisch fehl.
Der neue Weg: Zwei Images, ein Unterschied
Schritt 1 – Schau, was das Image enthält
Bevor irgendetwas signiert wird, listet man die Schriftdateien auf. Die Anzahl verwandelt eine vage Ausnahme in eine Diagnose, weil null Schriftarten und ein falscher Familienname unterschiedliche Behebungen benötigen:
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",
};
Beachte, was fehlt: System.Drawing. System.Drawing.Common ist ab .NET 7 nur für Windows verfügbar und wirft unter Linux, sodass auf ihm basierender Schriftcode im Container aus einem zweiten, nicht verwandten Grund fehlschlägt.
Schritt 2 – Die Schrift‑Ebene hinzufügen
Vier Pakete, ein RUN, und das Versagen verschwindet:
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 ist der Resolver und liefert dir fc-list zum Debuggen. fonts-dejavu-core ist das Minimum für Lateinisch, Griechisch und Kyrillisch. fonts-liberation stellt metrisch kompatible Ersatzschriften für Arial, Times New Roman und Courier New bereit – genau das, worauf Dokumente, die unter Windows erstellt wurden, verweisen. fonts-noto-cjk deckt Chinesisch, Japanisch und Koreanisch ab.
Schritt 3 – Eine Familie auflösen statt eine zu benennen
Der portable Weg, eine Schrift zu wählen, besteht darin, pro Kandidat eine Wegwerf‑Signatur zu versuchen und die erste zu behalten, die keinen Fehler wirft:
foreach (string candidate in candidates)
{
if (TryFamily(sourcePath, candidate).Ok)
{
return candidate;
}
}
return null;
Die Erkennung über Dateinamen ist die verlockende Abkürzung und sie ist falsch. Das Debian‑Paket fonts-noto-cjk installiert NotoSansCJK-Regular.ttc, dessen Familienname Noto Sans CJK JP lautet. Ein Dateinamen‑Match verpasst vorhandene Schriften und behauptet Familien, die nicht aufgelöst werden können, wenn sie an SignatureFont übergeben werden.
Schritt 4 – Signiere das Aufgelöste, prüfe das Signierte
Eine aufgelöste lateinische Familie ist erforderlich; eine aufgelöste CJK‑Familie ist optional und ihr Fehlen bedeutet ein Überspringen, keinen Absturz:
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);
Dann die Datei wieder einlesen, weil CJK ohne CJK‑Schriftart als leere Kästchen rendern kann, ohne überhaupt einen Fehler zu melden:
var options = new TextSearchOptions { AllPages = true };
List<TextSignature> found = signature.Search<TextSignature>(options);
Seite an Seite: Vorher vs. Nachher
Dockerfile.nofonts |
Dockerfile |
|
|---|---|---|
| Schriftdateien im Image | 0 | DejaVu, Liberation, Noto CJK |
| Lateinische Textsignatur | schlägt fehl, Exit 3 | geschrieben und beim Einlesen wiedergefunden |
| CJK‑Textsignatur | schlägt fehl | geschrieben und beim Einlesen wiedergefunden |
| Fehler aufgetreten | Font <name> was not found |
keiner |
| Code‑Unterschied | keiner – gleiche Binärdatei | keiner – gleiche Binärdatei |
Die letzte Zeile ist der Punkt. Nichts an der Anwendung hat sich zwischen den beiden Durchläufen geändert. Das Beispiel‑Repository liefert beide Dateien, sodass der Vergleich zwei docker build‑Aufrufe erfordert, anstatt zu vertrauen. Bewahre die schrifflose Variante im Repository ebenfalls auf: Sie ist der schnellste Weg, das Versagen zu reproduzieren, wenn jemand sechs Monate später das Basis‑Image wechselt und die Signaturen plötzlich nicht mehr erscheinen.
Warum nicht einfach jede Schrift installieren?
Weil die Image‑Größe ein echter Engpass ist und die vier Pakete oben bereits die Skripte abdecken, die die meisten Dokumente benötigen. fonts-dejavu-core allein reicht für Lateinisch, Griechisch und Kyrillisch; Liberation ist wichtig, wenn Dokumente die Windows‑Familienamen verwenden; Noto CJK ist das, was wirklich groß ist und nur dann Kosten verursacht, wenn du ostasiatischen Text signierst. Installiere, was deine Dokumente benötigen, und prüfe dann mit einem Einlesevorgang.
Praxisbeispiel: Der Batch‑Signing‑Worker
Ein Queue‑Worker signiert nachts einige tausend PDFs. Mit Auflösung beim Start protokolliert er eine Zeile, in der die Familien genannt werden, die er verwenden wird, und wenn nichts aufgelöst wird, beendet er sich, bevor er die Queue berührt, anstatt pro Nachricht zu scheitern. Diese Start‑Prüfung verwandelt ein Schrift‑Problem von einer Flut gescheiterter Jobs in einen Container, der sich mit einer einzeiligen Begründung weigert zu starten.
Der Aufwand für das Proben ist klein genug, um beim Start ignoriert zu werden, und zu groß, um ihn pro Dokument zu wiederholen. Jeder Probe‑Durchlauf ist eine echte Signatur, die in eine temporäre Datei geschrieben wird, sodass die lateinische Liste bis zu vier davon kostet und die CJK‑Liste bis zu acht, alles gegen ein einseitiges PDF. Einmal auflösen, die beiden Familiennamen cachen, und der Pfad pro Dokument ist exakt derselbe wie vorher: Optionen bauen, Sign aufrufen, Ergebnis‑Anzahl lesen.
Ich habe einen Nachmittag damit verloren, die Version zu benutzen, die rät. Sie durchsuchte das Schrift‑Verzeichnis, fand NotoSansCJK-Regular.ttc, meldete CJK als verfügbar und scheiterte dann an jedem Familiennamen, den ich aus diesem Dateinamen ableitete. Das Proben mit einer echten Signatur war sowohl einfacher als auch korrekt.
Was sonst noch in einem Container Probleme verursacht?
Noch etwas, das nichts mit Schriftarten zu tun hat: InvariantGlobalization=true. Das ist ein gängiger Ratschlag, um ICU aus einem .NET‑Image zu trimmen, und mit GroupDocs.Signature lässt es den allerersten new Signature(...) eine CultureNotFoundException: ... en-US is an invalid culture identifier werfen, weil SignatureSettings ein CultureInfo("en-US") erzeugt. Lass die Globalisierung aktiviert und lass ICU im Image bleiben. Die Seite mit den Systemanforderungen ist der Ort, um die Plattform‑Unterstützung zu prüfen, bevor du dich auf ein Basis‑Image festlegst.
Fazit
Ein Signatur‑Service, der lokal funktioniert und in Docker scheitert, fehlt fast immer an Schriftarten, und die Lösung besteht aus einer vier‑Paket‑Ebene plus Code, der eine Familie auflöst, anstatt eine anzunehmen. Baue beide Images aus dem Beispiel, führe sie nebeneinander aus und lies die [fonts]‑Zeilen: Das gesamte Argument passt in diesen einen Vergleich.