「TAURI」で「画像ビューア」のサンプルアプリを作ろう

2023年12月1日(金)
大西 武 (オオニシ タケシ)
第6回の今回は、本格的に「TAURI」を使ってpngやjpgファイルなどを読み込んで表示する「画像ビューア」のサンプルアプリを作っていきます。

バックエンドをJavaScriptから呼び出す

「viewer」→「src」→「main.js」のJavaScriptのコードで、下記のサンプルコードのようにバックエンドの「Rust」を呼び出してみます。Rustのコードを書かないと、ビルドして実行しても図5のようにファイルを開いたり保存したりするダイアログが出るだけで何も起きません。画像ファイルの拡張子は「png」「jpg」ファイルだけで十分かもしれません。

図5:ファイルを開くダイアログ

「open」関数と「save」関数でパス名を取得したときの「invoke」関数でRustにメッセージを送って返事を受け取りますが、「async」と「await」で非同期にメッセージをやり取りしないとデータの受け渡しが上手くいきません。

「open」関数でパス名を取得したときの「invoke」関数で受け取る戻り値は「タプル」です。JavaScriptの「Array」も同じ型の変数が複数並んだ「配列」ですが、「Array」は他にも何番目にくる型が異なることもあるタプルにもなります。

・「viewer」→「src」→「main.js」のサンプルコード
(前略)

//画像ファイルを開く
function openImage() {
  open({
    filters: [
      { name: 'PNG', extensions: ['png'] },
      { name: 'JPG', extensions: ['jpg'] },
      { name: 'Bitmap', extensions: ['bmp'] },
      { name: 'Targa', extensions: ['tga'] },
      { name: 'TIFF', extensions: ['tiff'] },
    ],
  }).then(async file => {
    let data = await invoke("open_img", { path: file });
    _canvas.width = data[0];
    _canvas.height = data[1];
    let img = _context.createImageData(_canvas.width, _canvas.height);
    for ( i = 0; i < img.data.length; i++ ) {
      img.data[i] = data[2][i]
    }
    _context.putImageData(img,0,0);
  });
}
//画像ファイルを保存
function saveImage() {
  let w = _canvas.width;
  let h = _canvas.height;
  let img = _context.getImageData(0, 0, w, h);
  let arr = [];
  img.data.forEach( (data) => { arr.push(data); });
  save({
    filters: [
      { name: 'PNG', extensions: ['png'] },
      { name: 'JPG', extensions: ['jpg'] },
      { name: 'Bitmap', extensions: ['bmp'] },
      { name: 'Targa', extensions: ['tga'] },
      { name: 'TIFF', extensions: ['tiff'] },
    ],
  }).then(async file => {
    await invoke("save_img", {
      path:file,
      width:w,
      height:h,
      data:arr
    });
  });
}

【サンプルコードの解説】
「openImage」関数でファイルを開くダイアログの「open」関数を呼び出します。フィルターで「png」「jpg」「bmp」「tga」「tiff」拡張子だけ開けるようにします。ファイルが選択されたらバックエンドのRustに向けて"open_img"メッセージを呼び出してパス名を渡し、Rustで画像を読み込んだ生データをmain.jsで受け取り、canvasタグにその画像の幅と高さを指定し表示します。ただし次のRustのコードを書くまでまだ機能しません。
「saveImage」関数でcanvasグラフィックスの幅と高さを取得し、canvasグラフィックスの生データをすべて「arr」配列に追加(push)します。ファイルを保存ダイアログの「save」関数を呼び出します。フィルターで「png」「jpg」「bmp」「tga」「tiff」拡張子だけで保存できるようにします。ファイルが選択されたらバックエンドのRustに向けて"save_img"メッセージを呼び出し、ファイル名と幅と高さとarr配列を渡してRustで画像ファイルに保存します。ただし次のRustのコードを書くまでまだ機能しません。
ここでファイルダイアログの拡張子は「open」関数の「filters」引数で['png','jpg','bmp']などと複数の拡張子を指定することもできます。

画像ファイルの読み込み

Rustのコード「src-tauri」→「src」→「main.rs」ファイルを下記サンプルコードのように書きます。これをデバッグ実行すると、やっと「開く」ボタンで図6のように画像ファイルを開いて表示できるようになりました。

Rustでは、画像の生データは「Vec<u8>」の1バイトの正の整数(u8)の数値の羅列になります。順番にR→G→B→A→R→G→B→A→R→G→B→A…という順に並んでいます。RはRed、GはGreen、BはBlue、AはAlphaで、それぞれ0~255で色を表します。

図6:画像ファイルを開いたところ

