1.2.0 ā€¢ Published 3 years ago

@bsmth/img-loader v1.2.0

Weekly downloads
-
License
MIT
Repository
github
Last release
3 years ago

šŸŒ„šŸ§™šŸ¼ā€ā™€ļø 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

NameTypeDefaultDescription
namestring'[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.
outputPathstring''Specifies where the output files will be placed.
optionHashLengthnumber4the length of the options-hash that may be appended to the output filename.
generateDeclarationsbooleanfalsewhether to emit typescript declarations for all image imports. See Typescript

Image options

NameTypeDefaultDescription
skipCompressionbooleanfalsedisables image compression/optimisation for PNG, JPEG, SVG and GIF outputs
forcePowerOfTwobooleanfalsewhether 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.
emitAvifbooleanfalse
emitWebpbooleantruewhether a WebP version should also be created.
emitBasisbooleanfalsewhether a basis version should also be created.
thumbnailfalse \| objectsee beloweither a config object (see below) or false to disable.
qualityLevelsRecord<string, QualityLevel>see default configan object of quality levels. See below.
defaultQualityLevelstring'medium'the quality level used if none is explicitly set
modesRecord<string, Mode>see default configan object of modes. See below.
sizesRecord<string, Size>see default configan 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:

NameTypeDescription
avifobjectAVIF compression options. See sharp AVIF output options.
webpobjectWebP compression options. See sharp WebP output options.
pngquantobjectPNG compression options. See imagemin-pngquant options.
mozjpegobjectJPEG compression options. See imagemin-mozjpeg options.
gifsicleobjectGIF compression options. See imagemin-gifsicle options.
svgoobjectSVG compression options. See imagemin-svgo options.
basisobjectBasis 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:

NameTypeDefaultDescription
scalenumber < 11Dimension 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.

NameTypeDefaultDescription
widthnumber4thumbnail width in pixels
heightnumber4thumbnail 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:

NameTypeCodecbasisu equivalent / description
forceAlphaboolean-force_alpha
mipmapsboolean-mipmaps
mipFilterstring-mip_filter filter, defaults to 'lanczos3'.See BasisOptions.ts for all possible values.
linearboolean-linear, also sets -mip_linear
yFlipboolean-y_flip
normalMapboolean-normal_map
separateRgToColorAlphaboolean-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.
compLevelnumberETC1S-comp_level number
noEndpointRdobooleanETC1S-no_endpoint_rdo
noSelectorRdobooleanETC1S-no_selector_rdo
disableHierarchicalEndpointCodebookbooleanETC1S-disable_hierarchical_endpoint_codebook
qualitynumberETC1S-q number, ignored if maxEndpoints or maxSelectors is set.
maxEndpointsnumberETC1S-max_endpoints number
maxSelectorsnumberETC1S-max_selectors number
uastcLevelnumberUASTC-uastc_level number
uastcRdoQnumberUASTC-uastc_rdo_q number

To-dos

  • better cache cleaning
  • 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.