Search code examples
node.jsimagebase64image-compressionsharp

How can I generate the most compact plain color image in base64 using node.js (server-side)?


In a project of mine, I need to generate some placeholders for images on server, to be served to the client embedded in a json file. These placeholders will later be replaced by their actual counterparts downloaded separately as regular image files.

The setup constrains me to base64 encoding embedded in the json for the placeholders. The placeholders must be RGB and have the same dimensions as the actual images they're meant to be replaced with. These dimensions range up to about 8192x8192 pixels. The placeholders for two different pictures of the same size have to be different (at least the base64 encoded string must be). They also have to be readable by any browser.

I suspect there must exist a form of image encoding that will pretty much be down to:

  • A few header bytes
  • The image size
  • Some instruction repeating the same color on the entire bitmap

Which should result in less than a 100 base64 characters.

But I just can't find a way to achieve such compression, or at least improve on what I've got.

For now, I'm using sharp to generate these placeholders with the following code :

async function placeHolderFor(width: number, height: number, index: number) : Promise<Buffer> {
  const v = 255 - index;
  const buf = await sharp({
        create: {
        width: width,
        height: height,
        channels: 4,
        background: { r: v, g: v, b: v, alpha: 1 }
      }
    })
    .avif({ quality: 1 })
    .toBuffer();
  return buf;
}

And then converting the buffer to base64 :

const buf = placeHolderFor(2048, 2048, 1);
const base64 = 'data:image/avif;base64,' + buf.toString('base64');

I've fiddled quite a lot with formats and options for the encoding, testing mostly with 2048x2048 as a size.

The best I've come up with so far is avif encoding with worst quality, which gives me a 528 octet buffer translating to a base64 string of 727 characters.


Solution

  • Turns out I was very bad at googling that day.

    Answer found here : https://cloudinary.com/blog/a_one_color_image_is_worth_two_thousand_words

    To this day, the best widely supported format for plain-color images is lossless webp, which turns out to give me a 340 characters base64 string after generating an compressing the 2048x2048 image using sharp:

    const buf = await sharp({
        create: {
          width: 2048,
          height: 2048,
          channels: 4,
          background: { r: 255, g: 255, b: 255, alpha: 1 }
        }
      })
      .webp({ lossless: true })
      .toBuffer();
    const base64 = 'data:image/webp;base64,' + buf.toString('base64');
    console.log(base64);
    

    Additionally, it seems like FLIF would be the format of choice with an output of 19 bytes before encoding, regardless of image size. This format is not supported by browsers. According to caniuse.com though:

    [it has] been superseded by JPEG XL which is being implemented in browsers

    Source : https://caniuse.com/?search=flif