💡 Esempio completo funzionante disponibile su GitHub:
sign-pdf-in-linux-container-fonts-dotnet

Il vecchio metodo era doloroso

Il servizio firma fatture. Funziona su un laptop con trecento font installati, supera la revisione e viene containerizzato di venerdì. Il lunedì il primo lavoro nel cluster termina con codice diverso da zero con Sign document error: Font Arial was not found, e qualcuno trascorre la mattina a leggere gli stack trace prima che chiunque pensi di chiedere quali font contiene realmente un’immagine mcr.microsoft.com/dotnet/runtime:8.0.

La risposta è: nessuno. Zero file di font, misurati sull’immagine in cui gira il campione di questo articolo.

Vale la pena sapere come si comportano gli altri runtime, perché il fallimento appare diverso su ciascuno. eclipse-temurin:17-jre include 8 file DejaVu e node:18-bookworm ne include 6, entrambi per AWT, il che spiega perché le immagini JVM e Node firmano testo latino senza problemi e si bloccano solo quando arriva una stringa giapponese o cinese. python:3.11-slim non ne contiene alcuno, come l’immagine runtime .NET, quindi fallisce alla prima firma. Nessuno ottiene CJK gratuitamente su nessuna di esse.

Il provisioning dei font nel container è il passaggio che rende possibile la firma di testo in un’immagine Linux con GroupDocs.Signature per .NET. È importante perché la libreria non sostituisce una famiglia mancante: nominare un font non installato genera un errore e non scrive alcun documento. Questo articolo mette a confronto l’immagine senza font con quella corretta, mostra cosa è cambiato e descrive la risoluzione a runtime che mantiene lo stesso codice funzionante su una macchina di sviluppo.

C’è un modo migliore

Devono essere vere due cose. L’immagine deve contenere almeno un font e il codice deve smettere di presumere quale sia.

La prima è un layer Dockerfile. La seconda è un passaggio di risoluzione: invece di codificare in modo rigido Arial, chiedi alla libreria quale delle diverse famiglie candidate può effettivamente usare, e tieni la prima che funziona. Il risultato gira invariato in un container slim, su Windows e in CI, perché non fa mai assunzioni sull’ambiente che non ha verificato.

Una cosa che non funziona, e vale la pena dirlo chiaramente perché è la prima cosa che la gente prova: lasciare il font non impostato. Senza SignatureFont, GroupDocs.Signature richiede il proprio default, Times New Roman, che anche l’immagine senza font non possiede. La chiamata fallisce identicamente.

Il nuovo modo: due immagini, una differenza

Passo 1 – Guarda cosa contiene l’immagine

Prima di firmare qualsiasi cosa, elenca i file dei font. Il conteggio trasforma un’eccezione vaga in una diagnosi, perché zero font e un nome di famiglia errato richiedono correzioni diverse:

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

Nota ciò che manca: System.Drawing. System.Drawing.Common è disponibile solo su Windows a partire da .NET 7 e genera eccezioni su Linux, quindi il codice relativo ai font costruito su di esso fallisce nel container per un secondo motivo, non correlato.

Passo 2 – Aggiungi il layer dei font

Quattro pacchetti, un RUN, e il fallimento scompare:

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 è il risolutore e ti fornisce fc-list per il debug. fonts-dejavu-core è il minimo per latino, greco e cirillico. fonts-liberation fornisce sostituti metricamente compatibili per Arial, Times New Roman e Courier New, che è ciò a cui i documenti creati su Windows fanno realmente riferimento. fonts-noto-cjk copre cinese, giapponese e coreano.

Passo 3 – Risolvi una famiglia invece di nominarne una

Il modo portabile per scegliere un font è provare una firma di prova per ciascun candidato e tenere la prima che non genera eccezioni:

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

return null;

La rilevazione basata sul nome del file è una scorciatoia allettante ma errata. Il pacchetto Debian fonts-noto-cjk installa NotoSansCJK-Regular.ttc, il cui nome di famiglia è Noto Sans CJK JP. Un confronto con il nome del file non trova i font presenti e segnala famiglie che non si risolveranno quando passate a SignatureFont.

