DEV Community

MartinJ
MartinJ

Posted on

Building a React Image Cropper - a whole world of unexpected problems

Last reviewed: February 2024

Piet Mondrian: Composition No. 11. 1913
Piet Mondrian: Composition No. 11. 1913

Background

I needed a utility to prepare strictly-formatted "thumbnail" graphics for my webapp. I decided that some sort of custom-built file cropping tool would make life easier.

Six weeks later I wasn't so sure - but I'd certainly learned a ton of new Javascript!

The basic idea was to persuade my webapp to read a local file, display it in an <img> under some sort of draggable and resizable "frame"" and then provide a button to "crop" the area under this and upload it. The "frame" would be configured to maintain a fixed aspect ration and the button click would scale its content to a fixed width. Seemed a simple idea. Here's a picture of the finished arrangement. The "frame", or "cropping window" as I now term it as positioned at bottom right (with its bottom/right margins lying just outside the base image) and the little "aqua" box at top left provides a resizing handle.

React Image Cropper clip

Problem 1. I need to use <canvas> rather than <img>

I found there was no way to capture a subset of an <img>. I realised I needed to learn about <canvas>

Problem 2. I still need an <img> source though

I couldn't populate the <canvas> from a local file directly. I found I needed to do this via an "upstream" <img>. No problem. I'd just load this from the local file as previously planned and hide it with a "display: none".

Problem 3. Shriek - my <canvas> is tainted! Need to source it from an <img> that has come from a remote url

The upstream <img> is fine as a data source for your <canvas> until you try to pull data out of it with a "canvas.toDataUrl". At this point I received the sweetest error message ever. "Tainted canvases may not be exported.". Apparently, to ensure security, the only acceptable source for me would be a url in my Firebase Cloud Storage. No problem - I'd just upload the local file into a temporary location in the Cloud.

Problem 4. Groan, the remote url needs an "Access-Control-Allow-Origin" header

"Corrs" error. Seems that the Cloud Source needs to be a public file delivered with a Corrs "Access-Control-Allow-Origin" header. No problem, I know how to use gsutil to do this (see Set the CORS configuration on a bucket)

Problem 5. Will this ever end? The <img> needs a 'crossorigin="anonymous"' property

But the "tainted canvas" error persisted. Seemed that I needed to add a 'crossorigin="anonymous"' property to my upstream <img>. Allowing cross-origin use of images and canvas refers. Now what?

Problem 6. toDataUrl won't let me create a .pngfile

While negotiating all the above nonsense, I'd been trying various tricks for managing the cropping window movement. Basically these just consisted of manipulation of the top/left/width/height properties of a rectangle styled as "position absolute".

This seemed a simple challenge but, with all the overarching junk involved in preventing the cropping window being dragged outside the <canvas> boundary, maintaining its aspect ratio and so on, I found it necessary to keep things simple. I'd originally intended to provide a "sizing handle" at each rectangle corner, but it became clear that this would more than double the code's complexity. Eventually I hit on the idea that a single handle positioned at top left of the rectangle could be positioned so that it "rode" automatically on its parent. That's why my cropping window is initialised at bottom right!

So I'd now got to a point where I could actually crop a section and upload it as a file. A ".png" would be nice? At this point I realised that no such ability was available for a canvas!

As mentioned earlier, I'd already had a run-in with the canvas.toDataUrl tool, in connection with the "tainted canvas" issue. Now I found that I needed to look a bit deeper. For once, things here actually turned out to my advantage. The canvas.dataToUrl method creates a character string version of your image source. Of course, there's nothing to stop you saving this as a file but I realised this arrangement was actually encouraging me to save the image as a field in a database document. Things now fell together together rather neatly because the files I was creating already had parent firestore documents. Merging the two concepts simplified things considerably. Onward and upward!

Problem 7. A heart-stopping moment - how do I get this all to work in a React webapp?

Things were getting a bit ragged by now and here's where I hit the real nightmare. Everything I'd done in testing had been done through an html testbed. But the system into which I was now proposing to embed the tool actually used React. How would this pan out. Should I be using React "state" to manage my cropping window positioning?

Result! Happy days.

As things turned out, I was right to be concerned. An attempt to use React "state" failed miserably due to poor performance and it it didn't looked like I'd fix this without a deep dive into React internals. So, in desperation I reverted to the old arrangement whereby I updated styles directly into the DOM. The only concession to React now was the use of React useRefs where I'd previously had tag "id"s. Performance improved remarkably.

Where I now struggled was with the stable initialisation of the completed "CroppingTool" component. Here, the comonent was rendered via a classic state change in the component's parent triggered by successful Cloud upload of the user-supplied graphic. At this point the "CroppingTool" has to "wait" again while it downloads the temporary Cloud file and then initialise the base-image display. At first I thought this might be best achieved through a useEffect defining an onLoad function on the base-image. But while this "sort of" worked, it only did so intermittently.

Finally, I hit on the idea of simply coding the onLoad function directly in the <canvas> html itself. Nobody was more surprised than me when this performed flawlessly.

So I now have an operational React image cropper. Not many people can say that!

If anyone would like to have a look at the code, there's a copy here

Thanks for reading this. Hope you enjoyed it.

Top comments (0)