【Tauri】画像をファイルドロップ→リサイズするデスクトップ・アプリをつくる方法

こんにちは!
九保すこひです(フリーランスのITコンサルタント、エンジニア)

さてさて、このブログには記事が550以上ありますが、執筆時に「毎回めんどいなぁ…」と思うことがあります!

それは・・・・・・

画像リサイズ

です。

というのも、ブログには次のような画像が必要ですが、すべて手動でリサイズするのは、マジ手間だからですね😱

  • アイキャッチ画像(メイン画像)
  • スクリーンショットすべて

ちなみに、こちらの記事ではElectronで画像をリサイズしましたが、1点大きな欠点がありました。

それは・・・・・・

Electronはビルド後、ファイル容量が大きい

というものです。

たとえば、あるページでは「Electron、50MBでデカい?俺のは80MBいってるぜ!」というコメントがあります。

引用:Electron app file size too big / Alternatives to Electron

これでは気軽に配布したりできないため、代案を探していました。
そして、発見したのが

Tauri(たうり)

です。

Tauriは、HTMLやJavaScriptでデスクトップ・アプリがつくれて、WindowsやMacOS、Linuxもサポート(つまり、1コードで複数OSに対応)

そして、なんといってもビルド後ファイル容量が小さいので、気軽に配布もできます。(今回つくったもので11.6MBでした)

そこで!

今回は「Tauri」で次のようなアプリをつくってみたいと思います。

  • ファイルドロップすると画像リサイズ
  • 画像のサイズは指定できる
  • 横、縦どちらを基準にするか選べる
  • 複数ファイルのリサイズにも対応

ソースコードをコピーするだけで再現できます。
ぜひ最後まで読んでくださいね!

「知識アップデート完了!
WPはブロックエディタを使う(遅っ😱)」

Tauriの開発で気をつけること

実際の開発に進む前に、1点だけ気をつけるべきことがあります。

それは「バージョン」です。

この記事を書いている2024.12.7現在、Tauriの最新版はバージョン2となっていますが、ネットを検索しても、AIに聞いてもバージョン1の話が多く出てきたりします。

今回のソースコードもv2で作っているので、改変する場合は取得する情報に気をつけてください!

Tauriの開発で必要になる知識

Tauriの基本構造は次のとおりです。

  • フロントエンド:HTMLやJavaScript(ReactとかVueを選べる)
  • バックエンド:基本はRustで、Swift、Kotlinも使える

つまり、フロントエンドはウェブ開発者にとっては楽勝ですが、バックエンドはおそらく馴染みがないので、ある程度学習が必要という点に注意が必要です。

※ただし「見た目だけでOKだよ」というアプリの場合はRust、Swift、Kotlinは全く触る必要はありません👍

Tauriのインストール

Tauriは、次の3つの環境で開発ができます。

  • Windows
  • MacOS
  • Linux

各OSのインストール方法は以下のページを参照してください。

📝参考ページ:Prerequisites | Tauri(英語)

※残念ながら、中国語の翻訳はあれど日本語はありません。日本のプレゼンスが低下しているのを感じてしまいますね…😭

Tauriプロジェクトを準備する3ステップ

Tauriは開発環境だけでなく、プロジェクト作成も多様なやり方があります。

  • Bash
  • PowerShell
  • npm
  • Yarn
  • pnpm
  • deno
  • bun
  • Cargo

今回は私が慣れているnpmでのインストールで話を進めます!
なお、プロジェクト名は「my-tauri」です。

Step1:プロジェクトをつくる

では、コマンドラインからプロジェクト作成したいフォルダ内で以下のコマンドを実行してください。

npm create tauri-app@latest my-tauri

すると、いくつか質問されるので以下を参考にして入力・選択してください。

質問やること
Identifier識別IDを入力
何もせずエンターでOK
Choose which language to use for your frontendフロントエンドで使いたい言語の選択
今回はTypeScript / JavaScript
Choose your package managerパッケージマネージャ
今回はnpm
Choose your UI templateUIテンプレート
今回はVue > JavaScriptを選択

すると、my-tauriというフォルダが作成されます。

Step2:必要なパッケージをインストール

そのままフォルダ中へ移動して必要なパッケージをインストールします。
以下のコマンド2つを実行してください。

cd my-tauri/
npm install

Step3:Tauri(開発バージョン)で起動する

では、以下のコマンドでTauriアプリを起動してみましょう!

npm run tauri dev

すると、このようにデスクトップ・アプリが起動します。

Tauri:基本的な開発、5つの要点

今回のケースだとVue 3(+Vite)を使うので、基本的なウェブ開発との違いはほぼありません。

1:基本的な構成

Tauri開発を進めるにあたって最低限必要なのは以下2つのフォルダです。

  • フロントエンド:srcフォルダ(Vueが入っている)
  • バックエンド:src-tauri/srcフォルダ(Rustが入っている)

今回のケースでは、ファイルをドロップするのはフロントエンド(Vue)で、画像のリサイズはバックエンド(Rust)で実装する形になります。

2:検証ツール

