Building an online image compressor
Tags: opensource, webdev, development, frontend
Published: 2024-01-09 08:57:46

NOTE: You can find original post on my personal blog https://www.negrel.dev/blog/building-an-online-image-compressor/index.html

Recently, I stumbled upon [this Reddit post](https://www.reddit.com/r/SideProject/comments/18u925v/i_made_a_tool_in_2_hours_it_made_me_1000_so_far) where OP says he made $1000 from his image compressor. He built it in 2 hours. After quickly analyzing the website, I figured it couldn't be too hard to make my own.

Objectives were to build a **fast** image compressor with:

- no upload nor sign up
- no images limit
- no file size limit
- privacy-friendly in mind

After one week of work, [www.bulkcompress.photos](https://www.bulkcompress.photos/) was born.

This post summarizes how I built it.

## JPEG encoding

One of the first thing I did is research about [JPEG](https://en.wikipedia.org/wiki/JPEG), and it’s efficient lossy compression algorithm. JPEG is the most popular image format or photography, it is widely known and supported so there is a tons of resources about it.

My original plan was to apply JPEG compression to other image format as follow:

![decode image -> apply jpeg compression in memory -> encode image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gh90szs416t55xhh8f7l.png)

I’ve already had some knowledge about since I’ve studied it a few years back. It was time for a refresh. This [article](https://www.freecodecamp.org/news/how-jpg-works-a4dbd2316f35/) and the [JPEG Wikipedia page](https://www.freecodecamp.org/news/how-jpg-works-a4dbd2316f35/) are great to understand the overall functioning of the algorithm.

![JPEG encoding algorithm steps](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8xxstm4kqzzv6vkbph1.png)

Interesting part of JPEG encoding is the **[quantization](https://en.wikipedia.org /wiki/Quantization_(image_processing))** step. This is where data loss occurs. Many videos and images encoding systems favors quantization of [chrominance](https://en.wikipedia.org/wiki/Chrominance) (middle image) over luminance (left image) as human eye is more sensible to the latter:

![chroma luma quantization example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2akb3tmj0ekjh6jv3p7w.png)

You can play with chrominance and luminance on
[this playground](https://jakearchibald.github.io/image-experiments/channels/)
to get better insight.

## Implementation

Images processing require manipulating bytes and performing some complex
computation. JavaScript isn’t a good fit for this purpose but it’s 2024 and web
assembly is a thing.


![Web assembly logo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ck59r0uzrm71zmqqukw1.png)

One of the most complete image compressor out there,
[squoosh.app](https://squoosh.app/) by Google, uses web assembly for decoding/encoding images and it works pretty well.

My first implementation was based on
[photon_rs](https://github.com/silvia-odwyer/photon), a Rust based web assembly
library for image processing. It worked pretty well, but it was slower than OP
website. Without digging much it seems logical since photon isn’t optimized for performance.

After some research, I found [libvips](https://www.libvips.org/), a
[demand-driven, horizontally threaded](https://github.com/libvips/libvips/wiki/Why-is-libvips-quick)
image processing library. It is designed to run quickly while using as little as memory as possible.

After some _manual_ tests, to my surprise, it was **still slower** than OP website. Digging into their source code shows out that their aren’t even using web assembly.

## JavaScript is faster than web assembly?

Nope, but JavaScript call to standard web APIs implemented in C++ by web browsers and improved over decades of existence is.

It turns out that to compress an image, the following code is all you need:

```jsx
async compress(imageFile, { quality }) {
    let bitmap = await createImageBitmap(imageFile);

    const offscreenCanvas = new OffscreenCanvas(bitmap.width, bitmap.height);
    const ctx = offscreenCanvas.getContext("2d");
    ctx.drawImage(bitmap, 0, 0, drawWidth, drawHeight);

    // Encode canvas bitmap as an image of type imageFile.type.
    // https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas/convertToBlob
    const blob = await offscreenCanvas.convertToBlob({
      type: imageFile.type,
      quality, // only used if imageFile.type is "image/jpeg" or "image/webp"
    });

    return blob.arrayBuffer();
}
```

[www.bulkcompress.photos](http://bulkcompress.photos) is just an optimized GUI of the above function. I’ve also added support for resizing and converting images into another format. Finally, to improve performance, I used the worker
pool I’ve developed for my load testing tool: https://github.com/denoload/denoload.

If you’re interested in Chromium’s implementation of [`OffscreenCanvas.convertToBlob`](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/offscreencanvas/offscreen_canvas.cc;l=193;drc=e5f8ff312fe542f39c7d34b4440f18d1500b15f9) here is a link to the code that perform the actual encoding: [`ImageDataBuffer::EncodeImageInternal`](https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/graphics/image_data_buffer.cc;drc=e5f8ff312fe542f39c7d34b4440f18d1500b15f9;bpv=1;bpt=1;l=143?gsn=EncodeImageInternal&gs=KYTHE%3A%2F%2FKoQDCr8Ba3l0aGU6Ly9jaHJvbWl1bS5nb29nbGVzb3VyY2UuY29tL2NvZGVzZWFyY2gvY2hyb21pdW0vc3JjLy9tYWluP2xhbmc9YyUyQiUyQj9wYXRoPXRoaXJkX3BhcnR5L2JsaW5rL3JlbmRlcmVyL3BsYXRmb3JtL2dyYXBoaWNzL2ltYWdlX2RhdGFfYnVmZmVyLmNjIzZCU3paMGhFTHExSzBGU1BpNWVsLThRbThfM2N5TW40aEI4UElCeGswdVEKvwFreXRoZTovL2Nocm9taXVtLmdvb2dsZXNvdXJjZS5jb20vY29kZXNlYXJjaC9jaHJvbWl1bS9zcmMvL21haW4_bGFuZz1jJTJCJTJCP3BhdGg9dGhpcmRfcGFydHkvYmxpbmsvcmVuZGVyZXIvcGxhdGZvcm0vZ3JhcGhpY3MvaW1hZ2VfZGF0YV9idWZmZXIuY2MjaVp3MmxuTW5vSDhjaDJPQmk5QmpMU0VpanN5OWk5XzRMcncwd0pJSjlHOA%3D%3D).

## Conclusion

Originally, I wanted to create a paid version of my image compressor, but it didn't feel right. Despite spending a significant amount of time on it, charging users to use their own computing power and an algorithm implemented by someone
else seemed wrong. In the end, I decided that the service is and will remain free and ad-free.

Also, the source code is available [here](https://github.com/negrel/bulkcompress.photos).