「アスペクト比が勝手に変わる!」と怒られた日 — 一括リサイズUIを根本から見直した話

2026.04.22
Share:

ある朝、画像リサイズツールへのフィードバックが届いた。

一括変換でアスペクト比が勝手に変わる!!! ファイル名も変えないで!!!」 「ちゃんと確認されているのでしょうか!?」

テキストから怒りが伝わった。読んだ瞬間、焦りと冷静な分析が同時に走った。「謝罪より先に直す」という判断が即座に出た。

この日、8ラウンドにわたる全面改修が始まった。

Round 1 — 設計の根本的な欠陥を認める

最初に問題の本質を特定した。

一括モードのUIは「幅 × 高さ」の2フィールドで設定するようになっていた。これが根本的な設計ミスだった。

比率の異なる複数の画像に対して、単一の「W × H」を強制すれば、どれかの画像は必ず歪むか潰れる。数学的に避けられない。なぜ最初にこのUIで作ったのか。正直に振り返ると「直感的にわかりやすそう」という思い込みだった。

主要な画像処理ツールはすべて長辺指定か片辺指定を採用していた。業界標準から外れたUIを作っておきながら、ユーザーテストも不十分なまま公開していた。ユーザーの怒りは正当だった。

Round 1 で変えたこと

  • 一括モードのUIを「幅 × 高さ」2フィールドから「長辺の最大サイズ」1入力に変更
  • 各画像を個別に比率保持でスケーリングする computeScaledDimensions ユーティリティを新設
  • ダウンロード時のファイル名を元ファイル名維持に変更(拡張子のみ変換後の形式に追従)
  • ZIP 内でファイル名が衝突した場合のみ連番サフィックスを付与
  • 一括モード用プリセットを「SNS投稿 1080px / ブログ 1200px / Web最大 1920px」に刷新
ZeronovaZeronova
長辺指定に変えると、1枚モードで「OGP 1200×630 のような正確なサイズ指定」ができなくなる。この機能はどこかで残すべきか?
Claude Code
1枚モードと一括モードで役割を分けるのが自然だと思います。1枚モードは「特定のサイズに合わせたい」ユースケース、一括モードは「複数の画像をまとめて縮小したい」ユースケースです。モードごとに異なるUIを持つことで、ユーザーのメンタルモデルにも合います。

この「モード別の役割分担」という設計方針が、その後の判断の軸になった。

Round 2 — バグを直しただけで終わらせない

Round 1 でコアバグを修正した後、自分で実装を見直した。「ちゃんと確認されているのでしょうか」という言葉が頭に残っていた。

セルフレビューで見えてきた UX の欠陥が4つあった。ZIP 生成中に設定を変更できてしまう問題、進捗バーが最後の数件で止まったように見える問題、処理に失敗した画像があっても通知がない問題、処理中もサムネイルの削除と追加ができてしまう問題だ。

機能が「動く」ことと「使える」ことは別物だ。バグを直しただけで「完了」にする誘惑はあった。でも今日の怒りは「確認が足りない」という本質を突いていた。

// ZIP生成中の操作を抑止する state
const [isBatchProcessing, setIsBatchProcessing] = useState(false);

// 処理中はすべての設定変更を無効化
<button disabled={isBatchProcessing}>変換</button>

// 失敗ファイルを集計してトーストで警告
const [failedCount, setFailedCount] = useState(0);
// 完了後: failedCount > 0 なら「N 件処理できませんでした」を表示

進捗更新の位置もイテレーション末尾に移し、最終フェーズは「ZIP を生成中...」と明示するように変更した。ユーザーが「本当に動いているのか」と不安になる余地を潰した。

Round 3 — 「別タスクにする理由を教えてください」

EXIF orientation の問題は最初、「今日の対応範囲外」と判断していた。

iPhone で縦に撮った写真が処理後に横向きになる問題は知っていた。でも「アスペクト比の修正とは別カテゴリ」として、別の PR に分けるつもりだった。

そこでユーザーから一言が来た。「別タスクにする理由を教えてください

答えを考えた。正直に出てくる理由は「工数が増えるから」だった。それは逃げだ。

「アスペクト比が変わる」「ファイル名が変わる」「向きが変わる」「サイズがサイレントに縮む」は、開発者視点では異なるカテゴリに見える。でもユーザー視点ではすべて「勝手に画像が変わる」という同じ体験だ。ユーザーのメンタルモデル基準で分類すれば、全部を今日対応すべきだと認めるしかなかった。

