Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Canvas returns null after cropping image iOS #578

Open
YuriDavello opened this issue Jan 31, 2024 · 1 comment
Open

Canvas returns null after cropping image iOS #578

YuriDavello opened this issue Jan 31, 2024 · 1 comment

Comments

@YuriDavello
Copy link

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
Copy link

massimopibiri commented Apr 2, 2024

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants