From 1e5d8ba9af4eb017382263653216e5247d96ab79 Mon Sep 17 00:00:00 2001 From: Oussama Bennaci <9404365+obennaci@users.noreply.github.com> Date: Fri, 9 Sep 2022 21:13:59 +0100 Subject: [PATCH] [@astrojs/image] support additional resize options (#4438) * working draft * more sharp params * add changeset * fix typing * wip * add missing docblocks * update lock file * remove enlargement and reduction resize options * add tests * Add docs * support crop options in pictures * cleanup * define crop types in docs * cleanup * remove kernel option Co-authored-by: Tony Sullivan --- .changeset/breezy-flowers-pay.md | 5 +++ packages/integrations/image/README.md | 44 ++++++++++++++++++- .../image/components/Picture.astro | 12 ++++- .../integrations/image/src/lib/get-picture.ts | 12 +++-- .../integrations/image/src/loaders/index.ts | 37 ++++++++++++++++ .../integrations/image/src/loaders/sharp.ts | 35 ++++++++++++--- .../integrations/image/test/sharp.test.js | 6 +++ 7 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 .changeset/breezy-flowers-pay.md diff --git a/.changeset/breezy-flowers-pay.md b/.changeset/breezy-flowers-pay.md new file mode 100644 index 0000000000..04c16a239e --- /dev/null +++ b/.changeset/breezy-flowers-pay.md @@ -0,0 +1,5 @@ +--- +'@astrojs/image': minor +--- + +Support additional Sharp resize options diff --git a/packages/integrations/image/README.md b/packages/integrations/image/README.md index d1eb00ab37..fe3a699011 100644 --- a/packages/integrations/image/README.md +++ b/packages/integrations/image/README.md @@ -205,7 +205,27 @@ The parameter can be a [named HTML color](https://www.w3schools.com/tags/ref_col color representation with 3 or 6 hexadecimal characters in the form `#123[abc]`, or an RGB definition in the form `rgb(100,100,100)`. -### ` +#### fit + +

+ +**Type:** `'cover' | 'contain' | 'fill' | 'inside' | 'outside'`
+**Default:** `'cover'` +

+ +How the image should be resized to fit both `height` and `width`. + +#### position + +

+ +**Type:** `'top' | 'right top' | 'right' | 'right bottom' | 'bottom' | 'left bottom' | 'left' | 'left top' | 'north' | 'northeast' | 'east' | 'southeast' | 'south' | 'southwest' | 'west' | 'northwest' | 'center' | 'centre' | 'cover' | 'entropy' | 'attention'`
+**Default:** `'centre'` +

+ +Position of the crop when fit is `cover` or `contain`. + +### `` #### src @@ -304,6 +324,28 @@ The parameter can be a [named HTML color](https://www.w3schools.com/tags/ref_col color representation with 3 or 6 hexadecimal characters in the form `#123[abc]`, or an RGB definition in the form `rgb(100,100,100)`. +#### fit + +

+ +**Type:** `'cover' | 'contain' | 'fill' | 'inside' | 'outside'`
+**Default:** `'cover'` +

+ +How the image should be resized to fit both `height` and `width`. + +#### position + +

+ +**Type:** `'top' | 'right top' | 'right' | 'right bottom' | 'bottom' | 'left bottom' | 'left' | 'left top' | + 'north' | 'northeast' | 'east' | 'southeast' | 'south' | 'southwest' | 'west' | 'northwest' | + 'center' | 'centre' | 'cover' | 'entropy' | 'attention'`
+**Default:** `'centre'` +

+ +Position of the crop when fit is `cover` or `contain`. + ### `getImage` This is the helper function used by the `` component to build `` attributes for the transformed image. This helper can be used directly for more complex use cases that aren't currently supported by the `` component. diff --git a/packages/integrations/image/components/Picture.astro b/packages/integrations/image/components/Picture.astro index e28f5bf409..a2d80354a3 100644 --- a/packages/integrations/image/components/Picture.astro +++ b/packages/integrations/image/components/Picture.astro @@ -38,7 +38,9 @@ const { sizes, widths, aspectRatio, + fit, background, + position, formats = ['avif', 'webp'], loading = 'lazy', decoding = 'async', @@ -49,7 +51,15 @@ if (alt === undefined || alt === null) { warnForMissingAlt(); } -const { image, sources } = await getPicture({ src, widths, formats, aspectRatio, background }); +const { image, sources } = await getPicture({ + src, + widths, + formats, + aspectRatio, + fit, + background, + position, +}); --- diff --git a/packages/integrations/image/src/lib/get-picture.ts b/packages/integrations/image/src/lib/get-picture.ts index 2476686b73..132d93ee1b 100644 --- a/packages/integrations/image/src/lib/get-picture.ts +++ b/packages/integrations/image/src/lib/get-picture.ts @@ -10,7 +10,9 @@ export interface GetPictureParams { widths: number[]; formats: OutputFormat[]; aspectRatio?: TransformOptions['aspectRatio']; + fit?: TransformOptions['fit']; background?: TransformOptions['background']; + position?: TransformOptions['position']; } export interface GetPictureResult { @@ -41,7 +43,7 @@ async function resolveFormats({ src, formats }: GetPictureParams) { } export async function getPicture(params: GetPictureParams): Promise { - const { src, widths } = params; + const { src, widths, fit, position, background } = params; if (!src) { throw new Error('[@astrojs/image] `src` is required'); @@ -64,8 +66,10 @@ export async function getPicture(params: GetPictureParams): Promise getSource(format))); diff --git a/packages/integrations/image/src/loaders/index.ts b/packages/integrations/image/src/loaders/index.ts index 5001a17a99..801a19300b 100644 --- a/packages/integrations/image/src/loaders/index.ts +++ b/packages/integrations/image/src/loaders/index.ts @@ -21,6 +21,31 @@ export type ColorDefinition = | `rgb(${number}, ${number}, ${number})` | `rgb(${number},${number},${number})`; +export type CropFit = 'cover' | 'contain' | 'fill' | 'inside' | 'outside'; + +export type CropPosition = + | 'top' + | 'right top' + | 'right' + | 'right bottom' + | 'bottom' + | 'left bottom' + | 'left' + | 'left top' + | 'north' + | 'northeast' + | 'east' + | 'southeast' + | 'south' + | 'southwest' + | 'west' + | 'northwest' + | 'center' + | 'centre' + | 'cover' + | 'entropy' + | 'attention'; + export function isOutputFormat(value: string): value is OutputFormat { return ['avif', 'jpeg', 'png', 'webp'].includes(value); } @@ -105,6 +130,18 @@ export interface TransformOptions { * @example "rgb(255, 255, 255)" - an rgb color */ background?: ColorDefinition; + /** + * How the image should be resized to fit both `height` and `width`. + * + * @default 'cover' + */ + fit?: CropFit; + /** + * Position of the crop when fit is `cover` or `contain`. + * + * @default 'centre' + */ + position?: CropPosition; } export interface HostedImageService { diff --git a/packages/integrations/image/src/loaders/sharp.ts b/packages/integrations/image/src/loaders/sharp.ts index 09a653375d..ac1f10c8ac 100644 --- a/packages/integrations/image/src/loaders/sharp.ts +++ b/packages/integrations/image/src/loaders/sharp.ts @@ -1,11 +1,12 @@ import sharp from 'sharp'; -import { isAspectRatioString, isColor, isOutputFormat } from '../loaders/index.js'; +import { ColorDefinition, isAspectRatioString, isColor, isOutputFormat } from '../loaders/index.js'; import type { OutputFormat, SSRImageService, TransformOptions } from './index.js'; class SharpService implements SSRImageService { async getImageAttributes(transform: TransformOptions) { // strip off the known attributes - const { width, height, src, format, quality, aspectRatio, background, ...rest } = transform; + const { width, height, src, format, quality, aspectRatio, fit, position, background, ...rest } = + transform; return { ...rest, @@ -37,10 +38,18 @@ class SharpService implements SSRImageService { searchParams.append('ar', transform.aspectRatio.toString()); } + if (transform.fit) { + searchParams.append('fit', transform.fit); + } + if (transform.background) { searchParams.append('bg', transform.background); } + if (transform.position) { + searchParams.append('p', encodeURI(transform.position)); + } + return { searchParams }; } @@ -76,11 +85,16 @@ class SharpService implements SSRImageService { } } + if (searchParams.has('fit')) { + transform.fit = searchParams.get('fit') as typeof transform.fit; + } + + if (searchParams.has('p')) { + transform.position = decodeURI(searchParams.get('p')!) as typeof transform.position; + } + if (searchParams.has('bg')) { - const background = searchParams.get('bg')!; - if (isColor(background)) { - transform.background = background; - } + transform.background = searchParams.get('bg') as ColorDefinition | undefined; } return transform; @@ -95,7 +109,14 @@ class SharpService implements SSRImageService { if (transform.width || transform.height) { const width = transform.width && Math.round(transform.width); const height = transform.height && Math.round(transform.height); - sharpImage.resize(width, height); + + sharpImage.resize({ + width, + height, + fit: transform.fit, + position: transform.position, + background: transform.background, + }); } // remove alpha channel and replace with background color if requested diff --git a/packages/integrations/image/test/sharp.test.js b/packages/integrations/image/test/sharp.test.js index 81172504bb..8e2d1d3afd 100644 --- a/packages/integrations/image/test/sharp.test.js +++ b/packages/integrations/image/test/sharp.test.js @@ -15,6 +15,8 @@ describe('Sharp service', () => { ['aspect ratio string', { src, aspectRatio: '16:9' }], ['aspect ratio float', { src, aspectRatio: 1.7 }], ['background color', { src, format: 'jpeg', background: '#333333' }], + ['crop fit', { src, fit: 'cover' }], + ['crop position', { src, position: 'center' }], ].forEach(([description, props]) => { it(description, async () => { const { searchParams } = await sharp.serializeTransform(props); @@ -32,6 +34,8 @@ describe('Sharp service', () => { verifyProp(props.width, 'w'); verifyProp(props.height, 'h'); verifyProp(props.aspectRatio, 'ar'); + verifyProp(props.fit, 'fit'); + verifyProp(props.position, 'p'); verifyProp(props.background, 'bg'); }); }); @@ -55,6 +59,8 @@ describe('Sharp service', () => { `f=jpeg&bg=%23333333&href=${href}`, { src, format: 'jpeg', background: '#333333' }, ], + ['crop fit', `fit=contain&href=${href}`, { src, fit: 'contain' }], + ['crop position', `p=right%20top&href=${href}`, { src, position: 'right top' }], ].forEach(([description, params, expected]) => { it(description, async () => { const searchParams = new URLSearchParams(params);