Image Manipulation with react-easy-crop

Image Manipulation with react-easy-crop

by Clarence Bakosi

Image manipulation involves altering an image using various methods and techniques to achieve desired results. This allows you to concentrate on an image aspect while ignoring others. As a result, the image aspect ratio, form, or size are reduced. Image manipulation plays an important role in front-end apps.

The react-easy-crop package is a free open-source JavaScript library. It supports image formats (JPEG, PNG, and GIF) as URL or base64 string and video formats supported in HTML5. It's an efficient tool for manipulating images/videos in front-end apps.

In this article, our focus will be on manipulating images with react-easy-crop. We will create a simple file upload application. The result of the cropped area will be displayed using the following functionalities supported by react-easy-crop:

  • Drag feature
  • Aspect ratio
  • Cropping
  • Zooming
  • Crop shape
  • Rotate interactions

Creating our React app

To start, use the commands below to launch the React application.

 npx create-react-app easy-crop
 cd easy-crop
 npm i react-easy-crop
 npm i @material-ui/core
 npm start

Before we begin, we will modify app.css to add some styling to the application. Copy and paste the code block below into app.css.

.App {
  background-color: #c4c4c4;
  font-family: 'Helvetica Neue', sans-serif;
}

.App-header {
  padding-top: 20px;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: flex-start;
  color: #fff;
}

label {
  font-weight: bold;
  padding-left: 10px;
}

button {
  z-index: 2;
  cursor: pointer;
  width: 100%;
  background-color: #000;
  color: #fff;
  padding: 10px 0;
}

.cropped-image {
  height: 600px;
  width: auto;
}

.cropped-image-container {
  margin-top: 20px;
  display: flex;
  flex-direction: column;
}

._coverImage-holder  {
  padding: 25px 40px;
  background-color:black;
  border-radius: 5px;
  cursor: pointer;
  margin-bottom: 20px;
}


.container {
  display: flex;
  flex-direction: column;
  position: relative;
}

.crop-container {
  height: 600px;
  width: 600px;
}

.controls {
  display: flex;
  flex-direction: column;
  width: 600px;
  position: absolute;
  bottom: -15px;
}

Setting up react-easy-crop

We'll create a component called EasyCrop.js and paste the code block below into it.

import React, { useState } from "react";
import Cropper from 'react-easy-crop';

const EasyCrop = () => {
 const [crop, setCrop] = useState({ x: 0, y: 0 });

  return (
    <Cropper
      image="https://cdn.pixabay.com/photo/2016/07/07/16/46/dice-1502706__340.jpg"
      crop={crop}
      onCropChange={setCrop}
    />
  )
}

export default EasyCrop;

Before we dive into the functionalities that react-easy-crop supports, we will update our App.js component with the code block below.

import EasyCrop from "./EasyCrop";

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <EasyCrop  />
      </header>
    </div>
  );
}

export default App;

The drag functionality is enabled by default. Moving forward, we will implement other functionalities react-easy-crop has to offer.

1 Drag

Aspect Ratio

The aspect ratio represents the image's width and height. It can be manipulated by adding the aspect prop to the Cropper component.

The prop's value syntax is width/height, and its default value is 4/3.

We will modify the value to 5/5 for our' Cropper' component.

<Cropper
  image="https://cdn.pixabay.com/photo/2016/07/07/16/46/dice-1502706__340.jpg"
  crop={crop}
  aspect={5 / 5}
  onCropChange={setCrop}
/>

2 Aspect Ratio

Crop Shape

The image shape can be changed by adding the cropShape prop. This prop accepts a string of rect or round. The default value is rect; to make the image rounded, we will change the value of cropShape to round.

<Cropper
  image="https://cdn.pixabay.com/photo/2016/07/07/16/46/dice-1502706__340.jpg"
  crop={crop}
  aspect={4 / 4}
  cropShape="round"
  onCropChange={setCrop}
/>

3 Crop Shape

Zoom

