1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust + WASM + Chrome拡張で「サーバ不要の帳票プレビューエンジン」を作った

1
Last updated at Posted at 2026-05-13

はじめに

帳票(伝票・請求書・ラベルなど)の出力は、日本のビジネス現場では今も重要な課題です。

既存の帳票ツールには共通した問題があります。

  • 高額なライセンス費用(数十万〜数百万円)
  • 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と同じアーキテクチャです。

帳票という枯れた分野に、現代のアーキテクチャで切り込んでいます。


リンク

1
1
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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?