Resizing images client-side with vanilla JS
Tags: javascript, tutorial, webdev
Published: 2020-06-13 02:26:15

# Table of Contents
- [Backstory](#backstory)
- [Code](#code)
- [Getting to work](#getting-to-work)
   - [The canvas element](#canvas)
      - [Overview](#canvas-overview)
      - [Code](#canvas-code)
   - [Overall Structure](#overall-structure)
      - [Overview](#structure-overview)
      - [Code](#structure-code)
   - [Scaling a canvas](#half-scale)
      - [Overview](#scale-overview)
      - [Code](#scale-code)
- [Conclusion](#conclusion)
- [Please Note](#please-note)
- [Promisify?](#promisify)

# Backstory <a name="backstory" />
I recently came upon a need to optimize images client-side prior to uploading them to a back end (in this case AWS S3). Ordinarily, this would be done on the back end (i.e. your front end sends a request containing an unoptimized image to the back end, which then optimizes that image before saving it), but for this project I really wanted to do this on the client.

# Code <a name="code" />
All the code for this can be found [here](https://gist.github.com/TaylorBeeston/48148e61eb4a9db89d7482081e8714ca).

# Getting to work <a name="getting-to-work" />
## The canvas element <a name="canvas" />
### Overview <a name="canvas-overview" />
It turns out that the best way (in this case) to create an image with javascript is by using a [canvas element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas)! How do we do that? By creating a 2d context, drawing our image in it, then calling the `toBlob` method.

### Code <a name="canvas-code" />
For this particular project, I am working with images as [File Objects](https://developer.mozilla.org/en-US/docs/Web/API/File), obtained, for example, by using a function such as
```js
(e) => e.target.files[0];
```
on an HTML file `input` element's `onchange` event. 

Because of this, let's write the helper function `readPhoto`, which creates and returns a canvas element containing the image given to it. The code for this function is as follows:
```js
const readPhoto = async (photo) => {
  const canvas = document.createElement('canvas');
  const img = document.createElement('img');

  // create img element from File object
  img.src = await new Promise((resolve) => {
    const reader = new FileReader();
    reader.onload = (e) => resolve(e.target.result);
    reader.readAsDataURL(photo);
  });
  await new Promise((resolve) => {
    img.onload = resolve;
  });

  // draw image in canvas element
  canvas.width = img.width;
  canvas.height = img.height;
  canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);

  return canvas;
};
```

Let's break down what this code is doing.

First we create two HTML elements, an `img` and a `canvas`. 

Why do we need the `img`? Because the [drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage) method we will be using expects a [CanvasImageSource](https://developer.mozilla.org/en-US/docs/Web/API/CanvasImageSource) as one of its parameters, and an [HTMLImageElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement) is going to be the most convenient for us to create.

Next we read the photo into the `img` element using the [readAsDataURL](https://developer.mozilla.org/en-US/docs/Web/API/FileReader/readAsDataURL) method and a cute little [promisify](#promisify) trick.

After that, we make sure we wait for the `img` to load using the [promisify](#promisify) trick again with the following:
```js
await new Promise((resolve) => {
  img.onload = resolve;
});
```

Once we have our photo into `img`, and `img` has loaded, we draw it onto our canvas and return.
```js
// draw image in canvas element
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);

return canvas;
```

## Overall Structure <a name="overall-structure" />
### Overview <a name="structure-overview" />
Okay, so we now know how to get a [File](https://developer.mozilla.org/en-US/docs/Web/API/File) into a [canvas](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement). Great! Now, let's talk about what we're going to do with it by looking at the function `optimizePhoto`, the canonical `main` of our little helper file.

Basically, what we're doing is taking our image, shrinking it to a maximum width that is set via an environment variable (or really any way you'd like to set this!), and then returning it as a [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob).

To add a little bit of complexity, I have found that it's best to first keep shrinking our image in half until we *need* to use bilinear interpolation (aka scaling by a factor not divisible by 2) to finish the job. This is a very quick and easy thing to do, so we'll go ahead and add it to this function.

### Code <a name ="structure-code" />
The function looks like this:
```js
export default async (photo) => {
  let canvas = await readPhoto(photo);

  while (canvas.width >= 2 * MAX_WIDTH) {
    canvas = scaleCanvas(canvas, .5);
  }

  if (canvas.width > MAX_WIDTH) {
    canvas = scaleCanvas(canvas, MAX_WIDTH / canvas.width);
  }

  return new Promise((resolve) => {
    canvas.toBlob(resolve, 'image/jpeg', QUALITY);
  });
};
```

Nothing too crazy (besides maybe the use of our little [promisify](#promisify) trick), but we're going to need to talk about one new function that this function depends on: `scaleCanvas`.

## Scaling a canvas <a name="scale" />
### Overview <a name="scale-overview" />
Scaling a canvas actually turns out to be pretty simple, as we can reuse that [drawImage](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/drawImage) method, just using a `canvas` as input instead of an `img` as input.

To do this, we simply make a new `canvas`, set its width and height to our desired dimensions, then call `drawImage` with the new width/height.

### Code <a name="scale-code" />
The code for this is as follows:
```js
const scaleCanvas = (canvas, scale) => {
  const scaledCanvas = document.createElement('canvas');
  scaledCanvas.width = canvas.width * scale;
  scaledCanvas.height = canvas.height * scale;

  scaledCanvas
    .getContext('2d')
    .drawImage(canvas, 0, 0, scaledCanvas.width, scaledCanvas.height);

  return scaledCanvas;
};
```

# Conclusion <a name="conclusion" />
And that is it! Now we can simply pass an image to `optimizePhoto` and get a resized photo. 

For example, assuming the following HTML
```html
<input id="file-input" type="file" multiple />
```

We can generate upload resized photos with the following javascript:
```js
const input = document.getElementById('file-input');

const input.onChange = (e) => {
  e.target.files.forEach(async (photo) => {
    const resizedPhoto = await optimizePhoto(photo);
    await uploadPhoto(resizedPhoto); // or do whatever
  });
}
```

# Please Note <a name="please-note" />
The algorithm used to resize photos by a factor other than 2 is not necessarily bilinear interpolation. At least as far as I've been able to find. From my own personal testing, it seems as though Firefox and Chrome will both user bilinear interpolation, which looks just fine in most cases. However, it is possible to manually bilinearly interpolate an image, which I may make another post about. If you happen to have a need for it, this also applies to using another scaling algorithm such as nearest neighbor or bicubic interpolation.

# Promisify? <a name="promisify" />
I wrote about this cute little trick right [here](https://dev.to/taylorbeeston/updating-callback-style-code-to-use-async-await-1jl9).

{% link https://dev.to/taylorbeeston/updating-callback-style-code-to-use-async-await-1jl9 %}

Basically, you create a new Promise that wraps around a function that relies on callbacks, then simply use resolve in the callback to magically 'promisify' that function!