💡 Voll funktionsfähiges Beispiel auf GitHub verfügbar:
nodejs-docker-signing-with-fonts

Einführung

Die Schriftauflösung ist der Teil des Container‑Signierens, der entscheidet, ob Ihr Node‑Dienst Dokumente erzeugt oder Ausnahmen wirft. GroupDocs.Signature ersetzt nicht eine fehlende Schriftfamilie: Nennen Sie eine, die das Image nicht enthält, und der Aufruf schlägt fehl, ohne etwas zu schreiben. Das Löschen der Schrift ist ebenfalls kein Work‑around, da die Bibliothek dann nach ihrer eigenen Standardschrift fragt und auf dieselbe Weise fehlschlägt.

Es gibt drei Wege, welche Familie übergeben wird, und nur einer davon überlebt in einem Container. Dieser Artikel vergleicht sie, behandelt anschließend die Bereitstellung und das Bindungsverhalten, das den Code um sie herum prägt, weil Node.js über Java mehr von beidem bietet als jede andere Plattform, auf der diese Bibliothek verfügbar ist.

Warum das bei Node.js wichtiger ist

Das Paket ist eine Brücke: node-java lädt eine JVM im Prozess. Daher benötigt ein Node‑Signing‑Image ein JDK, die node-gyp‑Toolchain zum Bauen der Brücke und LD_LIBRARY_PATH, das auf libjvm.so zeigt – alles, bevor Schriften relevant werden. node:18-bookworm liefert dann 6 DejaVu‑Schriftdateien für AWT – genug für Latin, nichts für CJK.

Diese Kombination erzeugt Fehler, die wie Anwendungs‑Bugs aussehen. Ein fehlender JVM‑Pfad, eine fehlende Schrift und ein Marshalling‑Fehler treten alle als Error running instance method auf, weil das genau das ist, was node-java für alles meldet, was auf der Java‑Seite geworfen wird.

Voraussetzungen

Node 18 – die Brücke wird gegen NAN gebaut, das nicht gegen das V8 in Node 20 oder 22 kompiliert ('AccessorSignature' is not a member of 'v8'). JDK 8 bis 17: Auf JDK 25 schlägt die Imaging‑Schicht mit Cannot open an image. The image size can not be 0! fehl.

Installation

npm install @groupdocs/groupdocs.signature

Im Image muss die Installation build-essential und python3 bereitstellen, plus openjdk-17-jdk-headless und den Loader‑Pfad:

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 – Schriftfamilie fest codieren

Die Version, die jeder zuerst schreibt: Arial auswählen, mitliefern, weiter. Sie funktioniert auf der Entwickler‑Maschine und schlägt beim ersten Containerlauf fehl, weil Debian‑Images Arial nicht installieren – sie installieren Liberation Sans, das metrisch kompatibel, aber unter einem anderen Familiennamen ist.

Es gibt keinen Code, der hier gezeigt werden müsste, und genau das ist der Punkt. Der gesamte Inhalt der Methode ist ein String‑Literal, das in einer Umgebung wahr ist.

Methode 2 – Schriften aus dem Dateisystem erkennen

Die natürliche Lösung: die Schriftverzeichnisse scannen, sehen, was vorhanden ist, etwas auswählen. Die Hälfte davon ist wirklich nützlich – das Inventar sagt Ihnen, ob das Image 0 Schriften oder 6 hat:

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

Die andere Hälfte funktioniert nicht. Schriftdateien enthalten selten den Familien‑String, den ein Aufrufer übergeben muss: Debian’s fonts-noto-cjk installiert NotoSansCJK-Regular.ttc, dessen Familie Noto Sans CJK JP ist. Leitet man aus dem Dateinamen die Familie ab, erhält man NotoSansCJK-Regular, das zu nichts auflöst. Die Dateinamen‑Erkennung verpasst sowohl vorhandene Schriften als auch meldet zuversichtlich Familien, die fehlschlagen werden.

Behalten Sie das Inventar als Diagnose. Verwenden Sie es nicht zur Auswahl. Die Anzahl beantwortet die Frage, ob das Image überhaupt bereitgestellt wurde – eine andere und ebenso nützliche Frage.

Methode 3 – Die Bibliothek fragen

Versuchen Sie ein Wegwerf‑Signature‑Objekt pro Kandidatenfamilie und behalten Sie das erste, das nicht wirft. Das kostet ein PDF‑Schreiben pro Kandidat und ist die einzige Methode, deren Ergebnis autoritativ ist, weil es derselbe Aufruf ist, den die echte Signatur später macht.

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

Unter Node benötigt die Probe ein zusätzliches Stück. node-java fasst jede Java‑Exception zu Error running instance method zusammen, sodass die eigentliche Meldung aus dem umschlossenen Stack‑Trace extrahiert werden muss:

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

