💡 Exemplo completo em funcionamento disponível no GitHub:
sign-pdf-in-linux-container-fonts-dotnet

O Antigo Método Era Doloroso

O serviço assina faturas. Ele roda em um laptop com trezentas fontes instaladas, passa na revisão e é conteinerizado numa sexta‑feira. Na segunda‑feira o primeiro trabalho no cluster sai com código de erro diferente de zero com Sign document error: Font Arial was not found, e alguém passa a manhã lendo rastros de pilha antes que alguém pense em perguntar quais fontes uma imagem mcr.microsoft.com/dotnet/runtime:8.0 realmente contém.

A resposta é nenhuma. Zero arquivos de fonte, medidos na imagem em que a amostra deste artigo é executada.

Vale a pena saber como os outros runtimes se comparam, porque a falha se apresenta de forma diferente em cada um. eclipse-temurin:17-jre inclui 8 arquivos DejaVu e node:18-bookworm inclui 6, ambos para AWT, o que explica por que as imagens JVM e Node assinam texto latino silenciosamente e só falham quando chega uma string japonesa ou chinesa. python:3.11-slim não inclui nenhuma, assim como a imagem do runtime .NET, então falha na primeira assinatura. Ninguém obtém CJK de graça em nenhum deles.

O provisionamento de fontes no contêiner é a etapa que faz a assinatura de texto funcionar em uma imagem Linux com GroupDocs.Signature para .NET. Isso importa porque a biblioteca não substitui uma família ausente: nomear uma fonte que não está instalada gera um erro e não grava nenhum documento. Este artigo coloca a imagem sem fontes ao lado da corrigida, mostra o que mudou e cobre a resolução em tempo de execução que mantém o mesmo código funcionando em uma máquina de desenvolvedor.

Existe uma Maneira Melhor

Duas coisas precisam ser verdadeiras. A imagem precisa ter ao menos uma fonte, e o código precisa parar de assumir qual é.

A primeira é uma camada no Dockerfile. A segunda é uma etapa de resolução: em vez de codificar Arial, pergunte à biblioteca qual das várias famílias candidatas ela realmente pode usar, e mantenha a primeira que funcionar. O resultado roda inalterado em um contêiner slim, no Windows e no CI, porque nunca afirma nada sobre o ambiente que não tenha verificado.

Uma coisa que não funciona, e vale a pena declarar claramente porque é a primeira coisa que as pessoas tentam: deixar a fonte não definida. Sem SignatureFont, GroupDocs.Signature pede seu próprio padrão, Times New Roman, que a imagem sem fontes também não possui. A chamada falha da mesma forma.

O Novo Caminho: Duas Imagens, Uma Diferença

Etapa 1 – Verifique o que a imagem tem

Antes de assinar qualquer coisa, liste os arquivos de fonte. A contagem transforma uma exceção vaga em um diagnóstico, porque zero fontes e um nome de família errado exigem correções diferentes:

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

Observe o que está ausente: System.Drawing. System.Drawing.Common é somente para Windows a partir do .NET 7 e lança exceção no Linux, portanto o código de fonte construído sobre ele falha no contêiner por um segundo motivo, não relacionado.

Etapa 2 – Adicione a camada de fontes

Quatro pacotes, um RUN, e a falha desaparece:

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 é o resolvedor e fornece fc-list para depuração. fonts-dejavu-core cobre o mínimo latino, grego e cirílico. fonts-liberation fornece substitutos métricos compatíveis para Arial, Times New Roman e Courier New, que é o que documentos criados no Windows realmente referenciam. fonts-noto-cjk cobre chinês, japonês e coreano.

Etapa 3 – Resolva uma família em vez de nomear uma

A forma portátil de escolher uma fonte é tentar uma assinatura descartável por candidato e manter a primeira que não lançar exceção:

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

return null;

A detecção por nome de arquivo é o atalho tentador, mas está errado. O pacote Debian fonts-noto-cjk instala NotoSansCJK-Regular.ttc, cujo nome de família é Noto Sans CJK JP. Uma correspondência por nome de arquivo perde fontes que estão presentes e reivindica famílias que não serão resolvidas quando passadas para SignatureFont.

