【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】SassとTypescriptでレイアウトを構成

  2. 【NextJS】Hooks-useContext・useReducer…

  3. 【NextJS】Firestore

  4. 【NextJS】FullCalendar

  5. AWS

    【AWS】Deploy Serverless NextJS app w…

  6. 【NextJS】Dynamic Routes

最近の記事

  1. AWS
  2. AWS
  3. AWS

制作実績一覧

  1. Checkeys