💡 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.