【NextJS】Cropping a portion of an image with React Cropper

1. 概要

前回はPDFを表示&ダウンロードする内容についてでした。今回は画像を切り取りし、保存する内容になります。

対象としては開発を1年程やってて自分で最初から開発してみたい方になります。そのため細かい用語などの説明はしません。

2. nodeのインストール

こちらを参考

3. プロジェクトを作成

3-1-1. こちらを参考

4. 必要なライブラリをインストール

4-1-1. こちらを参考

npm install --save react-cropper

5. ソースコード

※前回より差分のみを記載

5-1-1. src/app/components/component12/image-cropper.tsx

import React, { useState, RefObject, useRef } from "react";
import Image from "next/image";
import { Stack, Button, Fab } from "@mui/material";
import CropIcon from "@mui/icons-material/Crop";
import DownloadIcon from "@mui/icons-material/Download";
import UndoIcon from "@mui/icons-material/Undo";
import Cropper, { ReactCropperElement } from "react-cropper";
import "cropperjs/dist/cropper.css";
import scss from "./page.module.scss";

const imageLoader = ({
  src,
  width,
  quality,
}: {
  src: string;
  width: number;
  quality?: number;
}) => {
  return `${src}?w=${width}&q=${quality || 75}`;
};

const ImageCropper = ({
  defaultSrc,
  downloadImgName,
}: {
  defaultSrc: string;
  downloadImgName: string;
}) => {
  const [image, setImage] = useState<string>(defaultSrc);
  const [cropData, setCropData] = useState<string>("#");
  const cropperRef: RefObject<ReactCropperElement> =
    useRef<ReactCropperElement>(null);

  const onChange = (e: any) => {
    e.preventDefault();
    let files;
    if (e.dataTransfer) {
      files = e.dataTransfer.files;
    } else if (e.target) {
      files = e.target.files;
    }
    const reader = new FileReader();
    reader.onload = () => {
      setImage(reader.result as any);
    };
    reader.readAsDataURL(files[0]);
  };

  const getCropData = () => {
    if (typeof cropperRef.current?.cropper !== "undefined") {
      const cropper = cropperRef.current?.cropper;
      setCropData(cropper.getCroppedCanvas().toDataURL());
    }
  };

  const downloadImage = () => {
    if (cropData === "#") {
      alert(`Please crop image first.`);
      return;
    }
    const aLink = document.createElement("a");
    aLink.href = cropData;
    aLink.download = downloadImgName;
    aLink.click();
  };

  return (
    <div>
      <Stack direction="column" spacing={1} sx={{ width: "90%" }}>
        <Stack
          direction="row"
          spacing={1}
          justifyContent="center"
          alignItems="center"
          sx={{ width: "100%" }}
        >
          <input type="file" onChange={onChange} />
          <Button
            variant="contained"
            startIcon={<UndoIcon />}
            size="small"
            color="secondary"
            onClick={() => setImage(defaultSrc)}
          >
            Use Default Image
          </Button>
        </Stack>
        <Stack
          direction="row"
          spacing={1}
          justifyContent="center"
          alignItems="center"
          sx={{ width: "100%" }}
        >
          <Cropper
            ref={cropperRef}
            style={{ width: "100%", height: 400 }}
            zoomTo={0.5}
            initialAspectRatio={1}
            preview="#img_preview"
            src={image}
            viewMode={1}
            minCropBoxHeight={10}
            minCropBoxWidth={10}
            background={true}
            responsive={true}
            autoCropArea={1}
            checkOrientation={false}
            guides={true}
          />
        </Stack>
        <Stack
          direction="row"
          spacing={1}
          justifyContent="center"
          alignItems="center"
          sx={{ width: "100%" }}
        >
          <Stack
            direction="column"
            spacing={1}
            justifyContent="center"
            alignItems="center"
            sx={{ width: "42%" }}
          >
            <h2>Preview</h2>
            <div
              id="img_preview"
              className={scss.img_preview}
              style={{
                width: "100%",
                float: "left",
                height: 300,
              }}
            />
          </Stack>
          <Stack
            direction="column"
            spacing={1}
            justifyContent="center"
            alignItems="center"
            sx={{ width: "16%" }}
          >
            <Fab
              color="primary"
              aria-label="Crop image"
              onClick={() => getCropData()}
            >
              <CropIcon fontSize="large" />
            </Fab>
            <Fab
              color="primary"
              aria-label="Download image"
              onClick={() => downloadImage()}
            >
              <DownloadIcon fontSize="large" />
            </Fab>
          </Stack>
          <Stack
            direction="column"
            spacing={1}
            justifyContent="center"
            alignItems="center"
            sx={{ width: "42%" }}
          >
            <h2>Cropped</h2>
            {cropData === "#" ? (
              <div>Not yet</div>
            ) : (
              <Image
                loader={imageLoader}
                src={cropData}
                alt="Cropped image"
                width={0}
                height={0}
                sizes="100vw"
                style={{ width: "100%", height: "auto" }}
              />
            )}
          </Stack>
        </Stack>
      </Stack>
      <br style={{ clear: "both" }} />
    </div>
  );
};