Passo 4 – Firma ciò che è stato risolto, verifica ciò che hai firmato

È necessaria una famiglia latina risolta; una famiglia CJK risolta è opzionale e la sua assenza comporta semplicemente un salto, non un 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);

Poi leggi nuovamente il file, perché il CJK senza un font CJK può essere visualizzato come caselle vuote senza generare alcun errore:

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

Confronto affiancato: prima vs. dopo

Dockerfile.nofonts Dockerfile
File dei font nell’immagine 0 DejaVu, Liberation, Noto CJK
Firma di testo latino fallisce, exit 3 scritta e recuperata in lettura
Firma di testo CJK fallisce scritta e recuperata
Errore visualizzato Font <name> was not found nessuno
Differenza nel codice nessuna – stesso binario nessuna – stesso binario

L’ultima riga è il punto cruciale. Nulla nell’applicazione è cambiato tra le due esecuzioni. Il repository di esempio fornisce entrambi i file così il confronto richiede due comandi docker build anziché affidarsi a ipotesi. Mantieni anche la variante senza font nel repository: è il modo più veloce per riprodurre il fallimento quando qualcuno cambia le immagini di base sei mesi dopo e le firme smettono silenziosamente di apparire.

Perché non installare tutti i font?

Perché le dimensioni dell’immagine sono un vincolo reale e i quattro pacchetti sopra coprono già gli script più usati nei documenti. fonts-dejavu-core da solo è sufficiente per firmare latino, greco e cirillico; Liberation è importante quando i documenti fanno riferimento alle famiglie Windows per nome; Noto CJK è quello che occupa davvero spazio e paga solo se firmi testo dell’Est asiatico. Installa solo i font di cui hanno bisogno i tuoi documenti, poi verifica con una lettura.

Esempio reale: il worker di firma batch

Un worker in coda firma qualche migliaio di PDF ogni notte. Con la risoluzione all’avvio, registra una riga con i nomi delle famiglie che utilizzerà, e se nulla si risolve esce prima di toccare la coda invece di fallire per ogni messaggio. Questo controllo all’avvio è ciò che trasforma un problema di font da una serie di job falliti a un container che rifiuta di avviarsi con una ragione in una sola riga.

Il costo della prova è abbastanza piccolo da ignorarlo all’avvio e troppo grande per ripeterlo per documento. Ogni prova è una vera firma scritta su un file temporaneo, quindi l’elenco latino costa fino a quattro firme e quello CJK fino a otto, tutti contro un PDF di una pagina. Risolvi una volta, memorizza nella cache i due nomi di famiglia, e il percorso per documento è esattamente quello di prima: costruisci le opzioni, chiama Sign, leggi il conteggio dei risultati.

Ho perso un pomeriggio con la versione che indovinava. Scansionava la directory dei font, trovava NotoSansCJK-Regular.ttc, segnalava CJK come disponibile e poi falliva su ogni nome di famiglia derivato da quel nome file. Provare con una vera firma è stato sia più semplice che corretto.

Cos’altro crea problemi in un container?

Un altro aspetto, non correlato ai font: InvariantGlobalization=true. È un consiglio standard per rimuovere ICU da un’immagine .NET, e con GroupDocs.Signature fa sì che il primo new Signature(...) lanci CultureNotFoundException: ... en-US is an invalid culture identifier, perché SignatureSettings crea un CultureInfo("en-US"). Mantieni la globalizzazione abilitata e lascia che ICU rimanga nell’immagine. La pagina dei requisiti di sistema è il luogo dove verificare il supporto della piattaforma prima di scegliere un’immagine di base.

Conclusione

Un servizio di firma che funziona localmente e fallisce in Docker manca quasi sempre di font, e la soluzione è un layer di quattro pacchetti più del codice che risolve una famiglia invece di assumerne una. Costruisci entrambe le immagini dal campione, esegui il confronto affiancato e leggi le righe [fonts]: l’intero argomento si riassume in quel singolo confronto.

Risorse aggiuntive