💡 Exemplo completo disponível no GitHub:
nodejs-docker-signing-with-fonts

Introdução

A resolução de fontes é a parte da assinatura em contêiner que decide se o seu serviço Node produz documentos ou exceções. GroupDocs.Signature não substitui uma família ausente: se o nome não estiver presente na imagem, a chamada gera erro, sem gerar nada. Limpar a fonte não é uma solução alternativa, pois a biblioteca então solicita sua própria fonte padrão e falha da mesma forma.

Existem três maneiras de decidir qual família passar, e apenas uma delas sobrevive em um contêiner. Este artigo as compara e, em seguida, aborda o provisionamento e o comportamento de binding que moldam o código ao redor delas, porque o Node.js via Java tem mais desses aspectos do que qualquer outra plataforma em que esta biblioteca é distribuída.

Por que isso importa mais no Node.js

O pacote é uma ponte: node-java carrega uma JVM no processo. Portanto, uma imagem de assinatura Node precisa de um JDK, da cadeia de ferramentas node-gyp para compilar a ponte e de LD_LIBRARY_PATH apontando para libjvm.so, tudo antes que as fontes se tornem relevantes. node:18-bookworm então fornece 6 arquivos de fonte DejaVu para AWT – suficiente para Latin, nada para CJK.

Essa combinação produz falhas que parecem bugs de aplicação. Um caminho JVM ausente, uma fonte faltando e um descompasso de marshaling aparecem todos como Error running instance method, porque é isso que node-java relata para qualquer exceção lançada do lado Java.

Pré‑requisitos

Node 18 – a ponte compila contra NAN, que não compila contra o V8 no Node 20 ou 22 ('AccessorSignature' is not a member of 'v8'). JDK 8 a 17: no JDK 25 a camada de imagem falha com Cannot open an image. The image size can not be 0!.

Instalação

npm install @groupdocs/groupdocs.signature

Na imagem, essa instalação requer build-essential e python3 presentes, além de openjdk-17-jdk-headless e o caminho do carregador:

ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
ENV PATH="${JAVA_HOME}/bin:${PATH}"
# node-java dlopens libjvm.so at run time; it is not on the default loader path.
ENV LD_LIBRARY_PATH="${JAVA_HOME}/lib/server:${LD_LIBRARY_PATH}"

Método 1 – Codificar a família de fonte

A versão que todo mundo escreve primeiro: escolher Arial, enviá‑la, seguir em frente. Funciona na máquina do desenvolvedor e falha na primeira execução do contêiner, porque as imagens Debian não instalam Arial – elas instalam Liberation Sans, que é compatível metricamente sob um nome de família diferente.

Não há código relevante para mostrar aqui, e esse é o ponto. O conteúdo inteiro do método é um literal de string que acontece de ser verdadeiro em um ambiente.

Método 2 – Detectar fontes a partir do sistema de arquivos

A correção natural: escanear os diretórios de fontes, ver o que existe, escolher algo. Metade disso é realmente útil – o inventário indica se a imagem tem 0 fontes ou 6:

const roots = [
  '/usr/share/fonts',
  '/usr/local/share/fonts',
  path.join(home, '.fonts'),
  path.join(home, '.local', 'share', 'fonts'),
  '/System/Library/Fonts',
  '/Library/Fonts',
];

A outra metade não funciona. Arquivos de fonte raramente carregam a string de família que o chamador deve passar: o pacote Debian fonts-noto-cjk instala NotoSansCJK-Regular.ttc, cuja família é Noto Sans CJK JP. Derivar uma família a partir desse nome de arquivo gera NotoSansCJK-Regular, que não resolve para nada. A detecção por nome de arquivo tanto perde fontes que estão presentes quanto relata com confiança famílias que falharão.

Mantenha o inventário como diagnóstico. Não o use para escolher. A contagem responde se a imagem foi provisionada ou não, o que é uma pergunta diferente e igualmente útil.

Método 3 – Perguntar à biblioteca

Tente uma assinatura descartável por família candidata e mantenha a primeira que não lançar exceção. Custa uma escrita de PDF por candidata e é o único método cuja resposta é autoritária, porque é a mesma chamada que a assinatura real fará.

for (const candidate of candidates) {
  if (tryFamily(sourcePath, candidate) === null) {
    return candidate;
  }
}
return null;

No Node a sondagem precisa de um detalhe extra. node-java colapsa toda exceção Java em Error running instance method, portanto a mensagem real precisa ser recuperada da pilha de rastreamento envolvida:

const stack = err.stack || '';
const match = stack.match(/com\.groupdocs\.signature\.exception\.[^\n]*/);
return match ? match[0].trim() : (err.message || String(err));

Sem essas duas linhas, um contêiner sem fontes e um caminho JVM quebrado produzem logs idênticos. Gastei mais tempo do que gostaria admitindo comparando dois contêineres que imprimiam o mesmo erro por razões totalmente diferentes antes de adicionar a expressão regular.

O que a sondagem custa

A objeção à sondagem é que ela grava arquivos, e de fato grava: um pequeno PDF por candidata, excluído imediatamente. A lista Latin no exemplo tem quatro entradas e a lista CJK tem oito, portanto um início a frio grava no máximo doze documentos de uma página no diretório temporário antes que o serviço esteja pronto.

Isso é um custo de inicialização, não por requisição, e fornece uma linha de log nomeando ambas as famílias resolvidas. Comparado a um contêiner que inicia limpo e então falha no primeiro documento do cliente com um erro de ponte, doze arquivos temporários não são um trade‑off difícil.

Comparação dos Métodos: Quando Usar Cada Um

Método Melhor para Principais Vantagens Limitações
Família codificada um ambiente controlado único trivial, sem custo de inicialização quebra em qualquer imagem que não possua essa família exata
Detecção por nome de arquivo diagnosticar o que a imagem contém rápido, sem chamadas de assinatura nomes de arquivo não são nomes de família, portanto escolhas derivadas falham
Sondagem da biblioteca qualquer cenário containerizado ou portátil autoritário, funciona tanto no laptop quanto na imagem grava um PDF por candidata, então resolva na inicialização e faça cache

As Duas Peculiaridades do Binding que Vale a Pena Saber

Uma vez que a família é resolvida, a chamada de assinatura em si tem uma forma específica ao Node. A API Java aceita uma lista de opções, mas um array JavaScript não é marshaled para java.util.List, portanto passar um produz Could not find method "sign(java.lang.String, [Ljava.lang.Object;)". A solução alternativa é encadear a sobrecarga de única opção e fazer um estágio através de um arquivo temporário:

new signatureLib.Signature(sourcePath)
  .sign(firstOutput, buildTextOptions(LATIN_TEXT, latinFamily, 50));

if (stageTwo) {
  new signatureLib.Signature(firstOutput)
    .sign(outputPath, buildTextOptions(CJK_TEXT, cjkFamily, 120));
}

A segunda peculiaridade é a leitura de volta. TextVerifyOptions não faz round‑trip através desse binding: verify levanta o mesmo erro genérico da ponte, portanto o exemplo devolve um sentinel e imprime unavailable ao invés de fingir que a assinatura falhou. O pacote npm está na versão 24.12.0, publicado em dezembro de 2024, e inclui um engine 23.6.1 enquanto .NET está em 26.6 e Java em 26.5. A assinatura não é afetada; apenas o caminho de verificação está ausente.

Ainda devo usar o binding Node.js em produção?

Para assinaturas apenas em Latin, sim: assina corretamente, e uma fonte ausente gera erro ao invés de degradar silenciosamente, então o modo de falha é evidente. Para trabalhos com scripts mistos, pese a ausência de leitura de volta, já que nada no processo pode então confirmar que glifos CJK foram incorporados ao invés de serem renderizados como caixas. Um verificador pequeno em .NET ou Java no mesmo pipeline cobre essa lacuna.

Boas Práticas e Dicas

  • Provisionar na ordem: JDK e cadeia de ferramentas, caminho do carregador, fontes, depois a aplicação. Cada camada falha de forma diferente e misturá‑las dificulta o diagnóstico.
  • Resolver as famílias uma única vez na inicialização e registrá‑las ao lado da contagem de fontes.
  • Fixar Node 18 e um JDK entre 8 e 17, tratando ambos como infraestrutura fixa ao invés de atualizações rotineiras.
  • Manter o Dockerfile sem fontes no repositório, para que a falha permaneça a um build de distância.

Conclusão

Três formas de escolher uma fonte, uma que sobrevive à implantação. Sonde a biblioteca, faça cache da resposta e deixe o inventário servir como diagnóstico ao invés de decisão. Então trabalhe com o binding como ele é: assine uma opção por vez, extraia a exceção Java da pilha de rastreamento e relate a verificação ausente de forma honesta, em vez de escondê‑la. O repositório de exemplo constrói ambas as imagens, de modo que cada afirmação aqui pode ser verificada em dois comandos.

Recursos Adicionais