ブラウザと同じく右クリックして「Inspect Element」を選択すると、Google Chromeのようなツールが表示されます(TauriはOSのWebViewを使うので、環境によって変わるはずです)

3:オートリロード

あります。今回のケースだとsrc/App.vueなどのファイルを保存すると、すぐさま変更が反映されます。

ちなみに、src-tauri/src/フォルダ内を変更すると一度ビルドしなおすので、再起動される形になります。

4:npmパッケージのインストール

新しくパッケージをインストールしたい場合は、プロジェクトのルートフォルダで、以下のようにすればOKです(lodashの例です)

npm i lodash --save-dev

使い方もウェブ開発と同じで「src/App.vue」内で次のようにします。

import _ from "lodash";

5:アプリ化する

開発が完了し、ソースコードをビルドしてアプリ化するには、さきほどの開発バージョンの起動をCtrl + Cなどでストップし、以下コマンドを実行してください。

npm run tauri build

すると、「/src-tauri/target/release/」フォルダにアプリが作成されます。

Tauriで画像リサイズを実装する3ステップ

少し前置きが長かったですが、ここからが本題の「画像リサイズ」の部分です。
大まかな流れは次のとおりです。

  1. Vueでファイルドロップ部分をつくる
  2. Rustで画像リサイズする部分をつくる

では順を追って紹介していきましょう!

Step1:Tailwind CSSが使えるようにする

Tauriではウェブの技術が使えるので、もちろん「Tailwind CSS」が使えます。

ただし、今回は(めんどうなので)CDNで呼び出すことにしました。

<!doctype html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Drop resize</title>
<script src="https://cdn.tailwindcss.com/3.4.15"></script>
</head>

<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

※気になる方はnpmでインストールしてください🙇

Step2:ファイル・ドロップする部分をつくる(Vue部分)

いきなり実際のソースコードです。

src/App.vue

<script setup>
import { ref } from "vue";
import { invoke } from '@tauri-apps/api/core';
import { listen } from "@tauri-apps/api/event";

// データ
const orientation = ref('horizontal'); // `horizontal` or `vertical`
const size = ref(1000);

// ドラッグ・アンド・ドロップ
const classNames = ref('bg-stone-100');
const setBackgroundActive = (active) => {

classNames.value = (active === true) ? 'bg-stone-300' : 'bg-stone-100';

};
listen('tauri://drag-drop', async (e) => {

if(isResizing.value === true) {

setMessage('現在の処理が終了するまでお待ちください');
return;

}

setMessage('');

isResizing.value = true;
const paths = e.payload.paths;
let resizedImageCount = 0;

for(let path of paths) {

try {

await resizeImage(path); // 1件ずつ処理を待ちながら画像リサイズ
resizedImageCount++;

} catch(error) {

setMessage(error);

}

}

setBackgroundActive(false);
setMessage(`${resizedImageCount}件の画像をリサイズしました`);
isResizing.value = false;

});
listen('tauri://drag-enter', (e) => {

setBackgroundActive(true);

});
listen('tauri://drag-leave', (e) => {

setBackgroundActive(false);

});

// 画像のリサイズ
const isResizing = ref(false);
const getOutputPath = (path) => {

const dir = path.split('/')
.slice(0, -1)
.join('/');
const fileName = path.split('/').pop();
const [name, extension] = fileName.split('.');

return `${dir}/${name}_${orientation.value}_${size.value}.${extension}`;

};
const resizeImage = async (path) => {

return new Promise((resolve, reject) => {

const extension = path.split('.')
.pop()
.toLowerCase();
const validExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];

if (validExtensions.includes(extension)) { // 拡張子のチェック

const outputPath = getOutputPath(path);

invoke('resize_image', { // Rust内で画像をリサイズする
inputPath: path,
outputPath: outputPath,
orientation: orientation.value,
size: size.value
})
.then(() => {

resolve();

})
.catch((error) => {

reject(error);

});

}

});

};

// メッセージ
const message = ref('');
const setMessage = (text) => {

message.value = text;

};

</script>

<template>
<main class="flex flex-col bg-stone-100 h-screen" :class="classNames">
<div class="relative w-full flex flex-1 items-center justify-center font-bold text-gray-500">
<span class="text-sky-500">ファイル</span>をここに<span class="text-sky-500">ドロップ</span>してください
<div class="absolute top-2 font-normal text-black flex items-center text-sm text-gray-500" v-if="isResizing">
<img src="/images/ajax-loader.gif" class="w-3 h-3 mr-1"> 処理中..
</div>
<div class="absolute bottom-1 font-normal text-black w-full text-center text-sm">{{ message }}</div>
</div>
<div class="flex gap-3 p-3 bg-gradient-to-t from-sky-500 to-sky-400 border-t border-sky-500">
<div class="flex-1 p-1">
<label class="text-lg">
<input type="radio" value="horizontal" v-model="orientation"> 横
</label>
<label class="text-lg mr-2">
<input type="radio" value="vertical" v-model="orientation"> 縦
</label>
<small>にあわせる</small>
</div>
<div class="flex-1">
<input class="w-full p-1 border" min="1" type="number" placeholder="長さ" v-model="size" />
</div>
</div>
</main>
</template>