Ohne diese beiden Zeilen erzeugen ein schriftloser Container und ein defekter JVM‑Pfad identische Logs. Ich habe länger gebraucht, als ich zugeben möchte, um zwei Container zu vergleichen, die denselben Fehler aus völlig unterschiedlichen Gründen ausgaben, bevor ich den Regex hinzufügte.

Was die Probe kostet

Der Einwand gegen das Proben ist, dass Dateien geschrieben werden – und das tun sie: ein kleines PDF pro Kandidat, sofort gelöscht. Die lateinische Liste im Beispiel hat vier Einträge und die CJK‑Liste acht, also schreibt ein Kaltstart höchstens zwölf einseitige Dokumente in das temporäre Verzeichnis, bevor der Service bereit ist.

Das ist ein Start‑Kosten‑Faktor, kein pro‑Request‑Kosten‑Faktor, und er liefert eine Log‑Zeile, die beide aufgelösten Familien nennt. Im Vergleich zu einem Container, der sauber startet und dann beim ersten Kunden‑Dokument mit einem Bridge‑Fehler scheitert, sind zwölf Temp‑Dateien kein schwieriger Kompromiss.

Methoden vergleichen: Wann welche verwenden

Methode Am besten für Hauptvorteile Einschränkungen
Fest codierte Familie eine einzelne, kontrollierte Umgebung trivial, keine Start‑Kosten bricht bei jedem Image, dem diese genaue Familie fehlt
Dateinamen‑Erkennung Diagnose dessen, was ein Image enthält schnell, keine Signatur‑Aufrufe Dateinamen sind keine Familiennamen, abgeleitete Entscheidungen schlagen fehl
Bibliotheks‑Probe alles containerisiert oder portabel autoritativ, funktioniert sowohl auf Laptop als auch im Image ein PDF‑Schreiben pro Kandidat, daher beim Start auflösen und cachen

Die beiden Bindungs‑Eigenheiten, die man kennen sollte

Sobald eine Familie aufgelöst ist, hat der eigentliche Signatur‑Aufruf eine Node‑spezifische Form. Die Java‑API nimmt eine Liste von Optionen, aber ein JavaScript‑Array wird nicht zu java.util.List marshalliert, sodass das Übergeben eines Arrays Could not find method "sign(java.lang.String, [Ljava.lang.Object;)" erzeugt. Der Work‑around besteht darin, die Einzel‑Option‑Überladung zu chainen und über eine temporäre Datei zu arbeiten:

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

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

Die zweite Eigenheit ist das Zurücklesen. TextVerifyOptions funktioniert nicht über diese Bindung: verify wirft denselben generischen Bridge‑Fehler, sodass das Beispiel einen Sentinel zurückgibt und unavailable ausgibt, anstatt vorzutäuschen, die Signatur sei fehlgeschlagen. Das npm‑Paket hat die Version 24.12.0, veröffentlicht im Dezember 2024, und bündelt eine 23.6.1‑Engine, während .NET bei 26.6 und Java bei 26.5 liegt. Das Signieren ist unbeeinflusst; nur der Verifikations‑Pfad fehlt.

Sollte ich die Node.js‑Bindung trotzdem in der Produktion einsetzen?

Für rein lateinische Signaturen ja: sie signiert korrekt, und eine fehlende Schrift löst einen Fehler aus, anstatt still zu degradieren, sodass der Fehlermodus laut ist. Für gemischte Skripte muss man das fehlende Rücklesen abwägen, da dann nichts im Prozess bestätigen kann, dass CJK‑Glyphen eingebettet wurden und nicht als Kästchen gerendert werden. Ein kleiner Verifier in .NET oder Java im selben Pipeline‑Schritt schließt diese Lücke.

Best Practices und Tipps

  • Bereitstellung in der Reihenfolge: JDK und Toolchain, Loader‑Pfad, Schriften, dann die Anwendung. Jede Schicht schlägt auf unterschiedliche Weise fehl, und das Mischen verlangsamt die Diagnose.
  • Familien einmal beim Start auflösen und neben der Schrift‑Anzahl protokollieren.
  • Node 18 und ein JDK zwischen 8 und 17 festpinnen und beide als feste Infrastruktur behandeln, nicht als Routine‑Upgrades.
  • Das schriftlose Dockerfile im Repository behalten, sodass der Fehler immer nur einen Build entfernt bleibt.

Fazit

Drei Wege, eine Schrift zu wählen, einer überlebt die Bereitstellung. Bibliothek probieren, Ergebnis cachen und das Inventar als Diagnose statt Entscheidung nutzen. Dann mit der Bindung arbeiten, wie sie ist: eine Option nach der anderen signieren, die Java‑Exception aus dem Stack‑Trace auslesen und das fehlende Verifikations‑Feature ehrlich melden, anstatt es zu verbergen. Das Beispiel‑Repository baut beide Images, sodass jede Behauptung hier mit zwei Befehlen geprüft werden kann.

Weitere Ressourcen