💡 Повний робочий приклад доступний на GitHub:
nodejs-docker-signing-with-fonts

Вступ

Визначення шрифту — це частина підписування контейнера, яка вирішує, чи ваш Node‑сервіс створює документи чи генерує виключення. GroupDocs.Signature не підставляє відсутню сімейство: вкажіть таке, якого в образі немає, і виклик підніме виключення, нічого не записуючи. Очищення шрифту також не є обхідним рішенням, оскільки бібліотека потім запитує власний шрифт за замовчуванням і зазнає тієї ж помилки.

Існує три способи визначити, яку сімейство передати, і лише один з них працює в контейнері. У цій статті вони порівнюються, а потім розглядаються процеси підготовки та поведінка прив’язки, які формують код навколо них, оскільки Node.js через Java має більше обох аспектів, ніж будь‑яка інша платформа, на якій постачається ця бібліотека.

Чому це важливіше для Node.js

Пакет є мостом: node-java завантажує JVM у процес. Тому образ Node‑підпису потребує JDK, інструментального ланцюжка node-gyp для збірки моста та LD_LIBRARY_PATH, що вказує на libjvm.so, ще до того, як шрифти стануть актуальними. node:18-bookworm додає 6 файлів шрифтів DejaVu для AWT — достатньо для латиниці, нічого для CJK.

Таке поєднання призводить до збоїв, які виглядають як помилки застосунку. Відсутній шлях до JVM, відсутній шрифт і невідповідність маршалінгу — усі вони проявляються як Error running instance method, бо саме так node-java повідомляє про будь‑яке виключення на стороні Java.

Передумови

Node 18 — міст працює проти NAN, який не компілюється проти V8 у Node 20 або 22 ('AccessorSignature' is not a member of 'v8'). JDK 8‑17: у JDK 25 шар зображень падає з помилкою Cannot open an image. The image size can not be 0!.

Встановлення

npm install @groupdocs/groupdocs.signature

У образі ця установка потребує build-essential і python3, а також openjdk-17-jdk-headless та шляху завантажувача:

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

Метод 1 — Жорстко закодоване ім’я сімейства

Версія, яку всі пишуть першою: вибрати Arial, включити його, рухатися далі. Працює на машині розробника і падає при першому запуску контейнера, бо образи Debian не встановлюють Arial — вони встановлюють Liberation Sans, який метрично сумісний, але має інше ім’я сімейства.

Коду, який варто показати, тут немає, і це саме суть. Увесь зміст методу — це рядковий літерал, який випадково є правильним в одному середовищі.

Метод 2 — Виявлення шрифтів у файловій системі

Природне виправлення: просканувати каталоги шрифтів, подивитися, що є, вибрати щось. Половина цього дійсно корисна — інвентаризація підказує, чи образ має 0 шрифтів чи 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',
];

Інша половина не працює. Файли шрифтів рідко містять рядок сімейства, який має передати виклик: fonts-noto-cjk у Debian встановлює NotoSansCJK-Regular.ttc, сімейство якого — Noto Sans CJK JP. Виведення сімейства з назви файлу дає NotoSansCJK-Regular, що не розв’язується. Визначення за назвою файлу пропускає присутні шрифти і впевнено повідомляє про сімейства, які все одно зазнають провалу.

Тримайте інвентар як діагностичний інструмент. Не використовуйте його для вибору. Кількість відповідає на питання, чи був образ взагалі підготовлений, що є іншим і так само корисним питанням.

Метод 3 — Запит до бібліотеки

Спробуйте одноразовий підпис для кожного кандидата сімейства і залиште перший, який не кине виключення. Це вимагає одного запису PDF на кожного кандидата і є єдиним методом, відповідь якого є авторитетною, бо це той самий виклик, який робить реальний підпис.

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

У Node‑середовищі проба потребує ще одного кроку. node-java зводить будь‑яке виключення Java до Error running instance method, тому реальне повідомлення треба витягнути зі стек‑трейсу:

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

Без цих двох рядків контейнер без шрифтів і з поламаним шляхом до JVM генерують ідентичні логи. Я довго порівнював два контейнери, які виводили одну й ту ж помилку з різних причин, перш ніж додав регулярний вираз.

Які витрати на пробу

Заперечення проти проби полягає в тому, що вона записує файли, і це так: один маленький PDF на кожного кандидата, який одразу ж видаляється. У прикладі латинський список має чотири елементи, а CJK‑список — вісім, тому холодний старт записує максимум дванадцять односторінкових документів у тимчасовий каталог перед тим, як сервіс стане готовим.

Це стартовий витрат, а не витрата на кожен запит, і він дає рядок логу з іменами обох розв’язаних сімейств. Порівняно з контейнером, який стартує без проблем, а потім падає на першому документі користувача через помилку моста, дванадцять тимчасових файлів — не складна плата.

Порівняння методів: коли використовувати кожен

Метод Краще підходить для Ключові переваги Обмеження
Жорстко закодоване сімейство одного контрольованого середовища тривіально, без стартових витрат ламається в будь‑якому образі, де немає саме цього сімейства
Визначення за назвою файлу діагностики вмісту образу швидко, без викликів підпису назви файлів не є сімействами, тому вибір за ними зазнає провалу
Проба бібліотеки будь‑якого контейнеризованого або портативного середовища авторитетно, працює і на ноутбуці, і в образі один запис PDF на кандидата, тому варто виконувати під час старту і кешувати результат

Два нюанси прив’язки, які варто знати

Після того, як сімейство розв’язано, сам виклик підпису має специфічну форму для Node. Java API приймає список опцій, але масив JavaScript не маршалиться в java.util.List, тому передача його призводить до Could not find method "sign(java.lang.String, [Ljava.lang.Object;)". Обхідний шлях — використати перевантаження з однією опцією і проміжно записати у тимчасовий файл:

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

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

Другий нюанс — читання назад. TextVerifyOptions не проходить круговий шлях через цю прив’язку: verify піднімає той самий загальний помилковий код моста, тому приклад повертає sentinel і виводить unavailable замість того, щоб вдаватись, що підпис провалився. npm‑пакет має версію 24.12.0, опубліковану в грудні 2024, і включає движок 23.6.1, тоді як .NET — 26.6, а Java — 26.5. Підпис не постраждав; лише шлях верифікації відсутній.

Чи варто все ще використовувати прив’язку Node.js у продакшені?

Для підпису лише латиницею — так: підпис працює коректно, і відсутність шрифту піднімає виключення, а не тихо деградує, тому режим збою голосний. Для змішаних скриптів треба зважати на відсутність читання назад, оскільки процес не зможе підтвердити, що CJK‑гліфи вбудовані, а не відображаються у вигляді коробок. Маленька верифікація на .NET або Java у тому ж конвеєрі заповнює цей прогалину.

Кращі практики та поради

  • Підготовка у порядку: JDK і інструментальний ланцюжок, шлях завантажувача, шрифти, потім застосунок. Кожен шар падає по‑різному, і їх змішування уповільнює діагностику.
  • Розв’язуйте сімейства один раз під час старту і логуйте їх поряд з кількістю шрифтів.
  • Фіксуйте Node 18 і JDK між 8 та 17, розглядайте їх як стабільну інфраструктуру, а не як рутинні оновлення.
  • Тримайте Dockerfile без шрифтів у репозиторії, щоб збій залишався на один збірковий крок.

Висновок

Три способи вибору шрифту, один з яких переживає розгортання. Пробуйте бібліотеку, кешуйте відповідь і використовуйте інвентар лише як діагностичний інструмент, а не як рішення. Потім працюйте з прив’язкою так, як вона є: підписуйте одну опцію за раз, витягуйте виключення Java зі стек‑трейсу і чесно повідомляйте про відсутність верифікації, а не приховуйте її. Прикладний репозиторій збирає обидва образи, тому кожну заяву тут можна перевірити двома командами.

Додаткові ресурси