Skip to content

Commit

Permalink
[@astrojs/image] support additional resize options (#4438)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
obennaci and Tony Sullivan authored Sep 9, 2022
1 parent 2737cab commit 1e5d8ba
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/breezy-flowers-pay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/image': minor
---

Support additional Sharp resize options
44 changes: 43 additions & 1 deletion packages/integrations/image/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)`.

### `<Picture /`>
#### fit

<p>

**Type:** `'cover' | 'contain' | 'fill' | 'inside' | 'outside'` <br>
**Default:** `'cover'`
</p>

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

#### position

<p>

**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'` <br>
**Default:** `'centre'`
</p>

Position of the crop when fit is `cover` or `contain`.

### `<Picture />`

#### src

Expand Down Expand Up @@ -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

<p>

**Type:** `'cover' | 'contain' | 'fill' | 'inside' | 'outside'` <br>
**Default:** `'cover'`
</p>

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

#### position

<p>

**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'` <br>
**Default:** `'centre'`
</p>

Position of the crop when fit is `cover` or `contain`.

### `getImage`

This is the helper function used by the `<Image />` component to build `<img />` attributes for the transformed image. This helper can be used directly for more complex use cases that aren't currently supported by the `<Image />` component.
Expand Down
12 changes: 11 additions & 1 deletion packages/integrations/image/components/Picture.astro
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ const {
sizes,
widths,
aspectRatio,
fit,
background,
position,
formats = ['avif', 'webp'],
loading = 'lazy',
decoding = 'async',
Expand All @@ -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,
});
---

<picture {...attrs}>
Expand Down
12 changes: 9 additions & 3 deletions packages/integrations/image/src/lib/get-picture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -41,7 +43,7 @@ async function resolveFormats({ src, formats }: GetPictureParams) {
}

export async function getPicture(params: GetPictureParams): Promise<GetPictureResult> {
const { src, widths } = params;
const { src, widths, fit, position, background } = params;

if (!src) {
throw new Error('[@astrojs/image] `src` is required');
Expand All @@ -64,8 +66,10 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe
src,
format,
width,
fit,
position,
background,
height: Math.round(width / aspectRatio!),
background: params.background,
});
return `${img.src} ${width}w`;
})
Expand All @@ -84,8 +88,10 @@ export async function getPicture(params: GetPictureParams): Promise<GetPictureRe
src,
width: Math.max(...widths),
aspectRatio,
fit,
position,
background,
format: allFormats[allFormats.length - 1],
background: params.background,
});

const sources = await Promise.all(allFormats.map((format) => getSource(format)));
Expand Down
37 changes: 37 additions & 0 deletions packages/integrations/image/src/loaders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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<T extends TransformOptions = TransformOptions> {
Expand Down
35 changes: 28 additions & 7 deletions packages/integrations/image/src/loaders/sharp.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 };
}

Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions packages/integrations/image/test/sharp.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
});
});
Expand All @@ -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);
Expand Down

0 comments on commit 1e5d8ba

Please sign in to comment.