猫の手ツール

Algorithm & UI/UX

クライアントサイド画像処理の極致:安定性とプライバシーを両立するヘッダーカッターの実装

SNSのヘッダー作成は、各プラットフォーム独自の「推奨サイズ」と「見切れ」の戦いです。本ツールでは、サーバーを一切介さず、ブラウザ上のリソースだけでHEIC変換から超高解像度画像の安定処理までを実現しました。そのコアとなるフロントエンド・ロジックを紐解きます。

1. モバイル・ファーストを実現するHEICの透明な変換処理

iPhoneで撮影された写真は標準でHEIC形式ですが、ブラウザの<img>タグやCanvas APIではそのまま扱えないケースが多々あります。本ツールでは、ユーザーが「形式」を意識することなく編集を開始できるよう、動的インポートを用いたクライアントサイド変換を実装しています。

メリット: サーバーに画像をアップロードすることなく変換を行うため、プライバシーを完全に守りつつ、iPhoneユーザーに「PC不要」の編集体験を提供できます。

async function handleHeicProcessing(file: File) {
    if (file.name.match(/\.(heic|heif)$/i)) {
        // 必要になったタイミングで変換ライブラリを動的ロード
        const { default: heic2any } = await import('heic2any');
        
        // クライアントサイドでJPEGに変換
        const convertedBlob = await heic2any({
            blob: file,
            toType: "image/jpeg",
            quality: // 独自のチューニング値
        });
        return convertedBlob;
    }
    return file;
}

2. 超高解像度画像による「ブラウザ・クラッシュ」の防止策

近年のスマホカメラは1億画素を超えるものもあり、そのままcropperjs等のライブラリに渡すと、デバイスのメモリを食いつぶしてブラウザが強制終了する原因になります。本ツールでは、エディタを起動する前にCanvasを用いて「編集に最適な解像度」へダウンサンプリングする独自のプリ・プロセッシングを行っています。

メリット: メモリ制限の厳しいスマホブラウザでも動作を安定させ、かつスクロールやズームといった編集操作のレスポンス(FPS)を劇的に向上させます。

// エディタ起動前のプレ・リサイズ処理
const STABILITY_THRESHOLD = // 独自のしきい値;

if (image.naturalWidth > STABILITY_THRESHOLD || image.naturalHeight > STABILITY_THRESHOLD) {
    const canvas = document.createElement('canvas');
    // アスペクト比を維持したリサイズ計算
    const { width, height } = calculateOptimalDimensions(image, STABILITY_THRESHOLD);
    
    canvas.width = width;
    canvas.height = height;
    
    const ctx = canvas.getContext('2d');
    // 低負荷で高品質なダウンサンプリングを実行
    ctx.drawImage(image, 0, 0, width, height);
    
    // リサイズ後のURLでエディタを再初期化
    const optimizedUrl = canvas.toDataURL('image/jpeg', // チューニング値);
    startCropper(optimizedUrl);
}

3. SNSプリセットと「フリーサイズ」の動的最適化アルゴリズム

X(Twitter)やYouTubeなどの固定比率と、自由な比率(フリー)を瞬時に切り替えるロジックを搭載しています。特にフリーサイズ時は、出力画像が巨大になりすぎないよう最大幅を制限しつつ、現在の切り抜き範囲からリアルタイムに「出力後のピクセル数」を逆算して表示するUXを構築しました。

メリット: 「保存してみるまでサイズがわからない」という不安を解消。SNSの推奨ピクセル数を維持しつつ、ユーザーの自由度も確保しています。

function updateExportDimensions(cropData: any) {
    let exportWidth = Math.round(cropData.width);
    let exportHeight = Math.round(cropData.height);

    if (currentMode === 'free') {
        const MAX_EXPORT_WIDTH = // 独自の制限値;
        if (exportWidth > MAX_EXPORT_WIDTH) {
            // 比率を保ったまま最大幅にスケールダウンする計算処理
            // ここにスケール変換のロジックが入ります
        }
    } else {
        // 選択されたSNSプリセット(X, YouTube等)の固定解像度を適用
        exportWidth = snsConfigs[currentMode].width;
        exportHeight = snsConfigs[currentMode].height;
    }
    
    // UI上の「現在の出力サイズ」表示をリアルタイム更新
    renderSizeIndicator(exportWidth, exportHeight);
}

Developer's Note

「画像を切り抜く」という行為自体は単純ですが、それを「スマホで」「安全に」「高画質で」実現しようとすると、フロントエンド特有の泥臭い最適化が必要になります。

特にこだわったのは、クライアントサイド完結という点です。ユーザーの大切な写真を一時的にでもサーバーに保存するのは、開発者としてのプライバシーに対する矜持が許しませんでした。そのため、HEIC変換もリサイズもすべてユーザーの手元で完結させています。

また、iPhoneのSafariで「画像を保存する」ボタンがうまく動作しない問題(Blobの扱いの差異)に対しても、専用の保存ガイドをモーダルで用意するなど、技術的な制約をUXでカバーする設計を心がけました。