
Client Side Image Upload Preprocessing
One of the things I'm trying to do with the CMS I'm writing is to make it use a few server resources as possible. This is important because I want to make things low cost and easy to use.
The average phone, and certainly the average computer, is faster than the servers that host the software. They have more memory, a faster CPU, and more disk space! Where I can, I plan to take advantage of that. One particular aspect of that is uploading images.
On the sites there's no point in having super-large images. You can't see all the pixels in very big images, so it's just wasted data that is stored and downloaded. To make the downloads smaller, we can also compress the images a bit. Not too much or they'll start to look bad, but just enough that they're small enough without taking away from the visuals when displayed on a website. The last thing is that I chose the file format .webp for all images. That's because it's a modern format with reasonably small file sizes that does okay for everything.
I could have somebody upload a file as a png, jpeg, whatever and have the server receive it. Then on the server I can convert that to a webp, resize it if necessary, and compress it. That's certainly the traditional way of doing it. But because I want the servers to not be very powerful, I decided to try to do this on the client side.
The principle of doing it on the client side is mostly the same. I want to have the user choose the file to upload. They normally do so in an HTML file statement like this:
<input type="file" id="inp">
When the user chooses a file, I don't want to actually submit it. But instead create an image element in the user's browser to hold it. So I do something like:
window.onload = function () {
document.getElementById('inp').onchange = function(e) {
var img = new Image();
img.onload = draw;
img.src = URL.createObjectURL(this.files[0]);
};
}
Which is great. Now when the user chooses a file, it gets loaded into the image variable and it hasn't touched the server yet. But next I want to do some work on it - I want to resize it if it's too big. Handy that we put that draw function in for when the image is loaded. First I add a hidden canvas element to the page:
<canvas id="canvas" style="display:none;"></canvas>
The canvas element is going to do some basic image processing for us. Here's the draw function:
function draw(img) {
var ratio = this.width / this.height;
var canvas = document.getElementById('canvas');
if (this.width > 1200) {
canvas.width = 1200;
canvas.height = canvas.width / ratio;
} else {
canvas.width = this.width;
canvas.height = this.height;
}
var ctx = canvas.getContext('2d');
ctx.drawImage(this, 0,0, this.width, this.height, 0, 0, canvas.width, canvas.height);
uploadFile();
}
First of all it calculates the width to height ratio. It uses this if the image is wider than 1200 pixels, to scale it down. Then it draws it into the canvas scaled to the correct size. There's nothing really special here. I now have an image in the browser that's the right size, but it's in a canvas element. I need to get this uploaded to the server. See at the end, I'm calling a function uploadFile(). That's its job!
Let's have a look at that:
async function uploadFile() {
var filen = document.getElementById('inp').files[0].name;
console.log(filen);
var byteString = atob(document.getElementById('canvas').toDataURL("image/webp", 0.75).split(",")[1]);
const mimeString = "image/webp";
const ab = new ArrayBuffer(byteString.length)
const ia = new Uint8Array(ab)
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i)
}
const bb = new Blob([ia], { type: mimeString })
let formData = new FormData();
formData.append("file", bb, filen);
l = await fetch('/upload_url', {
method: "POST",
body: formData
})
.then((response) => response.text())
.then((text) => {
document.getElementById('inp').value = '';
});
}
So here's the heavy lifting! First it does the simple job of getting the filename for the file the user chose. I'll need this to tell the server. Then I use the toDataURL method of the canvas to return the binary data as a compressed webp file. The browser is doing the smart stuff there, I'm just piggybacking off it. The problem is it returns it base64 encoded, so I decode that. I create a blob from it, with code I just borrowed off the internet! I need to do that to create the FormData object with all these file details. Finally I upload it with fetch and the magic is complete.
The server is happy to accept the file upload, but I've tricked the browser into doing all the server's work for it. The server just needs to verify it and save it.