1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[React]プロフィール画像のトリミング、圧縮機能の実装

Posted at

はじめに

個人開発中のSNSでプロフィール画像の設定を実装した際、トリミング機能とデータ圧縮機能を用いたので、今回は私が詰まった点も含めて共有します。


開発環境

  • React 18.2.0

使用ライブラリ

  • cropper.js 1.5.13
  • react-cropper 2.3.3

手順

cropper.jsとreact-cropperをダウンロード

npm install cropperjs@1.5.13
npm install react-cropper

react-cropper を利用する場合は注意が必要です。
最新の cropper.js(v2系)では、react-cropper が必要とする一部のデータや機能が含まれていないため、正常に動作しません。

そのため、react-cropper を使うときは cropperjs@1.x 系をインストールするのがおすすめ です。

トリミング用のモーダルコンポーネント作成

ここで以下の3つを行いました。

  • クロップの形を円形で表示
  • 画像サイズを縦横最大500pxにリサイズ
  • 画質を落として画像容量圧縮

クロップの形を円形で表示

現在開発中のプロダクツではプロフィール画像は円形表示だったので、トリミング時もクロップが円形の方が直感的に操作しやすいです。
しかし、cropper.jsはクロップ領域を四角形でしか切り抜けません。
そのため実際に切り抜かれる画像データは四角形ですが、見た目だけクロップ枠を丸くすることでユーザーが直感的にプロフィール画像をトリミングできるようにしました。

CropperModal.jsx
  // cropBoxを丸くする
  useEffect(() => {
    const style = document.createElement('style');
    style.innerHTML = `
      .cropper-crop-box, .cropper-view-box {
        border-radius: 50% !important;
      }
    `;
    document.head.appendChild(style);
    return () => {
      document.head.removeChild(style);
    };
  }, []);

画像サイズを縦横最大500pxにリサイズ・画質を落として画像容量圧縮

SNSでは、多くのユーザーがプロフィール画像を登録します。
さらにプロフィール画像は表示される回数も非常に多いため、
画像データのサイズをなるべく小さくすることが重要です。

その対策として、プロフィール画像をトリミングする際に

  • 画像のリサイズ(縦横のサイズ調整)
  • 画質の調整

を行い、容量を圧縮できるようにしました。

CropperModal.jsx
const handleCrop = () => {
    const cropper = cropperRef.current.cropper;
    const croppedCanvas = cropper.getCroppedCanvas();
    // 元画像のサイズを取得
    const originalWidth = croppedCanvas.width;
    const originalHeight = croppedCanvas.height;
    // 最大500pxにリサイズ
    let targetWidth = originalWidth;
    let targetHeight = originalHeight;
    if (originalWidth > originalHeight && originalWidth > 500) {
      targetWidth = 500;
      targetHeight = Math.round((originalHeight / originalWidth) * 500);
    } else if (originalHeight >= originalWidth && originalHeight > 500) {
      targetHeight = 500;
      targetWidth = Math.round((originalWidth / originalHeight) * 500);
    }
    // リサイズして圧縮
    cropper
      .getCroppedCanvas({
        width: targetWidth,
        height: targetHeight,
        imageSmoothingQuality: 'high',
      })
      .toBlob(
        blob => {
          onCrop(new File([blob], 'cropped.jpg', { type: blob.type }));
          onClose();
        },
        'image/jpeg',
        0.7 // 圧縮率
      );
  };

コンポーネント全体

CropperModal.jsx
import { useRef, useEffect } from 'react';
import Cropper from 'react-cropper';
import 'cropperjs/dist/cropper.css';

export default function CropperModal({ src, onClose, onCrop }) {
  const cropperRef = useRef();

  // cropBoxを丸くする
  useEffect(() => {
    const style = document.createElement('style');
    style.innerHTML = `
      .cropper-crop-box, .cropper-view-box {
        border-radius: 50% !important;
      }
    `;
    document.head.appendChild(style);
    return () => {
      document.head.removeChild(style);
    };
  }, []);

  const handleCrop = () => {
    const cropper = cropperRef.current.cropper;
    const croppedCanvas = cropper.getCroppedCanvas();
    // 元画像のサイズを取得
    const originalWidth = croppedCanvas.width;
    const originalHeight = croppedCanvas.height;
    // 最大500pxにリサイズ
    let targetWidth = originalWidth;
    let targetHeight = originalHeight;
    if (originalWidth > originalHeight && originalWidth > 500) {
      targetWidth = 500;
      targetHeight = Math.round((originalHeight / originalWidth) * 500);
    } else if (originalHeight >= originalWidth && originalHeight > 500) {
      targetHeight = 500;
      targetWidth = Math.round((originalWidth / originalHeight) * 500);
    }
    // リサイズして圧縮
    cropper
      .getCroppedCanvas({
        width: targetWidth,
        height: targetHeight,
        imageSmoothingQuality: 'high',
      })
      .toBlob(
        blob => {
          onCrop(new File([blob], 'cropped.jpg', { type: blob.type }));
          onClose();
        },
        'image/jpeg',
        0.7 // 圧縮率
      );
  };

  return (
    <div className='fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50'>
      <div className='bg-white p-4 rounded shadow-lg'>
        <Cropper
          src={src}
          style={{ height: 300, width: 300 }}
          aspectRatio={1}
          guides={false}
          ref={cropperRef}
          viewMode={1}
          dragMode='move'
          background={false}
          responsive={true}
          autoCropArea={1}
          checkOrientation={false}
        />
        <div className='flex justify-end gap-2 mt-2'>
          <button
            type='button'
            className='px-4 py-1 rounded bg-gray-300'
            onClick={onClose}
          >
            キャンセル
          </button>
          <button
            type='button'
            className='px-4 py-1 rounded bg-blue-600 text-white'
            onClick={handleCrop}
          >
            トリミングして決定
          </button>
        </div>
      </div>
    </div>
  );
}

まとめ

  • react-cropper を利用する場合は cropperjs@1.x 系を使うのがおすすめ
  • プロフィール画像は 登録数・表示回数が多いため容量削減が重要
  • トリミング画面では「円形のクロップ枠」を表示することで、ユーザーに直感的な操作感を提供
  • トリミング時に リサイズ(最大500px)画質調整(圧縮率0.7) を行うことで、画像サイズを効率的に削減可能
  • この仕組みを入れることで、表示速度の改善サーバー負荷・ストレージコストの軽減 が期待できる
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?