sekoyo / react-image-crop

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

Please update the docs to include an explanation of how to access and use the cropped image #529

Open andersr opened 1 year ago

andersr commented 1 year ago

I am not able to locate any information in the docs on how to actually access the cropped version of the image, so that it can be used. It appears I am not alone. These all seems to be variations on the same question: #525, #523, #522, #501, #457, and #501.

If you could please update the docs (or point it out in case I missed it) for how to actually access the cropped version of the image, that would great. Thank you.

Joshuajrodrigues commented 1 year ago

Hey did you figure thus out by any chance ?

sekoyo commented 1 year ago

Hey there’s an older version of the codesandbox example which has it, but currently travelling with just a phone until Feb. I’ll update the example with a downloadable version again but you should just be able to check the revision history there.

It’s not specific to this library - you need to convert the canvas to a blob and then create a URL ( https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL ) and a button which calls that URL.

Joshuajrodrigues commented 1 year ago

Is there a possibility to do this without canvas ? Like what if the user doesnt want a preview. Just drop, crop and submit ?

sekoyo commented 1 year ago

I think you would still need to make an offscreen canvas with the crop copied to it, even if the user never sees it.

On Tue, 17 Jan 2023 at 18:37, Joshua @.***> wrote:

Is there a possibility to do this without canvas ? Like what if the user doesnt want a preview. Just drop, crop and submit ?

— Reply to this email directly, view it on GitHub https://github.com/DominicTobias/react-image-crop/issues/529#issuecomment-1385293642, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAFZT6RHCXWLFY6XG47C7XLWSZ755ANCNFSM6AAAAAATO5VBIQ . You are receiving this because you commented.Message ID: @.***>

andersr commented 1 year ago

@DominicTobias thanks for this tip. By offscreen, is this simply a css-based off screen, or are you referring to this: https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas

sekoyo commented 1 year ago

I was thinking with CSS but this sounds like a better solution 👍

On Tue, 17 Jan 2023 at 20:41, Anders Ramsay @.***> wrote:

@DominicTobias https://github.com/DominicTobias thanks for this tip. By offscreen, is this simply a css-based off screen, or are you referring to this: https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas

— Reply to this email directly, view it on GitHub https://github.com/DominicTobias/react-image-crop/issues/529#issuecomment-1385440913, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAFZT6RUA5Z7HPVXDW7IXA3WS2OQ5ANCNFSM6AAAAAATO5VBIQ . You are receiving this because you were mentioned.Message ID: @.***>

Joshuajrodrigues commented 1 year ago

Hey sorry to disturb here again but how would you handle this for a circular crop ?

sekoyo commented 1 year ago

Hey sorry to disturb here again but how would you handle this for a circular crop ?

I’ve not personally tried to do a circle crop to canvas. Normally I’d say just do it with CSS for reasons explained here - https://github.com/DominicTobias/react-image-crop/issues/409#issuecomment-797857537

but if you want to create a downloadable version with a circle you’d have to do it on the canvas as a black circular border or something ⚫️ which I haven’t tried to do before but if you can find some code to do it, then you can just paste it after the other canvas code to do it over the top

sekoyo commented 1 year ago

I was thinking with CSS but this sounds like a better solution 👍

for example:

https://pqina.nl/blog/applying-a-circular-crop-mask-to-an-image/#applying-a-circular-mask-using-canvas

lucasprins commented 1 year ago
function onSubmitCrop() {
    if (completedCrop) {
      // create a canvas element to draw the cropped image
      const canvas = document.createElement("canvas");

      // get the image element
      const image = imgRef.current;

      // draw the image on the canvas
      if (image) {
        const crop = completedCrop;
        const scaleX = image.naturalWidth / image.width;
        const scaleY = image.naturalHeight / image.height;
        const ctx = canvas.getContext("2d");
        const pixelRatio = window.devicePixelRatio;
        canvas.width = crop.width * pixelRatio * scaleX;
        canvas.height = crop.height * pixelRatio * scaleY;

        if (ctx) {
          ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
          ctx.imageSmoothingQuality = "high";

          ctx.drawImage(
            image,
            crop.x * scaleX,
            crop.y * scaleY,
            crop.width * scaleX,
            crop.height * scaleY,
            0,
            0,
            crop.width * scaleX,
            crop.height * scaleY
          );
        }

        const base64Image = canvas.toDataURL("image/png"); // can be changed to jpeg/jpg etc

        if (base64Image) {
          // @ts-ignore
          const fileType = base64Image.split(";")[0].split(":")[1];

          const buffer = Buffer.from(
            base64Image.replace(/^data:image\/\w+;base64,/, ""),
            "base64"
          );
          const file = new File([buffer], fileName, { type: fileType });
          onSubmit(file); // function passed as a prop
        }
      }
    }
  }

