Search code examples
javascriptnode.jsimageexpresssharp

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


Solution

  • 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;
    }