sekoyo / react-image-crop

A responsive image cropping tool for React
ISC License
3.9k stars 344 forks source link

Canvas returns null after cropping image iOS #578

Open YuriDavello opened 10 months ago

YuriDavello commented 10 months ago

I'm having some issues when cropping an image taken from an iOS device, specially iPhones. Apparently, the canvas width and height, after cropping, are greater than the original image and there's also a canvas size limit on iOS devices, thus, the canvas.toBlob returns null because there's no actual canvas.

I've also tried the sanbBox demo and the same issue occurs.

How could I solve it? is there a way to resize the dimensions so it fits in the canvas? maintaining the proportions of the crop area.

My code below:

  const getCroppedImg = () => {
    const { crop, image } = cropRef.current.getCroppedArea();

    return new Promise(resolve => {
      const canvas = document.createElement('canvas');

      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;
      const pixelRatio = window.devicePixelRatio;

      if (crop.unit === '%') {
        canvas.width = (image.naturalWidth * crop.width) / 100;
        canvas.height = (image.naturalHeight * crop.height) / 100;
      } else {
        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 rotate = 0;

      const rotateRads = rotate * TO_RADIANS;

      const centerX = image.naturalWidth / 2;
      const centerY = image.naturalHeight / 2;

      ctx.save();

      ctx.translate(-cropX, -cropY);
      ctx.translate(centerX, centerY);
      ctx.rotate(rotateRads);
      ctx.scale(1, 1);
      ctx.translate(-centerX, -centerY);
      ctx.drawImage(
        image,
        0,
        0,
        image.naturalWidth,
        image.naturalHeight,
        0,
        0,
        image.naturalWidth,
        image.naturalHeight
      );

      ctx.restore();

      canvas.toBlob(blobFile => {
        const croppedImageUrl = URL.createObjectURL(blobFile);
        resolve({ file: blobFile, src: croppedImageUrl });
        URL.revokeObjectURL(croppedImageUrl);
      }, 'image/jpeg');
    });
  };
massimopibiri commented 7 months ago

I struggled all day with this problem and I found a solution. Basically, IOS limit the memory for a canvas, and pictures of the iphones always pass that limit. So, you have to compress the image that you are loading to the canvas. My function to select the image looks like this:

  import imageCompression from 'browser-image-compression'

  ...

  const onSelectFile = async (e1: React.ChangeEvent<HTMLInputElement>) => {
    setFieldTouched(name, true)
    const file = e1?.target?.files?.[0]

    if (!file) return

    const reader = new FileReader()

    reader.addEventListener('load', () => {
      const imgElement = new Image()
      const imgUrl = reader.result?.toString() || ''
      imgElement.src = imgUrl

      setShowCrop(true)
      setImgSrc(imgUrl)
    })

    // must compress to avoid iphone failing for canvas over sized
    const compressedBlob = await imageCompression(file as File, {
      maxSizeMB: 1,
      maxWidthOrHeight: 1400,
      useWebWorker: true,
    })

    const compressedFile = new File([compressedBlob], file.name, {
      type: file.type,
      lastModified: file.lastModified,
    })

    reader.readAsDataURL(compressedFile)
  }

imgUrl is the value of the img tag. It will be then enought compressed to be cropped by the canvas. By the way, since I did it, the crop is a lot faster for Android too. Canvas cropping is quite heavy.

PS. Remember that file compression degeneration is exponential. If you compress in backend too, the image can get a lot wrost.

MohammadrasiM commented 3 months ago

massimopibiri

Hi , thx it truly worked .