💡 完全な動作例は GitHub で入手可能です:
nodejs-docker-signing-with-fonts

はじめに

フォント解決は、コンテナ署名の一部であり、Node サービスがドキュメントを生成するか例外を投げるかを決定します。GroupDocs.Signature は、画像に存在しないファミリ名を代替しません。画像にその名前が無い場合、呼び出しは例外を発生させ、何も書き込みません。フォントをクリアすることも回避策にはなりません。ライブラリは自分のデフォルトフォントを要求し、同じ方法で失敗します。

ファミリ名を決める方法は 3 つあり、コンテナ内で生き残るのはそのうちの 1 つだけです。本稿ではそれらを比較し、プロビジョニングとバインディングの挙動についても解説します。Node.js は Java 経由で動作するため、他のプラットフォームよりも多くの要素が関与します。

Why This Matters More on Node.js

このパッケージはブリッジです。node-java がプロセス内に JVM をロードします。そのため、Node の署名イメージには JDK、node-gyp ツールチェーン、LD_LIBRARY_PATH が libjvm.so を指す設定が必要で、フォントが関係する前にこれらが揃っていなければなりません。node:18-bookworm には AWT 用の DejaVu フォントが 6 ファイル含まれていますが、これはラテン文字用であり CJK 用のフォントは含まれていません。

この組み合わせにより、アプリケーションのバグのように見える失敗が発生します。JVM のパスが欠如している、フォントが欠如している、マシャリングの不一致がすべて Error running instance method として表面化します。これは node-java が Java 側でスローされた例外をすべて同じ形で報告するためです。

前提条件

Node 18 – ブリッジは NAN に対してビルドされますが、Node 20 や 22 の V8 ではコンパイルできません('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 は実行時に libjvm.so を dlopen します。デフォルトのローダーパスには含まれていません。
ENV LD_LIBRARY_PATH="${JAVA_HOME}/lib/server:${LD_LIBRARY_PATH}"

Method 1 - Hard-code the family name

誰もが最初に書くバージョンです: Arial を選んでイメージに含め、先へ進みます。開発マシンでは動作しますが、最初のコンテナ実行時に失敗します。Debian 系イメージは Arial をインストールせず、代わりに Liberation Sans をインストールします。Liberation Sans はメトリック互換ですが、ファミリ名が異なるためです。

ここに示す価値のあるコードはありません。メソッドの全内容は、ある環境でだけ真になる文字列リテラルです。

Method 2 - Detect fonts from the filesystem

自然な修正策です: フォントディレクトリを走査し、存在するものを確認して選択します。実際に有用なのは半分程度で、インベントリはイメージにフォントが 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',
];

残りの半分は機能しません。フォントファイルは呼び出し側が渡すべきファミリ文字列をほとんど持っていません。たとえば Debian の fonts-noto-cjk は NotoSansCJK-Regular.ttc をインストールしますが、そのファミリは Noto Sans CJK JP です。ファイル名からファミリを導出すると NotoSansCJK-Regular となり、何も解決できません。ファイル名検出は実際に存在するフォントを見逃すこともあり、失敗するファミリ名を自信を持って報告してしまいます。

インベントリは診断情報として保持してください。選択に使用しないでください。カウントはイメージがそもそもプロビジョニングされているかどうかを示す質問への答えであり、別の有用な質問です。

Method 3 - Ask the library

各候補ファミリに対して使い捨ての署名を試み、例外が出なかった最初のものを保持します。候補ごとに PDF 書き込みが 1 回必要ですが、実際の署名が行う呼び出しと同じなので、唯一権威ある回答が得られます。

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));

この 2 行がなければ、フォントなしコンテナと壊れた JVM パスは同一のログを出力します。正規表現を追加するまで、全く異なる原因で同じエラーが出る 2 つのコンテナを比較していた時間は、正直言って想像以上に長くなっていました。

What the probe costs

プローブの批判点は「ファイルを書き込む」ことです。実際にそうなります。候補ごとに小さな PDF を 1 つ書き込み、すぐに削除します。サンプルのラテン文字リストは 4 件、CJK リストは 8 件なので、コールドスタート時に最大 12 ページの PDF が一時ディレクトリに書き込まれ、サービスが利用可能になるまでに残ります。

これはリクエストごとのコストではなく、起動時のコストです。解決されたファミリ名をログに残すことができるので、クリーンに起動し、最初の顧客ドキュメントでブリッジエラーが発生するコンテナと比べても、12 個の一時ファイルは大きなトレードオフとは言えません。

Comparing Methods: When to Use Each

方法 最適なケース 主な利点 制限事項
ハードコードされたファミリ 単一の管理された環境 手軽、起動コストなし 正確なファミリが無いイメージでは失敗
ファイル名検出 イメージに何が含まれているかの診断 高速、署名呼び出し不要 ファイル名はファミリ名ではないため、そこから導出した選択は失敗
ライブラリプローブ コンテナ化またはポータブルな環境全般 権威ある結果、ラップトップとイメージの両方で動作 候補ごとに PDF 書き込みが必要なので、起動時に解決しキャッシュする必要あり

The Two Binding Quirks Worth Knowing

ファミリが解決すると、署名呼び出し自体に 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));
}

2 つ目の奇妙な点は読み戻しです。TextVerifyOptions はこのバインディングを通してラウンドトリップできません。verify は同じ汎用ブリッジエラーを発生させるため、サンプルは sentinel を返し、unavailable と出力します。npm パッケージは 2024 年 12 月にリリースされた 24.12.0 で、エンジンは 23.6.1 をバンドルしています。一方 .NET は 26.6、Java は 26.5 です。署名機能は影響を受けませんが、検証パスが欠如しています。

Should I still use the Node.js binding in production?

ラテン文字のみの署名であれば、はい。正しく署名でき、フォントが欠如している場合は黙って劣化するのではなく例外が発生するため、失敗モードが明確です。混在スクリプトの作業では、読み戻しが欠如している点を考慮してください。プロセス内で CJK グリフが埋め込まれたかどうかを確認できなくなるためです。同じパイプライン内で .NET または Java の小さな検証ツールを併用すれば、このギャップは埋められます。

Best Practices and Tips

  • プロビジョニングは順序通りに行う: JDK とツールチェーン、ローダーパス、フォント、そしてアプリ。各層は異なる形で失敗し、順序を混ぜると診断が遅くなります。
  • 起動時にファミリを一度だけ解決し、フォント数とともにログに残す。
  • Node 18 と JDK 8〜17 の組み合わせを固定し、インフラとして扱い、定期的なアップグレード対象にしない。
  • フォントなし Dockerfile をリポジトリに残しておき、失敗が常に 1 ビルド先にある状態を保つ。

結論

フォント選択の方法は 3 つ、デプロイで生き残るのは 1 つだけです。ライブラリをプローブし、結果をキャッシュし、インベントリは診断情報としてだけ利用しましょう。その上でバインディングの形に合わせて、1 オプションずつ署名し、スタックトレースから Java 例外を抽出し、検証が欠如していることは正直に報告します。サンプルリポジトリは両方のイメージをビルドするので、ここで述べたすべての主張は 2 つのコマンドで検証できます。

追加リソース