Hari Bhandari
Hari Bhandari

Reputation: 15

Need help in image handling in node.js

Is there any way to get resize image when needed for example. want to make is so that /image.jpg give me original image but image_200x200.jpg gives me resized image with 200px width and height. I want to make it so that image gets resized when rendered so i wouldn't have to store resized image. Thanks in advance

Upvotes: 0

Views: 640

Answers (1)

blex
blex

Reputation: 25659

Is there any way?

Yes, there is. But you must be aware that by doing so, the image will be resized every time it is requested. If you expect to have heavy traffic on your site, this is not a sustainable solution unless you actually store these resized images in a cache of some sort.

And even if you do cache them, be aware that without any URL-signing (ensuring that the URL was issued by your server), a malicious user might want to SPAM your server with requests where they use random sizes every time, to force a resize operation on your server and overwhelm it.

Now that this is out of the way, let's try to find a solution.

How can we route the requests in Express?

Express provides a way to serve static files, which we can keep for when we don't need resizing. If a file is not found at this location, the next routes will be checked against. So in the next one, we'll try to check if a resize is wanted, before trying to match other routes. We can target those by using a regex matching a specific pattern.

This example will match URLs ending with _{n}x{n} and a "jpeg", "jpg" or "png" extension:

app.use(express.static(Path.join(__dirname, 'public'))); // If a file is found
app.use('/(*_\\d+x\\d+.(jpe?g|png))', resizingMiddleware); // If resizing is needed
// ... Other routes

The middleware

A middleware is a function called by Express when a route is requested. In this case, we can declare it like this:

function resizingMiddleware(req, res, next)  {
  const data = parseResizingURI(req.baseUrl); // Extract data from the URI

  if (!data) { return next(); } // Could not parse the URI

  // Get full file path in public directory
  const path = Path.join(__dirname, 'public', data.path);

  resizeImage(path, data.width, data.height)
    .then(buffer => {
      // Success. Send the image
      res.set('Content-type', mime.lookup(path)); // using 'mime-types' package
      res.send(buffer);
    })
    .catch(next); // File not found or resizing failed
}

Extracting data from the URI

As you've seen above, I used a parseResizingURI to get the original file name, along with the requested dimensions. Let's write this function:

function limitNumberToRange(num, min, max) {
  return Math.min(Math.max(num, min), max);
}

function parseResizingURI(uri) {
  // Attempt to extract some variables using Regex
  const matches = uri.match(
    /(?<path>.*\/)(?<name>[^\/]+)_(?<width>\d+)x(?<height>\d+)(?<extension>\.[a-z\d]+)$/i
  );

  if (matches) {
    const { path, name, width, height, extension } = matches.groups;
    return {
      path: path + name + extension, // Original file path
      width: limitNumberToRange(+width, 16, 2000),   // Ensure the size is in a range
      height: limitNumberToRange(+height, 16, 2000), // so people don't try 999999999
      extension: extension
    };
  }
  return false;
}

Resizing the image

In the middleware, you may also see a resizeImage function. Let's write it using sharp:

function resizeImage(path, width, height) {
  return sharp(path).resize({
    width,
    height,
    // Preserve aspect ratio, while ensuring dimensions are <= to those specified
    fit: sharp.fit.inside,
  }).toBuffer();
}

Put it all together

In the end, we get the code below:

// Don't forget to install all used packages:
// $ npm install --save mime-types express sharp

const Path = require('path');
const mime = require('mime-types')
const sharp = require('sharp');
const express = require('express');
const app = express();

// Existing files are sent through as-is
app.use(express.static(Path.join(__dirname, 'public')));
// Requests for resizing
app.use('/(*_\\d+x\\d+.(jpe?g|png))', resizingMiddleware);
// Other routes...
app.get('/', (req, res) => { res.send('Hello World!'); });

app.listen(3000);

function resizingMiddleware(req, res, next)  {
  const data = parseResizingURI(req.baseUrl); // Extract data from the URI

  if (!data) { return next(); } // Could not parse the URI

  // Get full file path in public directory
  const path = Path.join(__dirname, 'public', data.path);

  resizeImage(path, data.width, data.height)
    .then(buffer => {
      // Success. Send the image
      res.set('Content-type', mime.lookup(path)); // using 'mime-types' package
      res.send(buffer);
    })
    .catch(next); // File not found or resizing failed
}

function resizeImage(path, width, height) {
  return sharp(path).resize({
    width,
    height,
    // Preserve aspect ratio, while ensuring dimensions are <= to those specified
    fit: sharp.fit.inside,
  }).toBuffer();
}

function limitNumberToRange(num, min, max) {
  return Math.min(Math.max(num, min), max);
}

function parseResizingURI(uri) {
  // Attempt to extract some variables using Regex
  const matches = uri.match(
    /(?<path>.*\/)(?<name>[^\/]+)_(?<width>\d+)x(?<height>\d+)(?<extension>\.[a-z\d]+)$/i
  );

  if (matches) {
    const { path, name, width, height, extension } = matches.groups;
    return {
      path: path + name + extension, // Original file path
      width: limitNumberToRange(+width, 16, 2000),   // Ensure the size is in a range
      height: limitNumberToRange(+height, 16, 2000), // so people don't try 999999999
      extension: extension
    };
  }
  return false;
}

Upvotes: 2

Related Questions