To activate the zoom functionality, react-easy-crop offers five props. These props include zoom, zoomWithScroll, zoomSpeed, minZoom, and maxZoom.

import React, { useState } from "react";
import Slider from "@material-ui/core/Slider";
import Cropper from "react-easy-crop";

const EasyCrop = () => {
  const [crop, setCrop] = useState({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);

  return (
    <div>
      <div className="crop-container">
        <Cropper
          image="https://cdn.pixabay.com/photo/2016/07/07/16/46/dice-1502706__340.jpg"
          crop={crop}
          zoom={zoom}
          zoomSpeed={4}
          maxZoom={3}
          zoomWithScroll={true}
          showGrid={true}
          aspect={4 / 3}
          onCropChange={setCrop}
          onZoomChange={setZoom}
        />
      </div>
      <div className="controls">
        <label>
          Zoom
          <Slider
            value={zoom}
            min={1}
            max={3}
            step={0.1}
            aria-labelledby="zoom"
            onChange={(e, zoom) => setZoom(zoom)}
            className="range"
          />
        </label>
      </div>
    </div>
  );
};

export default EasyCrop;

4 Zoom

Rotation

Rotation functionality can be added using two props: rotation and onRotationChange.

import React, { useState } from "react";
import Slider from "@material-ui/core/Slider";
import Cropper from "react-easy-crop";

  const EasyCrop = () => {
  const [crop, setCrop] = useState({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);
  const [rotation, setRotation] = useState(0);

  return (
     <div>
        <div className="crop-container">
          <Cropper
            image='https://cdn.pixabay.com/photo/2016/07/07/16/46/dice-1502706__340.jpg'
            crop={crop}
            rotation={rotation}
            zoom={zoom}
            zoomSpeed={4}
            maxZoom={3}
            zoomWithScroll={true}
            showGrid={true}
            aspect={4 / 3}
            onCropChange={setCrop}
            onZoomChange={setZoom}
            onRotationChange={setRotation}
          />
        </div>
        <div className="controls">
          <label>
            Rotate
            <Slider
              value={rotation}
              min={0}
              max={360}
              step={1}
              aria-labelledby="rotate"
              onChange={(e, rotation) => setRotation(rotation)}
              className="range"
            />
          </label>
        </div>
      </div>
  );
};

export default EasyCrop;

5 Rotation

Open Source Session Replay

OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.

replayer.png

Start enjoying your debugging experience - start using OpenReplay for free.

Display cropped image

Next, using the supported functionalities that react-easy-crop offers, we will implement the file upload feature into the application to make it dynamic. To do this, we will first create a component named Crop.js and paste the code block below.

export const createImage = (url) =>
  new Promise((resolve, reject) => {
    const image = new Image();
    image.addEventListener("load", () => resolve(image));
    image.addEventListener("error", (error) => reject(error));
    image.setAttribute("crossOrigin", "anonymous"); 
    image.src = url;
  });

export function getRadianAngle(degreeValue) {
  return (degreeValue * Math.PI) / 180;
}

export function rotateSize(width, height, rotation) {
  const rotRad = getRadianAngle(rotation);

  return {
    width:
      Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
    height:
      Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
  };
}

export default async function getCroppedImg(
  imageSrc,
  pixelCrop,
  rotation = 0,
  flip = { horizontal: false, vertical: false }
) {
  const image = await createImage(imageSrc);
  const canvas = document.createElement("canvas");
  const ctx = canvas.getContext("2d");

  if (!ctx) {
    return null;
  }

  const rotRad = getRadianAngle(rotation);

  const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
    image.width,
    image.height,
    rotation
  );

  // set canvas size to match the bounding box
  canvas.width = bBoxWidth;
  canvas.height = bBoxHeight;

  ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
  ctx.rotate(rotRad);
  ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
  ctx.translate(-image.width / 2, -image.height / 2);

  ctx.drawImage(image, 0, 0);

  const data = ctx.getImageData(
    pixelCrop.x,
    pixelCrop.y,
    pixelCrop.width,
    pixelCrop.height
  );


  canvas.width = pixelCrop.width;
  canvas.height = pixelCrop.height;

  ctx.putImageData(data, 0, 0);

  return new Promise((resolve, reject) => {
    canvas.toBlob((file) => {
      resolve(URL.createObjectURL(file));
    }, "image/jpeg");
  });
}

The subsequent stage involves modifying the EasyCrop.js component, making the image prop dynamic.

import { useCallback, useState } from "react";
import Slider from "@material-ui/core/Slider";
import Cropper from "react-easy-crop";
import getCroppedImg from "./Crop";

const EasyCrop = ({ image }) => {
  const [crop, setCrop] = useState({ x: 0, y: 0 });
  const [zoom, setZoom] = useState(1);
  const [rotation, setRotation] = useState(0);
  const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
  const [croppedImage, setCroppedImage] = useState(null);

  const onCropComplete = useCallback((croppedArea, croppedAreaPixels) => {
    setCroppedAreaPixels(croppedAreaPixels);
  }, []);

  const showCroppedImage = useCallback(async () => {
    try {
      const croppedImage = await getCroppedImg(
        image,
        croppedAreaPixels,
        rotation
      );
      console.log("donee", { croppedImage });
      setCroppedImage(croppedImage);
    } catch (e) {
      console.error(e);
    }
  }, [croppedAreaPixels, rotation, image]);

  const onClose = useCallback(() => {
    setCroppedImage(null);
  }, []);

  return (
    <div>
      <button
        style={{
          display: image === null || croppedImage !== null ? "none" : "block",
        }}
        onClick={showCroppedImage}
      >
        Crop
      </button>
      <div
        className="container"
        style={{
          display: image === null || croppedImage !== null ? "none" : "block",
        }}
      >
        <div className="crop-container">
          <Cropper
            image={image}
            crop={crop}
            rotation={rotation}
            zoom={zoom}
            zoomSpeed={4}
            maxZoom={3}
            zoomWithScroll={true}
            showGrid={true}
            aspect={4 / 3}
            onCropChange={setCrop}
            onCropComplete={onCropComplete}
            onZoomChange={setZoom}
            onRotationChange={setRotation}
          />
        </div>
        <div className="controls">
          <label>
            Rotate
            <Slider
              value={rotation}
              min={0}
              max={360}
              step={1}
              aria-labelledby="rotate"
              onChange={(e, rotation) => setRotation(rotation)}
              className="range"
            />
          </label>
          <label>
            Zoom
            <Slider
              value={zoom}
              min={1}
              max={3}
              step={0.1}
              aria-labelledby="zoom"
              onChange={(e, zoom) => setZoom(zoom)}
              className="range"
            />
          </label>
        </div>
      </div>
      <div className="cropped-image-container">
        {croppedImage && (
          <img className="cropped-image" src={croppedImage} alt="cropped" />
        )}
        {croppedImage && <button onClick={onClose}>close</button>}
      </div>
    </div>
  );
};

export default EasyCrop;

Additionally, we make the final changes to the App.js component, including the file upload feature.

import React, { useState } from "react";
import EasyCrop from "./EasyCrop";

function App() {
  const [image, setImage] = useState(null);

  const handleImageUpload = async (e) => {
    setImage(URL.createObjectURL(e.target.files[0]));
  };

  return (
    <div className="App">
      <header className="App-header">
        <label className="_coverImage-holder">
          Upload Image
          <input
            type="file"
            name="cover"
            onChange={handleImageUpload}
            accept="img/*"
            style={{ display: "none" }}
          />
        </label>
        <EasyCrop image={image}  />
      </header>
    </div>
  );
}

export default App;

6 Final output

Conclusion

React-easy-crop is efficient in manipulating images in React applications. It offers the flexibility to achieve desired results when altering images for the web. Here are links to the source code and live app.

A TIP FROM THE EDITOR: On the topic of working with images, our React 18 - What's New and How it Will Benefit Developers article highlights some advantages of the latest version of React.

newsletter