DEV Community

Cover image for Create responsive images with gulp-sharp-responsive
Anwar
Anwar

Posted on

Create responsive images with gulp-sharp-responsive

Hello everyone, and welcome to this tutorial. Today I would like to introduce a new plugin for Gulp that I created to optimize images for our web browser users.

Introducting gulp-sharp-responsive

gulp-sharp-responsive is based on the Sharp NPM package, a fast image processing library, and aims to simplify this tedious task. Making images responsive and declined for differents format becomes simple because we only have to configure it and the rest is done automatically for us.

Context

For this tutorial, let's imagine we have the following folder:

.
├── src/
│   └── img/
│       └── lion.jpg
├── .gitignore
├── gulpfile.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

Let's say We want to output our lion.jpg image into the folder dist/img. We also would like to have images in differents sizes:

  • 640 (mobile)
  • 768 (tablet)
  • 1024 (desktop)

And differents formats:

  • jpeg
  • webp
  • avif

Using gulp-sharp-responsive

To this purpose, here is how you can use this library.

Installation

First, let's install Gulp and this plugin:

npm install --save-dev gulp gulp-sharp-responsive
Enter fullscreen mode Exit fullscreen mode

Usage

Next, head on your gulpfile.js file and append this code:

// gulpfile.js
const { src, dest } = require("gulp");
const sharpResponsive = require("gulp-sharp-responsive");
Enter fullscreen mode Exit fullscreen mode

Then, let's write our "img" task:

// gulpfile.js
const { src, dest } = require("gulp");
const sharpResponsive = require("gulp-sharp-responsive");

const img = () => src("src/img/*.jpg")
  .pipe(sharpResponsive({
    formats: [
      // jpeg
      { width: 640, format: "jpeg", rename: { suffix: "-sm" } },
      { width: 768, format: "jpeg", rename: { suffix: "-md" } },
      { width: 1024, format: "jpeg", rename: { suffix: "-lg" } },
      // webp
      { width: 640, format: "webp", rename: { suffix: "-sm" } },
      { width: 768, format: "webp", rename: { suffix: "-md" } },
      { width: 1024, format: "webp", rename: { suffix: "-lg" } },
      // avif
      { width: 640, format: "avif", rename: { suffix: "-sm" } },
      { width: 768, format: "avif", rename: { suffix: "-md" } },
      { width: 1024, format: "avif", rename: { suffix: "-lg" } },
    ]
  }))
  .pipe(dest("dist/img"));
Enter fullscreen mode Exit fullscreen mode

Finally, let's expose this task so that we can use it through npm run img

// gulpfile.js
const { src, dest } = require("gulp");
const sharpResponsive = require("gulp-sharp-responsive");

const img = () => src("src/img/*.jpg")
  .pipe(sharpResponsive({
    formats: [
      // jpeg
      { width: 640, format: "jpeg", rename: { suffix: "-sm" } },
      { width: 768, format: "jpeg", rename: { suffix: "-md" } },
      { width: 1024, format: "jpeg", rename: { suffix: "-lg" } },
      // webp
      { width: 640, format: "webp", rename: { suffix: "-sm" } },
      { width: 768, format: "webp", rename: { suffix: "-md" } },
      { width: 1024, format: "webp", rename: { suffix: "-lg" } },
      // avif
      { width: 640, format: "avif", rename: { suffix: "-sm" } },
      { width: 768, format: "avif", rename: { suffix: "-md" } },
      { width: 1024, format: "avif", rename: { suffix: "-lg" } },
    ]
  }))
  .pipe(dest("dist/img"));

