見出し画像

ブログに使う画像の拡張子を変換したい!自分の理想を形にした画像ツールの開発裏話

はじめに

記事を書き終えて、あとは画像を貼るだけ。そこで手が止まる。

スマホから送った写真の拡張子が .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つだけだ。

  1. 変換先の形式を選ぶ(迷ったらWebP)

  2. 画像をドラッグ&ドロップする

  3. ZIPでまとめて受け取る

画像変換ツールは世の中に山ほどある。それでも作ってよかったと思うのは、「自分の使い方にちょうどいい」という感覚が、既製品では手に入らなかったからだ。

HEICが開けなくて手が止まる。WebP変換のために毎回サイトを探す。どこかにアップロードすることが、なんとなく気持ち悪い。

ひとつでも心当たりがあるなら、一度使ってみてほしい。ブラウザだけで、必要なときにサッと終わる。今回たどり着いた答えは、それくらいシンプルなものだ。

いいなと思ったら応援しよう!