@bsmth/img-loader
v1.2.0
Published
Magical image loading for webpack.
Downloads
17
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.