🔎

react-image-cropを使って、プロフィールの丸い画像を作ってみる

2022/11/06に公開1

色々探してみて以下2つが良さそうだと感じました。

https://www.npmjs.com/package/react-image-crop
https://www.npmjs.com/package/react-cropper

Weekly Downloadsを見てみると、react-image-cropの方がダウンロード数が多そうなので、今回はこちらを使用します。

作ってみたもの

まず画像をアップロードして、切り取りたいところを選択して、切り取りボタンを押すと、

選択部分を切り取れるものになります。

ソースコード

import React, { ChangeEvent, FormEvent, useEffect, useState } from "react";
import ReactCrop from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
import { Crop } from "react-image-crop/dist/types";

const CropImg = () => {
  const [fileData, setFileData] = useState<File | undefined>();
  const [objectUrl, setObjectUrl] = useState<string | undefined>();

  //プロフィールイメージ
  const [profileImg, setProfileImg] = useState<string>("");

  //Crop
  const [crop, setCrop] = useState<Crop>({
    unit: "px", // 'px' または '%' にすることができます
    x: 0,
    y: 0,
    width: 200,
    height: 200,
  });

  //アップロードした画像のObjectUrlをステイトに保存する
  useEffect(() => {
    if (fileData instanceof File) {
      objectUrl && URL.revokeObjectURL(objectUrl);
      setObjectUrl(URL.createObjectURL(fileData));
    } else {
      setObjectUrl(undefined);
    }
  }, [fileData]);

  //切り取った画像のObjectUrlを作成し、ステイトに保存する
  const makeProfileImgObjectUrl = async () => {
    if (objectUrl) {
      const canvas = document.createElement("canvas");
      canvas.width = crop.width;
      canvas.height = crop.height;
      const ctx = canvas.getContext("2d") as CanvasRenderingContext2D;
      ctx.beginPath();
      ctx.arc(
        canvas.width / 2,
        canvas.height / 2,
        canvas.width / 2,
        0,
        2 * Math.PI,
        false
      );
      ctx.clip();

      const img = await loadImage(objectUrl);
      console.log(img.width, img.naturalWidth);
      ctx.drawImage(
        img,
        crop.x,
        crop.y,
        crop.width,
        crop.height,
        0,
        0,
        crop.width,
        crop.height
      );

      canvas.toBlob((result) => {
        if (result instanceof Blob) setProfileImg(URL.createObjectURL(result));
      });
    }
  };

  // canvasで画像を扱うため
  // アップロードした画像のObjectUrlをもとに、imgのHTMLElementを作る
  const loadImage = (src: string): Promise<HTMLImageElement> => {
    return new Promise((resolve) => {
      const img = new Image();
      img.src = src;
      img.onload = () => resolve(img);
    });
  };

  return (
    <div>
      <form
        onSubmit={(e: FormEvent<HTMLFormElement>) => {
          e.preventDefault();
          makeProfileImgObjectUrl();
        }}
      >
        <input
          type="file"
          onChange={(e: ChangeEvent<HTMLInputElement>) => {
            e.target.files && setFileData(e.target.files[0]);
          }}
        />
        <button>切り取り</button>
      </form>
      <div>
        {objectUrl && (
          <ReactCrop
            crop={crop}
            onChange={(c) => setCrop(c)}
            aspect={1}
            circularCrop={true}
            keepSelection={true}
          >
            <img src={objectUrl} alt="" style={{ width: "100%" }} />
          </ReactCrop>
        )}
      </div>
      <div>
        {profileImg ? <img src={profileImg} alt="プロフィール画像" /> : ""}
      </div>
    </div>
  );
};

export default CropImg;

Discussion

schemelispschemelisp

画像がずれるということですが、おそらくImageのnaturalWidth, naturalHeightとwidth, heightの比率を考えてないからだと思われます。
また(あんまりないと思いますが)デバイスによってピクセルの縦横比が違うことも考慮に入れないとユーザーのデバイスによってはずれたりずれなかったりなどがあると思うため、このあたりもケアしてあげないと結果的にずれるといったことになると思います。
私は次のようなコードになりました…(一部抜粋です)

    const scaleX = image.naturalWidth / image.width;
    const scaleY = image.naturalHeight / image.height;
    const pixelRatio = window.devicePixelRatio;
    canvasEl.width = (crop?.width ?? 0) * scaleX * pixelRatio;
    canvasEl.height = (crop?.height ?? 0) * scaleY * pixelRatio;
    const ctx = canvasEl.getContext('2d');
    ctx.drawImage(
      image,
      (crop?.x ?? 0) * scaleX,
      (crop?.y ?? 0) * scaleY,
      (crop?.width ?? 0) * scaleX,
      (crop?.height ?? 0) * scaleY,
      0,
      0,
      (crop?.width ?? 0) * scaleX,
      (crop?.height ?? 0) * scaleY
    );