LoginSignup
1
2

Rust で libtiff の tiff2pdf を WebAssembly 化する

Last updated at Posted at 2023-08-08

作ってみました。下記ページ開くと TIFF 画像が WebAssembly で PDF に変換されてダウンロードされます。

動機

わが社ではいにしえの図面は TIFF 画像でサーバに置かれています。一方で最近の図面は PDF なので、違うビューワが立ち上がってしまうので作業効率が悪いです。ので、TIFF 画像を自動的に PDF にへんかんする CLI ツールを作りました。このツールは Rust で書かれていて、FFI で libtiff の API を叩いています。WebAssembly に興味があったので 「wasm-pack を使えば、この CLI ツールも簡単に WebAssembly 化してブラウザ上で実行できるんだろう」と試してみたら、いくつか詰まったところがあったので記事にしておきます。

WebAssembly 化する方法

2023年現在 Rust を WebAssembly にするには以下の方法があるようです。

  • wasm-pack/wasm32-unknown-unknown
  • cargo-wasi/wasm32-wasi
  • emsdk/wasm32-unknown-emscripten

wasm-pack/wasm32-unknown-unknown

2023年現在 wasm-pack では C への FFI を含む Rust を WebAssembly 化するのは不可能のようです。たぶんこれからも無理そうです。わたしも下記の記事同様 env の罠にかかって wasm-pack での WebAssembly 化はあきらめました。

cargo-wasi/wasm32-wasi

下記の記事を参考に llvm-ar と WASI で WebAssembly 化できます。node 上で wasm 化した tiff2pdf は実行できました。これを webpack しようとすると失敗するので、これを解決するのが最近のわたしの課題です。(追記:webpack できたので後述します。)

emsdk/wasm32-unknown-emscripten

emsdk を使用することでブラウザ上で tiff2pdf できる WebAssembly が生成できました。下記の記事を参考にしました。

リポジトリはこちら。JPEG とか ZIP のサポートは省いているので変換できない TIFF 画像はいっぱいあると思います。

libtiff

Read and write on memory

libtiff の tiff2pdf は fopen とか fclose を使ってファイルの読み書きをしていました。WebAssembly 化するにあたってメモリ上で IO する必要があると思ったので、ここを Rust で書きました。(emscripten にはメモリファイルシステムが用意されているとも知らずに…)

この stackoverflow の回答を参考にしています。

Read はこう。

t2p/libtiff-sys/src/lib.rs
struct Input<'a>(Cursor<&'a [u8]>);

// tiff 変数に &[u8] の TIFF 画像データが入っている
let input_memory = Input(Cursor::new(tiff));
// 名前は適当で良い
let name = CString::new("MemoryInput")?;
// Read only モードで開く
let mode = CString::new("rm")?;

let input_tiff = unsafe {
    TIFFClientOpen(
        name.as_ptr(),
        mode.as_ptr(),
        &input_memory as *const _ as _, // コールバック関数に渡すデータ
        Some(input_read),
        Some(input_write),
        Some(input_seek),
        Some(dummy_close), // close, size, map, unmap は使用しないので空の関数で良い
        Some(dummy_size),
        Some(dummy_map),
        Some(dummy_unmap),
    )
};

unsafe extern "C" fn input_read(handle: thandle_t, buf: tdata_t, size: tmsize_t) -> tmsize_t {
    // TIFFClientOpen の第 3 引数に渡したデータ
    let input_memory = &mut *(handle as *mut Input);
    // libtiff が用意しているバッファを slice に変換
    let buf = slice::from_raw_parts_mut(buf as *mut u8, size as _);
    // その slice に TIFF データを読み込む
    input_memory.0.read_exact(buf).expect("failed to read.");
    // 読み込んだバイト数を返す
    size
}

unsafe extern "C" fn input_write(_: thandle_t, _: tdata_t, _: tmsize_t) -> tmsize_t {
    0
}

unsafe extern "C" fn input_seek(handle: thandle_t, offset: u64, whence: i32) -> u64 {
    let input_memory = &mut *(handle as *mut Input);
    let pos = match whence {
        // TIFF データの先頭からのオフセット
        0 => SeekFrom::Start(offset as _),
        // TIFF データの Cursor の現在位置からのオフセット
        1 => SeekFrom::Current(offset as _),
        // TIFF データの末尾からのオフセット
        2 => SeekFrom::End(offset as _),
        _ => unimplemented!(),
    };
    // Cursor 位置を動かして、現在の位置を返す
    input_memory.0.seek(pos).expect("failed to seek")
}

Rust には Vec、Cursor などがあるので C より簡単に書けますね。

続いて Write。

t2p/libtiff-sys/src/lib.rs
struct Output<'a>(Cursor<&'a mut Vec<u8>>);

// この Vec に PDF データが書き込まれる
let mut buf = vec![];
let mut output_memory = Output(Cursor::new(&mut buf));

let name = CString::new("MemoryOutput")?;
// Write モードで開く
let mode = CString::new("w")?;

let output_tiff = unsafe {
    TIFFClientOpen(
        name.as_ptr(),
        mode.as_ptr(),
        &output_memory as *const _ as _,
        Some(output_read),
        Some(output_write),
        Some(output_seek),
        Some(dummy_close),
        Some(dummy_size),
        Some(dummy_map),
        Some(dummy_unmap),
    )
};

unsafe extern "C" fn output_read(_: thandle_t, _: tdata_t, _: tmsize_t) -> tmsize_t {
    0
}

unsafe extern "C" fn output_write(handle: thandle_t, data: tdata_t, size: tmsize_t) -> tmsize_t {
    let output_memory = &mut *(handle as *mut Output);
    // libtiff が生成した PDF データを slice にする
    let data = slice::from_raw_parts(data as *const u8, size as _);
    // そのデータを上で用意した Vec に書き込む
    output_memory.0.write_all(data).expect("failed to write.");
    size
}

unsafe extern "C" fn output_seek(_: thandle_t, offset: u64, _: i32) -> u64 {
    offset
}

これでメモリ上で読み書きできるようになりました。

t2p_write_pdf

上記で準備したinput_tiffoutput_tifft2p_write_pdf関数に渡すだけです。第 1 引数はt2p_initで取得した T2P 構造体へのポインタです。

t2p/libtiff-sys/src/lib.rs
let t2p = unsafe { t2p_init() };
let ret = unsafe { t2p_write_pdf(t2p, input_tiff, output_tiff) };

しかし、これではうまく行きません。TIFFClientOpen は書き込みモードで開くと最初の 4 バイトに TIFF ヘッダを書き込んで、Cursor のオフセットを 4 進めてしまうのです。ので、t2p_write_pdfを呼ぶ前に Cursor を 0 に戻す必要があります。

t2p/libtiff-sys/src/lib.rs
let output_tiff = unsafe {
    TIFFClientOpen(
        // 省略
    )
};

// Cursor 位置を 0 に戻す
output_memory.0.set_position(0);

let ret = unsafe { t2p_write_pdf(t2p, input_tiff, output_tiff) };

エラー処理、メモリの自動開放も追加して libtiff-sys の lib.rs はこうなりました。

Emscripten

上記の libtiff-sys のgenerate_pdfは引数に slice を取って、戻り値は anyhow::Result に包まれた Vec となっていますが、これを Emscripten で扱うのは面倒なので C の関数っぽくラップします。MDN に WebP エンコーダを WebAssembly にする例があったので参考にしました。

t2p/src/lib.rs
// 生成した PDF データを保存しておくグローバル変数
static BUF: Global<ManuallyDrop<Vec<u8>>> = Global(RefCell::new(None));
struct Global<T>(RefCell<Option<T>>);
unsafe impl<T> Sync for Global<T> {}

// no_mangle 属性でマングリングされないようにする
// 第 1 引数は TIFF データへのポインタ
// 第 2 引数は TIFF データの長さ
// 返り値は PDF データへのポインタ
#[no_mangle]
pub fn generate_pdf(ptr: *const u8, len: usize) -> *const u8 {
    // TIFF データを slice にする
    let tiff = unsafe { slice::from_raw_parts(ptr, len) };
    // PDF に変換
    let pdf = libtiff_sys::generate_pdf(tiff).expect("failed to generate pdf.");
    // 関数抜けたときに PDF データが開放されないように ManuallyDrop に包む(必要ないかも。明示的だからいいか)
    let pdf = ManuallyDrop::new(pdf);
    let ptr = pdf.as_ptr();
    // グローバル変数に PDF データを保存
    *BUF.0.borrow_mut() = Some(pdf);
    ptr
}

そして、EMCC_CFLAGS-s EXPORTED_FUNCTIONS=['_generate_pdf']を追加して cargo でビルドしてやると JavaScript からgenerate_pdf関数を呼べるようになります。関数名の前に_を追加するルールのようです。

t2p/build.sh
export EMCC_CFLAGS="-O3 -o t2p.js -s EXPORTED_FUNCTIONS=['_generate_pdf','_buf_len','_free_buf','_malloc','_free'] -s ALLOW_MEMORY_GROWTH=1 -s EXPORTED_RUNTIME_METHODS=ccall"
cargo build --release --target wasm32-unknown-emscripten

