Socket
Socket
Sign inDemoInstall

@dreamonkey/responsive-image-loader

Package Overview
Dependencies
194
Maintainers
2
Versions
16
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

    @dreamonkey/responsive-image-loader

A webpack loader to automagically bring your website images to a whole new level of responsiveness!


Version published
Maintainers
2
Created

Changelog

Source

0.4.0

  • Chore: update deps
  • Feat: add thumbor-docker transformer
  • Chore: deprecate thumbor transformer
  • Feat: support Windows and Mac using docker to run Thumbor

Readme

Source

responsive-image-loader

New package, who dis?

A webpack loader to automagically bring your website images to a whole new level of responsiveness!

This loader tackles in an unified way three main problems with images on the web nowadays:

  • usage of most efficient image formats (automatic conversion);
  • images resizing to always serve the lightest bundle possible (resolution switching);
  • intelligent images transformation based on focal points of an image (art direction).

Moreover, we aim to automatize everything that doesn't strictly require your input:

  • calculating best breakpoints for resolution switching;
  • ordering sources by most efficient image format;
  • providing sensible defaults;
  • serving a fallback for older browsers;
  • and more!

We also focused on flexiblity and customizability: conversion, resizing and transformation engines can be easily switched with your implementation, which you can then PR here and make available to others.

Aren't there other tools doing the same stuff? Well yes, but actually no We found some notable tools while evaluating if it was worth to create our own package, but none of them combines all the requirements we now offer:

  • manages together conversion, resolution switching and art direction, with all their weird interactions;
  • process images both when used via <img> tags and background-image CSS rules;
  • framework agnostic;
  • operates at build time (did anyone said SSG?);
  • works offline;
  • free;
  • open source;
  • customizable and flexible at its core.

For more info, check out the issue from which this package spawned.

Table of contents

Roadmap

Features we'd like to implement, by most-wanted order.

Donations and shameless self-advertisement

Dreamonkey is a software house based in Reggio Emilia, Italy. We release packages as open-source when we feel they could benefit the entire community, nontheless we spend a considerabile amount of time studying, coding, maintaining and enhancing them.

Does your business or personal projects depend on our packages? Consider donating here on Github to help us maintain them and allow us to create new ones!

Do you need a UX and quality driven team to work on your project? Get in touch with us through our incredibly elaborate quotation request page or our much less cool contact form and let's find out if we are the right choice for you!

Installation

Install via yarn add -D @dreamonkey/responsive-image-loader or npm install -D @dreamonkey/responsive-image-loader.

Loader

Normal usage

Add the loader into your webpack rules targetting .html files.

webpackConf.module.rules.push({
  test: /\.html$/,
  loader: '@dreamonkey/responsive-image-loader',
  options: {
    /* ... */
  },
});

If you plan to process CSS background images, you should also include the package as you'd do with a polyfill.

webpackConf.entry['responsive-bg-image-handler'] =
  '@dreamonkey/responsive-image-loader';