export default ImageCropper;

5-1-2. src/app/components/component12/client-page.tsx

"use client";

import ImageCropper from "./image-cropper";

const ClientPage = () => {
  const defaultSrc: string = "/panko-lineup.png";
  const downloadImgName: string = "cropped-image.png";

  return (
    <div>
      <ImageCropper defaultSrc={defaultSrc} downloadImgName={downloadImgName} />
    </div>
  );
};

export default ClientPage;

5-1-3. src/app/components/component12/page.module.scss

.component {
  & ul {
    margin-left: 20px;
    & li {
      list-style: disc;
    }
  }
  & h2 {
    font-size: 20px;
    font-weight: bold;
  }
  & .img_preview {
    overflow: hidden;
  }
}

5-1-4. src/app/components/component12/page.tsx

import Divider from "@mui/material/Divider";
import GoBack from "@/lib/components/go-back";
import scss from "./page.module.scss";
import ClientPage from "./client-page";

const Component12 = () => {
  return (
    <div className={scss.component}>
      <GoBack />
      <br />
      <br />
      <ul>
        <li>画像トリミング</li>
        <ul>
          <li>react-cropper</li>
        </ul>
      </ul>
      <Divider sx={{ marginTop: 2, marginBottom: 2 }} />
      <div>
        <ClientPage />
      </div>
    </div>
  );
};

export default Component12;

5-1-5. src/app/components/page.module.scss

.components {
  color: blue;
  & ul {
    margin-left: 20px;
    & li {
      list-style: disc;
    }
  }
}

5-1-6. src/app/components/page.tsx

"use client";

import React from "react";
import { Link } from "@mui/material";

import scss from "./page.module.scss";

const Components = () => {
  return (
    <div className={scss.components}>
      <ul>
        <li>
          <Link href="/components/component01" underline="hover">
            Component01
          </Link>
        </li>
        <li>
          <Link href="/components/component02" underline="hover">
            Component02
          </Link>
        </li>
        <li>
          <Link href="/components/component03" underline="hover">
            Component03
          </Link>
        </li>
        <li>
          <Link href="/components/component04" underline="hover">
            Component04
          </Link>
        </li>
        <li>
          <Link href="/components/component05" underline="hover">
            Component05
          </Link>
        </li>
        <li>
          <Link href="/components/component06" underline="hover">
            Component06
          </Link>
        </li>
        <li>
          <Link href="/components/component07" underline="hover">
            Component07
          </Link>
        </li>
        <li>
          <Link href="/components/component08" underline="hover">
            Component08
          </Link>
        </li>
        <li>
          <Link href="/components/component09" underline="hover">
            Component09
          </Link>
        </li>
        <li>
          <Link href="/components/component10" underline="hover">
            Component10
          </Link>
        </li>
        <li>
          <Link href="/components/component11" underline="hover">
            Component11
          </Link>
        </li>
        <li>
          <Link href="/components/component12" underline="hover">
            Component12
          </Link>
        </li>
      </ul>
    </div>
  );
};

export default Components;

6. サーバーを起動

npm run dev

7. ブラウザで確認

  • http://localhost:3000

7-1-1. 切り取り

7-2-1. ダウンロード

8. ディレクトリの構造

省略

9. 備考

今回は画像を切り取りし、保存する内容についてでした。

10. 参考

投稿者プロフィール

Sondon
開発好きなシステムエンジニアです。
卓球にハマってます。

関連記事

  1. 【NextJS】Canvasを使い、図形を描画

  2. 【NextJS】Error Handling

  3. 【NextJS】Events

  4. 【NextJS】Local Storage

  5. 【NextJS】Dynamic Routes

  6. 【NextJS】ChatApp with Realtime updat…

最近の記事

制作実績一覧

  1. Checkeys