・画像を読み込む「main.rs」のサンプルコード
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
//画像を扱うクレート
use image::{self, GenericImageView,Rgba};
//画像ファイルを開くコマンド
#[tauri::command]
fn open_img(path: &str) -> (usize,usize,Vec<u8>) {
    let img = image::open(path).expect("Dimage");
    let width = img.width() as usize;
    let height = img.height() as usize;
    let img_src = img.into_rgba8();
    (width,height,img_src.to_vec())
}
//メイン関数
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            open_img])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

【サンプルコードの解説】
「use」文で「image」クレートにアクセスできるようにします。
「#[tauri::command]」属性で「open_img」関数をTAURIの「コマンド」として宣言します。「コマンド」にすることでフロントエンドのJavaScriptから呼び出してTAURIのプログラムを実行できます。
imageクレートの「open」関数で「path」名の画像ファイルを開きます。「width」変数に画像の幅を、「height」変数に画像の高さを、「img_src」変数にRGBAがそれぞれ1バイトずつの画像の生データを取得し、open_img関数の戻り値でその3つをタプルにして返します。
「main」関数でtauriクレートの「Builder」構造体の「invoke_handler」メソッドで「main.js」から送られてきたメッセージの「open_img」と同名の関数とやり取りし、「run」メソッドでコマンドを走らせます。

画像ファイルの書き出し

最後にRustのコード「src-tauri」→「src」→「main.rs」を下記のようにコードを書きます。これで「保存」ボタンでデスクトップなどに画像を名前を付けて保存できます。

画像をファイルに保存する場合は、画像の生データをimageクレートの「ImageBuffer」構造体のデータに変換(「from_vec」メソッド)して、その「ImageBuffer」構造体の「save」メソッドで画像ファイルに保存します。「save」メソッドは引数に渡される「path」名の拡張子により自動的にその拡張子のファイル形式で保存されます。

・画像を書き出す「main.rs」サンプルコード
(前略)
//画像ファイルを保存するコマンド
#[tauri::command]
fn save_img(path: &str,width:u32,height:u32,data:Vec<u8>) {
    let img = image::ImageBuffer::<Rgba<u8>, Vec<u8>>::from_vec(
        width, height, data).unwrap();
    img.save(path).expect("error save");
}
//メイン関数
fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![
            open_img,
            save_img])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

【サンプルコードの解説】
「#[tauri::command]」属性で「save_img」関数をTAURIのコマンドとして宣言します。
imageクレートの「ImageBuffer」構造体のfrom_vecメソッドで「width」引数に画像の幅を、「height」引数に画像の高さを、「buf」引数(ここでは「data」変数を渡している)にRGBAがそれぞれ1バイトずつの画像の生データを渡し、生成された「ImageBuffer」構造体のインスタンスを「img」変数に代入します。
img変数の「save」関数で「path」名に画像ファイルを保存します。
「main」関数でtauriクレートの「Builder」構造体の「invoke_handler」メソッドで「main.js」から送られてきたメッセージの「save_img」と同名の関数とやり取りし、「run」メソッドでコマンドを走らせます。

おわりに

今回は、TAURIを使ったサンプルアプリとして画像ビューアを作りました。フロントエンドのJavaScriptでファイルダイアログを表示し、バックエンドのRustで画像ファイルを読み込んだり書き出したりしました。

次回は今回の画像ビューアを改造して、お絵描きアプリを開発していきます。これは単なるお絵描きアプリではなく、丸シールアート風の円だけで絵を描く丸アートアプリです。

著者
大西 武 (オオニシ タケシ)
1975年香川県生まれ。大阪大学経済学部経営学科中退。プログラミング入門書など30冊以上を商業出版する作家。Microsoftで大賞やNTTドコモでグランプリなど20回以上全国区のコンテストに入賞するアーティスト。オリジナルの間違い探し「3Dクイズ」が全国放送のTVで約10回出題。
https://profile.vixar.jp

連載バックナンバー

開発言語技術解説
第15回

「TAURI」でデータベースを使ってみよう

2024/5/10
第15回の今回は「TAURI」でオープンソースのデータベース「SQLite3」を使用して、テーブル表に表示する解説をしていきます。
開発言語技術解説
第14回

「TAURI」で気象庁の「CSVデータ」を解析する

2024/5/1
第14回の今回は気象庁のWebサイトから指定した地域の1年間の気象データをダウンロードして「TAURI」で解析していきます。
開発言語技術解説
第13回

「TAURI」で「簡易RSSリーダー」を開発してみよう

2024/4/16
第13回の今回は「TAURI」で「RSSフィード」を読み込んでWebページに一覧表示し、リンクのページを開くための新規ウィンドウを作成するところまでを解説します。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています