ブログに使う画像の拡張子を変換したい!自分の理想を形にした画像ツールの開発裏話
はじめに
記事を書き終えて、あとは画像を貼るだけ。そこで手が止まる。
スマホから送った写真の拡張子が .HEIC で、そのままではWordPressに上げられない。上げられたとしても1枚3MB。表示速度が落ちるから、WebPに変換して軽くしたい。
そこで変換ソフトを立ち上げる。あるいは検索で出てきた変換サイトに画像を放り込む。ただ、後者には毎回ひっかかるものがあった。この写真、どこのサーバーに上がったんだろう、と。
記事1本につき10枚。月に20本。この作業を年間何回繰り返すのか。
計算してうんざりしたので、自分で作ることにした。
この記事では、完成したツールの使い方を手順で追いながら、途中でサーバー処理をまるごと捨てた判断と、その後にぶつかった失敗を書く。同じように「自分用のツール」を作りたい人には、そのまま使える判断材料になるはずだ。
作ったもの
ブログ用画像に特化した、画像拡張子コンバーター。HEIC、JPG、PNG、GIFを、WebPかPNGへ変換して軽量化する。

画面はこれで全部だ。設定項目をこれ以上増やさなかった。基準はひとつ、「自分が毎日、迷わず使えるか」。
登録は不要。無料。そして画像はサーバーに送られない。
使い方の手順
手順1 変換先の形式を選ぶ
最初にプルダウンで出力形式を決める。WebPかPNGだ。

迷ったらWebPでいい。ブログの本文中に置く画像なら、これがほぼ最適解になる。同じ見た目でファイルサイズが目に見えて落ちる。
PNGを選ぶ場面は限られる。透過を残したいロゴ、スクリーンショットの中でも文字の輪郭を1ピクセルも崩したくないもの。このあたりだ。
なお、WebPの表示は現行の主要ブラウザがすべて対応している。数年前のように picture タグでJPEGのフォールバックを用意する必要は、実務上ほとんどなくなった。それでも古い環境まで拾いたいなら、こう書いておけば保険になる。
<picture>
<source srcset="/img/photo.webp" type="image/webp">
<img src="/img/photo.jpg" alt="説明文" loading="lazy" width="1200" height="800">
</picture>
手順2 画像を放り込む
点線の枠に、画像をドラッグ&ドロップする。ボタンからファイルを選んでもいい。複数枚まとめて投げ込める。

