0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Node.js/ElectronでHeadless Chromiumを使わずPDF帳票を生成する(napi-rs) Edit コンテストに応募する

0
Posted at

はじめに

先日、開発のなかで少し長めの仕様書をVS CodeでMarkdown表示したところ、「Ctrl+Shift+V」で綺麗に表示されました。ただ、確認のために印刷しようとしたところ、プレビューの実体が「HTML」であるため印刷もPDF保存もできないことに気づき、ACRをVS Codeの機能拡張として対応させました(こちらの記事で解説しています)。

もともとACRはWindows/Mac/Linuxのマルチベンダー対応で、VS Code自体もElectronで作られています。そこで今回、Node.js対応の npm パッケージとしてもリリースすることにしました。

Node.jsやElectronアプリで帳票(請求書・納品書など)をPDF/PNGとして出力したいとき、よくある選択肢は次のどちらかだと思います。

  1. Puppeteer等でHeadless Chromiumを起動し、HTMLを印刷する
  2. 別の帳票生成ツールを子プロセスで呼び出す

どちらも動きはしますが、Chromiumプロセスの起動コストやメモリ使用量、子プロセスのエラーハンドリング・stdout/stderrのパースなど、本質的ではない複雑さを抱え込むことになります。

今回、ACR (Across Report) という帳票レンダリングエンジンのNode.js/Electron向けバインディング @acrossreport/acr-node を napi-rs で作り、npmに公開しました。この記事では、なぜこの構成にしたか、どう作ったかを書きます。

ACRとは

ACRは、JSON形式の帳票テンプレートをRust + Google Skiaで描画する、プリンタ非依存の帳票レンダリングエンジンです。

テンプレート(JSON) → レイアウトエンジン → 描画エンジン(Skia) → PDF / PNG
  • 同じテンプレートがWindows/macOS/Linuxで同一のピクセル結果を生成する(WYSIWYG)
  • プレビューと印刷結果が完全一致する(1ドットの狂いもない設計方針)
  • 業務帳票で馴染みのある「セクション型」のレイアウトモデル(PageHeader/GroupHeader/Detail/GroupFooter/PageFooter)

これを使ったCLIツール(acr_cli)、VS Code拡張、デスクトップのデザイナーアプリなどは既に存在していましたが、「Node.js/Electronアプリに直接組み込みたい」というニーズに対する製品がありませんでした。

なぜnapi-rsか

検討したのは大きく2案です。

案A: CLIラッパー(子プロセスでacr_cliを叩く)

一番手軽ですが、以下のデメリットがあります。

  • プロセス起動のオーバーヘッド(帳票を大量に生成するサーバー用途では無視できない)
  • stdout/stderr経由でのエラーハンドリングが脆い
  • Windows/macOS/Linuxでバイナリの配置・権限周りの考慮が増える

案B: napi-rsでネイティブアドオン化

  • RustのACRコア(acr_core / acr_renderer_skia)をそのままFFI越しに呼べる
  • プロセス境界がないため、オーバーヘッドが小さい
  • napi-rsはNode.js/Electron両対応の実績が厚く、prebuilt binaryの配布の仕組みが整っている

既にRust実装のACRエンジンがある以上、わざわざプロセス境界を挟む理由がなく、案Bを採用しました。

実は社内にはVS Code拡張向けに同様のnapi-rsバインディング(acr_vscode_native)が既にあったため、そのパターンを踏襲する形で実装できました。

実装の方針

やったことはシンプルで、既存のRustクレート(acr_core / acr_renderer_skia)のロジックには一切手を入れず、#[napi] マクロで薄いラッパー関数を被せただけです。既存製品(CLI/VS Code拡張)と挙動が食い違うリスクを避けられますし、実装量も最小限で済みました。

いくつか気をつけた点だけ挙げておきます。

  • panicのハンドリング: napi-rs境界を越えてRust側がpanicすると、Node.jsプロセスごと落ちてしまいます。テンプレートJSONが壊れている等のユーザー起因のエラーでアプリ全体が落ちるのは避けたいので、公開関数はcatch_unwindで包んでいます。
  • ライセンスの扱い: 無償利用はできますが、既存のCLIツールと同じ方針で、ライセンスキー未登録時は出力にウォーターマークが入ります。ライセンスのキャッシュファイルは他のACR製品(CLI/Designer/Viewer)と共通フォーマットなので、同じマシンで既にどれか1つアクティベート済みなら、acr-nodeは自動的にそのライセンスを使います。

クロスプラットフォームビルド

napi-rsは index.<platform-triple>.node という命名規則でプラットフォームごとのバイナリを配置し、読み込み時にOS/CPUを見て適切なものをrequireするローダー(index.js)を生成してくれます。

switch (platform) {
  case 'win32':
    switch (arch) {
      case 'x64':
        nativeBinding = require('./index.win32-x64-msvc.node');
        break;
      case 'arm64':
        nativeBinding = require('./index.win32-arm64-msvc.node');
        break;
    }
    break;
  case 'linux':
    // glibc/musl、x64/arm64 の判定...
  case 'darwin':
    // x64/arm64 の判定...
}

今回は以下の6プラットフォーム分を、それぞれ実機(Windows x64機、Windows ARM64機、Intel Mac、Apple Silicon Mac、Ubuntu x64、Ubuntu ARM64 on WSL2)でビルドして同梱しました。

プラットフォーム 状態
Windows x64 ✅
Windows ARM64 ✅
Linux x64 (glibc) ✅
Linux ARM64 (glibc) ✅
macOS (Apple Silicon) ✅
macOS (Intel) ✅

CI(GitHub Actions)でのクロスビルドは未整備で、今回は手元の実機を一台ずつ回って cargo build --release した結果を集めました。次の課題はここの自動化です。

使い方

npm install @acrossreport/acr-node
import { renderPdf, renderPreview, renderPngZip, listDataFields, activate, licenseStatus } from "@acrossreport/acr-node";
import { readFileSync } from "node:fs";

const templateJson = readFileSync("invoice.template.json", "utf8");
const dataJson = readFileSync("invoice.data.json", "utf8");

// PDF出力(72dpi)。戻り値はページ数。
const pageCount = renderPdf(templateJson, dataJson, "invoice.pdf");

// 画面プレビュー用PNG配列(96dpi)
const pages = renderPreview(templateJson, dataJson);
// pages[0].png は Buffer

// ライセンスキーのアクティベート(同期HTTPリクエスト)
await activate("you@example.com", "ACR-XXXX-XXXX-XXXX-XXXX");

Electronのメインプロセスでも、素のNode.jsサーバー(Express、Next.jsのAPI route等)でも、同じAPIで動きます。

まとめ

  • Node.js/Electronでの帳票PDF/PNG生成を、Chromiumにも子プロセスにも頼らず、napi-rs経由でRust製の描画エンジンに直結した
  • 既存のRustロジック(acr_core / acr_renderer_skia)には手を入れず、薄いバインディング層だけを追加した
  • ライセンス周りも既存製品と共通化し、同一マシン上での重複アクティベートを避けられるようにした
  • 6プラットフォーム分のプリビルドバイナリを同梱し、利用側には追加のビルド環境を要求しない

リポジトリ: https://github.com/acrossreport/acr-node
npm: https://www.npmjs.com/package/@acrossreport/acr-node

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?