Skip to content

Instantly share code, notes, and snippets.

@Porges
Created September 23, 2021 08:19
Show Gist options
  • Save Porges/5973f436c25cd6c6aa18a4ea635e19ec to your computer and use it in GitHub Desktop.
Save Porges/5973f436c25cd6c6aa18a4ea635e19ec to your computer and use it in GitHub Desktop.
import { Plugin, LoadResult } from 'rollup';
import { createFilter } from '@rollup/pluginutils';
import { extname, basename } from 'path';
import { promises as fs } from 'fs';
import * as sharp from 'sharp';
import * as mime from 'mime';
export type Options = {
include: string,
exclude: string,
inlineSizeLimit: number,
sizes: number[],
}
const defaultOptions: Options = {
include: '**/*.{jpg,jpeg,png,webp}',
exclude: '',
inlineSizeLimit: 1024,
sizes: [300, 600, 800, 1200, 1600],
}
export type ImageImport = {
/** The original, unmodified, image URI. */
src: string,
/** The width of the original image. */
width: number,
/** The height of the original image. */
height: number,
/** The resized images in srcset format (for use on `<img>`).
* Note that to use the `srcset` you will also need to provide your
* own `sizes` attribute, which this library cannot create for you.
* Refer to MDN’s [Reponsive Images](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)
* article for more. */
srcset: string,
/** The resized images in a structured format, in case
* you need to do anything else with them. */
images: { src: string, width: number }[],
}
export function imageResizer(userOptions: Partial<Options> = {}): Plugin {
const options = { ...defaultOptions, ...userOptions };
const filter = createFilter(options.include, options.exclude);
return {
name: 'my-plugin',
async load(id: string): Promise<LoadResult> {
if (!filter(id)) {
return null;
}
const data = await fs.readFile(id, 'binary');
const rawData = Buffer.from(data, 'binary');
const img = sharp(rawData).rotate(/* handle EXIF orientation */);
const { width, height } = await img.metadata();
if (!width || !height) {
throw new Error(`Image metadata not found for ${id}`);
}
// small enough files are simply inlined
if (rawData.length < options.inlineSizeLimit) {
const encoded = rawData.toString('base64');
const mimeType = mime.getType(id);
const src = `data:${mimeType};base64,${encoded}`;
const value: ImageImport = {
src,
width,
height,
images: [{ src, width }],
srcset: '',
};
return `export default ${JSON.stringify(value)}`;
}
const fileExt = extname(id);
const baseFileName = basename(id, fileExt);
const goodSizes = options.sizes.filter(s => s < width);
const images = await Promise.all(goodSizes.map(async size =>({
width: size,
src: `${baseFileName}-${size}${fileExt}`,
data:
await img.clone()
.resize(size, null)
.jpeg({mozjpeg: true})
.toBuffer()
})));
// also include unmodified version
images.push({ width, src: `${baseFileName}${fileExt}`, data: rawData })
for (const img of images) {
const refId = this.emitFile({
type: 'asset',
name: img.src,
source: img.data,
});
img.src = `import.meta.ROLLUP_FILE_URL_${refId}`;
}
const value: ImageImport = {
src: images[images.length-1].src,
width, height,
images: images.map(img => ({ src: img.src, width: img.width })),
srcset: images.map(img => `${img.src} ${img.width}w`).join(', '),
};
return `export default ${JSON.stringify(value)}`;
},
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment