Technical Docs
Favicon Generator の設計
Canvas API と純粋なArrayBuffer操作でICOバイナリを自前構築するブラウザ完結型faviconジェネレーターの設計・実装を解説します。
1. 設計思想 — ブラウザ完結とICO自前構築
faviconを生成するアプローチとして、サーバーサイドで変換する方法と ブラウザ上で完結させる方法の2択があります。本ツールは後者を選択しました。
- プライバシー保護: アップロードした画像はネットワークに送出されません。すべての処理はユーザーのデバイス上で完結するため、 企業ロゴや未公開のブランドアセットを扱う際も安心して使用できます。
- 外部依存ゼロ: ImageMagick や Sharp などのサーバーサイドライブラリ、外部変換APIへの依存がありません。 Canvas API(全モダンブラウザに標準搭載)のみで動作します。
- Cloudflare Pages Edge Runtime対応: 本サービスはCloudflare Pagesでデプロイされており、Node.js APIが使えないEdge Runtimeが動作環境です。 サーバーサイドでの画像処理(SharpなどNode.jsバイナリ依存)は使用できないため、 ブラウザ完結のアーキテクチャは制約への自然な対応でもあります。
ICOバイナリの構築も外部ライブラリに頼らず、ArrayBuffer操作のみで自前実装しています。 ICOフォーマットの仕様はシンプルであり、依存を増やすよりも純粋関数として実装する方が テスタビリティと可搬性の面で優れています。
2. 技術アーキテクチャ
本ツールは3層で構成されています。Server Actions は使用しません(すべての処理がクライアントサイドで完結するため不要)。
- 純粋関数層(
src/lib/tools/favicon-generator.ts): ブラウザAPIに依存しない処理のみを純粋関数として実装します。 具体的には、ファイルバリデーション(validateImageFile)、 ICOバイナリ構築(buildICOBinary)、 ファイルサイズ表示(formatFileSize)の3関数と、 受け付けるMIMEタイプ(ACCEPTED_TYPES)、最大ファイルサイズ(MAX_FILE_SIZE_MB)、 サポートサイズ一覧(ALL_SIZES)の定数群をエクスポートします。 Canvas API への依存がないため Vitest(jsdom環境)で高速にテストできます。 - Client Component層(
app/tools/favicon-generator/_components/): 2つのコンポーネントで構成されます。FaviconGeneratorClient.tsx— 状態管理の親コンポーネント。 画像メタ情報・選択サイズ・生成結果・エラーの各状態を管理し、 Canvas処理ヘルパー(resizeToSquare・canvasToPNGBytes)をインライン定義します。DropZone.tsx— ドラッグ&ドロップとファイル選択UIを担当します。
- ページルート層(
app/tools/favicon-generator/page.tsx): Server Componentとして実装し、export const metadataでSEOを設定します。 Canvas処理はクライアントサイドで完結するためruntime = 'edge'の指定は不要です (Server Actionsがないページはデフォルトでstatic generationが適用されます)。
3. ICOバイナリフォーマットと自前構築
ICOファイルはWindowsが定めたバイナリフォーマットです。buildICOBinary 関数はArrayBufferを直接操作してICOバイナリを構築します。
- ICONDIRヘッダー(6バイト): ファイル先頭の6バイトに格納されます。
reserved(2バイト・常に0)、type(2バイト・ICOは1)、count(2バイト・含まれる画像数)の3フィールドで構成されます。 すべてリトルエンディアンで書き込みます。 - ICONDIRENTRY(1枚につき16バイト): ヘッダーに続いて画像数分のエントリが並びます。各エントリは
width・height(各1バイト)、colorCount・reserved(各1バイト)、planes・bitCount(各2バイト)、bytesInRes(4バイト・PNG データサイズ)、imageOffset(4バイト・ファイル先頭からのオフセット)で構成されます。 - 256px の特殊処理: ICO仕様では256×256の画像を格納する際、
width・heightフィールドに0を書き込むことで256を表します(1バイトでは256を表現できないため)。buildICOBinaryではsize % 256の計算でこの変換を自動処理しています。 - データオフセット計算: 各画像のPNGデータはすべてのICONDIRENTRYの後ろに連続して格納されます。 先頭からのオフセットは
6 + N × 16 + 前の画像データの累計バイト数で計算します(Nは総画像枚数)。 - PNGデータの埋め込み: 各エントリのデータはリサイズ後のCanvasから取得したPNGバイト列(
Uint8Array)をそのまま格納します。 ICO仕様ではBMPエンコードとPNGエンコードの両方が許容されており、 256×256以下のサイズでもPNGを使うことで透過情報と圧縮効率を維持できます。
4. Canvasリサイズアルゴリズム
resizeToSquare 関数は画像を任意サイズの正方形にリサイズします。 単純な1段階リサイズではなく、段階的縮小(ステップダウン)アルゴリズムを採用しています。
- 中央基準の正方形クロップ: 縦横比が異なる画像は短辺を基準に中央からクロップします。 元画像の幅・高さの差分を2で割り、描画開始座標(
sx・sy)をオフセットすることで 中央部分を切り出します。 - 段階的縮小(ハーフステップ): 一度に大幅に縮小するとブラウザの補間処理が粗くなり、画質が劣化します。 幅が目標サイズの2倍以上である間、
while (w / 2 >= targetSize)のループで 1/2ずつ縮小を繰り返し、最後に目標サイズへ最終リサイズします。 各ステップでimageSmoothingQuality: 'high'を指定することで 高品質な補間が適用されます。 - SVGへの対応: SVGファイルは明示的な
width・height属性がない場合、HTMLImageElementのnaturalWidth・naturalHeightが0を返します。 このケースはsource.naturalWidth || 512で512pxにフォールバックし、 処理を継続します。 - PNGバイト列への変換:
canvasToPNGBytesはcanvas.toBlob(callback, 'image/png')を Promiseでラップし、Blob.arrayBuffer()経由でUint8Arrayに変換します。toBlobがコールバックベースのAPIであるため、new Promise((resolve, reject)でPromise化しています。
5. UIコンポーネント設計
本ツールのUIは「アップロード前」と「アップロード後」の2フェーズに分かれます。
- DropZone(アップロード前): ドラッグ&ドロップとクリックによるファイル選択の両方に対応します。 子要素をまたぐ際に
DragLeaveが誤発火する問題をdragCounterRef(useRefでレンダーを発生させない、rerender-use-ref-transient-valuesパターン)で解決しています。 EnterとLeaveのカウントを管理し、カウンターがゼロになった時点でのみドラッグ状態を解除します。 - サイズ選択(ラジオグループ): 16・32・48・64・128・256pxの6サイズから1つを選択します。 shadcn/uiのRadioGroupコンポーネントを使わず、
role="radiogroup"コンテナ内にrole="radio"・aria-checkedを付与した<button>要素をグリッド配置することでセマンティクスとアクセシビリティを確保しています。 選択状態を持つ「ラジオボタン風トグル」のため、常に1つが選択された状態を維持します。 - ObjectURLのライフサイクル管理:
URL.createObjectURL(file)で生成したプレビュー用URLは、 別の画像に切り替える際にURL.revokeObjectURL(prev)で即座に解放します。useStateのfunctional update(setImageObjectUrl((prev) => ...))を利用して 前回のURLを取得・解放し、メモリリークを防ぎます。 - 生成結果のリセット: 別のサイズを選択した時点で
setResult(null)を呼び出し、 古い生成結果を即座に非表示にします。 これにより「16×16で生成した結果を見ながら32×32に変更してダウンロードする」という 操作ミスを防ぎます。
6. 品質管理
本ツールは単体テストとE2Eテストの両方でカバーしています。
- 純粋関数テスト(Vitest)(
src/lib/tools/__tests__/favicon-generator.test.ts):validateImageFile・buildICOBinary・formatFileSizeに対して26件のテストを実装しています。 PNG・JPEG・SVG・WebPの正常受け付け、GIF・PDFの拒否、上限ちょうど・上限超過のサイズ判定に加え、 ICONDIRヘッダーの各フィールド値、ICONDIRENTRYのサイズフィールド(256px→0変換を含む)、 データオフセット計算、出力の総バイト数まで詳細に検証しています。 Canvas APIに依存する処理(resizeToSquare・canvasToPNGBytes)はLayer 2に配置することで jsdom環境でも安定してテストできます。 - E2Eテスト(Playwright)(
e2e/favicon-generator.spec.ts): 6件のシナリオで主要なUIフローを検証します。 ページタイトル・DropZone・対応形式ガイドの表示確認、 GIF・サイズオーバーファイルアップロード時のバリデーションエラー、 有効なPNGアップロード後のプレビュー表示とデフォルトサイズ選択確認(aria-checked="true"が1件)、 生成実行後の結果セクションとダウンロードボタン表示をカバーします。 role="alert"の衝突回避: Next.jsのルートアナウンサー(id="__next-route-announcer__")は 常にrole="alert"を持ちDOMに存在します。 E2Eテストでエラー要素を特定する際は.filter({ hasText: '...' })で絞り込んでいます。