💡 Pełny działający przykład dostępny na GitHubie:
nodejs-docker-signing-with-fonts
Wprowadzenie
Rozwiązywanie czcionek jest częścią podpisywania kontenera, która decyduje, czy Twój serwis Node generuje dokumenty, czy wyjątki. GroupDocs.Signature nie podstawia brakującej rodziny: jeśli nazwa nie istnieje w obrazie, wywołanie podnosi błąd, nie zapisując nic. Czyszczenie czcionki nie jest obejściem, ponieważ biblioteka wtedy żąda własnej domyślnej i ponownie kończy się niepowodzeniem.
Istnieją trzy sposoby określenia, którą rodzinę przekazać, i tylko jeden z nich przetrwa w kontenerze. Ten artykuł porównuje je, a następnie omawia provisioning i zachowanie wiązania, które kształtują kod wokół nich, ponieważ Node.js przez Java ma ich więcej niż jakakolwiek inna platforma, na której ta biblioteka jest dostępna.
Dlaczego ma to większe znaczenie w Node.js
Pakiet jest mostem: node-java ładuje JVM w procesie. Dlatego obraz podpisujący w Node potrzebuje JDK, łańcucha narzędzi node-gyp do zbudowania mostu oraz LD_LIBRARY_PATH wskazującego na libjvm.so, wszystko przed tym, jak czcionki staną się istotne. node:18-bookworm dostarcza wtedy 6 plików czcionek DejaVu dla AWT – wystarczających dla łacińskiego, nic dla CJK.
To połączenie powoduje błędy wyglądające jak błędy aplikacji. Brak ścieżki JVM, brak czcionki i niezgodność marshalling’u wszystkie pojawiają się jako Error running instance method, ponieważ tak node-java raportuje wszystko, co zostanie rzucone po stronie Java.
Wymagania wstępne
Node 18 – most buduje się przeciwko NAN, który nie kompiluje się z V8 w Node 20 lub 22 ('AccessorSignature' is not a member of 'v8'). JDK 8‑17: w JDK 25 warstwa obrazowania kończy się błędem Cannot open an image. The image size can not be 0!.
Instalacja
npm install @groupdocs/groupdocs.signature
W obrazie ta instalacja wymaga obecności build-essential i python3, plus openjdk-17-jdk-headless oraz ścieżki loadera:
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}"
Metoda 1 – Hard‑code nazwy rodziny
Wersja, którą każdy pisze najpierw: wybrać Arial, dostarczyć ją, i iść dalej. Działa na maszynie deweloperskiej i zawodzi przy pierwszym uruchomieniu kontenera, ponieważ obrazy Debian nie instalują Arial – instalują Liberation Sans, który jest metrically‑compatible pod inną nazwą rodziny.
Nie ma tu kodu wartego pokazania, i właśnie o to chodzi. Cała zawartość metody to literał łańcucha, który jest prawdziwy w jednym środowisku.
Metoda 2 – Wykrywanie czcionek z systemu plików
Naturalne rozwiązanie: przeskanować katalogi czcionek, zobaczyć, co jest dostępne, wybrać coś. Połowa tego jest naprawdę użyteczna – inwentaryzacja mówi, czy obraz ma 0 czcionek czy 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',
];
Druga połowa nie działa. Pliki czcionek rzadko zawierają ciąg rodziny, który wywołujący musi przekazać: fonts-noto-cjk w Debianie instalują NotoSansCJK-Regular.ttc, którego rodzina to Noto Sans CJK JP. Wyprowadzenie rodziny z tej nazwy pliku daje NotoSansCJK-Regular, co nie rozwiązuje się w nic. Wykrywanie po nazwie pliku zarówno pomija czcionki, które są obecne, jak i pewnie zgłasza rodziny, które i tak zawiodą.
Trzymaj inwentaryzację jako diagnostykę. Nie używaj jej do wyboru. Liczba odpowiada na pytanie, czy obraz został w ogóle przygotowany, co jest innym, równie użytecznym pytaniem.
Metoda 3 – Zapytaj bibliotekę
Spróbuj jednorazowego podpisu dla każdej kandydackiej rodziny i zachowaj pierwszą, która nie rzuci wyjątku. Kosztuje to jeden zapis PDF na kandydata i jest jedyną metodą, której odpowiedź jest autorytatywna, ponieważ jest to to samo wywołanie, które wykona prawdziwy podpis.
for (const candidate of candidates) {
if (tryFamily(sourcePath, candidate) === null) {
return candidate;
}
}
return null;
W Node potrzebny jest dodatkowy element. node-java zamienia każdy wyjątek Java na Error running instance method, więc prawdziwa wiadomość musi zostać odzyskana ze stosu śladowego:
const stack = err.stack || '';
const match = stack.match(/com\.groupdocs\.signature\.exception\.[^\n]*/);
return match ? match[0].trim() : (err.message || String(err));
Bez tych dwóch linii kontener bez czcionek i ze złamaną ścieżką JVM generują identyczne logi. Spędziłem więcej czasu niż chciałbym przyznać, porównując dwa kontenery, które drukowały ten sam błąd z zupełnie innych przyczyn, zanim dodałem wyrażenie regularne.
Co kosztuje sondowanie
Zastrzeżenie wobec sondowania jest takie, że zapisuje pliki, i tak jest: jeden mały PDF na kandydata, usuwany natychmiast. Lista łacińska w przykładzie ma cztery pozycje, a lista CJK osiem, więc zimny start zapisuje maksymalnie dwanaście jednopaginowych dokumentów w katalogu tymczasowym, zanim usługa będzie gotowa.
To koszt uruchomienia, nie koszt per‑request, i zapewnia linię logu z nazwami obu rozwiązanych rodzin. W porównaniu z kontenerem, który uruchamia się czysto, a potem upada przy pierwszym dokumencie klienta z błędem mostu, dwanaście plików tymczasowych nie jest trudnym kompromisem.
Porównanie metod: Kiedy używać której
| Metoda | Najlepsze dla | Kluczowe zalety | Ograniczenia |
|---|---|---|---|
| Hard‑coded family | jednego kontrolowanego środowiska | trywialne, brak kosztu uruchomienia | psuje się w każdym obrazie, który nie ma tej dokładnej rodziny |
| Wykrywanie po nazwie pliku | diagnozowania, co zawiera obraz | szybkie, brak wywołań podpisu | nazwy plików nie są nazwami rodzin, więc wybory z nich pochodzące zawodzą |
| Sondowanie biblioteki | wszystkiego, co jest konteneryzowane lub przenośne | autorytatywne, działa zarówno na laptopie, jak i w obrazie | jeden zapis PDF na kandydata, więc rozwiąż przy starcie i cache’uj |
Dwie ciekawostki wiązania, które warto znać
Gdy rodzina zostanie rozwiązana, samo wywołanie podpisu ma specyficzny kształt Node‑a. API Java przyjmuje listę opcji, ale tablica JavaScript nie jest marshallowana do java.util.List, więc przekazanie jej powoduje Could not find method "sign(java.lang.String, [Ljava.lang.Object;)". Obejściem jest łańcuchowanie przeciążenia jednoparametrowego i etapowanie przez plik tymczasowy:
new signatureLib.Signature(sourcePath)
.sign(firstOutput, buildTextOptions(LATIN_TEXT, latinFamily, 50));
if (stageTwo) {
new signatureLib.Signature(firstOutput)
.sign(outputPath, buildTextOptions(CJK_TEXT, cjkFamily, 120));
}
Druga ciekawostka to odczyt zwrotny. TextVerifyOptions nie przechodzi w pełni przez to wiązanie: verify podnosi ten sam ogólny błąd mostu, więc przykład zwraca sentinel i drukuje unavailable zamiast udawać, że podpis się nie powiódł. Pakiet npm ma wersję 24.12.0, opublikowaną w grudniu 2024, i zawiera silnik 23.6.1, podczas gdy .NET jest w wersji 26.6, a Java w 26.5. Podpis nie jest dotknięty; brak tylko ścieżki weryfikacji.
Czy nadal powinienem używać wiązania Node.js w produkcji?
Dla podpisów wyłącznie łacińskich, tak: podpisuje poprawnie, a brak czcionki podnosi wyjątek zamiast cicho degradować, więc tryb awarii jest głośny. Dla pracy ze skryptami mieszanymi, rozważ brak odczytu zwrotnego, ponieważ nic w procesie nie może wtedy potwierdzić, że glify CJK zostały osadzone, a nie wyświetlają się jako kwadraty. Mały weryfikator w .NET lub Java w tym samym pipeline pokrywa tę lukę.
Najlepsze praktyki i wskazówki
- Provision w kolejności: JDK i łańcuch narzędzi, ścieżka loadera, czcionki, a potem aplikacja. Każda warstwa zawodzi inaczej, a ich mieszanie spowalnia diagnozę.
- Rozwiąż rodziny raz przy starcie i zaloguj je obok liczby czcionek.
- Zablokuj Node 18 oraz JDK w przedziale 8‑17 i traktuj je jako stałą infrastrukturę, a nie rutynowe aktualizacje.
- Trzymaj Dockerfile bez czcionek w repozytorium, aby awaria była zawsze o jeden build dalej.
Zakończenie
Trzy sposoby wyboru czcionki, jeden, który przetrwa wdrożenie. Sonduj bibliotekę, cache’uj odpowiedź i niech inwentaryzacja służy diagnostyce, a nie decyzji. Następnie pracuj z wiązaniem takim, jakie jest: podpisuj jedną opcję naraz, wyciągaj wyjątek Java ze stosu i zgłaszaj brak weryfikacji szczerze, zamiast go ukrywać. Repozytorium przykładu buduje oba obrazy, więc każde twierdzenie tutaj można sprawdzić dwoma poleceniami.