Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/common-carrots-follow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/enhanced-img': minor
---

feat: add configuration options
74 changes: 47 additions & 27 deletions packages/enhanced-img/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,42 @@ import { imagetools } from 'vite-imagetools';
import { image_plugin } from './vite-plugin.js';

/**
* @param {import('types/index.js').VitePluginOptions} [opts]
* @returns {import('vite').Plugin[]}
*/
export function enhancedImages() {
const imagetools_instance = imagetools_plugin();
export function enhancedImages(opts) {
const imagetools_instance = imagetools_plugin(opts);
return !process.versions.webcontainer
? [image_plugin(imagetools_instance), imagetools_instance]
: [];
}

/**
* @param {import('sharp').Metadata} meta
* @returns {string}
* @param {import('types/index.js').VitePluginOptions} [opts]
* @returns {import('vite').Plugin}
*/
function fallback_format(meta) {
if (meta.pages && meta.pages > 1) {
return meta.format === 'tiff' ? 'tiff' : 'gif';
}
if (meta.hasAlpha) {
return 'png';
}
return 'jpg';
}
function imagetools_plugin(opts) {
const get_formats = opts?.defaultFormats ?? default_formats;
const get_widths = opts?.defaultWidths ?? default_widths;

function imagetools_plugin() {
/** @type {Partial<import('vite-imagetools').VitePluginOptions>} */
const imagetools_opts = {
defaultDirectives: async ({ pathname, searchParams: qs }, metadata) => {
if (!qs.has('enhanced')) return new URLSearchParams();
...opts?.imagetools,

defaultDirectives: async (url, metadata) => {
const { pathname, searchParams: qs } = url;

if (!qs.has('enhanced')) {
if (typeof opts?.imagetools?.defaultDirectives === 'function') {
return opts.imagetools.defaultDirectives(url, metadata);
}

if (opts?.imagetools?.defaultDirectives) {
return opts.imagetools.defaultDirectives;
}

return new URLSearchParams();
}

const meta = await metadata();
const img_width = qs.get('imgWidth');
Expand All @@ -41,29 +49,42 @@ function imagetools_plugin() {
return new URLSearchParams();
}

const { widths, kind } = get_widths(width, qs.get('imgSizes'));
return new URLSearchParams({
as: 'picture',
format: `avif;webp;${fallback_format(meta)}`,
w: widths.join(';'),
...(kind === 'x' && !qs.has('w') && { basePixels: widths[0].toString() })
format: get_formats(meta),
...get_widths(width, qs.get('imgSizes'))
});
},
namedExports: false
};

// TODO: should we make formats or sizes configurable besides just letting people override defaultDirectives?
// TODO: generate img rather than picture if only a single format is provided
// by resolving the directives for the URL in the preprocessor
return imagetools(imagetools_opts);
}

/**
* @param {import('sharp').Metadata} meta
* @returns {string}
*/
function default_formats(meta) {
let fallback = 'jpg';

if (meta.pages && meta.pages > 1) {
fallback = meta.format === 'tiff' ? 'tiff' : 'gif';
} else if (meta.hasAlpha) {
fallback = 'png';
}

return `avif;webp;${fallback}`;
}

/**
* @param {number} width
* @param {string | null} sizes
* @returns {{ widths: number[]; kind: 'w' | 'x' }}
* @returns {{ w: string; basePixels?: string }}
*/
function get_widths(width, sizes) {
function default_widths(width, sizes) {
// We don't really know what the user wants here. But if they have an image that's really big
// then we can probably assume they're always displaying it full viewport/breakpoint.
// If the user is displaying a responsive image then the size usually doesn't change that much
Expand All @@ -78,9 +99,7 @@ function get_widths(width, sizes) {
// https://screensiz.es/
// https://gs.statcounter.com/screen-resolution-stats (note: logical. we want physical)
// Include 1080 because lighthouse uses a moto g4 with 360 logical pixels and 3x pixel ratio.
const widths = [540, 768, 1080, 1366, 1536, 1920, 2560, 3000, 4096, 5120];
widths.push(width);
return { widths, kind: 'w' };
return { w: `540;768;1080;1366;1536;1920;2560;3000;4096;5120;${width}` };
}

// Don't need more than 2x resolution. Note that due to this optimization, pixel density
Expand All @@ -93,5 +112,6 @@ function get_widths(width, sizes) {
// data. Even true 3x resolution screens are wasteful as the human eye cannot see that level of
// detail without something like a magnifying glass.
// https://blog.twitter.com/engineering/en_us/topics/infrastructure/2019/capping-image-fidelity-on-ultra-high-resolution-devices.html
return { widths: [Math.round(width / 2), width], kind: 'x' };
const small_width = Math.round(width / 2).toString();
return { w: `${small_width};${width}`, basePixels: small_width };
}
63 changes: 60 additions & 3 deletions packages/enhanced-img/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,66 @@
import type { HTMLImgAttributes } from 'svelte/elements';
import type { Plugin } from 'vite';
import type { Picture } from 'vite-imagetools';
import type { Picture, VitePluginOptions as ImagetoolsOptions } from 'vite-imagetools';
import './ambient.js';
import { Metadata } from 'sharp';

export { Picture };
export type { Picture };

export type VitePluginOptions = {
/**
* Get the default formats for enhanced images.
*
* @param meta Metadata for the source image.
* @returns A ';'-separated list of output formats like 'avif;webp;jpg'.
*
* The default value is the following function:
*
* ```
* (meta) => {
* let fallback = 'jpg';
* if (meta.pages && meta.pages > 1) {
* fallback = meta.format === 'tiff' ? 'tiff' : 'gif';
* } else if (meta.hasAlpha) {
* fallback = 'png';
* }
* return `avif;webp;${fallback}`;
* }
* ```
*/
defaultFormats?: (meta: Metadata) => string;
/**
* Get the default widths for enhanced images.
*
* @param width Original image width in physical pixels.
* @param sizes The `sizes` attribute value from `<enhanced:img>`, or `null` when not provided.
* @returns Width configuration for `vite-imagetools` directives:
* - `w`: A ';'-separated list of target output widths.
* - `basePixels`: Optional base width used to generate pixel-density (`x`) descriptors.
*
* The default value is the following function:
*
* ```
* (width, sizes) => {
* if (sizes) {
* return { w: `540;768;1080;1366;1536;1920;2560;3000;4096;5120${width}` };
* }
*
* const small_width = Math.round(width / 2).toString();
* return { w: `${small_width};${width}`, basePixels: small_width };
* }
* ```
*/
defaultWidths?: (width: number, sizes: string | null) => { w: string; basePixels?: string };
/**
* Options for the 'vite-imagetools' plugin
*
* `namedExports` is always set to `false` and cannot be overridden.
*
* `defaultDirectives` is only used for images that aren't handled by the preprocessor,
* i.e. images that aren't imported with `?enhanced` query param and aren't imported through `<enhanced:img src="..." />`.
*/
imagetools?: Omit<ImagetoolsOptions, 'namedExports'>;
};

type EnhancedImgAttributes = Omit<HTMLImgAttributes, 'src'> & { src: string | Picture };

Expand All @@ -14,4 +71,4 @@ declare module 'svelte/elements' {
}
}

export function enhancedImages(): Promise<Plugin[]>;
export function enhancedImages(opts?: VitePluginOptions): Promise<Plugin[]>;
Loading