npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@bsmth/img-loader

v1.2.0

Published

Magical image loading for webpack.

Downloads

21

Readme

🌄🧙🏼‍♀️ bismuth image loader

Magical image loading for webpack! ✨

Motivations

Dealing with images can be really messy, when you have to support multiple resolutions, formats and compression levels. @bsmth/img-loader attempts to solve this by doing all conversions, resizing and compressions automatically and on demand, when you import an image.

https://user-images.githubusercontent.com/5791070/120940715-e92e3d00-c71e-11eb-9ab9-a94815fce5fc.mp4


Installation

yarn add --dev @bsmth/img-loader @bsmth/loader-cache
npm i --save-dev @bsmth/img-loader @bsmth/loader-cache

Setup

You'll need to add the loader and its cache management plugin to your webpack config.

import { CachePlugin } from "@bsmth/loader-cache";

export default {
	module: {
		rules: [
			// ...
			{
				test: /\.(jpe?g|png|gif|svg)$/i,
				use: [
					{
						loader: "@bsmth/img-loader",
						options: {
							// ...
						},
					},
				],
			},
		],
	},
	plugins: [
		// ...
		new CachePlugin({
			// ...
		}),
	],
};

Usage

Inside your project you can now import images like so:

import myImg from "./img.png";

By default, myImg will give you the following object:

{
	src: 'path/to/compressed/img.png',
	webp: 'path/to/webp/img.webp',
	prefix: 'your/webpack/public/path/',
	width: 500,
	height: 500,
	aspect: 1,	// aspect ratio of source image
	alpha: true,	// whether the image contains transparent areas
	thumbnail: {
		width: 4,
		height: 4,
		data: 'base64 encoded raw RGBA data',
	},
	sizes: {},
}

You can specify quality and mode by adding a query string:

import myImg from "./img.png?mode=texture&quality=high";

In this case you will get the same as above plus a .basis version:

{
	// ...
	basis: 'path/to/basis/img.basis',
}

Config

General options

| Name | Type | Default | Description | | ---------------------- | --------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | name | string | '[name].[contenthash:6].[ext]' | Specifies the output filename template. Note, that @bsmth/img-loader may append an option hash for different renditions of the same input. | | outputPath | string | '' | Specifies where the output files will be placed. | | optionHashLength | number | 4 | the length of the options-hash that may be appended to the output filename. | | generateDeclarations | boolean | false | whether to emit typescript declarations for all image imports. See Typescript |

Image options

| Name | Type | Default | Description | | --------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | skipCompression | boolean | false | disables image compression/optimisation for PNG, JPEG, SVG and GIF outputs | | forcePowerOfTwo | boolean | false | whether to force a power of 2 resolution. | | powerOfTwoStrategy | 'upscale' \| 'downscale' \| 'nearest' \| 'area' | 'area' | how the power of 2 resolution is calculated. upscale, downscale and nearest should be self-descriptive. area rounds to the nearest power of 2 while attempting to match the source images area. | | emitAvif | boolean | false | | | emitWebp | boolean | true | whether a WebP version should also be created. | | emitBasis | boolean | false | whether a basis version should also be created. | | thumbnail | false \| object | see below | either a config object (see below) or false to disable. | | qualityLevels | Record<string, QualityLevel> | see default config | an object of quality levels. See below. | | defaultQualityLevel | string | 'medium' | the quality level used if none is explicitly set | | modes | Record<string, Mode> | see default config | an object of modes. See below. | | sizes | Record<string, Size> | see default config | an object of sizes. See below. | | resizeKernel | 'nearest' \| 'cubic' \| 'mitchell' \| 'lanczos2' \| 'lanczos3' | 'lanczos3' | the interpolation kernel used for downscaling |


Quality levels

Quality levels give you granular control over how your images are compressed.

A quality level may be set by adding a ?quality= query parameter to the import statement. If none is set, the defaultQualityLevel is chosen.

If you override the default config, you must specify at least one quality level.

Each quality level object accepts the following properties:

| Name | Type | Description | | ---------- | -------- | ---------------------------------------------------------------------------------------------------------------- | | avif | object | AVIF compression options. See sharp AVIF output options. | | webp | object | WebP compression options. See sharp WebP output options. | | pngquant | object | PNG compression options. See imagemin-pngquant options. | | mozjpeg | object | JPEG compression options. See imagemin-mozjpeg options. | | gifsicle | object | GIF compression options. See imagemin-gifsicle options. | | svgo | object | SVG compression options. See imagemin-svgo options. | | basis | object | Basis compression options. See basis options below. |


Modes

Modes let you selectively override all image options (including sizes) and quality levels. (the specified overrides will be merged into their respective targets)

They may be triggered by adding a ?mode= query parameter to the import statement, or by a test function.

Example

Enable basis output and force power of 2 sizes for all images imported with ?mode=example or with 'example' in their path:

// webpack loader options
{
	// ...
	modes: {
		example: {
			test: ( relativePath, absolutePath ) => relativePath.includes( 'example' ),
			emitBasis: true,
			forcePowerOfTwo: true,
		},
	},
}

Sizes

Sizes let you generate multiple named resolutions of the same source image.

Configuring a size adds a corresponding set of additional exports to the sizes object of the image.

Each size object accepts the any combination of the following properties:

| Name | Type | Default | Description | | ------- | ------------ | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | scale | number < 1 | 1 | Dimension scalar. The dimensions of the original image are multiplied by this. | | max | | { width: Infinity, height: Infinity } | Dimension cap in pixels. All images will be downscaled to at least fit this resolution | | min | | { width: 1, height: 1 } | Minimum pixel dimensions an image may have after downscaling. Images originally smaller than this will not be upscaled. |

Note that forcePowerOfTwo takes precedence over this, so depending on your powerOfTwoStrategy setting, you may get files that are larger or smaller than expected.

Scaling of GIFs and SVGs is not supported currently. The exports are still created, but they point to the full size image.

You can override the default behaviour (root src/webp/basis exports) by specifying a default size.

Example

Cap the size of all images at 4000px×3000px and export an additional "mobile" size at half res, not downscaling below 300px×300px but at least to 2000px×2000px:

// webpack loader options
{
	// ...
	sizes: {
		default: {
			max: {
				width: 4000,
				height: 3000,
			}
		},
		mobile: {
			scale: .5,
			min: {
				width: 300,
				height: 300,
			},
			max: {
				width: 2000,
				height: 2000,
			},
		},
	},
}

Given a 5000px×5000px input image, output resolutions are the following:

  • default: 3000px×3000px
  • mobile: 2000px×2000px

While a 500px×500px input image yields:

  • default: 500px×500px
  • mobile: 300px×300px

Importing that image then gives you:

{
	src: 'path/to/compressed/img.png',
	webp: 'path/to/webp/img.webp',
	prefix: 'your/webpack/public/path/',
	width: 500,
	height: 500,
	aspect: 1,
	alpha: true,
	thumbnail: {
		width: 4,
		height: 4,
		data: 'base64 encoded raw RGBA data',
	},
	sizes: {
		mobile: {
			width: 300,
			height: 300,
			src: 'path/to/mobile/img.png',
			webp: 'path/to/mobile/img.webp',
		},
	},
}

Thumbnail

@bsmth/img-loader can generate a tiny thumbnail that is available synchronously.

| Name | Type | Default | Description | | -------- | ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | width | number | 4 | thumbnail width in pixels | | height | number | 4 | thumbnail height in pixels | | format | 'raw' \| 'png' | 'raw' | specifies the thumbnail data encoding format.'raw' gives you the raw RGBA data as a base64 encoded string, while 'png' outputs a data URL that you can use directly, e.g. as an <img> src |


Default config

The default config can be found here.


Typescript

@bsmth/img-loader can auto-generate declarations for your image imports, based on your config!

By setting generateDeclarations to true in your config, @bsmth/img-loader will emit a file named img-imports.d.ts into your project root, containing declarations for every possible file extension, quality and mode combination.