<!--
NB: `src` attribute value could change dependending on your webpack `output.filename` (https://webpack.js.org/configuration/output/#outputfilename) and `output` configuration, you're not bound to ``
-->
<script src="./responsive-bg-image-handler.js">
On Quasar framework

Presumely due to some kind of incompatibility with theirs HTML loader, you must tap into low level Vue template to use this loader with Quasar framework (on which it has been tested and developed).

webpackConf.module.rules.push({
  test: /\.vue$/,
  resourceQuery: /type=template/,
  loader: '@dreamonkey/responsive-image-loader',
  options: {
    paths: {
      /* Quasar output folder */
      outputDir: '/img/',
      /* Quasar webpack aliases */
      aliases: {
        '~': 'src/',
        /* ... */
      },
    },
    /* ... */
  },
});

It is not possible to specify only test: /\.vue$/ because Vue templates are actually processed many times (one for general file plus one per each used tag) and this would break the loader workflow. A caching mechanism (as suggested by Vue creator in this cases) won't work efficiently and will break framework-agnosticism.

If you plan to process CSS background images, you should also include the package as you'd do with a polyfill.

webpackConf.entry['responsive-bg-image-handler'] =
  '@dreamonkey/responsive-image-loader';

You don't need to manually include it via a script tag as Quasar already does it automatically for every entry property.

Engines

Conversion, art direction and resolution switching are powered via an adapter by a fully decoupled and swappable engine. Every engine has its installation guide (independent from this loader) and you can also provide your custom adapter to support a new engine (in which case, we welcome PRs!)

sharp (conversion | resolution switching)

Everything should "Just Work™" out-of-the-box. It's installed by default when adding the loader dependency, but check for libvips dependency if something doesn't work properly. If you get build errors at the first run, try deleting and re-installing the whole node_modules folder.

thumbor (art direction, Linux-only)

Local installation of thumbor is currently a pain in the ass and docs are out-of-sync, we highly suggest you to use thumbor-docker engine instead. If you still want to install it locally, you can check here how I managed to make it work.

This adapter ships with a preset configuration, but you can overwrite string config options via environment variables.

This engine leverage Thumbor cache and as such build time will decrease on subsequent runs. This engine will be deprecated when a custom cache mechanism will be added on the loader.

thumbor-docker (art direction, cross-platform)

This engine is the same as thumbor one, but it works cross-platform and it's installation is much more easier, thanks to the usage of docker. When releasing v1, this loader will be renamed as thumbor and will be the default one.

First setup Docker on your system:

Then pull docker Thumbor image running docker pull minimalcompact/thumbor.

This engine ships with a preset configuration, unlike thumbor engine you currently cannot overwrite string config options via environment variables. If needed, we will take into consideration adding this feature.

Due to its nature of spawning a brand new container for every build cycle, using thumbor-docker will not leverage Thumbor cache mechanism as thumbor engine does, meaning build time will not decrease on subsequent runs.

Usage

On <img> tags

Add responsive attribute over an <img> component and it will be enhanced with conversion and resolution switching!

<img responsive src="my-little-calogero.jpg" />

By default all classes on <img> will also be copied over to the wrapping <picture>. If you want to change classes which are applied to <img> after the rewrite took place, you can use responsive-img-class attribute. If you want to manually specify which classes should be applied to <picture>, you can use responsive-picture-class attribute. If you add either responsive-img-class or responsive-picture-class without any value or with an empty value, classes on <img> and <picture> will be erased.

<img class="hello there" responsive src="my-little-calogero.jpg" />

<!-- WILL BECOME -->

<picture class="hello there">
  <source />
  <source />
  <!-- ... -->
  <img class="hello there" responsive src="something.jpg" />
</picture>
<img
  class="hello there"
  responsive
  responsive-img-class="master kenobi"
  src="my-little-calogero.jpg"
/>

<!-- WILL BECOME -->

<picture class="hello there">
  <source />
  <source />
  <!-- ... -->
  <img class="master kenobi" responsive src="something.jpg" />
</picture>
<img
  class="hello there"
  responsive
  responsive-img-class
  src="my-little-calogero.jpg"
/>

<!-- WILL BECOME -->

<picture class="hello there">
  <source />
  <source />
  <!-- ... -->
  <img responsive src="something.jpg" />
</picture>

You can opt-in to art direction adding responsive-ad attribute. You can also provide an encoded inline transformation as the attribute value which will be merged on top of default transformations.
This allow to overwrite size or ratio of an existing transformation on a single image.

The syntax for inline transformations is:

  • it can contain one or more properties;
  • each property definition starts with the property name (ratio, path, size, etc.) followed by an equality sign (=) and one or more options separated by a comma (,);
  • every option is composed by a value and, optionally, one or more viewports to which it must be applied;
  • wiewports must be enclosed into curly braces ({}) and separated by a pipe char (|).

Adding a responsive-ad-ignore attribute without value will disable all default transformations, while providing a pipe-separated list of transformation names will disable only the selected ones.

Notice that you can use both a viewport width or an alias to reference a transformation in the value of both attributes.

<!-- Opt-in to art direction -->
<img responsive responsive-ad src="my-little-nicola.jpg" />
<!--
  Define inline transformations:
  - the first uses a viewport width as name and explicitly define `ratio` and `size`.
  - the second uses an alias as name and define a custom image
    (it will be used "as-is"); `size` has not been specified and
    will be inferred from the default size.
-->
<img
  responsive="size=0.5{699}"
  responsive-ad="ratio=3:2{699};path=./custom_example.jpg{md}"
  src="my-little-francisco.jpg"
/>
<!--
  Define inline transformations:
  - on `xs` and `md` viewports the `size` is `0.5`, while it's `0.33` on `sm` one. All other viewports will use the default size.
  - on `xs` and `md` viewports the `ratio` is `1:2`, while it's `3:2` on `sm` one. All other viewports will use the default ratio (which is the original image ratio).
-->
<img
  responsive="size=0.33{sm},0.5{xs|md}"
  responsive-ad="ratio=3:2{sm},1:2{xs|md}"
  src="my-little-francisco.jpg"
/>
<!--
  Ignore all default transformations and only apply the one specified.
-->
<img
  responsive-ad-ignore
  responsive-ad="ratio=2:3{1023}"
  src="my-little-kappa.jpg"
/>
<!-- Ignore only 'xs' and '1500' transformations, apply all other default ones -->
<img
  responsive
  responsive-ad-ignore="xs|1500"
  responsive-ad
  src="my-little-cuenta.jpg"
/>

On background-image CSS rules

Add responsive and responsive-bg attributes on any tag whose background-image you want to manage. The latter should be initialized to the path of the source image.

<div class="enhanced-bg-div" responsive responsive-bg="my-little-calogero.jpg">
  <p>Hey there, I'm famous</p>
</div>

All conversion, resolution switching and art direction options apply with the same API as if they were used on an <img> tag.

To keep the same GUI both in development and production mode you should add a fallback background-image CSS rule (usually with the same value as responsive-bg attribute) which conditionally target the element when the loader is not applied. A data-responsive-bg attribute is added to every enhanced element for this reason.

.enhanced-bg-div:not([data-responsive-bg]) {
  background-image: url(my-little-calogero.jpg);
}

Adding a fallback without the :not([data-responsive-bg]) selector will cause the browser to load the un-optimized image anyway, causing harm instead of benefit.

Configuration

You can check out the default configuration here.

// Full configuration, you won't ever need all this options
const fullOptionsExample: ResponsiveImageLoaderConfig = {
  defaultSize: 1.0,
  viewportAliases: {
    xs: '699', // 0-699
    md: '1439', // 700-1439
  },
  paths: {
    outputDir: '/images/',
    aliases: {
      '@randomjapp': 'src',
      /* ... */
    },
  },
  conversion: {
    converter: 'sharp',
    enabledFormats: {
      webp: true,
      jpg: true,
    },
  },
  resolutionSwitching: {
    resizer: 'sharp',
    breakpoints: {
      minViewport: 200,
      maxViewport: 3840,
      maxSteps: 5,
      minStepSize: 35,
    },
  },
  artDirection: {
    transformer: 'thumbor',
    defaultRatio: 'original',
    defaultTransformations: {
      xs: { ratio: '4:3' },
      md: { ratio: '2:3', size: 0.5 },
    },
  },
};

// Example of a typical configuration, if using art direction
const options: DeepPartial<ResponsiveImageLoaderConfig> = {
  viewportAliases: {
    xs: '699', // 0-699
    sm: '1023', // 700-1023
    md: '1439', // 1024-1439
    lg: '1919', // 1440-1919
    xl: '3400', // 1920-3400
  },
  paths: {
    outputDir: '/img/',
    aliases: {
      '~': 'src/',
    },
  },
  artDirection: {
    transformer: 'thumbor',
    defaultTransformations: {
      xs: { ratio: '4:3' },
      sm: { ratio: '2:1' },
      md: { ratio: '2:3' },
      lg: { ratio: '16:9' },
      xl: { ratio: '21:9' },
    },
  },
};

Global configuration

viewportAliases (default: {})

Maps of aliases to viewport widths which is used when specifying different sizes for resolution switching or when referencing a transformation.

const opts = {
  viewportAliases: {
    xs: '699', // 0-699
    sm: '1023', // 700-1023
    md: '1439', // 1201-1439
    lg: '1919', // 1440-1919
    xl: '3400', // 1920-3400
  },
};
defaultSize (default: 1.0);

Will be used when applying transformations or creating resolution switching breakpoints. If provided as a percentage (size <= 1.00) it's considered as the width size multiplier with respect to the maxViewport. If provided as a number bigger than 300 it's considered as the width in pixels. Value is capped to 0.10 on lower bound.

Paths

outputDir (default: '/')

Specify a folder which will prefix images uri emitted by this loader. Your production bundle probably isn't organized with a flat folder structure, so you'll want to use this options most of the time.

// All images will be emitted into the bundle `img` folder
const opt = { outputDir: '/img/' };
aliases (default: {})

Specify a map of aliases which is used to correctly resolve source image paths. Most of the times this will match your webpack aliases map (we still don't know how to programmatically get those ones, we welcome PRs!).

In case of multiple matches, the first one win.

// Make `~` point to `src/` folder
const opt = { aliases: { '~': 'src/' } };

Conversion

converter (default: 'sharp')

Specify the adapter function to use for image format conversion. You can provide the name of a preset adapter (only sharp for now) after you installed it properly on your system. Providing null disables conversion.

The adapter cannot be a lambda function, or it won't inherit the loader context

// Disables conversion
const opt = { converter: null };

// Provide custom adapter, **never use a lambda function**
const opt = {
  converter: function (sourcePath, destinationPath, uriWithoutHash, format) {
    /**/
    return breakpoint;
  },
};

// Provide custom adapter defined elsewere, **never use a lambda function**
const conversionAdapter: ConversionAdapter = function (
  sourcePath,
  destinationPath,
  uriWithoutHash,
  format,
) {
  /**/
  return breakpoint;
};
const opt = { converter: conversionAdapter };
enabledFormats (default: jpg and webp enabled)

Keys of this object represents available formats (jpg or webp), while their value represent their enabled status.

// Only serve webp formats
const opt = { enabledFormats: { webp: true, jpg: false } };

Source will be ordered by format efficiency: webp > jpg

Resolution switching

Breakpoints generation adds as many breakpoints as possible into narrow viewports (smartphones), which suffer high bundle sizes the most (eg. when using data network); it also grants some breakpoints to wider viewports (laptops, desktops), where is less critical to save bandwidth. If narrow viewports need less breakpoints than originally allocated for them, those breakpoints are re-allocated to wider viewports and removed when they cannot be used in the widest viewport available.

resizer (default: 'sharp')

Specify the adapter to use for image resizing. You can provide the name of a preset adapter (only sharp for now) after you installed it properly on your system. Providing null disables resolution switching.

The adapter cannot be a lambda function, or it won't inherit the loader context

// Disables resolution switching
const opt = { resizer: null };

// Provide custom adapter, **never use a lambda function**
const opt = {
  resizer: function (sourcePath, destinationPath, breakpointWidth) {
    /**/
    return breakpoint;
  },
};

// Provide custom adapter defined elsewere, **never use a lambda function**
const resizingAdapter: ResizingAdapter = function (
  sourcePath,
  destinationPath,
  breakpointWidth,
) {
  /**/
  return breakpoint;
};
const opt = { resizer: resizingAdapter };
minViewport (default: 200)

The minimum viewport which will be considered when automatically generating breakpoints.

maxViewport (default: 3840)

The maximum viewport which will be considered when automatically generating breakpoints.

maxBreakpointsCount (default: 5)

Maximum number of breakpoints which can be generated, the actual count can be lower due to minSizeDifference option. It doesn't include breakpoints generated by art direction transformations.

minSizeDifference (default: 35)

Minimum size difference (expressed in KB) there should be between a breakpoint and both its preceding and following ones.

Art direction

transformer (default: null)

Specify the adapter to use for image transformations. You can provide the name of a preset adapter after you installed it properly on your system. Providing null disables art direction.

The adapter cannot be a lambda function, or it won't inherit the loader context

// Disables art direction
const opt = { transformer: null };

// Provide custom adapter, **never use a lambda function**
const opt = {
  transformer: function (imagePath, transformations) {
    /**/
    return transformationSource;
  },
};

// Provide custom adapter defined elsewere, **never use a lambda function**
const transformationAdapter: TransformationAdapter = function (
  imagePath,
  transformations,
) {
  /**/
  return transformationSource;
};
const opt = { transformer: transformationAdapter };
defaultRatio (default: 'original');

The ratio which will be used when applying transformations, if not explicitly provided.

defaultTransformations (default: {});

Map of default transformations.

const opts = {
  defaultTransformations: {
    xs: { ratio: '4:3' },
    sm: { ratio: '2:1' },
    md: { ratio: '2:3' },
    lg: { ratio: '16:9' },
    xl: { ratio: '21:9' },
  },
};

Caveats & FAQ

Does it work in every possible scenario?

NO! Being a webpack loader, it has limits derived by being a build-time tool: it will only work for images statically referenced in your code. If you are dynamically changing your <img> src attribute, this loader cannot help you. If you are doing so with a JS framework via dynamic bindings (Vue :src="...", Angular [src]="...", etc), changing your component to use slots instead could help you and make your components more flexible.

Only use in production

The compilation time overhead of this loader is high, due to image processing. It is not advisable to use it during development unless you have a really valid motivation to do so. You'll probably want to apply it conditionally to your webpack chain only when compiling for production.

if (process.env.NODE_ENV === "production") {
    webpackConfig.module.rules.push({ ... });
}

Pay attention to CSS selectors

<img> will be wrapped into a <picture> when the loader kicks in. Use a class to reference the image in your selectors and avoid direct-descendent selector. Check out class management into the Usage section.

<div class="container">
  <img
    class="positioning-class"
    responsive
    responsive-img-class="inner-image-class"
    src="something.jpg"
  />

  <img class="my-image" responsive src="something.jpg" />
</div>

will become

<div class="container">
  <picture class="positioning-class">
    <source />
    <source />
    <!-- ... -->
    <img class="inner-image-class" responsive src="something.jpg" />
  </picture>

  <picture class="my-image">
    <source />
    <source />
    <!-- ... -->
    <img class="my-image" responsive src="something.jpg" />
  </picture>
</div>

so the selector should take into accout both structures, depending on the context

/* Should access direct child, whoever it is (eg. positioning or spacing) */
/* (preferred) */
.positioning-class {
  /* ... */
}

/* Or */
.container > img,
.container > picture {
  /* ... */
}

/* Should access original image tag */
/* (preferred) */
.inner-image-class {
  /* ... */
}

/* Or */
/* (preferred) */
.container .my-image {
  /* ... */
}

/* Or */
.container img {
  /* ... */
}

/* Or */
.container > img,
.container > picture > img {
  /* ... */
}

The image I provided for the custom transformation isn't working...

Custom transformation images' path currently cannot contain _ or : characters, check if your does and if it's the case update the file name!

How do I enable/disable conversion and/or resolution switching?

Conversion and resolution-switching are enabled by default. If you want to disable them globally, set conversion.converter and/or resolutionSwitching.resizer to null into the loader options. Currently there is no way to disable them on a per-image basis.

Which default value should I use for defaultSize?

defaultSize, which is a global configuration option, will be used both for art direction and resolution switching. In the latter case, it is used in particular when:

  • a breakpoint is generated after the last art direction source;
  • there are no art direction sources at all.

Because of this, you should set defaultSize to be the one of the image on the biggest screen possible.

Example: if the image occupies 100% of the viewport width on the maximum supported width of my website, default size will be 1.0. If it occupies 50%, default size will be 0.5.

Why doesn't the loader kick in on my images?

The loader won't process the image if responsive attribute is missing or if src attribute is missing or empty. Also, art direction won't take place if responsive-ad is missing.

The fallback background image is downloaded anyway even when responsive-bg is active

You must manually prevent the fallback background-image CSS rule from being applied when the loader kicks in. Remember to wrap it into a :not([data-responsive-bg]) selector!

My child-referencing CSS selectors break when I use the background-image optimization feature

Due to poor flexibility of image-set() CSS function (HTML srcset attribute counterpart), background images management exploits the same HTML features used for <img> tags.

An hidden <picture> element, whose purpose is to detect the best image to use, is added as the first child of the enhanced element. This could break CSS cardinality selectors like :first-child, :first-of-type and :nth-child.

The enhanced element background-image property is updated via a globally available JavaScript handler every time the <picture> inner <img> element loads a new image.

<div class="enhanced-bg-div" responsive responsive-bg="my-little-calogero.jpg">
  <p>Hey there, I'm famous</p>
</div>

<!-- WILL BECOME -->

<div
  class="enhanced-bg-div"
  responsive
  responsive-bg="my-little-calogero.jpg"
  data-responsive-bg
>
  <picture class="responsive-bg-holder">
    <source />
    <source />
    <!-- ... -->
    <img
      class="responsive-bg-holder"
      responsive
      src="my-little-calogero.jpg"
      style="display: none"
      onload="**handler invocation**"
    />
  </picture>
  <p>Hey there, I'm famous</p>
</div>

## <span id="contributing"></span> Contributing

Please see [CONTRIBUTING](CONTRIBUTING.md) for details.

## <span id="security"></span> Security

If you discover any security related issues, please email security@dreamonkey.com instead of using the issue tracker.

## <span id="license"></span> License

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

Keywords

FAQs

Last updated on 14 Sep 2020

Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc