💡 Volledig werkend voorbeeld beschikbaar op GitHub:
sign-pdf-in-linux-container-fonts-dotnet

De oude manier was pijnlijk

De service ondertekent facturen. Hij draait op een laptop met driehonderd fonts geïnstalleerd, doorloopt de review en wordt op een vrijdag in een container geplaatst. Op maandag geeft de eerste taak in het cluster een non‑zero exit met Sign document error: Font Arial was not found, en iemand besteedt de ochtend aan het lezen van stacktraces voordat iemand zich afvraagt welke fonts een mcr.microsoft.com/dotnet/runtime:8.0‑image eigenlijk bevat.

Het antwoord is geen. Nul fontbestanden, gemeten in de image waarin het voorbeeld van dit artikel draait.

Het is de moeite waard om te weten hoe de andere runtimes zich verhouden, want de fout ziet er per runtime anders uit. eclipse-temurin:17-jre bundelt 8 DejaVu‑bestanden en node:18-bookworm bundelt 6, beide voor AWT, waardoor JVM‑ en Node‑images Latijnse tekst stil ondertekenen en alleen falen wanneer een Japanse of Chinese string verschijnt. python:3.11-slim levert nul, net als de .NET‑runtime‑image, dus faalt hij bij de eerste handtekening. Niemand krijgt CJK gratis in een van deze images.

Container‑font‑provisioning is de stap die tekstondertekening laat werken in een Linux‑image met GroupDocs.Signature voor .NET. Het is belangrijk omdat de bibliotheek geen ontbrekende familie vervangt: een font benoemen dat niet geïnstalleerd is veroorzaakt een fout en schrijft geen document. Dit artikel zet de font‑loze image naast de gefixeerde, laat zien wat er veranderd is, en behandelt de runtime‑resolutie die dezelfde code op een ontwikkelmachine laat werken.

Er is een betere manier

Twee dingen moeten waar zijn. De image heeft minstens één font nodig, en de code moet stoppen met aannemen welk font dat is.

Het eerste is een Dockerfile‑laag. Het tweede is een resolutiestap: in plaats van hard‑coded Arial vraag je de bibliotheek welke van de verschillende kandidaat‑families hij daadwerkelijk kan gebruiken, en houd je de eerste die werkt. Het resultaat draait ongewijzigd in een slanke container, op Windows en in CI, omdat het nooit iets beweert over de omgeving die niet is gecontroleerd.

Één ding dat niet werkt, en het is het waard om duidelijk te stellen omdat het het eerste is wat mensen proberen: het font oningesteld laten. Zonder SignatureFont vraagt GroupDocs.Signature om zijn eigen standaard, Times New Roman, die de font‑loze image ook mist. De oproep faalt identiek.

De nieuwe manier: twee images, één verschil

Stap 1 - Bekijk wat de image bevat

Voordat je iets ondertekent, lijst je de fontbestanden op. Het aantal maakt van een vage uitzondering een diagnose, omdat nul fonts en een verkeerde familienaam verschillende oplossingen vereisen:

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",
};

Let op wat ontbreekt: System.Drawing. System.Drawing.Common is vanaf .NET 7 Windows‑only en gooit een fout op Linux, dus fontcode die daarop is gebouwd faalt in de container om een tweede, niet‑gerelateerde reden.

Stap 2 - Voeg de fontlaag toe

Vier pakketten, één RUN, en de fout verdwijnt:

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 is de resolver en geeft je fc-list voor debugging. fonts-dejavu-core is het minimum voor Latijn, Grieks en Cyrillisch. fonts-liberation levert metrisch‑compatibele vervangers voor Arial, Times New Roman en Courier New, wat documenten die op Windows zijn gemaakt daadwerkelijk refereren. fonts-noto-cjk dekt Chinees, Japans en Koreaans.

Stap 3 - Los een familie op in plaats van er één te benoemen

De draagbare manier om een font te kiezen is om per kandidaat een wegwerphandtekening te proberen en de eerste te behouden die geen uitzondering gooit:

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

return null;

Bestandsnaamdetectie is de verleidelijke shortcut en is onjuist. Debian’s fonts-noto-cjk installeert NotoSansCJK-Regular.ttc, waarvan de familienaam Noto Sans CJK JP is. Een overeenkomst op bestandsnaam mist fonts die aanwezig zijn en claimt families die niet resolven wanneer ze aan SignatureFont worden doorgegeven.

Stap 4 - Onderteken wat is opgelost, verifieer wat je hebt ondertekend

Een opgeloste Latijn‑familie is vereist; een opgeloste CJK‑familie is optioneel en afwezigheid betekent een overslaan, geen crash:

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);

Lees daarna het bestand terug, want CJK zonder een CJK‑font kan renderen als lege vakjes zonder überhaupt iets te melden:

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

Zij-aan-zij: vóór vs. na

Dockerfile.nofonts Dockerfile
Fontbestanden in de image 0 DejaVu, Liberation, Noto CJK
Latijnse teksthandtekening faalt, exit 3 geschreven en teruggelezen
CJK-teksthandtekening faalt geschreven en teruggelezen
Fout weergegeven Font <name> was not found geen
Codeverschil geen - dezelfde binary geen - dezelfde binary

De laatste rij is het punt. Niets in de applicatie veranderde tussen de twee runs. Het voorbeeld‑repository levert beide bestanden zodat de vergelijking twee docker build‑commando’s vereist in plaats van vertrouwen. Houd de font‑loze variant daarna ook in het repository: het is de snelste manier om de fout te reproduceren wanneer iemand zes maanden later de basis‑images wisselt en de handtekeningen stilletjes verdwijnen.

Waarom niet gewoon elke font installeren?

Omdat de image‑grootte een reëel beperking is en de vier pakketten hierboven al de scripts dekken die de meeste documenten gebruiken. fonts-dejavu-core alleen is voldoende voor Latijn, Grieks en Cyrillisch ondertekenen; Liberation is belangrijk wanneer documenten de Windows‑families bij naam refereren; Noto CJK is degene die echt groot is en alleen zichzelf betaalt als je Oost‑Aziaanse tekst ondertekent. Installeer wat jouw documenten nodig hebben, en verifieer daarna met een teruglezen.

Praktijkvoorbeeld: de batch-ondertekeningswerker

Een queue‑worker ondertekent ’s nachts enkele duizenden PDF’s. Met resolutie bij opstarten logt hij één regel met de families die hij zal gebruiken, en als er niets wordt gevonden, stopt hij voordat hij de queue aanraakt in plaats van per bericht te falen. Die opstart‑check is wat een font‑probleem verandert van een stroom mislukte jobs naar een container die weigert te starten met een één‑regelige reden.

De kosten van het probe‑proces zijn klein genoeg om bij opstarten te negeren en te groot om per document te herhalen. Elke probe is een echte handtekening die naar een tijdelijk bestand wordt geschreven, dus de Latijn‑lijst kost er maximaal vier en de CJK‑lijst maximaal acht, allemaal tegen een één‑pagina PDF. Los één keer op, cache de twee familienamen, en het per‑document‑pad is precies wat het eerder was: bouw de opties, roep Sign aan, lees het resultaat‑aantal.

Ik verloor een middag aan de versie die gokte. Hij doorzocht de font‑directory, vond NotoSansCJK-Regular.ttc, meldde CJK als beschikbaar, en faalde vervolgens op elke familienaam die ik uit die bestandsnaam afleidde. Probe‑en met een echte handtekening was zowel eenvoudiger als correct.

Wat bijt er nog meer in een container?

Nog één punt, en het heeft niets met fonts te maken: InvariantGlobalization=true. Het is standaardadvies om ICU uit een .NET‑image te trimmen, en met GroupDocs.Signature zorgt het ervoor dat de allereerste new Signature(...) een CultureNotFoundException: ... en-US is an invalid culture identifier gooit, omdat SignatureSettings een CultureInfo("en-US") bouwt. Houd globalisatie ingeschakeld en laat ICU in de image blijven. De system requirements pagina is de plek om platformondersteuning te controleren voordat je een basis‑image kiest.

Conclusie

Een ondertekeningsservice die lokaal werkt en in Docker faalt, mist bijna altijd fonts, en de oplossing is een vier‑pakket‑laag plus code die een familie resolveert in plaats van er één aan te nemen. Bouw beide images vanuit het voorbeeld, draai ze zij‑aan‑zij, en lees de [fonts]‑regels: het hele argument past in die ene vergelijking.

Aanvullende bronnen