Naturally, this file can become quite large, depending on your config. To somewhat mitigate this, we assume that mode always comes before quality in any given query string. E.g. *.png?mode=texture&quality=medium is valid, *.png?quality=medium&mode=texture isn't.

Note that only the mode set via a query parameter can be detected by Typescript. You may see inaccurate types for files where mode is set via a test function.

To include the declarations in your TS setup, add this to your tsconfig.json:

{
	"include": [
		"./img-imports.d.ts"
	]
}

This will also give you access to the BismuthImage type for your convenience.


Caching

@bsmth/img-loader will cache all processed images and intermediates on disk. This is handled by the CachePlugin exported by @bsmth/loader-cache, which accepts some options.


Pitfalls/Shortcomings

Speed

Image conversion / compression can be slow, especially when working with .basis files on higher quality settings. Since webpack has to wait for the compression to complete, hot reloading will be blocked during that time.

If things get too slow, you can temporarily limit the amount of processing that needs to be done, by setting skipCompression and emitWebp/emitBasis conditionally. If you choose to do so, remember to also keep unused cache files by setting deleteUnusedFiles, otherwise already generated renditions may be deleted. Also, the renditions need to be generated at some point – You may inadvertently force that workload on your CI, if you forget to generate them locally.

I'm looking into ways to decouple the compression tasks from webpack, but this is still a ways away.

Working with git / CI

Without an up to date cache, @bsmth/img-loader will create all necessary renditions on startup. This can lead to insanely long build- and startup times. To circumvent this, it may be desirable to push the entire cache directory (.bsmth-loader-cache by default) to git LFS. While this is not ideal, all renditions will only be created once and reused on subsequent runs.

Size

Don't forget to configure your CDN / server to deliver your UASTC .basis files with gzip or brotli compression! Their disk size is 4 bytes per pixel, so a 1024x1024 texture is 4MB uncompressed. This may also baloon your git LFS size, so be aware of that when using UASTC textures.


Basis

@bsmth/img-loader ships with binaries of the Basis Universal Supercompressed GPU Texture Codec reference encoder.

Basis is a very complex topic in and of itself. @bsmth/img-loader exports a BasisOptions type to help you find an option combination that is valid. Please refer to the basis repo for info on the options.

A basis options object can have the following props:

| Name | Type | Codec | basisu equivalent / description | | ------------------------------------- | -------------------- | :-----: | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | forceAlpha | boolean | | -force_alpha | | mipmaps | boolean | | -mipmaps | | mipFilter | string | | -mip_filter filter, defaults to 'lanczos3'.See BasisOptions.ts for all possible values. | | linear | boolean | | -linear, also sets -mip_linear | | yFlip | boolean | | -y_flip | | normalMap | boolean | | -normal_map | | separateRgToColorAlpha | boolean | | -separate_rg_to_color_alpha | | codec | 'ETC1S' \| 'UASTC' | | switches between both codec options.Only options relevant for the active codec will be sent to basis. | | compLevel | number | ETC1S | -comp_level number | | noEndpointRdo | boolean | ETC1S | -no_endpoint_rdo | | noSelectorRdo | boolean | ETC1S | -no_selector_rdo | | disableHierarchicalEndpointCodebook | boolean | ETC1S | -disable_hierarchical_endpoint_codebook | | quality | number | ETC1S | -q number, ignored if maxEndpoints or maxSelectors is set. | | maxEndpoints | number | ETC1S | -max_endpoints number | | maxSelectors | number | ETC1S | -max_selectors number | | uastcLevel | number | UASTC | -uastc_level number | | uastcRdoQ | number | UASTC | -uastc_rdo_q number |


To-dos

  • [x] better cache cleaning
  • [x] support for generating multiple sizes
  • [ ] webpack config validation
  • [ ] better documentation
  • [ ] async generation (not blocking webpack while images are being compressed)
  • [ ] examples
  • [ ] tests

License

© 2021 the project bismuth authors, licensed under MIT.

This project uses the Basis Universal format and reference encoder which is © 2021 Binomial LLC, licensed under Apache 2.0.