[Tauri #06] Tauri v2:Honeyview置き換えの画像ビューア「Lookover」を作り、Windows / Mac / Linuxで公開した

[Tauri #06] Tauri v2:Honeyview置き換えの画像ビューア「Lookover」を作り、Windows / Mac / Linuxで公開した

はじめに

長年HoneyViewを使ってきました。Windowsの「フォト」は使いやすいけれど、ちょっと足りない。その「足りない」を埋めるアプリを、Tauri v2で作りました。名前は Lookover(ルックオーバー)です。

  • 普段は見るだけ(フォルダ内の送り、拡大縮小、等倍、全画面)
  • 必要なときだけその場で直す(回転・切り抜き・縮小・明るさ・モザイクなど)
  • 直したらワンキーで書き出す(Ctrl+1 で長辺1920のWebPを同じフォルダへ)

画像をNOTEやブログに上げる前に、回転や縮小やモザイクのために別のアプリを開くのが面倒だった、というのが出発点です。着手は昨日で、今日、Windows / Mac / Linux のインストーラーを公開するところまで来ました。

スクリーンショット

[Tauri #06] Lookover 編集パネル

リポジトリ(MIT):

前回の記事:

1. 構成:Rustは薄く、画面はバンドラーなしの素のJS

今回もフロントは素のJavaScriptです。ビルドツールも外部ライブラリも使わず、src/ をそのまま frontendDist に指定しています。

"build": { "frontendDist": "../src" }

Rust側がやっているのは、OSにしかできないことだけです。

  • ファイルを読む・書く、フォルダの列挙
  • ごみ箱へ送る(trash クレート)
  • 2つ目の起動を1つ目の窓へ渡す(tauri-plugin-single-instance)
  • ファイルダイアログ(tauri-plugin-dialog)、エクスプローラーで場所を開く(tauri-plugin-opener)
  • 画像の拡張子の関連付け(bundle.fileAssociations)

画像のデコード、編集、書き出しは、すべて画面側(Canvas / WebGL2)でやっています。

テストも画面側にあります。開発用の小さなHTTPサーバー(Rustのコマンドを真似るだけ)を立てて、Playwrightでブラウザから叩く構成です。Rustを起動せずに、編集機能を170項目以上で回せます。

2. 編集は「元に触らない」パイプライン

編集は、元の画像を書き換えず、パラメーターの列として持っています。

元の画像 → 向き → 描き込み(モザイク等) → 切り抜き → 大きさ → 色

書き出すときと画面に出すときで同じ順に通すので、見たままの結果が出ます。取り消しとやり直しは、この列を戻すだけです。

色の調整(明るさ・コントラスト・彩度・色温度・ハイライト・シャドウ・シャープ)は、WebGL2のシェーダーでやります。WebGL2が使えない環境のために、同じ計算をするCPU版も用意してあります。

3. キーは「表」から全部作る

ビューアは、キーボードで触る時間が長いアプリです。しかも、自分のホットキーとぶつかる場面が出てきました。

動作確認で、Ctrl+1 が自分の常駐ツールのホットキーと衝突していたのです。そこで、キーを自由に付け替えられるようにしました。

操作の表を1か所に置く

export const ACTION_TABLE = [
  { id: 'next',   group: '見る', label: '次の画像', keys: ['ArrowRight', 'ArrowDown', 'PageDown', 'Space'] },
  { id: 'rotR',   group: '直す', label: '右へ 90 度', keys: ['R'] },
  { id: 'export1', group: '出す', label: 'ワンキー書き出し 1 番', keys: ['Ctrl+1', 'Ctrl+E'] },
  // …
];

キーを押したときの処理、F1の操作一覧、吹き出し、キーの設定画面は、全部この表から作っています。キーを変えれば、一覧もステータスバーの表示も一緒に変わります。変えたところだけを settings.keys に覚えるので、あとで初期割り当てを見直しても、触っていないところは自然に追いつきます。

英語配列と日本語配列の罠

キーを文字列にするときに、英字と数字は event.code(キーの位置)で、記号は event.key(出る文字)で決めています。

if (/^Key[A-Z]$/.test(c)) return c.slice(3);   // KeyR → R
if (/^Digit[0-9]$/.test(c)) return c.slice(5); // Digit1 → 1
return e.key;                                  // 記号は出る文字

記号は Shift を付けない決まりにしました。日本語配列では「+」が Shift+「;」ですが、英語配列では Shift+「=」です。Shift を含めてしまうと、配列によって別のキーになってしまうからです。

4. WebKitではWebPが書き出せない

ここで一番ハマったのが、書き出しです。

ワンキー書き出しの初期設定は、長辺1920のWebPです。WindowsのWebView2では問題なく動きました。ところが、macOSのWKWebViewとLinuxのWebKitGTKでは、次のコードがPNGを返します。

canvas.toBlob(cb, 'image/webp'); // WebKit系では、できない形式を黙ってPNGにする

エラーも出ません。拡張子だけが .webp で中身がPNG、という最悪のファイルができあがります。

解決策:出てきた型を見て、できなければJPEGへ

let webpOk = null;
export async function canEncodeWebp() {
  if (webpOk !== null) return webpOk;
  const c = document.createElement('canvas');
  c.width = c.height = 2;
  const blob = await new Promise((r) => c.toBlob(r, 'image/webp'));
  return (webpOk = blob && blob.type === 'image/webp');
}

export async function usableFormat(format) {
  if (format === 'webp' && !(await canEncodeWebp())) return { format: 'jpeg', fellBack: true };
  return { format, fellBack: false };
}

小さなcanvasで一度だけ試して、返ってきたBlobの type を見ます。WebPにできない環境では、拡張子ごとJPEGに切り替えて、画面にも「この環境は WebP に書き出せないので、JPEG にした」と出すようにしました。テストでは、toBlob を上書きしてWebKitの挙動を真似ています。

5. 作ったあとに踏んだ小石

フォルダを移動すると、Rustのビルドが落ちる

アプリ名を仮名から変えたとき、フォルダも移動したら、cargo のビルドスクリプトが古い絶対パスを見に行って落ちました。src-tauri/target に絶対パスがキャッシュされているためで、target を消せば直ります。

Linuxで作ったロックファイルを持ち込まない

Linux上で作った package-lock.json をそのままWindowsに持っていったら、@tauri-apps/cli-win32-x64-msvc(Windows用のネイティブバインディング)が見つからないと言われました。ロックファイルはOS依存のパッケージを含むので、node_modules とロックファイルを消して npm install し直しです。

PowerShellでは npm run tauri build -- --bundles … が効かない

-- がnpmのラッパーに食われて、--bundles が渡りませんでした。npx tauri build --bundles nsis と、直接呼ぶと通ります。

MSIは諦めて、NSISだけにした

WindowsのMSI(WiX)が、手元でもGitHub Actionsでも light.exe で落ちました。VBScriptが無効という定番の原因を疑いましたが、入っていました。原因は追い切れていません。管理者権限は関係ありませんでした。

npx tauri build --bundles nsis

NSISのセットアップ(Lookover_0.1.1_x64-setup.exe、1.45MB)は問題なく作れて、インストールも起動も動きました。個人用のアプリなので、MSIは諦めてNSISだけにしました。

6. GitHub Actionsで3OSをまとめて作る

[Tauri #03] の構成をベースに、今回はジョブを4つに分けました。

strategy:
  fail-fast: false      # 1 つ落ちても、ほかは最後まで作る
  matrix:
    include:
      - name: windows
        os: windows-latest
        args: '--bundles nsis'
      - name: mac-apple-silicon
        os: macos-latest
        args: '--target aarch64-apple-darwin'
        rust_targets: aarch64-apple-darwin
      - name: mac-intel
        os: macos-latest
        args: '--target x86_64-apple-darwin'
        rust_targets: x86_64-apple-darwin
      - name: linux
        os: ubuntu-22.04
        args: '--bundles deb,appimage'

Macは、Apple SiliconとIntelを別のジョブで、--target を変えて作ります。タグ(v0.1.1 など)をpushすると動いて、Releasesに下書きで成果物が並びます。

結果は、Windows 5分、Mac 3分、Linux 6分で、全体は7分半でした。

ファイルサイズ
Windows x64-setup.exe1.45 MB
macOS aarch64.dmg / x64.dmg1.95 MB / 2.09 MB
Linux .deb2.59 MB
Linux .AppImage77.5 MB

AppImageだけ大きいのは、WebKitGTKを抱え込んでいるからです。WebView2やWKWebViewを借りるWindowsやMacは、2MB前後で済みます。

タグを打ち直すと、下書きに古い成果物が混ざる

今回、v0.1.1 のタグを2回打ち直しました。

  1. 修正をコミットしたつもりが、git commit に -m を付け忘れて中止されていた。それなのにタグだけpushしたので、タグが古いコミット(古いワークフロー)を指していた。
  2. 直して通ったが、バージョン番号(tauri.conf.json など3か所)を上げ忘れていて、成果物が 0.1.0 の名前になった。

タグの付け直しは、こう書きます。

git tag -d v0.1.1
git push origin :refs/tags/v0.1.1
git tag v0.1.1
git push origin v0.1.1

ただ、同じタグ名で2回走ると、下書きのリリースに前の実行のファイルが残ります。今回は 0.1.0 と 0.1.1 の名前が混ざっていたので、編集画面から古い5つを消してから公開しました。

さらに笑い話をひとつ。CIを待つ間に、MacとLinuxの手元でもビルドを始めました。Linuxの機械にはRustを入れるところから。ところがCIの方が先に全部緑になって、手元のビルドは完全に無駄でした。タグを打つだけで済むのが、Actionsの良いところです。

7. 署名していないので、最初に警告が出る

3つとも署名をしていないため、初回は警告が出ます。

  • Windows:SmartScreenが「不明な発行元」と出す。「詳細情報」→「実行」。
  • macOS:Gatekeeperが止める。右クリック →「開く」。効かないときは、システム設定 →「プライバシーとセキュリティ」→「このまま開く」。それでもだめなら、xattr -cr /Applications/Lookover.app。(Sequoia以降の挙動は [Tauri #05] にも書きました)
  • Linux:AppImageは chmod +x してから実行。

動作確認は、実機で次の3つでやりました。

  • Windows:NSISでインストールして起動
  • macOS(Intel):dmgから入れて起動
  • Linux:AppImageを直接起動して、画像を開く

Apple Silicon(aarch64)のMacは、手元に実機がなく未確認です。CIでは作っていますが、動くかは分かりません。リリースのノートにも、そう書きました。

まとめ:見るのが主で、直すのは一瞬

HoneyViewの置き換えを狙って、「見る」「直す」「出す」を1つにまとめました。

  • 編集は元に触らない。パラメーターの列で持つ
  • キーは表から全部作る。配列の違いは event.code で吸収する
  • WebKitは toBlob('image/webp') で黙ってPNGを返す。型を見てJPEGに逃がす
  • タグを打ち直すと、下書きに古い成果物が混ざる

「誰も使わないかもしれないが、自分専用のアプリを作る時代」と割り切って、完璧を待たずに公開しました。問題があれば直して出し直せばいい。しばらくはHoneyViewと併用しながら、困ったところを直していきます。