If anyone is still having trouble, this is how I did it.

sekoyo commented 1 year ago

You can create a hidden link:

const hiddenAnchorRef = useRef<HTMLAnchorElement>(null)
const blobUrlRef = useRef('')

<a
  ref={hiddenAnchorRef}
  download
  style={{
    position: 'absolute',
    top: '-200vh',
    visibility: 'hidden',
  }}
>
  Hidden download
</a>

And you can create a blob, create a URL with the blob, assign it to the anchor, trigger a click:

function onDownloadCropClick() {
    if (!previewCanvasRef.current) {
      throw new Error('Crop canvas does not exist')
    }

    previewCanvasRef.current.toBlob((blob) => {
      if (!blob) {
        throw new Error('Failed to create blob')
      }
      if (blobUrlRef.current) {
        URL.revokeObjectURL(blobUrlRef.current)
      }
      blobUrlRef.current = URL.createObjectURL(blob)
      hiddenAnchorRef.current!.href = blobUrlRef.current
      hiddenAnchorRef.current!.click()
    })
  }

Note that if you doubled the size of the canvas on e.g. retina screens to get extra sharpness (I do this in canvasPreview.ts based on window.devicePixelRatio), then you also need to size the canvas down by a factor of window.devicePixelRatio which makes things more complicated. You would have to copy to an offscreen canvas that is a normal size first.

AnnieTaylorCHEN commented 1 year ago
function onSubmitCrop() {
    if (completedCrop) {
      // create a canvas element to draw the cropped image
      const canvas = document.createElement("canvas");

      // get the image element
      const image = imgRef.current;

      // draw the image on the canvas
      if (image) {
        const crop = completedCrop;
        const scaleX = image.naturalWidth / image.width;
        const scaleY = image.naturalHeight / image.height;
        const ctx = canvas.getContext("2d");
        const pixelRatio = window.devicePixelRatio;
        canvas.width = crop.width * pixelRatio * scaleX;
        canvas.height = crop.height * pixelRatio * scaleY;

        if (ctx) {
          ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
          ctx.imageSmoothingQuality = "high";

          ctx.drawImage(
            image,
            crop.x * scaleX,
            crop.y * scaleY,
            crop.width * scaleX,
            crop.height * scaleY,
            0,
            0,
            crop.width * scaleX,
            crop.height * scaleY
          );
        }

        const base64Image = canvas.toDataURL("image/png"); // can be changed to jpeg/jpg etc

        if (base64Image) {
          // @ts-ignore
          const fileType = base64Image.split(";")[0].split(":")[1];

          const buffer = Buffer.from(
            base64Image.replace(/^data:image\/\w+;base64,/, ""),
            "base64"
          );
          const file = new File([buffer], fileName, { type: fileType });
          onSubmit(file); // function passed as a prop
        }
      }
    }
  }

This works magic! For anyone with buffer error, just npm i buffer and import {Buffer} from 'buffer'; it should solve the issue.

If your backend is using multer package to parce, you can do

const formDataForImage = new FormData();
 formDataForImage.append('file', fileCropped);

then attach that formDataForImage as obj in the body in the post request, it should work.

AnnieTaylorCHEN commented 1 year ago

@DominicTobias Can you also add the above solution in the documentation as reference? Thanks!

AnnieTaylorCHEN commented 1 year ago

Hi, it worked locally in dev but I ran into a lot of issues with the build because of the Buffer. 😅 Currently using Vite/React and I have referred to this solution: https://stackoverflow.com/questions/72773373/buffer-is-not-exported-by-vite-browser-externalbuffer

I used the OP's inject method:

rollupOptions: {
        plugins: [inject({Buffer: ['Buffer', 'Buffer']})],
        external: ['Buffer'],
      },

but I am getting errors Uncaught TypeError: Failed to resolve module specifier "Buffer". Relative references must start with either "/", "./", or "../". in deployment ( it pass the build in pipeline). So I don't think I write it correctly.... I know this is probably a Vite related issue, but in case anyone knows, please give me some tip on how to fix this? Thanks! 🙏

AnnieTaylorCHEN commented 1 year ago

Hi, I solved my issues now:

By now I completely ditched the previous solution.

  1. npm i process buffer.
  2. add those to the index.html, just under
    <script>
      window.global = window;
    </script>
    <script type="module">
      import {Buffer} from 'buffer/'; // <-- no typo here ("/")
      import process from 'process';
      window.Buffer = Buffer;
      window.process = process;
    </script>
  3. in vite.config.ts
    resolve: {
      alias: {
        process: 'process/browser',
      },
    },
  4. in the component where you need buffer, import {Buffer} from 'buffer/index.js';