How to create a placehold.it image generator clone with Node.js?

What if I tell you generating images is actually an easy task, and you can create a simple placehold.it clone with only a bunch of lines.

Installing dependencies

Let’s start with the basics. We will start with express and sharp and create a very very simple setup:

import express from 'express';
import sharp from 'sharp';

const port = 8000;
const app = express();

Creating a catch-all route and running the server

app.get('/*', async function(req, res) {
  res.end();
});

app.listen(port, () => {
  console.log(`Server started at port ${port}`)
});

As you can see, we are catching all URLs with /* match and just running the server. We will get to the image generation process later.

Now, let’s expand our catch-all route and add parseable parameters. How about we assume that first 2 parameters are always the image dimensions, and then we will have key-value pairs, just like this: /200/100/bg/ff0000. Splitting should be as simple as:

const params = req.params[0].split('/');
if (params.length < 2) {
  return res.status(400).end('At least 2 parameters are required for the image dimensions');
}

Next, let’s create an sharp object, which will be our canvas.

  // this is async, we don't wait for the result on purpose
  // because we are going to chain all the parameters
  const width = parseInt(params[0]);
  const height = parseInt(params[1]);
  try {
    var image = sharp({
      create: {
        width: width,
        height: height,
        channels: 4,
        background: {
          r: 0, g: 0, b: 0, alpha: 0
        }
      }
    });
  } catch(e) {
    return res.end(e.message);
  }

This piece of code is already enough to create an image with the width and height that we passed (we can call it an MVP of our product :)).

Now, let’s start making our loop over the params:

  for (var a = 2; a < params.length; a += 2) {
    const key = params[a];
    const value = params[a+1];

Notice that we start from 2 as 0 and 1 is already taken for width and height. Let’s start by making a bg parameter. We can do that easily be creating a simple svg document with a filled rectangle. We will also composite it with our main fully transparent image, effectively merging them together:

    switch(key) {
      case 'bg':
        const rect = Buffer.from(`<svg>
          <rect x="0" y="0" width="${width}" height="${height}" fill="#${value}" />
        </svg>`);
        image.composite([{
          input: rect
        }]);
        break;
      default: break;
    }

Now we just need to write the buffer to our response. Add some lazy error catching just to make sure our route doesn’t hang:


  try {
    const buf = await image.png().toBuffer();
    res.type('png');
    res.write(buf);
  } catch(e) {
    res.type('html');
    res.write(e.message);
  }

  res.end();

And that’s it. Loading it with http://localhost:8000/400/200/bg/ff9900 should give you an orange rectangle 400 by 200 pixels.

What’s next?

Keep adding parameters and expanding the options. For example, adding /text/Hello%20world/ could add a text overlay in the middle of the image. Note: multiple composite() calls don’t work in a single sharp pipeline. To add another operation, you will need to stack the composite array first, and then call one composite. Here’s an example of how it can look like when you add a /text/ parameter:

  const composite = [];
  for (var a = 2; a < params.length; a += 2) {
    const key = params[a];
    const value = params[a+1];
    switch(key) {
      case 'bg':
        const bg = Buffer.from(`<svg>
          <rect x="0" y="0" width="${width}" height="${height}" fill="#${value}" />
        </svg>`);
        composite.push({
          input: bg
        });
        break;
      case 'text':
        const text = Buffer.from(`<svg width="${width}" height="${height}">
          <text text-anchor="middle" alignment-baseline="middle" font-family="Arial, Helvetica, sans-serif" font-size="28" x="${width/2}" y="${height/2}">${value}</text>
        </svg>`);
        composite.push({
          input: text
        });
      default: break;
    }
  }

  if (composite.length) {
    image.composite(composite);
  }

Opening http://localhost:8000/400/200/bg/0891b2/text/Code%20with%20Node.js will then produce the next image:

Leave a Comment

Your email address will not be published.