💡 Volledig werkend voorbeeld beschikbaar op GitHub:
nodejs-docker-signing-with-fonts

Introductie

Lettertype‑resolutie is het onderdeel van container‑ondertekening dat bepaalt of je Node‑service documenten produceert of uitzonderingen gooit. GroupDocs.Signature vervangt een ontbrekende familie niet: noem er één die de afbeelding niet heeft en de aanroep faalt, zonder iets te schrijven. Het wissen van het lettertype is ook geen oplossing, omdat de bibliotheek dan naar zijn eigen standaard vraagt en op dezelfde manier faalt.

Er zijn drie manieren om te bepalen welke familie je moet doorgeven, en slechts één daarvan overleeft een container. Dit artikel vergelijkt ze, behandelt vervolgens de provisioning en het bindgedrag dat de code eromheen vormgeeft, omdat Node.js via Java meer van beide heeft dan elk ander platform waarop deze bibliotheek wordt geleverd.

Waarom dit op Node.js meer belangrijk is

Het pakket is een brug: node-java laadt een JVM in‑process. Dus een Node‑ondertekenings‑image heeft een JDK nodig, de node‑gyp‑toolchain om de brug te bouwen, en LD_LIBRARY_PATH dat wijst naar libjvm.so, alles voordat lettertypen relevant zijn. node:18-bookworm levert vervolgens 6 DejaVu‑lettertypebestanden voor AWT – voldoende voor Latijn, niets voor CJK.

Die combinatie veroorzaakt fouten die eruitzien als applicatie‑bugs. Een ontbrekend JVM‑pad, een ontbrekend lettertype en een marshaling‑mismatch verschijnen allemaal als Error running instance method, omdat dat is wat node-java rapporteert voor alles wat aan de Java‑kant wordt gegooid.

Voorvereisten

Node 18 – de brug bouwt tegen NAN, dat niet compileert tegen de V8 in Node 20 of 22 ('AccessorSignature' is not a member of 'v8'). JDK 8 tot 17: op JDK 25 faalt de imaging‑laag met Cannot open an image. The image size can not be 0!.

Installatie

npm install @groupdocs/groupdocs.signature

In de image moet die installatie build-essential en python3 aanwezig hebben, plus openjdk-17-jdk-headless en het loader‑pad:

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

Methode 1 – Hard‑code de familienaam

De versie die iedereen eerst schrijft: kies Arial, lever het, ga verder. Het werkt op de ontwikkelmachine en faalt bij de eerste container‑run, omdat Debian‑images Arial niet installeren – ze installeren Liberation Sans, dat metrisch compatibel is onder een andere familienaam.

Er is geen code die hier de moeite waard is om te laten zien, en dat is het punt. De volledige inhoud van de methode is een string‑literal die toevallig waar is in één omgeving.

Methode 2 – Detecteer lettertypen vanuit het bestandssysteem

De natuurlijke oplossing: scan de lettertype‑mappen, kijk wat er is, kies iets. De helft is echt nuttig – de inventaris vertelt je of de image 0 of 6 lettertypen heeft:

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

De andere helft werkt niet. Lettertype‑bestanden bevatten zelden de familienaam die een aanroeper moet doorgeven: Debian’s fonts-noto-cjk installeert NotoSansCJK-Regular.ttc, waarvan de familie Noto Sans CJK JP is. Een familie afleiden van die bestandsnaam levert NotoSansCJK-Regular op, wat naar niets resolveert. Bestandsnaamdectie mist zowel aanwezige lettertypen als rapporteert vol vertrouwen families die zullen falen.

Bewaar de inventaris als diagnostiek. Gebruik het niet om te kiezen. Het aantal beantwoordt de vraag of de image überhaupt is geprovisioneerd, wat een andere en even nuttige vraag is.

Methode 3 – Vraag de bibliotheek

Probeer een wegwerpsignature per kandidaat‑familie en houd de eerste die niet gooit. Het kost één PDF‑schrijf per kandidaat en het is de enige methode waarvan het antwoord gezaghebbend is, omdat het dezelfde aanroep is die de echte handtekening zal doen.

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

Op Node heeft de probe één extra stuk nodig. node-java collapseert elke Java‑exception tot Error running instance method, dus moet het echte bericht worden hersteld uit de ingepakte stack‑trace:

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