module.exports = {
  img,
};
Enter fullscreen mode Exit fullscreen mode
// package.json
{
  "scripts": {
    "img": "gulp img"
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, let's run this task once. In your terminal, run this command:

npm run img
Enter fullscreen mode Exit fullscreen mode

You should see something printed in the console like this:

$ npm run img

> img     
> gulp img

[14:11:00] Using gulpfile /home/khalyomede/gulpfile.js
[14:11:01] Starting 'build'...
[14:11:01] Starting 'img'...
[14:11:02] Finished 'img' after 1.92 s
[14:11:02] Finished 'build' after 1.93 s
Enter fullscreen mode Exit fullscreen mode

And if we inspect our folder tree this is what we should get now:

.
├── dist/
│   └── img/
│       ├── lions-lg.avif
│       ├── lions-lg.jpg
│       ├── lions-lg.webp
│       ├── lions-md.avif
│       ├── lions-md.jpg
│       ├── lions-md.webp
│       ├── lions-sm.avif
│       ├── lions-sm.jpg
│       └── lions-sm.webp
├── src/
│   └── img/
│       └── lion.jpg
├── .gitignore
├── gulpfile.js
└── package.json
Enter fullscreen mode Exit fullscreen mode

Conclusion

Image responsiveness can be of a good use when you want to improve your web page speed using this HTML technique:

<picture>
  <!-- avif -->
  <source srcset="/img/lion-sm.avif" media="(max-width: 640px)" type="image/avif" />
  <source srcset="/img/lion-md.avif" media="(max-width: 768px)" type="image/avif" />
  <source srcset="/img/lion-lg.avif" media="(max-width: 1024px)" type="image/avif" />
  <!-- webp -->
  <source srcset="/img/lion-sm.webp" media="(max-width: 640px)" type="image/webp" />
  <source srcset="/img/lion-md.webp" media="(max-width: 768px)" type="image/webp" />
  <source srcset="/img/lion-lg.webp" media="(max-width: 1024px)" type="image/webp" />
  <!-- jpeg -->
  <source srcset="/img/lion-sm.jpeg" media="(max-width: 640px)" type="image/jpeg" />
  <source srcset="/img/lion-md.jpeg" media="(max-width: 768px)" type="image/jpeg" />
  <source srcset="/img/lion-lg.jpeg" media="(max-width: 1024px)" type="image/jpeg" />
  <!-- original -->
  <img src="/img/lion.jpeg" class="img-responsive" alt="A lion in the jungle." />
</picture>
Enter fullscreen mode Exit fullscreen mode

This way, you are asking the browser:

  • To load the most modern image first
  • Load an image that fits the viewport width
  • Fallback to the <img> if a browser doesn't support it

If you check the sizes of each files, we can see users will benefit from newest files format small sizes:

Image Size Weight
lion.jpg Original 1 330 Ko
lions-lg.avif 1024px 52 Ko
lions-lg.jpg 1024px 141 Ko
lions-lg.webp 1024px 118 Ko
lions-md.avif 768px 30 Ko
lions-md.jpg 768px 81 Ko
lions-md.webp 768px 67 Ko
lions-sm.avif 640px 23 Ko
lions-sm.jpeg 640px 60 Ko
lions-sm.webp 640px 51 Ko

Learn more in this detail post:

Thanks for reading this tutorial, I hope you enjoyed it as much as I enjoyed writting for Dev.to!

You can learn more about this library, like how to keep the original file in the output images and much more!

GitHub logo khalyomede / gulp-sharp-responsive

A gulp plugin to generate responsives images.

gulp-sharp-responsive

A gulp plugin to generate responsives images.

Build Status npm NPM

Summary

About

I make web apps and I often need to generate images of multi formats and size from a single image. For example, an image "lion.jpeg", that is declined like this:

  • lion-sm.jpeg
  • lion-sm.webp
  • lion-sm.avif
  • lion-lg.jpeg
  • lion-lg.webp
  • lion-lg.avif

Sharp can do this, and since I use Gulp for my everyday tasks, I created a plugin to automatize this task.

Features

  • Based on Sharp
  • Takes options to generate images by sizes and format
  • Supports theses formats:
    • jpeg
    • png
    • gif
    • webp
    • avif
    • heif
    • tiff
  • Can pass Sharp specific options to customize even more the image generation
  • Written in TypeScript, so you get type hints for the options

Installation

In your terminal:

npm install --save-dev gulp-sharp-responsive
Enter fullscreen mode Exit fullscreen mode

With Yarn:

yarn add --dev gulp-sharp-responsive
Enter fullscreen mode Exit fullscreen mode

Examples

Sidenote: all the following example uses the TS version of gulpfile. This is why you will…

Happy optimizations!

Oldest comments (9)

Collapse
 
samsaltspringbc profile image
Sam Miller • Edited

Thanks for the great gulp tutorial. There isn't a lot of such gulp tutorials online so much appreciated. However, I am not getting any result from running the plugin. I am wondering if you might have any suggestions.

I've installed sharp and this plugin, "npm install sharp gulp-sharp-responsive --save-dev", following the instructions on the npm website (npmjs.com/package/gulp-sharp-respo...) and your tutorial.

I tried this:
...
const { src, dest } = require("gulp");
const sharpResponsive = require("gulp-sharp-responsive");

const img = () => src('src/assets/images/process/*.jpg')
.pipe(sharpResponsive({
formats: [
// jpeg
{ width: 640, format: "jpeg", rename: { suffix: "-sm" } },
{ width: 768, format: "jpeg", rename: { suffix: "-md" } },
{ width: 1024, format: "jpeg", rename: { suffix: "-lg" } },
// webp
{ width: 640, format: "webp", rename: { suffix: "-sm" } },
{ width: 768, format: "webp", rename: { suffix: "-md" } },
{ width: 1024, format: "webp", rename: { suffix: "-lg" } },
// avif
{ width: 640, format: "avif", rename: { suffix: "-sm" } },
{ width: 768, format: "avif", rename: { suffix: "-md" } },
{ width: 1024, format: "avif", rename: { suffix: "-lg" } },
]
}))
.pipe(dest('test'))

module.exports = {
img,
};
...
with this in my package.json:
...
"scripts": {
"img": "gulp img"
},
...
Alternatively, I tried the classic function approach but got the same result.
...
function sharpImages() {
return src('src/assets/images/process/*.jpg')
.pipe(sharpResponsive({
formats: [
// jpeg
{ width: 640, format: "jpeg", rename: { suffix: "-sm" } },
{ width: 768, format: "jpeg", rename: { suffix: "-md" } },
{ width: 1024, format: "jpeg", rename: { suffix: "-lg" } },
// webp
{ width: 640, format: "webp", rename: { suffix: "-sm" } },
{ width: 768, format: "webp", rename: { suffix: "-md" } },
{ width: 1024, format: "webp", rename: { suffix: "-lg" } },
// avif
{ width: 640, format: "avif", rename: { suffix: "-sm" } },
{ width: 768, format: "avif", rename: { suffix: "-md" } },
{ width: 1024, format: "avif", rename: { suffix: "-lg" } },
]
}))
.pipe(dest('test'))
}

exports.sharpImages = sharpImages;
...
In both cases, when I run "npm run img" or "gulp sharpImages", all I get is this in the terminal:
[14:12:35] Starting 'sharpImages'...
[14:12:35] Finished 'sharpImages' after 227 ms

No test directory, no images, nothing. I am using node v16.15.0. Any suggestions?

Collapse
 
anwar_nairi profile image
Anwar • Edited

Thanks so much for the appreciation 🙏

Could you please give me a public repository with your code? So I can give this a try on my machine to see what's wrong

Collapse
 
samsaltspringbc profile image
Sam Miller

Hi, so I thought I would give it another try, and this time it worked! Perhaps, I didn't have a dependency installed or perhaps I had a syntax error in the src url? In your tutorial, you don't mention installing sharp via "npm install sharp". Is that a requirement, or does "npm install --save-dev gulp-sharp-responsive" automatically install all the needed dependencies? Thanks for the awesome plugin!

Thread Thread
 
anwar_nairi profile image
Anwar • Edited

Hi, I'm so glad it finally worked for you!

Indeed installing solely the package will automatically require all necessary dependencies, including "sharp". Maybe your issue was during the compilation of sharp itself, I know I had lots of random issues (because it needs to recompile sharp from sources, and if for example the network is fluctuating or a filesystem issue occurs, the whole process finishes but then the executable is corrupted). That's my best guess...

At least now you're good to go, have fun with it!

Collapse
 
samsaltspringbc profile image
Sam Miller • Edited

Rather than specifying a media condition for each source, could the above html example be simplified to the following, letting the browser choose based on the widths given in the srcset element (see blogs.windows.com/msedgedev/2015/1...)? I haven't tested it, yet.

<picture>
<source type="image/avif" sizes=”100vw” srcset="/img/lion-sm.avif 640w, /img/lion-md.avif 768w, /img/lion-lg.avif 1024" />
<source type="image/webp" sizes=”100vw” srcset="/img/lion-sm.webp 640w, /img/lion-md.webp 768w, /img/lion-lg.webp 1024" />
<img sizes="100vw" src="/img/lion.jpeg" srcset="/img/lion-sm.jpeg 640w, /img/lion-md.jpeg 768w, /img/lion-lg.jpeg 1024" alt="lion" class="img-responsive">
</picture>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
samsaltspringbc profile image
Sam Miller • Edited

For those how might be curious, to group images into output directories named after their filename base, filename excluding suffixes, I used the gulp plugin "rename" (npmjs.com/package/gulp-rename) as in the following. Helpful when dealing with a lot of images.

function sharpImg() {
    return src('_sort/**/*.jpg')
        .pipe(rename(function (path) {
            path.dirname += "/" + path.basename;
        }))
        .pipe(sharpResponsive({
            formats: [
                // jpeg
                { width: 640, format: "jpeg", rename: { suffix: "-sm" } },
                { width: 768, format: "jpeg", rename: { suffix: "-md" } },
                { width: 1024, format: "jpeg", rename: { suffix: "-lg" } },
                // webp
                { width: 640, format: "webp", rename: { suffix: "-sm" } },
                { width: 768, format: "webp", rename: { suffix: "-md" } },
                { width: 1024, format: "webp", rename: { suffix: "-lg" } },
                // avif
                { width: 640, format: "avif", rename: { suffix: "-sm" } },
                { width: 768, format: "avif", rename: { suffix: "-md" } },
                { width: 1024, format: "avif", rename: { suffix: "-lg" } },
            ]
        }))
        .pipe(dest('_sorted'))
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
samsaltspringbc profile image
Sam Miller • Edited

Hello again, I want to be able to calculate widths based on the approximate percentage of the screen that they will occupy. For example, one of my sizes for a full screen image is 1200. For images that are 1/4 that width, how would I do math in the gulpfile (e.g., 1200/4) to produce a 300px image? I want to avoid having to manually calculate the new widths.

I know this is probably wrong, but something like this?
...
{ width: (1200 / 4), format: "jpeg", rename: { suffix: "-lg" }, jpegOptions: { quality: 50, progressive: true } },
{ width: (1400 / 4), format: "jpeg", rename: { suffix: "-xl" }, jpegOptions: { quality: 50, progressive: true } },

Ideally, I would like to be able to pass the divisor (command line option) when running the task--e.g., "gulp sharpImage --4". If there is no given command line divisor, the original size would remain unaffected. How would you do this, with an additional package such as npmjs.com/package/gulp-param or could I use node's process.argv to do this?
Thanks, Sam

Collapse
 
samsaltspringbc profile image
Sam Miller • Edited

So this is the solution I came up with following this tutorial, sitepoint.com/pass-parameters-gulp.... At the top of gulpfile.js add the following code:

// fetch command line arguments
const arg = (argList => {
    let arg = {}, a, opt, thisOpt, curOpt;
    for (a = 0; a < argList.length; a++) {
        thisOpt = argList[a].trim();
        opt = thisOpt.replace(/^\-+/, '');
        if (opt === thisOpt) {
            // argument value
            if (curOpt) arg[curOpt] = opt;
            curOpt = null;
        }
        else {

            // argument name
            curOpt = opt;
            arg[curOpt] = true;
        }
    }
    return arg;
})(process.argv);
Enter fullscreen mode Exit fullscreen mode

Assign div as arg.d and provide a fallback if arg is null to 1 (i.e., div = arg.d || 1). Note, since I mainly going to show featured images at full screen at widths 576px and below (mobile screens), I am not dividing the xs size by a divisor. Also since the the gulp-sharp-responsive is not able to process non-integer widths, I had to round the quotient with the round function. I am sure there is a better way with less redundant code. If you have any suggestions to improving it please let me know.

function sharpImg() {
    const div = arg.d || 1, xs = (Math.round(576 / div)), sm = (Math.round(769 / div)), md = (Math.round(992 / div)), lg = (Math.round(1200 / div)), xl = (Math.round(1400 / div)), xxl = (Math.round(2048 / div));
    return src(['_images/original/process/**/*.{jpeg,jpg,png,tiff,webp}', '!_images/original/raw/**'])
        .pipe($.rename(function (path) {
            path.dirname += "/" + path.basename;
        }))
        .pipe($.sharpResponsive({
            formats: [
                // jpeg
                { width: xs, format: "jpeg", rename: { suffix: "-xs" }, jpegOptions: { quality: 50, progressive: true } },
                { width: sm, format: "jpeg", rename: { suffix: "-sm" }, jpegOptions: { quality: 50, progressive: true } },
                { width: md, format: "jpeg", rename: { suffix: "-md" }, jpegOptions: { quality: 50, progressive: true } },
                { width: lg, format: "jpeg", rename: { suffix: "-lg" }, jpegOptions: { quality: 50, progressive: true } },
                { width: xl, format: "jpeg", rename: { suffix: "-xl" }, jpegOptions: { quality: 50, progressive: true } },
                { width: xxl, format: "jpeg", rename: { suffix: "-xxl" }, jpegOptions: { quality: 50, progressive: true } },
                // webp
                { width: xs, format: "webp", rename: { suffix: "-xs" }, webpOptions: { quality: 50 } },
                { width: sm, format: "webp", rename: { suffix: "-sm" }, webpOptions: { quality: 50 } },
                { width: md, format: "webp", rename: { suffix: "-md" }, webpOptions: { quality: 50 } },
                { width: lg, format: "webp", rename: { suffix: "-lg" }, webpOptions: { quality: 50 } },
                { width: xl, format: "webp", rename: { suffix: "-xl" }, webpOptions: { quality: 50 } },
                { width: xxl, format: "webp", rename: { suffix: "-xxl" }, webpOptions: { quality: 50 } },
                // avif
                { width: xs, format: "avif", rename: { suffix: "-xs" }, avifOptions: { quality: 50 } },
                { width: sm, format: "avif", rename: { suffix: "-sm" }, avifOptions: { quality: 50 } },
                { width: md, format: "avif", rename: { suffix: "-md" }, avifOptions: { quality: 50 } },
                { width: lg, format: "avif", rename: { suffix: "-lg" }, avifOptions: { quality: 50 } },
                { width: xl, format: "avif", rename: { suffix: "-xl" }, avifOptions: { quality: 50 } },
                { width: xxl, format: "avif", rename: { suffix: "-xxl" }, avifOptions: { quality: 50 } },
            ]
        }))
        .pipe(dest('_images/processed'))
}
Enter fullscreen mode Exit fullscreen mode

export task:

exports.sharpImg = sharpImg;
Enter fullscreen mode Exit fullscreen mode

The result:
Running "gulp sharpImg" results in the default const widths defined, whereas running "gulp sharpImg --d 4" results in images 1/4 their default width.

Collapse
 
samsaltspringbc profile image
Sam Miller • Edited

So,I posted the above code, knowing than it was less than ideal, on stackoverflow looking for feedback and someone submitted this gem (stackoverflow.com/questions/728107...

function sharpImg() {
    const BREAKPOINTS = {
        xs: 576,
        sm: 769,
        md: 992,
        lg: 1200,
        xl: 1400,
        xxl: 2048,
    };
    const onDiv = div => Object.entries(BREAKPOINTS).map(([bp, value]) => [Math.round(value / div), `-${bp}`]);
    // creates an array of [[1, "-xs"], [2, "-sm"], ... ] (obviously the values are 576/div etc)

    const div = arg.d || 1, bps = onDiv(div);

    const jpegOptions = { quality: 50, progressive: true };
    const webpOptions = { quality: 50 };
    const avifOptions = { quality: 50 };

    return src(['_images/original/process/**/*.{jpeg,jpg,png,tiff,webp}', '!_images/original/raw/**'])
        .pipe($.rename(function (path) {
            path.dirname += "/" + path.basename;
        }))
        .pipe($.sharpResponsive({
            formats: [
                // jpeg
                ...bps.map(([width, suffix]) => ({ width, format: "jpeg", rename: { suffix }, jpegOptions })),
                // webp
                ...bps.map(([width, suffix]) => ({ width, format: "webp", rename: { suffix }, webpOptions })),
                // avif
                ...bps.map(([width, suffix]) => ({ width, format: "avif", rename: { suffix }, avifOptions })),
            ]
        }))
        .pipe(dest('_images/processed'))
}
Enter fullscreen mode Exit fullscreen mode

Running

gulp sharpImg

resizes and reformat images according to the specified breakpoints. Whereas running

gulp sharpImg -d 3

(-d followed by a divisor number) will create fractional widths, in this case 1/3 of a full screen image. Using this format one can easily add other command options. For example, one could have a command option -w, for generating custumized widths through the command line rather than having to edit the gulpfile.js.