Skip to content

Commit 9bcc12c

Browse files
ascorbicLBgatsbybot
authored
feat(gatsby-plugin-image): Change fullWidth to use breakpoints (#29002)
* Use breakpoints in fluid * Don't add own size to full width, unless to replace larger bp * Update tests * Add breakpoints to plugin helper * Update image utils test * Update readme * Update packages/gatsby-plugin-sharp/src/image-data.ts Co-authored-by: LB <[email protected]> * Fix test * Fix defaults * Correctly handle default image for fullWidth Co-authored-by: LB <[email protected]> Co-authored-by: gatsbybot <[email protected]>
1 parent 168ff60 commit 9bcc12c

File tree

9 files changed

+147
-69
lines changed

9 files changed

+147
-69
lines changed

e2e-tests/visual-regression/src/pages/images/fullWidth.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const Page = () => {
99
query {
1010
file(relativePath: { eq: "cornwall.jpg" }) {
1111
childImageSharp {
12-
gatsbyImageData(width: 1024, layout: FULL_WIDTH)
12+
gatsbyImageData(layout: FULL_WIDTH)
1313
}
1414
}
1515
}

e2e-tests/visual-regression/src/pages/static-images/fullWidth.js

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const Page = () => {
1212
src="../../images/cornwall.jpg"
1313
loading="eager"
1414
layout="fullWidth"
15-
width={1024}
1615
alt="cornwall"
1716
/>
1817
</TestWrapper>

packages/gatsby-plugin-image/README.md

+11-9
Original file line numberDiff line numberDiff line change
@@ -316,26 +316,28 @@ The optional helper function `getImage` takes a file node and returns `file?.chi
316316

317317
These arguments can be passed to the `gatsbyImageData()` resolver:
318318

319-
- **width**: The display width of the generated image for layout = FIXED, if layout = CONSTRAINED it's the display width of the largest generated image. The actual largest image resolution will be this value multiplied by the largest value in outputPixelDensities.
320-
- **height**: If set, the height of the generated image. If omitted, it is calculated from the supplied width, matching the aspect ratio of the source image.
319+
- **width**: The display width of the generated image for layout = FIXED, if layout = CONSTRAINED it's the maximum display width. Ignored for FULL_WIDTH images.
320+
- **height**: If set, the height of the generated image. If omitted, it is calculated from the supplied width, matching the aspect ratio of the source image. Ignored for FULL_WIDTH images.
321+
- **aspectRatio**: Forces an image to the specified aspect ratio, cropping if needed. The value is a number, but can be clearer to express as a fraction, e.g. `aspectRatio={16/9}`
321322
- **placeholder**: Format of generated placeholder image.
322-
- `BLURRED`: (default) a blurred, low resolution image, encoded as a base64 data URI
323-
- `TRACED_SVG`: a low-resolution traced SVG of the image.
323+
- `DOMINANT_COLOR`: (default) A solid color, calculated from the dominant color of the image.
324+
- `BLURRED`: a blurred, low resolution image, encoded as a base64 data URI
325+
- `TRACED_SVG`: a single-color traced SVG of the image.
324326
- `NONE`: no placeholder. Set "background" to use a fixed background color.
325-
- `DOMINANT_COLOR`: a solid color, calculated from the dominant color of the image.
326327
- **layout**: The layout for the image.
327328
- `CONSTRAINED`: (default) Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size.
328329
- `FIXED`: A static image size, that does not resize according to the screen width
329330
- `FULL_WIDTH`: The image resizes to fit its container. Pass a "sizes" option if it isn't going to be the full width of the screen.
330-
- **outputPixelDensities**: A list of image pixel densities to generate, for high-resolution (retina) screens. It will never generate images larger than the source, and will always include a 1x image.
331-
Default is `[ 0.25, 0.5, 1, 2 ]`, for fullWidth/constrained images, and `[ 1, 2 ]` for fixed.
332331
- **sizes**: The "[sizes](https://developer.mozilla.org/en-US/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images)" attribute, passed to the `<img>` tag. This describes the display size of the image. This does not affect the generated images, but is used by the browser to decide which images to download. You can leave this blank for fixed images, or if the responsive image container will be the full width of the screen. In these cases we will generate an appropriate value. If, however, you are generating responsive images that are not the full width of the screen, you should provide a sizes property for best performance. You can alternatively pass this value to the component.
333332
- **formats**: an array of file formats to generate. The default is `[AUTO, WEBP]`, which means it will generate images in the same format as the source image, as well as in the next-generation [WebP](https://developers.google.com/speed/webp) format. We strongly recommend you do not change this option, as doing so will affect performance scores.
334333
- **quality**: The default quality. This is overridden by any format-specific options
335-
- **blurredOptions**: Options for the low-resolution placeholder image. Set placeholder to "BLURRED" to use this
334+
- **outputPixelDensities**: A list of image pixel densities to generate, for high-resolution (retina) screens. It will never generate images larger than the source, and will always include a 1x image.
335+
Default is `[ 0.25, 0.5, 1, 2 ]`, for `CONSTRAINED` images, and `[ 1, 2 ]` for `FIXED`. Ignored for `FULL_WIDTH`, which uses `breakpoints` instead.
336+
- **breakpoints**: Output widths to generate for full width images. Default is `[750, 1080, 1366, 1920]`, which is suitable for most common device resolutions. It will never generate an image larger than the source image. The browser will automatically choose the most appropriate.
337+
- **blurredOptions**: Options for the low-resolution placeholder image. Set placeholder to `BLURRED` to use this
336338
- width
337339
- toFormat
338-
- **tracedSVGOptions**: Options for traced placeholder SVGs. You also should set placeholder to "SVG".
340+
- **tracedSVGOptions**: Options for traced placeholder SVGs. You also should set placeholder to `TRACED_SVG`.
339341
- **jpgOptions**: Options to pass to sharp when generating JPG images.
340342
- quality
341343
- progressive

packages/gatsby-plugin-image/src/__tests__/image-utils.ts

+11-4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ const args: IGatsbyImageHelperArgs = {
3737

3838
const fluidArgs: IGatsbyImageHelperArgs = {
3939
...args,
40+
sourceMetadata: {
41+
width: 2000,
42+
height: 1500,
43+
format: `jpg`,
44+
},
4045
layout: `fullWidth`,
4146
}
4247

@@ -179,12 +184,14 @@ describe(`the image data helper`, () => {
179184
it(`returns URLs for fullWidth`, () => {
180185
const data = generateImageData(fluidArgs)
181186
expect(data?.images?.fallback?.src).toEqual(
182-
`https://example.com/afile.jpg/400/300/image.jpg`
187+
`https://example.com/afile.jpg/750/563/image.jpg`
183188
)
184189

185-
expect(data.images?.sources?.[0].srcSet).toEqual(
186-
`https://example.com/afile.jpg/100/75/image.webp 100w,\nhttps://example.com/afile.jpg/200/150/image.webp 200w,\nhttps://example.com/afile.jpg/400/300/image.webp 400w,\nhttps://example.com/afile.jpg/800/600/image.webp 800w`
187-
)
190+
expect(data.images?.sources?.[0].srcSet)
191+
.toEqual(`https://example.com/afile.jpg/750/563/image.webp 750w,
192+
https://example.com/afile.jpg/1080/810/image.webp 1080w,
193+
https://example.com/afile.jpg/1366/1025/image.webp 1366w,
194+
https://example.com/afile.jpg/1920/1440/image.webp 1920w`)
188195
})
189196

190197
it(`converts to PNG if requested`, () => {

packages/gatsby-plugin-image/src/image-utils.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { IGatsbyImageData } from "."
44
import type sharp from "gatsby-plugin-sharp/safe-sharp"
55

66
const DEFAULT_PIXEL_DENSITIES = [0.25, 0.5, 1, 2]
7+
const DEFAULT_BREAKPOINTS = [750, 1080, 1366, 1920]
78
const DEFAULT_FLUID_WIDTH = 800
89
const DEFAULT_FIXED_WIDTH = 400
910

@@ -38,6 +39,7 @@ export interface ISharpGatsbyImageArgs {
3839
webpOptions?: Record<string, unknown>
3940
avifOptions?: Record<string, unknown>
4041
blurredOptions?: { width?: number; toFormat?: ImageFormat }
42+
breakpoints?: Array<number>
4143
}
4244

4345
export interface IImageSizeArgs {
@@ -46,6 +48,7 @@ export interface IImageSizeArgs {
4648
layout?: Layout
4749
filename: string
4850
outputPixelDensities?: Array<number>
51+
breakpoints?: Array<number>
4952
fit?: Fit
5053
reporter?: IReporter
5154
sourceMetadata: { width: number; height: number }
@@ -89,6 +92,7 @@ export interface IGatsbyImageHelperArgs {
8992
sourceMetadata?: { width: number; height: number; format: ImageFormat }
9093
fit?: Fit
9194
options?: Record<string, unknown>
95+
breakpoints?: Array<number>
9296
}
9397

9498
const warn = (message: string): void => console.warn(message)
@@ -292,8 +296,10 @@ export function calculateImageSizes(args: IImageSizeArgs): IImageSizes {
292296

293297
if (layout === `fixed`) {
294298
return fixedImageSizes(args)
295-
} else if (layout === `fullWidth` || layout === `constrained`) {
299+
} else if (layout === `constrained`) {
296300
return responsiveImageSizes(args)
301+
} else if (layout === `fullWidth`) {
302+
return responsiveImageSizes({ breakpoints: DEFAULT_BREAKPOINTS, ...args })
297303
} else {
298304
reporter.warn(
299305
`No valid layout was provided for the image at ${filename}. Valid image layouts are fixed, fullWidth, and constrained.`
@@ -386,6 +392,8 @@ export function responsiveImageSizes({
386392
height,
387393
fit = `cover`,
388394
outputPixelDensities = DEFAULT_PIXEL_DENSITIES,
395+
breakpoints,
396+
layout,
389397
}: IImageSizeArgs): IImageSizes {
390398
let sizes
391399
let aspectRatio = imgDimensions.width / imgDimensions.height
@@ -430,11 +438,23 @@ export function responsiveImageSizes({
430438

431439
width = Math.round(width)
432440

433-
sizes = densities.map(density => Math.round(density * (width as number)))
434-
sizes = sizes.filter(size => size <= imgDimensions.width)
441+
if (breakpoints?.length > 0) {
442+
sizes = breakpoints.filter(size => size <= imgDimensions.width)
443+
444+
// If a larger breakpoint has been filtered-out, add the actual image width instead
445+
if (
446+
sizes.length < breakpoints.length &&
447+
!sizes.includes(imgDimensions.width)
448+
) {
449+
sizes.push(imgDimensions.width)
450+
}
451+
} else {
452+
sizes = densities.map(density => Math.round(density * (width as number)))
453+
sizes = sizes.filter(size => size <= imgDimensions.width)
454+
}
435455

436456
// ensure that the size passed in is included in the final output
437-
if (!sizes.includes(width)) {
457+
if (layout === `constrained` && !sizes.includes(width)) {
438458
sizes.push(width)
439459
}
440460
sizes = sizes.sort(sortNumeric)

packages/gatsby-plugin-sharp/src/__tests__/utils.js

+56-18
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe(`calculateImageSizes (fixed)`, () => {
103103
imgDimensions,
104104
}
105105
const { sizes } = calculateImageSizes(args)
106-
expect(sizes).toEqual(expect.arrayContaining([120, 240]))
106+
expect(sizes).toEqual([120, 240])
107107
})
108108

109109
it(`should create images of different sizes based on pixel densities with a given height`, () => {
@@ -114,7 +114,7 @@ describe(`calculateImageSizes (fixed)`, () => {
114114
imgDimensions,
115115
}
116116
const { sizes } = calculateImageSizes(args)
117-
expect(sizes).toEqual(expect.arrayContaining([120, 240]))
117+
expect(sizes).toEqual([120, 240])
118118
})
119119
})
120120

@@ -172,7 +172,7 @@ describe(`calculateImageSizes (fullWidth & constrained)`, () => {
172172
imgDimensions,
173173
}
174174
const { sizes } = calculateImageSizes(args)
175-
expect(sizes).toEqual(expect.arrayContaining([80, 160, 320, 640]))
175+
expect(sizes).toEqual([80, 160, 320, 640])
176176
})
177177

178178
it(`should create images of different sizes (0.25x, 0.5x, 1x) without any defined size provided`, () => {
@@ -182,63 +182,101 @@ describe(`calculateImageSizes (fullWidth & constrained)`, () => {
182182
imgDimensions,
183183
}
184184
const { sizes } = calculateImageSizes(args)
185-
expect(sizes).toEqual(expect.arrayContaining([200, 400, 800]))
185+
expect(sizes).toEqual([200, 400, 800])
186186
})
187187

188-
it(`should return sizes of provided srcSetBreakpoints`, () => {
189-
const srcSetBreakpoints = [50, 70, 150, 250, 300]
188+
it(`should return sizes of provided breakpoints in fullWidth`, () => {
189+
const breakpoints = [50, 70, 150, 250, 300]
190190
const width = 500
191191
const args = {
192192
layout: `fullWidth`,
193193
width,
194-
srcSetBreakpoints,
194+
breakpoints,
195195
file,
196196
imgDimensions,
197197
reporter,
198198
}
199199

200200
const { sizes } = calculateImageSizes(args)
201-
expect(sizes).toEqual(expect.arrayContaining([50, 70, 150, 250, 300, 500]))
201+
expect(sizes).toEqual([50, 70, 150, 250, 300])
202202
})
203203

204-
it(`should reject any srcSetBreakpoints larger than the original width`, () => {
205-
const srcSetBreakpoints = [
204+
it(`should include provided width along with breakpoints in constrained`, () => {
205+
const breakpoints = [50, 70, 150, 250, 300]
206+
const width = 500
207+
const args = {
208+
layout: `constrained`,
209+
width,
210+
breakpoints,
211+
file,
212+
imgDimensions,
213+
reporter,
214+
}
215+
216+
const { sizes } = calculateImageSizes(args)
217+
expect(sizes).toEqual([50, 70, 150, 250, 300, 500])
218+
})
219+
220+
it(`should reject any breakpoints larger than the original width`, () => {
221+
const breakpoints = [
206222
50,
207223
70,
208224
150,
209225
250,
210-
1250, // shouldn't be included, larger than original width
226+
1200,
227+
1800, // shouldn't be included, larger than original width
211228
]
212229
const width = 1500 // also shouldn't be included
213230
const args = {
214231
layout: `fullWidth`,
215232
width,
216-
srcSetBreakpoints,
233+
breakpoints,
234+
file,
235+
imgDimensions,
236+
reporter,
237+
}
238+
239+
const { sizes } = calculateImageSizes(args)
240+
expect(sizes).toEqual([50, 70, 150, 250, 1200])
241+
})
242+
243+
it(`should add the original width instead of larger breakpoints`, () => {
244+
const breakpoints = [
245+
50,
246+
70,
247+
150,
248+
250,
249+
1800, // shouldn't be included, larger than original width
250+
]
251+
const width = 1300
252+
const args = {
253+
layout: `fullWidth`,
254+
width,
255+
breakpoints,
217256
file,
218257
imgDimensions,
219258
reporter,
220259
}
221260

222261
const { sizes } = calculateImageSizes(args)
223-
expect(sizes).toEqual(expect.arrayContaining([50, 70, 150, 250]))
224-
expect(sizes).toEqual(expect.not.arrayContaining([1250, 1500]))
262+
expect(sizes).toEqual([50, 70, 150, 250, 1200])
225263
})
226264

227-
it(`should only uses sizes from srcSetBreakpoints when outputPixelDensities are also passed in`, () => {
228-
const srcSetBreakpoints = [400, 800] // should find these
265+
it(`should ignore outputPixelDensities when breakpoints are passed in`, () => {
266+
const breakpoints = [400, 800] // should find these
229267
const width = 500
230268
const args = {
231269
layout: `fullWidth`,
232270
width,
233271
outputPixelDensities: [2, 4], // and ignore these, ie [1000, 2000]
234-
srcSetBreakpoints,
272+
breakpoints,
235273
file,
236274
imgDimensions,
237275
reporter,
238276
}
239277

240278
const { sizes } = calculateImageSizes(args)
241-
expect(sizes).toEqual(expect.arrayContaining([400, 500, 800]))
279+
expect(sizes).toEqual([400, 800])
242280
})
243281

244282
it(`should adjust fullWidth sizes according to fit type`, () => {

packages/gatsby-plugin-sharp/src/image-data.ts

+15-10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { createTransformObject } from "./plugin-options"
99

1010
const DEFAULT_BLURRED_IMAGE_WIDTH = 20
1111

12+
const DEFAULT_BREAKPOINTS = [750, 1080, 1366, 1920]
13+
1214
type ImageFormat = "jpg" | "png" | "webp" | "avif" | "" | "auto"
1315
export type FileNode = Node & {
1416
absolutePath?: string
@@ -84,14 +86,20 @@ export async function generateImageData({
8486
}: IImageDataArgs): Promise<IGatsbyImageData | undefined> {
8587
const {
8688
layout = `constrained`,
87-
placeholder = `blurred`,
89+
placeholder = `dominantColor`,
8890
tracedSVGOptions = {},
8991
transformOptions = {},
9092
quality,
9193
} = args
9294

9395
args.formats = args.formats || [`auto`, `webp`]
9496

97+
if (layout === `fullWidth`) {
98+
args.breakpoints = args.breakpoints?.length
99+
? args.breakpoints
100+
: DEFAULT_BREAKPOINTS
101+
}
102+
95103
const {
96104
fit = `cover`,
97105
cropFocus = sharp.strategy.attention,
@@ -103,16 +111,12 @@ export async function generateImageData({
103111
reporter.warn(
104112
`Specifying fullWidth images will ignore the width and height arguments, you may want a constrained image instead. Otherwise, use the breakpoints argument.`
105113
)
106-
args.width = undefined
114+
args.width = metadata.width
107115
args.height = undefined
108116
}
109117

110118
if (!args.width && !args.height && metadata.width) {
111-
if (layout === `fullWidth`) {
112-
args.width = Math.round(metadata.width / 2)
113-
} else {
114-
args.width = metadata.width
115-
}
119+
args.width = metadata.width
116120
}
117121

118122
if (args.aspectRatio) {
@@ -206,9 +210,10 @@ export async function generateImageData({
206210

207211
const sizes = args.sizes || getSizes(imageSizes.unscaledWidth, layout)
208212

209-
const primaryIndex = imageSizes.sizes.findIndex(
210-
size => size === imageSizes.unscaledWidth
211-
)
213+
const primaryIndex =
214+
layout === `fullWidth`
215+
? imageSizes.sizes.length - 1 // The largest image
216+
: imageSizes.sizes.findIndex(size => size === imageSizes.unscaledWidth)
212217

213218
if (primaryIndex === -1) {
214219
reporter.error(

0 commit comments

Comments
 (0)