クリップボードの画像を楽にリサイズするツールを作った

クリップボードの画像を楽にリサイズして、リサイズ後の画像をコピーし直すツールを作った。

GitHub - castaneai/relights: 📋 Simple clipboard image resizer

f:id:castaneai:20200922211707g:plain

きっかけ

Macでスクリーンショットを撮ってブログに貼り付けたい場合、Cmd + Shift + 4を使って指定範囲をクリップボードにコピーしてから貼り付けると早くて便利。

しかし、RetinaディスプレイのMacでは撮影されたスクリーンショットの解像度が無駄にでかい。 画面を見るときは高解像度はキレイで嬉しいが、ブログにそのまま貼るとファイルサイズはでかい上に無駄にでかくて逆に見づらくなることすらある。

f:id:castaneai:20200922212508p:plain

▲たとえば、Finderのほんの一部分だけを撮ってもこのデカさになってしまう

よって、ブログに貼り付けるときはある程度縮小してから貼りたい。一度ファイルに保存して、Mac標準のプレビュー.appでリサイズして上書き保存して、それをまたコピーして貼り付ける・・・なんて毎回やるのも面倒である。

方針

最初は macOS標準添付「Automator」で、右クリックから画像を一括リサイズする環境を手に入れる | Developers.IO のようにAutomatorで解決するかとも思ったが、どれぐらいのサイズまで縮小したいかは画像によって異なるので、個別にサイズ調整したい という気持ちが強くなった。

よって、画像の大きさを手動で調整しつつ、調整完了したらすぐにクリップボードに縮小された画像が戻ってくる…そんなツールを作れば便利そうだと考えた。

開発環境

クリップボードを扱い常に起動しておきたいツールなのでデスクトップアプリが最適。 そこで、今回はWebの技術でデスクトップアプリを作れるElectronを選択した。

最初の構築には次の記事がとても参考になった。この通りにコマンドを打つだけで速攻でTypescript, Next.js で書き始める準備が整った。

Next.js + Electron がとても簡単になっていた | Zenn

実際はひとつの画面だけの単純なアプリなので、Next.js特有の機能はほぼ使ってない。 しかし、yarn start でLive-reloadつきのReact環境が準備されているという部分が最高で、これでUI周りを開発するときはストレスなく開発できた。

実装の詳細

大まかな流れとしては、

  1. UI上でクリップボードの貼り付けイベントを受け取って、画像を<img>タグに配置する
  2. 画像の上をドラッグして縮小をおこなう
  3. ドラッグを離したら縮小された画像がクリップボードにコピーされる

ほとんどはElectronのRenderer(UI側)のプロセスでやった。最後にクリップボードにコピーして、通知を出すところだけIPC経由でMainプロセスにやらせている。

マウスドラッグによる拡大縮小

画像を上下方向にドラッグすることでサイズを調整できるようにした。 こうすることで、実際の大きさを確認しながらマウスで細かな調整ができて直感的にリサイズができる。

f:id:castaneai:20200922214714g:plain

このリサイズのやり方はWindows用の画面キャプチャツール GifCam のリサイズ機能を参考にして作った。

ただ、ドラッグドロップのようなちょっと特殊なUI処理はReactであってもDOMを直接触る感じになってしまい、もっと良い書き方ないのかなぁとは思った。

書くときは Drag and drop with hooks - Draggable component · GitHub が参考になった。

React Hooks

画像のData URL, 現在の拡大率などを React Hooks の useState で保持している。 はじめてReact Hooksをまともに触ったので、普通に知識がなさすぎてむずかしかった。useEffectの第2引数を付け忘れて、画像を貼り付けるたびにレンダリング回数が倍増してしまうなんてこともあった・・。

Hooksが難しいというよりは、状態をもつGUIアプリは全部むずかしい!!

MainプロセスとRendererプロセスの通信

Electronのドキュメントを見ると、ipcMain, ipcRendererというものを使ってふたつのプロセス間通信ができる…と公式のFAQに書いてあったが、

Alternatively, you can use the IPC primitives that are provided by Electron. To share data between the main and renderer processes, you can use the ipcMain and ipcRenderer modules.

ぐらいでさらっと終わっていて、具体的にRenderer側からどうやってipcRendererを初期化すればいいのかが最初わからなかった。 公式以外の記事を調べてみると、普通に require('electron').ipcRenderer すれば使える風に書いてあったりしたが、require is not defined と怒られて無理だった。

これは最近のElectronではセキュリティ的な理由からNode.js側のランタイムがRendererから切り離されていることが理由らしい。 よって、Renderer側でNode.js側の何かが使いたい場合は最初に1回だけ実行されるpreload scriptで手動で使うものだけを公開すればいいとのこと。

Next.jsのElectron templateには確かに electron-src/preload.ts というファイルがあり、次のように ipcRenderer をRendererプロセス側のグローバル変数に追加していた。なるほど。

/* eslint-disable @typescript-eslint/no-namespace */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { ipcRenderer, IpcRenderer } from 'electron'

declare global {
  namespace NodeJS {
    interface Global {
      ipcRenderer: IpcRenderer
    }
  }
}

// Since we disabled nodeIntegration we can reintroduce
// needed node functionality here
process.once('loaded', () => {
  global.ipcRenderer = ipcRenderer
})

MainプロセスからBrowserWindowを作る際は、次のようにして preload.ts を実行すればいいらしい。

const mainWindow = new BrowserWindow({ 
  width: 800,
  height: 600,
  webPreferences: {
    nodeIntegration: false,
    preload: join(__dirname, 'preload.js'),
  },
})

その後、Renderer側で次のようにモジュール定義すれば、Typescriptのコンパイルも通って無事にipcRendererが使えるようになる。

declare global {
  interface Window {
    ipcRenderer: IpcRenderer
  }
}

まとめ

ElectronやReactは存在は知っていながらも実際にアプリを作った経験はほとんどなかったので、いろいろと勉強になってよかった。 また、クリップボード画像を楽にリサイズできてふつうに便利なので作ってよかった。