【重要な点1】イベントはTauriのものをつかう

この中で注意が必要なのが「ドロップイベント」です。

というのも(環境によるかもしれませんが)Vueを使ったドロップイベントだとうまく反応してくれないからです。

そこで代替案として使ったのが、Tauriが提供するイベントです。

  • ファイルドロップ:tauri://drag-drop
  • ファイルドラッグ開始:tauri://drag-enter
  • ファイルドラッグ終了:tauri://drag-leave

【重要な点2】画像のリサイズはひとつずつ実行する

今回は複数の画像がドロップされてもいいように、ループしてすべてリサイズするようにしています。

しかし、ここで問題点があります。

そう。普通にループしてしまうと、一気に全ファイルがリサイズされることになり、フリーズする可能性があるという点です。

そこで、リサイズはPromiseとawaitを使ってひとつずつ実行するようにしています。

【重要な点3】Rustの呼び出しはinvoke()

画像のリサイズは、次の流れで実施します。

  1. JavaScript(Vue)からRustを呼び出す
  2. Rust内で画像リサイズ
  3. 完了したらJavaScriptに通知

そして、これを実行するためにJavaScriptからRustを呼び出すのが「invoke()です。

第1引数は、あとでRust内につくる関数名を指定し、第2引数が関数に送るパラメータになります。

invoke('resize_image', { // Rust内で画像をリサイズする
inputPath: path,
outputPath: outputPath,
orientation: orientation.value,
size: size.value
})

Step3:Rust内で画像リサイズする

今回はRust内でリサイズするので、コードは「src-tauri/src」内にセットしてください。

src-tauri/src/lib.rs

use tauri::Manager;
use image::imageops::FilterType;

#[tauri::command]
async fn resize_image(input_path: String, output_path: String, orientation: String, size: u32) -> Result<(), String> {

// 画像を開く
let img = image::open(&input_path).map_err(|e| e.to_string())?;

// 画像をリサイズ
let resized = match orientation.as_str() {
"vertical" => { // 縦が基準のとき
let ratio = img.height() as f32 / size as f32;
let new_width = (img.width() as f32 / ratio) as u32;
img.resize(new_width, size, FilterType::Lanczos3)
},
_ => img.resize(size, u32::MAX, FilterType::Lanczos3), // それ以外
};

// 保存
resized.save(&output_path).map_err(|e| e.to_string())?;

Ok(())

}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.setup(|app| {
#[cfg(debug_assertions)]
{
let window = app.get_webview_window("main").unwrap();
window.open_devtools();
}
Ok(())
})
.invoke_handler(tauri::generate_handler![resize_image])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

ちなみにRustには「所有権」という考え方があって、JavaScriptやPHPとはちょっと違った部分もあるので注意してください(私もてこずりました😭)

テストしてみる

では、実際にテストしてみましょう!
すでに見た目は以下のようになっていると思います。

では、ここに静岡の絶景(今年ワーケーションで行きました👍)をファイルドロップしてみましょう。

どうなるでしょうか・・・・・・?

はい!

同じフォルダに新しいファイルが作成されました。
実際のその画像がこちらです。

元のサイズ4032x3024が、640x480に変更されました。
画質も悪くないですね(三保の松原、海の幸が最高だった!)

成功です!😊

企業様へのご提案(毎日の作業をショートカットしたい方へ)

Tauriを利用して貴社のメリットにつながるのは「ルーティーンワークの効率化」です。

日々の作業はやることが決まっていて、効率化しやすいためですね。

近年はAIを使うケースも増えましたが、出力される内容には毎回「ゆらぎ」があり、そもそもデータをインターネット上にアップしたくない場合もあるでしょう。

そういった場合は、Tauriを使って業務効率をしてみてはいかがでしょうか。

Tauriは1つのプログラムから、

  • Windows
  • MacOS
  • Linux

のデスクトップ・アプリを生成できます。

作業者のパソコンが統一されていなくても、すべての環境で動くアプリが作成できます。

ぜひそういった作業効率化をご希望でしたら、いつでもお気軽にお問い合わせからご相談ください。お待ちしております!

おわりに

ということで、今回はTauriでデスクトップ・アプリをつくってみました。

正直なところElectronがあればいいでしょ!とずっと思っていたんですが、Tauriは軽量ですしビルドも簡単、そしてRustの人気も上がってるのでチャレンジしてみました。

Rustの書き方はクセがあるのでとっつきにくいとは思うのですが、Copilotを使えば昔よりは楽に学習できますし、WebAssembly(ウェブに高速で動くコードをくっつける技術)も作れます。

ぜひ皆さんも一度試してみてくださいね。

ではでは〜!

「卒業してから四半世紀たってから、
また同期と飲めるの嬉しい!」

このエントリーをはてなブックマークに追加       follow us in feedly  
お問い合わせ、お待ちしております。
開発のご依頼はこちら: お問い合わせ
どうぞよろしくお願いいたします! by 九保すこひ