Zonder die twee regels produceren een lettertype‑loze container en een kapot JVM‑pad identieke logs. Ik heb langer dan ik wil toegeven besteed aan het vergelijken van twee containers die dezelfde fout printen om volledig verschillende redenen, voordat ik de regex toevoegde.

Wat de probe kost

De bezwaar tegen probing is dat het bestanden schrijft, en dat doet het: één klein PDF per kandidaat, direct verwijderd. De Latijn‑lijst in het voorbeeld heeft vier items en de CJK‑lijst heeft acht, dus een koude start schrijft hooguit twaalf één‑pagina‑documenten naar de tijdelijke map voordat de service klaar is.

Dat is een opstart‑kost, geen per‑request‑kost, en het levert een log‑regel op die beide opgeloste families benoemt. Vergeleken met een container die schoon start en dan faalt bij het eerste klant‑document met een bridge‑fout, is twaalf tijdelijke bestanden geen moeilijke trade‑off.

Methoden vergelijken: wanneer welke gebruiken

Methode Beste voor Belangrijkste voordelen Beperkingen
Hard‑gecodeerde familie een enkele gecontroleerde omgeving triviaal, geen opstart‑kosten breekt op elke image die die exacte familie mist
Bestandsnaamdectie diagnostiek van wat een image bevat snel, geen ondertekenings‑calls bestandsnamen zijn geen familienaam, dus afgeleide keuzes falen
Bibliotheek‑probing elke gecontaineriseerde of draagbare omgeving gezaghebbend, werkt op laptop en image gelijk één PDF‑schrijf per kandidaat, dus bij opstarten oplossen en cachen

De twee bind‑eigenaardigheden die je moet kennen

Zodra een familie is opgelost, heeft de ondertekenings‑aanroep zelf een Node‑specifieke vorm. De Java‑API neemt een lijst van opties, maar een JavaScript‑array wordt niet gemarshalled naar java.util.List, waardoor het doorgeven ervan Could not find method "sign(java.lang.String, [Ljava.lang.Object;)" oplevert. De workaround is om de overload met één optie te chainen en via een tijdelijk bestand te stage‑en:

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

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

De tweede eigenaardigheid is het teruglezen. TextVerifyOptions maakt geen round‑trip door deze binding: verify veroorzaakt dezelfde generieke bridge‑error, dus het voorbeeld retourneert een sentinel en print unavailable in plaats van te doen alsof de handtekening is mislukt. Het npm‑pakket heeft versie 24.12.0, gepubliceerd in december 2024, en bundelt een 23.6.1 engine terwijl .NET op 26.6 zit en Java op 26.5. Ondertekenen wordt niet beïnvloed; alleen het verificatiepad ontbreekt.

Moet ik de Node.js‑binding nog steeds in productie gebruiken?

Voor alleen‑Latijnse ondertekening, ja: het ondertekent correct, en een ontbrekend lettertype veroorzaakt een fout in plaats van stil te degraderen, dus de foutmodus is luid. Voor gemengde scripts moet je het ontbreken van de read‑back afwegen, want niets in het proces kan dan bevestigen dat CJK‑glyphs zijn ingebed in plaats van als blokken te renderen. Een kleine verifier op .NET of Java in dezelfde pipeline vult dat gat.

Best practices en tips

  • Provisioneer in volgorde: JDK en toolchain, loader‑pad, lettertypen, dan de app. Elke laag faalt anders en het mixen ervan maakt diagnose traag.
  • Los de families één keer op bij opstarten en log ze naast het aantal lettertypen.
  • Pin Node 18 en een JDK tussen 8 en 17, en beschouw beide als vaste infrastructuur in plaats van routine‑upgrades.
  • Houd het lettertype‑loze Dockerfile in de repository, zodat de fout één build verwijderd blijft.

Conclusie

Drie manieren om een lettertype te kiezen, één die de uitrol overleeft. Probe de bibliotheek, cache het antwoord, en laat de inventaris dienen als diagnostiek in plaats van beslissing. Werk vervolgens met de binding zoals die is: onderteken één optie per keer, haal de Java‑exception uit de stack‑trace, en rapporteer de ontbrekende verificatie eerlijk in plaats van te verbergen. De voorbeeld‑repository bouwt beide images, zodat elke bewering hier kan worden geverifieerd met twee commando’s.

Aanvullende bronnen