はじめに
帳票(伝票・請求書・ラベルなど)の出力は、日本のビジネス現場では今も重要な課題です。
既存の帳票ツールには共通した問題があります。
- 高額なライセンス費用(数十万〜数百万円)
- Windows専用(Mac・Linuxでは動かない)
- ベンダーロックイン(独自フォーマットで乗り換え困難)
- 試すだけで契約が必要(現場で気軽に評価できない)
これらの課題を解決するために、ACR(Across Report Renderer) というOSSの帳票エンジンを開発しています。
本記事では、ACRのブラウザプレビュー機能を実現した WASM + Chrome拡張 の実装について紹介します。
アーキテクチャ概要
[ブラウザ]
↓ chrome.runtime.sendMessage
[Chrome拡張 background.js(Service Worker)]
↓ WASM関数呼び出し
[acr_wasm_bg.wasm(Rust製)]
↓ tiny-skiaでピクセル描画
[PNG bytes]
↓ Base64エンコード
[ブラウザ <img>タグ表示]
ポイントは サーバが一切不要 な点です。レンダリング処理はすべてブラウザ内で完結します。データは外部に送信されません。
なぜWASMなのか
Rust製WASMは「難読化済み」
Chrome拡張のJavaScriptは誰でも読めます。しかしRust製WASMは:
- リリースビルドで関数名がマングリングされる
-
wasm-opt(Binaryenの最適化)でさらにシュリンク - 逆コンパイルは実質困難
FigmaやPhotoshop on Webも同じ戦略を採っています。Rust製WASMであること自体が難読化になっているわけです。
tiny-skiaを選んだ理由
当初はGoogle Skiaの使用を検討しましたが:
| ライブラリ | WASMビルド | 備考 |
|---|---|---|
| skia-safe(Rustバインディング) | ❌ 不可 | C++バインディングのため |
| canvaskit-wasm(公式WASM) | ✅ 可能 | ただし巨大 |
| tiny-skia(純Rust) | ✅ 可能 | 軽量・高速 |
tiny-skiaは純Rustで実装されたSkia互換ライブラリです。wasm-packでそのままWASMにビルドできます。
実装の核心部分
フォントの埋め込み
static FONT_GOTHIC: &[u8] = include_bytes!("/usr/share/fonts/opentype/ipafont-gothic/ipag.ttf");
static FONT_MINCHO: &[u8] = include_bytes!("/usr/share/fonts/opentype/ipafont-mincho/ipam.ttf");
IPAフォント2種類をinclude_bytes!でWASMバイナリに直接埋め込みます。これによりビルド後の.wasmファイルは約15MBになります。
Chrome Web Storeの上限(128MB)には余裕があります。
Chrome拡張の設計
manifest.json(抜粋)
{
"manifest_version": 3,
"permissions": ["storage", "tabs"],
"externally_connectable": {
"matches": ["http://localhost/*", "https://*/*"]
},
"content_security_policy": {
"extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'"
},
"background": {
"service_worker": "background.js",
"type": "classic"
}
}
wasm-unsafe-evalがWASM実行に必須です。
background.js(Service Worker)
はじめに
帳票(伝票・請求書・ラベルなど)の出力は、日本のビジネス現場では今も重要な課題です。
はじめに
帳票(伝票・請求書・ラベルなど)の出力は、日本のビジネス現場では今も重要な課題です。
はじめに
帳票(伝票・請求書・ラベルなど)の出力は、日本のビジネス現場では今も重要な課題です。
importScripts('./acr_wasm.js');
let wasmReady = false;
wasm_bindgen('./acr_wasm_bg.wasm').then(() => {
wasmReady = true;
console.log('ACR WASM ready');
});
chrome.runtime.onMessageExternal.addListener((request, sender, sendResponse) => {
if (request.type === 'render_png') {
chrome.storage.local.get('wasm_license_key', (result) => {
if (!result.wasm_license_key) {
chrome.tabs.create({ url: 'https://licensvr.acrossreport.com' });
sendResponse({ success: false, error: 'not_registered' });
return;
}
const bytes = wasm_bindgen.render_png(
request.template_json,
request.data_json,
request.page_index ?? 0
);
sendResponse({
success: true,
png_base64: uint8ToBase64(bytes),
page_count: wasm_bindgen.get_page_count(
request.template_json,
request.data_json
)
});
});
return true;
}
});
外部WebページからWASMを呼ぶ
chrome.runtime.sendMessage(
'gpblhkjoabpdpdjbfkpinlgjdniiidch',
{
type: 'render_png',
template_json: JSON.stringify(template),
data_json: JSON.stringify(data),
page_index: 0,
},
(r) => {
if (r.success) {
img.src = 'data:image/png;base64,' + r.png_base64;
}
}
);
ローカルプレビューサーバ(Rust + Axum)
ユーザーが手元で試せるように、ローカルサーバをRustで実装しました。
#[tokio::main]
async fn main() {
let port: u16 = std::env::args()
.nth(1)
.and_then(|p| p.parse().ok())
.unwrap_or(7878);
let app = Router::new()
.route("/", get(index_handler))
.route("/upload", post(upload_handler))
.route("/sample/template", get(sample_template))
.route("/sample/data", get(sample_data))
.layer(CorsLayer::permissive())
.with_state(state);
// ブラウザを自動で開く
#[cfg(target_os = "linux")]
let _ = std::process::Command::new("xdg-open")
.arg(format!("http://localhost:{}", port))
.spawn();
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
include_str!でHTMLをバイナリに埋め込むことで、単一バイナリで配布できます。
GitHub Actionsでクロスプラットフォームビルド
jobs:
build:
strategy:
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: macos-latest
target: aarch64-a## はじめに
帳票(伝票・請求書・ラベルなど)の出力は、日本のビジネス現場では今も重要な課題です。
## はじめに
帳票(伝票・請求書・ラベルなど)の出力は、日本のビジネス現場では今も重要な課題です。
pple-darwin
タグを打つだけでLinux/Windows/Macの3バイナリが自動生成され、Releasesに公開されます。
実際の動作
仕入伝票(25ページ)のレンダリング結果:
- レンダリング時間: 約30秒(25ページ全体)
- バイナリサイズ: 2.5MB(Linux)、3.5MB(Windows)
- WASM サイズ: 15MB(フォント2種込み)
ハマったポイント
1. Service Workerのスリープ問題
Manifest V3のService WorkerはアイドルになるとスリープしてWASMが消えます。
解決策:ページロード時にリトライ処理を入れる。
let retry = 0;
async function tryCheck() {
await runChecks();
if (!extOk && retry < 5) {
retry++;
setTimeout(tryCheck, 1500);
}
}
tryCheck();
2. get_license_statusのsuccessフィールド
render_pngは{ success: true, ... }を返しますが、get_license_statusは{ registered: true, key: ... }を返します。統一していなかったため、チェック関数が誤動作しました。
3. Prometheusのスコープ問題
メッセージタイプによって返り値の形式が違うため、sendToExtでタイプ別に分岐が必要です。
ビジネスモデル
WASM(無償)
└── PNG出力・無制限・商用利用可
└── 登録は初回のみ・以後オフライン
Engine(有償)
└── CLI/DLL/API
└── PDF・ESC/POS・ZPL出力
└── 導入サポート付き
Designer(β・無償)
└── GUIテンプレート作成
└── Mac/Windows対応
└── ActiveReports互換
「まず無償で触れる」ことが最大の差別化です。
まとめ
| 技術 | 用途 |
|---|---|
| Rust + tiny-skia | WASMレンダリングエンジン |
| wasm-pack | WASMビルド |
| Chrome拡張(MV3) | WASMのホスト |
| GitHub Actions | クロスプラットフォームビルド |
Rust製WASMはバイトコードで保護されており、逆コンパイルは実質困難です。FigmaやPhotoshop on Webと同じアーキテクチャです。
帳票という枯れた分野に、現代のアーキテクチャで切り込んでいます。
リンク
- ライセンス登録: https://licensvr.acrossreport.com
- プレビューツール: https://github.com/acrossreport/acr-wasmtest
- Web: https://acrossreport.com