「実は判断が甘かった」と認める瞬間は、少しバツが悪かった。でも即座に撤回することで、逆にユーザーの信頼を回復できた手応えがあった。

EXIF orientation を正しく処理する

// 旧:HTMLImageElement + FileReader(EXIF 回転がブラウザ依存で予測不能)
const img = new Image();
img.src = URL.createObjectURL(file);
await img.decode();
ctx.drawImage(img, 0, 0);

// 新:createImageBitmap + imageOrientation オプション(明示的に制御可能)
const bitmap = await createImageBitmap(file, {
  imageOrientation: 'from-image'
});
ctx.drawImage(bitmap, 0, 0, bitmap.width, bitmap.height);
bitmap.close(); // GPU メモリを明示的に解放

createImageBitmap{ imageOrientation: 'from-image' } を指定すると、EXIF の向き情報が正確に適用される。HTMLImageElement 経由では各ブラウザの実装差異に依存してしまうが、ImageBitmap は仕様として明示的に制御できる。

ImageBitmap.close() の呼び忘れは GPU メモリリークに直結する。unmount / reset / remove / mode change の4経路すべてに追加した。

この Round 3 では、Canvas の最大ピクセル数を MAX_CANVAS_PIXELS = 4M から 24M に引き上げる判断もした。現代のブラウザは十分に対応できるという根拠からだった。ところが、この判断は数時間後に自分で撤回することになる。

Round 4 — ツールの価値を「見える形」にする

バグ対応が一段落した後、UIの見た目に手を入れた。

画像リサイズツールの最大の価値は「ファイルが軽くなる」こと。だが、その削減率は3行の灰色テキストに埋もれていた。ユーザーがどれだけ恩恵を受けているかを、一瞥では伝えられていなかった。

SavingsBadge コンポーネントを新設した。「5.5 KB → 0.7 KB / −87%」のように、削減前後のサイズと削減率を emerald カラーのバッジで強調する。数値を大きく目立たせるのは装飾ではない。価値の可視化だ。ユーザーが「へえ、こんなに小さくなるんだ」と実感できれば、ツールへの信頼と共有の動機が生まれる。

1枚モードの操作UIも整理した。ファイル選択前は縦 150px のドロップゾーンが表示され、選択後は 60px のコンパクトなサムネイル行に切り替わる。横には「変更」ボタンを配置して、ファイルを変えたい時の導線を明示した。

設定セクションも「サイズ」「出力形式」の2グループに分離し、divider で視覚的に区切った。情報のまとまりが見えると、ユーザーは次に何を設定すれば良いかを迷わなくなる。

Round 5 — 1日で SEO を全面強化する

UIが整ったら、次は検索流入の土台を固めた。

SoftwareApplication JSON-LD に不足していたフィールドを追加した。datePublished / dateModified / inLanguage: ja / isAccessibleForFree: true / keywords などだ。さらに HowTo JSON-LD を新規追加した。5ステップ・総所要時間 2分・必要なものと使うものを明示した構造化データで、Google の HowTo リッチカルーセルの対象になる可能性がある。

ZeronovaZeronova
1枚モードと一括モードで別々の HowTo を作った方がいいか?
Claude Code
Google は「1ページ1 HowTo」を標準的な実装として推奨しています。情報密度を上げつつ一括モードを代表にする方が、リッチリザルトとしての評価が安定しやすいです。
ZeronovaZeronova
一括モードを canonical にして HowTo をひとつにまとめよう。一括処理の手順という核心を伝えられる。

Title と meta description にも手を入れた。「無料オンライン」「サイズ変更」「縮小」というユーザーが実際に検索するキーワードを自然な文脈で配置した。

H2「画像サイズ変更の基礎知識」も新設した。アスペクト比の概念、解像度と表示サイズの違い、EXIF orientation が何をするかを平易に解説するセクションだ。SEO リッチリザルトが SERP に並ぶ画を想像しながら実装した。「1日でここまで整えられるのは AI 協業の桁違いの効率だ」と改めて実感した瞬間だった。

コンテンツの深度は構造化データと同時に整えてこそ効果が出る。構造化データだけ足しても、ページの内容が薄ければ意味が薄い。

Round 6 — 自分が書いた実装を自分で否定する

Round 3 で MAX_CANVAS_PIXELS = 4M24M に引き上げた。現代のブラウザが十分に対応できると判断していた。