放り込んだ瞬間、プレビューが並ぶ。
この挙動は、意図して最優先に置いた。理由は単純で、反応のない画面ほど不安なものはないからだ。処理中なのか、失敗したのか、そもそも受け取られたのか。分からない数秒が、ツールへの信頼を削る。
だから、説明文を読ませるより先に、動きで伝えることにした。並んだサムネイルが「受け取った」という返事になる。
手順3 ZIPでまとめて受け取る
変換が終わったら、ZIP一括ダウンロードのボタンを押す。それだけだ。
この機能、地味だと思う。実際、機能一覧に書いても誰も反応しない。
ただ、これがないツールに戻るとよく分かる。10枚変換して、10回クリックして、10回保存先を確認する。1枚あたり3秒でも、30秒。その30秒のあいだに、書いていた記事の続きが頭から抜ける。
作業が止まる原因は、たいてい大きな不便ではない。小さな不便の繰り返しだ。
サーバー処理を捨てた話
ここからは実装の中身になる。今回いちばん悩み、そして方向転換した部分だ。
最初はPHPで書くつもりだった
開発の初期に想定していたのは、PHPのImagickを使ったサーバーサイド変換だった。
画質の制御が細かい。HEICの扱いに強い。ライブラリが枯れていて情報も多い。堅実な選択肢に見えた。
実際、少し書いてみた。動いた。それでも手が止まった。
3つの疑問
作りながら、こういう疑問が順番に浮かんだ。
まず、本当にサーバーを介す必要があるのか。自分のブログ用画像を変換するだけの処理に、往復の通信をはさむ意味はどこにあるのか。
次に、個人開発でこの構成を背負えるのか。アップロードされたファイルの検証、一時ファイルの掃除、同時アクセス時のメモリ、悪意あるファイルへの備え。全部やる覚悟があるのか。
最後に、もっと気軽に使える形はないのか。
3つ目で答えが出た。
JavaScriptとCanvasに振り切る
最終的に選んだのは、JavaScript + Canvasでブラウザ内に完結させる構成だ。
サーバー処理(PHP/Imagick) ブラウザ処理(Canvas) 画像の送信 必要 不要 待ち時間 アップロード+処理+ダウンロード 処理のみ 運用コスト 転送量・CPU・一時領域 ほぼゼロ 画質の制御 細かく効く 制限あり セキュリティ責任 預かる側にある そもそも預からない
画質の制御では負ける。そこは正直に認める。それでも、ブログ用途なら十分実用レベルまで持っていけると判断した。
実装の核は驚くほど短い。
// 画像を読み込み、Canvasへ描いて別形式で書き出す
const bitmap = await createImageBitmap(file);
const canvas = document.createElement('canvas');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
canvas.getContext('2d').drawImage(bitmap, 0, 0);
// 第3引数が品質。WebPは0.8前後がブログ用途の落としどころ
canvas.toBlob((blob) => {
results.push({ name: file.name, blob });
}, 'image/webp', 0.8);
bitmap.close(); // 後述するが、これが効く
ZIP化にはJSZipを使った。変換済みのBlobを順に詰めて、一度だけダウンロードさせる。
const zip = new JSZip();
results.forEach(({ name, blob }) => {
zip.file(name.replace(/\.[^.]+$/, '.webp'), blob);
});
const archive = await zip.generateAsync({ type: 'blob' });
送らないから速い。送らないから安全。送らないから安い。個人開発で3つ同時に取れる選択肢は、そう多くない。
失敗談:Canvasは万能ではなかった
方針転換したあとも、素直には進まなかった。
一眼で撮った高解像度の画像を20枚まとめて投げ込んだときのことだ。
メモリ使用量が跳ね上がる。処理が止まる。プレビューの画像が消える。タブが落ちる。
「これ、本当に大丈夫か?」
原因を追うと、全部を同時に読み込んでいたのがまずかった。ブラウザのメモリは無限ではない。4000×3000のRGBA画像は、展開した時点で1枚48MB近い。20枚で1GB近くになる計算だ。落ちて当然だった。
対策は3つ入れた。
ひとつ、同時処理の数を絞る。全部並列ではなく、数枚ずつ順番に流す。ふたつ、使い終わったImageBitmapとオブジェクトURLを明示的に解放する。放置すると回収されない。みっつ、ブログ用途として妥当な上限を超える画像は、長辺を基準に縮小してから変換する。
この3つ目が、今回いちばんの学びにつながった。
万能を目指さない。用途を絞る。
「あらゆる画像を、あらゆる形式へ、劣化なく」を追いかけていたら、たぶん今も完成していない。ブログに貼る画像、という前提を置いたから、上限を決められた。上限を決められたから、落ちなくなった。
制約は、機能を削る言い訳ではない。品質を保証できる範囲を確定させる作業だと思う。
公開して終わりにしないための設計
このツールは「ぼっちエンジニアのWeb工房」の業務系ツールカテゴリに置いている。
一覧ページ自体も自作で、WordPressプラグインで管理している。ツールを追加すると、JSON-LDの構造化データが自動生成される仕組みだ。手で書くと、たいてい書き忘れるか、書き間違える。

ツール画面の下には、使い方と仕様を短く並べた。中でも太字にしたのは最後の一行だ。サーバーには画像が送信されない。
これは技術仕様であると同時に、使う前に知りたい情報だと考えている。冒頭に書いた「この写真、どこに上がったんだろう」という引っかかり。あれを、使う側に持たせたくなかった。
まとめ
やることは3つだけだ。
変換先の形式を選ぶ(迷ったらWebP)
画像をドラッグ&ドロップする
ZIPでまとめて受け取る
画像変換ツールは世の中に山ほどある。それでも作ってよかったと思うのは、「自分の使い方にちょうどいい」という感覚が、既製品では手に入らなかったからだ。
HEICが開けなくて手が止まる。WebP変換のために毎回サイトを探す。どこかにアップロードすることが、なんとなく気持ち悪い。
ひとつでも心当たりがあるなら、一度使ってみてほしい。ブラウザだけで、必要なときにサッと終わる。今回たどり着いた答えは、それくらいシンプルなものだ。