Etapa 4 – Assine o que foi resolvido, verifique o que você assinou

É necessária uma família latina resolvida; uma família CJK resolvida é opcional e sua ausência significa pular, não travar:

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

Em seguida, leia o arquivo de volta, porque CJK sem uma fonte CJK pode ser renderizado como caixas vazias sem gerar nenhum erro:

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

Lado a Lado: Antes vs. Depois

Dockerfile.nofonts Dockerfile
Arquivos de fonte na imagem 0 DejaVu, Liberation, Noto CJK
Assinatura de texto latino falha, exit 3 escrita e recuperada na leitura
Assinatura de texto CJK falha escrita e recuperada
Erro apresentado Font <name> was not found nenhum
Diferença no código nenhuma – mesmo binário nenhuma – mesmo binário

A última linha é o ponto principal. Nada na aplicação mudou entre as duas execuções. O repositório de exemplo inclui ambos os arquivos, de modo que a comparação exige dois comandos docker build em vez de suposições. Mantenha a variante sem fontes no repositório também: é a maneira mais rápida de reproduzir a falha quando alguém troca a imagem base seis meses depois e as assinaturas silenciosamente deixam de aparecer.

Por que não instalar todas as fontes?

Porque o tamanho da imagem é uma restrição real e os quatro pacotes acima já cobrem os scripts que a maioria dos documentos usa. fonts-dejavu-core sozinho basta para assinaturas latinas, gregas e cirílicas; Liberation importa quando documentos referenciam as famílias Windows pelo nome; Noto CJK é o que realmente ocupa muito espaço e só paga por si mesmo se você assinar texto de línguas do Leste Asiático. Instale apenas o que seus documentos precisam e, em seguida, verifique com uma leitura de volta.

Exemplo Real: O Trabalhador de Assinatura em Lote

Um worker de fila assina alguns milhares de PDFs por noite. Com a resolução na inicialização, ele registra uma linha nomeando as famílias que usará, e se nada for resolvido ele sai antes de tocar a fila, em vez de falhar por mensagem. Essa verificação na inicialização é o que transforma um problema de fonte de uma sequência de jobs falhos em um contêiner que se recusa a iniciar com um motivo de uma linha.

O custo da sondagem é pequeno o suficiente para ser ignorado na inicialização e grande demais para ser repetido por documento. Cada sondagem gera uma assinatura real escrita em um arquivo temporário, então a lista latina custa até quatro delas e a lista CJK até oito, tudo contra um PDF de uma página. Resolva uma vez, armazene em cache os dois nomes de família, e o caminho por documento permanece exatamente como antes: construa as opções, chame Sign, leia a contagem de resultados.

Perdi uma tarde com a versão que adivinhava. Ela escaneou o diretório de fontes, encontrou NotoSansCJK-Regular.ttc, relatou CJK como disponível e então falhou em cada nome de família que eu derivei desse nome de arquivo. Sondar com uma assinatura real foi ao mesmo tempo mais simples e correto.

O Que Mais Complica um Contêiner?

Mais um ponto, e não tem relação com fontes: InvariantGlobalization=true. É a recomendação padrão para remover o ICU de uma imagem .NET, e com GroupDocs.Signature isso faz com que o primeiro new Signature(...) lance CultureNotFoundException: ... en-US is an invalid culture identifier, porque SignatureSettings cria um CultureInfo("en-US"). Mantenha a globalização habilitada e deixe o ICU permanecer na imagem. A página de requisitos do sistema é o local para verificar o suporte da plataforma antes de escolher uma imagem base.

Conclusão

Um serviço de assinatura que funciona localmente e falha no Docker quase sempre está sem fontes, e a solução é uma camada de quatro pacotes mais código que resolve uma família em vez de assumir uma. Construa ambas as imagens a partir do exemplo, execute-as lado a lado e leia as linhas [fonts]: todo o argumento cabe nessa única comparação.

Recursos Adicionais