Image crop scaling issue with react-image-crop

1.7k Views Asked by At

I just starting to learn React. I've been having issues with the react-image-crop package. The document they have was not newbie friendly, I just barely made it work at this point. Now my issue is that the result cropped image is totally different from the user's selection. My thought is that it might be caused by the scaling of the original image when you select the cropping area. I have limited the window size because some people might choose to upload a large image. If you have any experience using this package, please let me know what I could do to fix this issue, thank you.

import "react-image-crop/dist/ReactCrop.css";

import React, { useState, useRef } from "react";

import ReactCrop from "react-image-crop";

export default function ImageUploader(props) {
  const [imgSrc, setImgSrc] = useState();
  const [crop, setCrop] = useState();
  const [originalImg, setOrgImg] = useState(null);
  const imgRef = useRef(null);

  const handleImage = async (event) => {
    setImgSrc(URL.createObjectURL(event.target.files[0]));
  };

  const getCroppedImg = async (image, pixelCrop) => {
    try {
      const canvas = document.createElement("canvas");
      console.log(crop);
      canvas.width = pixelCrop.width;
      canvas.height = pixelCrop.height;
      const ctx = canvas.getContext("2d");

      // Here is what I think where the problem is at:
      ctx.drawImage(
        image,
        pixelCrop.x,
        pixelCrop.y,
        pixelCrop.width,
        pixelCrop.height,
        0,
        0,
        pixelCrop.width,
        pixelCrop.height
      );

      const base64Image = await canvas.toDataURL("image/jpeg", 1);

      props.setCurrentImages(pushImage(props.images, base64Image));
      console.log(base64Image);
      console.log(props.images);
    } catch (e) {
      console.log(e);
    }
  };

  function pushImage(array, newImage) {
    if (array.lengh === 0) return [newImage];
    return [...array, newImage];
  }
  function handleCropButton() {
    getCroppedImg(imgRef.current, crop);
    props.setUploadImg(false);
  }

  return (
    <div style={{ height: "600px" }}>
      <div>
        <input type="file" onChange={handleImage} accept="image/*" />
        <button onClick={handleCropButton}>Crop</button>
      </div>

      <ReactCrop
        crop={crop}
        aspect={1}
        onChange={(c) => setCrop(c)}
        onComplete={(crop) => setCrop(crop)}
      >
        <img
          src={imgSrc}
          alt=""
          style={{ height: "600px" }}
          onLoad={() => {
            setOrgImg({
              height: imgRef.current.clientHeight,
              width: imgRef.current.clientWidth,
            });
          }}
          ref={imgRef}
        />
      </ReactCrop>
    </div>
  );
}
2

There are 2 best solutions below

1
On
function getCroppedImg(
  image: HTMLImageElement, // useRef reference
  crop: PixelCrop,
  scale = 1,
  rotate = 0
) {

  const canvas = document.createElement('canvas');
  canvas.width = crop.width;
  canvas.height = crop.height;
  const ctx = canvas.getContext('2d');

  if (!ctx) {
    throw new Error('No 2d context');
  }

  const scaleX = image.naturalWidth / image.width;
  const scaleY = image.naturalHeight / image.height;
  // devicePixelRatio slightly increases sharpness on retina devices
  // at the expense of slightly slower render times and needing to
  // size the image back down if you want to download/upload and be
  // true to the images natural size.
  const pixelRatio = window.devicePixelRatio;
  // const pixelRatio = 1

  canvas.width = Math.floor(crop.width * scaleX * pixelRatio);
  canvas.height = Math.floor(crop.height * scaleY * pixelRatio);

  ctx.scale(pixelRatio, pixelRatio);
  ctx.imageSmoothingQuality = 'high';

  const cropX = crop.x * scaleX;
  const cropY = crop.y * scaleY;

  const rotateRads = rotate * TO_RADIANS;
  const centerX = image.naturalWidth / 2;
  const centerY = image.naturalHeight / 2;

  ctx.save();

  // 5) Move the crop origin to the canvas origin (0,0)
  ctx.translate(-cropX, -cropY);
  // 4) Move the origin to the center of the original position
  ctx.translate(centerX, centerY);
  // 3) Rotate around the origin
  ctx.rotate(rotateRads);
  // 2) Scale the image
  ctx.scale(scale, scale);
  // 1) Move the center of the image to the origin (0,0)
  ctx.translate(-centerX, -centerY);

  ctx.fillStyle = 'white';
  ctx.fillRect(0, 0, image.naturalWidth, image.naturalHeight);
  ctx.drawImage(
    image,
    0,
    0,
    image.naturalWidth,
    image.naturalHeight,
    0,
    0,
    image.naturalWidth,
    image.naturalHeight
  );

  ctx.restore();

  return new Promise<{
    render: string;
    file: Blob | null;
  }>((resolve) => {
    canvas.toBlob((file) => {
      if (file != null) {
        const files: Blob | MediaSource = file;
        resolve({
          render: URL.createObjectURL(files),
          file: file,
        });
      }
    }, 'image/jpeg');
  });
}

Where scale and rotate is, if you are using scale and rotate property then pass those variable.

0
On

Note that if you've scaled down your image in css, react-image-crop may not crop the correct area, as it does its crops based on the full size of the image.

For several hours I thought my canvas code was wrong, when it was just my image that had been scaled down by my MUI dialog.

My fix for this was to set a max image size for uploaded images.