続いて JavaScript 側の実装です。ccall関数を使って WebAssembly の関数を呼びます。第 1 引数が関数名。第 2 引数が返り値の型。第 3 引数が引数の型。第 4 引数が関数に渡す引数となっています。

t2p/index.html
const result_ptr = ccall("generate_pdf", "number", ["number", "number"], [ptr, array.length]);

ccall関数で使える型としてはstringnumberarrayがあるようです。当初arrayを使って TIFF データを渡そうとしましたが、これはスタックオーバーフローしてうまくいきませんでした。ので、大きなデータを WebAssembly 側に渡す場合にはヒープを使う必要があります。

t2p/index.html
// TIFF データを取得
const response = await fetch("test.tif");
const data = await response.arrayBuffer();
const array = new Uint8Array(data);
// WebAssembly 側にヒープ領域を確保
const ptr = Module._malloc(array.length);
// TIFF データを WebAssembly 側のヒープにコピー
Module.HEAPU8.set(array, ptr);

これで TIFF データを WebAssembly に渡して PDF に変換できました。次に WebAssembly 側のヒープにある PDF データを JavaScript 側にもってくる必要があります。generate_pdf関数は PDF データへのポインタを返すので、あとは PDF データの長さがあればどうにかなりそうです。ので、PDF データの長さを返す関数を Rust で書きます。

t2p/src/lib.rs
#[no_mangle]
pub fn buf_len() -> usize {
    BUF.0.borrow().as_ref().expect("no buffer.").len()
}

buf_len関数を JavaScript 側から呼び出すと PDF データのサイズを取得できます。前述の PDF データへのポインタとサイズでもって、WebAsssembly 側のヒープへの view を作り、データを JavaScript 側にコピーします。

t2p/index.html
// PDF データのサイズ取得
const len = ccall("buf_len", "number", [], []);
// WebAssembly 側のヒープへの view を作る
const view = new Uint8Array(Module.HEAPU8.buffer, result_ptr, len);
// JavaScript 側にデータをコピー
const result = new Uint8Array(view);

ついに、JavaScript 側に PDF データを持ってこれました!
あとは WebAssembly 側のヒープデータの後始末とか PDF のダウンロード処理を追加して index.html はこうなりました。

WASI

こちらが WASI 版のもの。

リポジトリはこちら。

@wasmer/wasi

WASI を Node やブラウザ上で実行するためのライブラリです。インメモリのファイルシステムがあるので TIFF 画像や、成果物の PDF はこのファイルシステムを使って Rust 側とやり取りしました。

t2p_wasi/index.mjs
// インメモリファイルを開いて
let file = wasi.fs.open("/input", {read: true, write: true, create: true});
// TIFF 画像を書き込む
file.write(tiff);
file.seek(0);

// Rust の main 関数が呼ばれる
wasi.start();

// Rust 側では "/output" に PDF データを書き込むので JavaScript 側に読み込む
const out = wasi.fs.open("/output", {read: true, write: false, create: false});
const pdf = out.read();

// PDF データの後始末
wasi.fs.removeFile("/output");
t2p_wasi/src/main.rs
fn main() -> Result<()> {
    // TIFF データの読み込み
    let tiff = fs::read("/input")?;
    // PDF へ変換
    let pdf = generate_pdf(&tiff)?;
    // インメモリファイルシステムに書き込む
    fs::write("/output", &pdf)?;
    Ok(())
}

webpack

Node で動くようになったので webpack でブラウザ用にまとめます。webpack.config.js はこうなりました。

t2p_wasi/webpack.config.js
const webpack = require("webpack");

module.exports = {
    entry: ["./index.mjs"],
    // Buffer がないって言われるので追加(あんまり意味わかってないので後で調べる)
    plugins: [
        new webpack.ProvidePlugin({
            Buffer: ["buffer", "Buffer"]
        })
    ],
    resolve: {
        fallback: {
            "buffer": require.resolve("buffer")
        }
    },
    // wasmer_wasi_js_bg.wasm がないって言われるので無視する 
    externals: {
        "wasmer_wasi_js_bg.wasm": true
    },
    // index.mjs の generate_pdf を index.html から window.generate_pdf() で呼べるようにする 
    output: {
        library: "generate_pdf",
        libraryTarget: "window",
        libraryExport: "default",
    }
};

まとめ

stdio.h とかを使っている C モジュールを FFI で使う Rust を WebAssembly 化するのはつらい。けど、どうにかなった:blush:

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