しかし docs/tools-dev-guidelines.md の「教訓6」と照合すると、サイト内の他ツール(ページ送り画像生成 / ダミー画像生成等)はすべて 4M を基準に設計されていた。数時間前に自分が書いた 24M の根拠は「現代ブラウザの能力」。プロジェクトガイドラインの根拠は「過去の実際のトラブルから生まれたルール」だ。

どちらが正しいか、答えは明白だった。ガイドラインは過去の痛みの記録だ。

ガイドラインに照合して違反を確認したため、4M に revert した。この理由を明示的に記録した。将来、同じ議論が起きても「なぜ戻したか」を辿れる。

// ガイドライン準拠:他ツールとの一貫性を保つ
const MAX_CANVAS_PIXELS = 4_000_000; // 4M(ガイドライン規定値)

// clamp helper(他ツールと同じ命名パターンで実装)
const clamp = (value: number, min: number, max: number): number =>
  Math.min(Math.max(value, min), max);

// 1枚モード入力上限:10000px → 4000px(ガイドライン例値に一致)
// 一括モード入力上限:4000px → 2000px(正方形で 2000² = 4M に収まる)
ZeronovaZeronova
clamp という名前で実装しているのは他ツールと合わせた判断か?
Claude Code
はい。既存の他ツールが同じ clamp という命名を使っているので、後続メンテナが「他ツールと同じパターン」と気づけます。独自名で作るとコードベース全体のパターンが壊れていきます。

ガイドラインに沿って自分の実装を否定するのは、少し抵抗があった。でも「ガイドラインは過去の痛みの記録」と捉え直すと、従うことは将来のメンテナへの配慮だとわかった。専門家が自分の判断を撤回できることは、専門性の否定ではなくその証拠だ。

devlog 体験・感情メモより(2026-04-17):

「専門家として」という言葉の重みを何度も問われた1日だった。専門家とは「判断できる」だけでなく「判断を撤回できる」人のことだと、手触りで理解した。

1日8ラウンドから得たもの

強い怒りのメッセージが、1日8ラウンドの全面改修を引き起こした。フィードバックの強度は、問題の本質への近さに比例することが多い。「ちゃんと確認されているのでしょうか」という言葉は、「設計の根を確認できていたか」という問いだった。

この日に整理された教訓を残しておく。

ユーザーのメンタルモデルでバグを分類する

「アスペクト比が変わる」「ファイル名が変わる」「向きが変わる」「サイズがサイレントに縮む」は、開発者視点では異なるカテゴリに見える。でもユーザー視点ではすべて「勝手に画像が変わる」という1つの体験だ。バグの分類はユーザーのメンタルモデル基準でやること。開発者都合での先送りは信頼の損失になる。

「別タスクにする」は常に正当か問い直す

「工数が増えるから別 PR にする」という判断が、責任回避になっていないかを問い直す。「理由を教えてください」と問われたとき、答えられない場合は判断が甘い証拠だった。

createImageBitmap + imageOrientation: 'from-image' が現代の正解

HTMLImageElement を経由した画像処理は EXIF 回転がブラウザ依存になる。createImageBitmap は仕様として明示的に制御でき、.close() で GPU メモリも管理できる。

ガイドラインへの自己撤回は美徳

当日書いたコードを同日に否定する判断は抵抗があった。しかし「過去の痛みの記録」であるガイドラインの一貫性は、個別最適より長期的な価値が高い。撤回の理由を明示的に記録すれば、将来の議論の源泉を断てる。

価値は数値で見える形にする

SavingsBadge が示す「−87%」という数値は、ツールの存在意義そのものだ。削減率を目立たせることは装飾ではなく、ユーザーに「使ってよかった」と気づかせる設計判断だ。

SavingsBadge コンポーネントは汎用化候補として残っている。動画・PDF・ePub 等の他のファイル変換ツールにも「削減率バッジ」のパターンを展開できる。怒りを起点にした1日の改修が、サイト全体の設計パターンを豊かにするきっかけになった。

なお、AI 協業を使ったコンテンツ制作一般については「開発記事でAIとの協業をちゃんと書くことにした理由」も合わせて読んでほしい。個人開発でガイドライン管理を真剣にやる話については「個人開発でも必要なライセンス監査」が参考になる。

Zeronova avatar

Zeronovaゼロノバ

Product Manager / AI-Native Builder

Web/IT業界19年以上・20以上のWebサービスを担当したPdM。東証プライム上場企業の子会社代表として事業経営を経験。現在はAIを駆使して企画から実装まで完結させる個人開発を実践中。