🤖

【react/next.js】Cropper.jsで画像アップロード&トリミング機能を作る(確認画面、ダウンロード付き)

2024/07/26に公開

ReactとNext.jsでcropper.jsを使った画像トリミング機能のコードの基本的なセットアップを以下に示します。この例では、react-cropperパッケージを使用しています。

インストール

まず、必要なパッケージをインストールします。

react-cropper

npm install cropperjs react-cropper

スタイルはbootstrapでやってます。

npm install react-bootstrap bootstrap

作成

必要なパッケージをインストールできたら、最初に画像のトリミング機能を提供するImageCropperコンポーネントを作成

app/component/ImageCropper

    "use client"; // クライアントサイドコンポーネントとして指定
    
    import React, { useState, useRef } from 'react';
    import Cropper from 'react-cropper';
    import 'cropperjs/dist/cropper.css'; // Cropper.jsのスタイルをインポート
    import { Modal, Button } from 'react-bootstrap'; // BootstrapのModalとButtonコンポーネントをインポート
    
    // ImageCropperコンポーネントの定義
    const ImageCropper = ({ imageSrc, onCancel }) => {
  // トリミング後の画像データを保持するための状態
  const [cropData, setCropData] = useState('');
  // モーダルの表示・非表示を管理するための状態
  const [modalShow, setModalShow] = useState(false);
  // Cropperインスタンスへの参照を保持するためのref
  const cropperRef = useRef(null);

  // 画像のトリミングを処理する関数
  const handleCrop = () => {
    if (cropperRef.current) {
      // Cropperインスタンスを取得
      const cropper = cropperRef.current.cropper;
      // トリミング後の画像データを取得
      const croppedImage = cropper.getCroppedCanvas().toDataURL();
      // 画像データを状態にセット
      setCropData(croppedImage);
      // モーダルを表示
      setModalShow(true);
    }
  };

  // 画像のダウンロードを処理する関数
  const handleDownload = () => {
    // ダウンロードリンクを作成
    const link = document.createElement('a');
    link.href = cropData;
    link.download = 'cropped-image.png'; // ダウンロードする画像のファイル名
    link.click(); // ダウンロードを開始
    // モーダルを非表示にする
    setModalShow(false);
  };

  // モーダルのキャンセル処理を行う関数
  const handleCancel = () => {
    // モーダルを非表示にする
    setModalShow(false);
    // 親コンポーネントから渡されたonCancel関数があれば呼び出す
    if (onCancel) {
      onCancel();
    }
  };

  return (
    <div className="col-md-6 col-12 mx-auto">
      {/* Cropperコンポーネントの表示 */}
      <Cropper
        src={imageSrc} // トリミング対象の画像のURL
        style={{ width: '100%' }} // スタイル設定: 幅を100%にする
        aspectRatio={1} // アスペクト比1:1に設定
        guides={false} // ガイド線を表示しない
        ref={cropperRef} // Cropperインスタンスへの参照を設定
        viewMode={1} // トリミングエリアが画像の外に出ないように設定
      />
      
      {/* トリミング実行ボタン */}
      <button className="col-md-auto col-12 btn btn-success mt-3" onClick={handleCrop}>これでくり抜く!</button>

      {/* モーダルの表示設定 */}
      <Modal show={modalShow} onHide={handleCancel}>
        <Modal.Header closeButton>
          {/* モーダルのヘッダー(閉じるボタンを含む) */}
        </Modal.Header>
        <Modal.Body>
          <h5 className="text-center mb-3 mt-3">これでOKですか?</h5>
          <div className="text-center">
            {/* トリミング後の画像のプレビュー */}
            <img src={cropData} alt="Cropped" style={{ maxWidth: '100%' }} />
          </div>
        </Modal.Body>
        <Modal.Footer>
          {/* モーダルのフッター(キャンセルボタンとダウンロードボタン) */}
          <Button variant="secondary" onClick={handleCancel}>やり直す</Button>
          <Button variant="primary" onClick={handleDownload}>ダウンロードする</Button>
        </Modal.Footer>
      </Modal>
    </div>
  );
};

    export default ImageCropper;

次にメインでは上で作ったImageCropperコンポーネントをインポート

app/cropper/page.jsx
    "use client";
    import 'bootstrap/dist/css/bootstrap.min.css'; // Bootstrapのスタイルシートをインポート
    import React, { useState, useRef } from 'react'; // Reactと必要なフックをインポート
    import dynamic from 'next/dynamic'; // 動的インポート用のNext.jsのダイナミックインポートをインポート

    // ImageCropperコンポーネントをダイナミックにインポート(サーバーサイドレンダリングを無効化)
    const ImageCropper = dynamic(() => import('../component/ImageCropper'), { ssr: false });
    
    const IndexPage = () => {
  // 画像のURLを状態として管理するためのuseStateフック
  const [imageSrc, setImageSrc] = useState(null);
  
  // ファイル入力の参照を管理するためのuseRefフック
  const fileInputRef = useRef(null);

  // ファイルが選択されたときに呼ばれるイベントハンドラー
  const handleFileChange = (e) => {
    const file = e.target.files[0]; // 選択されたファイルを取得
    if (file) {
      const reader = new FileReader(); // FileReaderを使ってファイルを読み込む
      reader.onloadend = () => {
        setImageSrc(reader.result); // 読み込んだファイルのデータURLを状態に設定
      };
      reader.readAsDataURL(file); // ファイルをDataURLとして読み込む
    }
  };

  // 「ファイルを選択」ボタンがクリックされたときにファイル入力をクリックする
  const handleButtonClick = () => {
    if (fileInputRef.current) {
      fileInputRef.current.click(); // 非表示のファイル入力をクリックしてファイル選択ダイアログを表示
    }
  };

  // 「キャンセル」ボタンがクリックされたときに呼ばれるイベントハンドラー
  const handleCancel = () => {
    // 画像をそのままにする(現在の画像URLをそのまま設定)
    setImageSrc((prevImageSrc) => prevImageSrc);
  };

  return (
    <div className="container mt-4 text-center">
      <h6 className="fw-bold mb-4">ファイルをアップロードしてね</h6> {/* アップロード指示のテキスト */}

      {/* 「ファイルを選択」ボタン */}
      <button className="col-md-auto col-12 btn btn-primary mb-3" onClick={handleButtonClick}>
        ファイルを選択
      </button>
      
      {/* ファイル入力(非表示) */}
      <input
        type="file"
        accept="image/*" // 画像ファイルのみ受け入れる
        onChange={handleFileChange} // ファイルが選択されたときにhandleFileChangeを呼び出す
        style={{ display: 'none' }} // ファイル入力を非表示にする
        ref={fileInputRef} // ファイル入力の参照を設定
      />
      
      {/* 画像が選択されている場合にImageCropperコンポーネントを表示 */}
      {imageSrc && <ImageCropper imageSrc={imageSrc} onCancel={handleCancel} />}
    </div>
  );
};

    export default IndexPage;

上記実行すると下記のような画面になります。

1、ファイルを選択する

2、アップロードされた画像をトリミングエリアで調整し、「これでくり抜く!」を押下する。

3、確認画面が出るので、「ダウンロードする」でダウンロード!

こんな感じです。
Cropperのオプションは色々設定できるみたいなので適宜調整してみてください。

自前のアプリに写真の編集機能をつけてダウンロードさせたりとかに使えるかなと。応用次第では色々できると思います。

以上ー

Discussion