💡 Esempio completo funzionante disponibile su GitHub: nodejs-docker-signing-with-fonts

Introduzione

La risoluzione dei font è la parte della firma del container che decide se il tuo servizio Node produce documenti o eccezioni. GroupDocs.Signature non sostituisce una famiglia mancante: se ne nomini una che l’immagine non possiede, la chiamata genera un’eccezione, senza produrre nulla. Rimuovere il font non è nemmeno una soluzione alternativa, poiché la libreria richiede allora il proprio default e fallisce allo stesso modo.

Ci sono tre modi per decidere quale famiglia passare, e solo uno di essi sopravvive in un container. Questo articolo li confronta, poi copre il provisioning e il comportamento del binding che modellano il codice intorno a loro, perché Node.js via Java ha più di entrambi rispetto a qualsiasi altra piattaforma su cui questa libreria è distribuita.

Perché è più importante su Node.js

Il pacchetto è un ponte: node-java carica una JVM in processo.
Quindi un’immagine di firma Node ha bisogno di un JDK, della toolchain node-gyp per costruire il ponte, e di LD_LIBRARY_PATH che punti a libjvm.so, tutto prima che i font siano rilevanti.
node:18-bookworm fornisce quindi 6 file di font DejaVu per AWT - sufficienti per il latino, nulla per CJK.

Questa combinazione produce errori che sembrano bug dell’applicazione. Un percorso JVM mancante, un font mancante e un mismatch di marshalling si manifestano tutti come Error running instance method, perché è ciò che node-java segnala per qualsiasi eccezione lanciata dal lato Java.

Prerequisiti

Node 18 - il ponte è compilato contro NAN, che non si compila contro V8 in Node 20 o 22 ('AccessorSignature' is not a member of 'v8').
JDK 8 fino a 17: su JDK 25 lo strato di imaging fallisce con Cannot open an image. The image size can not be 0!.

Installazione

npm install @groupdocs/groupdocs.signature

Nell’immagine, quell’installazione richiede build-essential e python3 presenti, più openjdk-17-jdk-headless e il percorso del loader:

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

Metodo 1 - Codificare in modo rigido il nome della famiglia

La versione che tutti scrivono per prima: scegliere Arial, includerla, e andare avanti. Funziona sulla macchina dello sviluppatore e fallisce al primo avvio del container, perché le immagini Debian non installano Arial - installano Liberation Sans, che è metricamente compatibile ma con un nome di famiglia diverso.

Non c’è codice degno di essere mostrato qui, ed è questo il punto. L’intero contenuto del metodo è un literal di stringa che risulta vero in un solo ambiente.

Metodo 2 - Rilevare i font dal filesystem

La soluzione naturale: scansionare le directory dei font, vedere cosa c’è, scegliere qualcosa. Metà di questo è davvero utile - l’inventario ti dice se l’immagine ha 0 font o 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',
];

L’altra metà non funziona. I file dei font raramente contengono la stringa della famiglia che il chiamante deve passare: fonts-noto-cjk di Debian installa NotoSansCJK-Regular.ttc, la cui famiglia è Noto Sans CJK JP. Derivare una famiglia da quel nome file ti restituisce NotoSansCJK-Regular, che non risolve a nulla. Il rilevamento basato sul nome file sia perde i font presenti sia segnala con certezza famiglie che falliranno.

Mantieni l’inventario come diagnostica. Non usarlo per scegliere. Il conteggio risponde se l’immagine è stata provisionata o meno, che è una domanda diversa e altrettanto utile.

Metodo 3 - Chiedere alla libreria

Prova una firma temporanea per ogni famiglia candidata e conserva la prima che non genera eccezione. Costa una scrittura PDF per candidato ed è l’unico metodo la cui risposta è autorevole, perché è la stessa chiamata che farà la firma reale.

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

Su Node il probe richiede un elemento extra. node-java collassa ogni eccezione Java in Error running instance method, quindi il messaggio reale deve essere recuperato dallo stack trace avvolto:

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

Senza quelle due righe, un container senza font e un percorso JVM rotto producono log identici. Ho impiegato più tempo di quanto voglia ammettere a confrontare due container che stampavano lo stesso errore per ragioni completamente diverse prima di aggiungere la regex.

Cosa costa il probe

L’obiezione al probing è che scrive file, e lo fa: un piccolo PDF per candidato, cancellato immediatamente. L’elenco Latin nel campione ha quattro voci e l’elenco CJK ne ha otto, quindi un avvio a freddo scrive al massimo dodici documenti di una pagina nella directory temporanea prima che il servizio sia pronto.

Questo è un costo di avvio, non per richiesta, e fornisce una riga di log che nomina entrambe le famiglie risolte. Confrontato con un container che parte correttamente e poi fallisce sul primo documento del cliente con un errore del bridge, dodici file temporanei non è uno scambio difficile.

Confronto dei Metodi: Quando Usare Ognuno

Metodo Ideale per Vantaggi principali Limitazioni
Hard-coded family un unico ambiente controllato banale, nessun costo di avvio si rompe su qualsiasi immagine che non possiede quella famiglia esatta
Filename detection diagnosticare ciò che contiene un’immagine veloce, nessuna chiamata di firma i nomi dei file non sono nomi di famiglia, quindi le scelte derivate da essi falliscono
Library probing qualsiasi cosa containerizzata o portabile autorevole, funziona sia su laptop che su immagine una scrittura PDF per candidato, quindi risolvere all’avvio e memorizzare nella cache

Le Due Peculiarità del Binding da Conoscere

Una volta che una famiglia è risolta, la chiamata di firma stessa ha una forma specifica per Node. L’API Java accetta una lista di opzioni, ma un array JavaScript non viene marshalled in java.util.List, quindi passarne uno produce Could not find method "sign(java.lang.String, [Ljava.lang.Object;)". La soluzione è concatenare il sovraccarico a singola opzione e passare attraverso un file temporaneo:

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

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

La seconda particolarità è il read-back. TextVerifyOptions non effettua il round-trip attraverso questo binding: verify solleva lo stesso errore generico del bridge, quindi il campione restituisce un sentinel e stampa unavailable invece di fingere che la firma sia fallita. Il pacchetto npm è alla versione 24.12.0, pubblicato a dicembre 2024, e include un motore 23.6.1 mentre .NET è alla 26.6 e Java alla 26.5. La firma non è influenzata; manca solo il percorso di verifica.

Devo ancora usare il binding Node.js in produzione?

Per la firma solo in latino, sì: firma correttamente, e un font mancante genera un’eccezione anziché degradare silenziosamente, quindi la modalità di errore è evidente. Per lavori con script misti, valuta l’assenza del read-back, poiché nulla nel processo può confermare che i glifi CJK siano incorporati anziché renderizzati come riquadri. Un piccolo verificatore su .NET o Java nella stessa pipeline colma questa lacuna.

Migliori pratiche e suggerimenti

  • Provisionare in ordine: JDK e toolchain, percorso del loader, font, poi l’app. Ogni livello fallisce in modo diverso e mescolarli rende la diagnosi lenta.
  • Risolvi le famiglie una volta all’avvio e registrale accanto al conteggio dei font.
  • Fissare Node 18 e un JDK tra 8 e 17, e trattarli entrambi come infrastruttura fissa piuttosto che come aggiornamenti di routine.
  • Mantieni il Dockerfile senza font nel repository, così il fallimento rimane a una build di distanza.

Conclusione

Tre modi per scegliere un font, uno che sopravvive al deployment. Interroga la libreria, memorizza la risposta nella cache, e lascia che l’inventario serva da diagnostica piuttosto che da decisione. Quindi lavora con il binding così com’è: firma un’opzione alla volta, leggi l’eccezione Java dallo stack trace, e segnala la verifica mancante onestamente invece di nasconderla. Il repository di esempio costruisce entrambe le immagini così ogni affermazione qui può essere verificata con due comandi.

Risorse aggiuntive