Skip to content

Commit

Permalink
feat(gatsby-plugin-image): Change fullWidth to use breakpoints (#29002)
Browse files Browse the repository at this point in the history
* 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 <laurie@gatsbyjs.com>

* Fix test

* Fix defaults

* Correctly handle default image for fullWidth

Co-authored-by: LB <laurie@gatsbyjs.com>
Co-authored-by: gatsbybot <mathews.kyle+gatsbybot@gmail.com>
  • Loading branch information
3 people committed Jan 14, 2021
1 parent 168ff60 commit 9bcc12c
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 69 deletions.
2 changes: 1 addition & 1 deletion e2e-tests/visual-regression/src/pages/images/fullWidth.js
Expand Up @@ -9,7 +9,7 @@ const Page = () => {
query {
file(relativePath: { eq: "cornwall.jpg" }) {
childImageSharp {
gatsbyImageData(width: 1024, layout: FULL_WIDTH)
gatsbyImageData(layout: FULL_WIDTH)
}
}
}
Expand Down
Expand Up @@ -12,7 +12,6 @@ const Page = () => {
src="../../images/cornwall.jpg"
loading="eager"
layout="fullWidth"
width={1024}
alt="cornwall"
/>
</TestWrapper>
Expand Down
20 changes: 11 additions & 9 deletions packages/gatsby-plugin-image/README.md
Expand Up @@ -316,26 +316,28 @@ The optional helper function `getImage` takes a file node and returns `file?.chi

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

- **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.
- **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.
- **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.
- **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.
- **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}`
- **placeholder**: Format of generated placeholder image.
- `BLURRED`: (default) a blurred, low resolution image, encoded as a base64 data URI
- `TRACED_SVG`: a low-resolution traced SVG of the image.
- `DOMINANT_COLOR`: (default) A solid color, calculated from the dominant color of the image.
- `BLURRED`: a blurred, low resolution image, encoded as a base64 data URI
- `TRACED_SVG`: a single-color traced SVG of the image.
- `NONE`: no placeholder. Set "background" to use a fixed background color.
- `DOMINANT_COLOR`: a solid color, calculated from the dominant color of the image.
- **layout**: The layout for the image.
- `CONSTRAINED`: (default) Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size.
- `FIXED`: A static image size, that does not resize according to the screen width
- `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.
- **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.
Default is `[ 0.25, 0.5, 1, 2 ]`, for fullWidth/constrained images, and `[ 1, 2 ]` for fixed.
- **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.
- **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.
- **quality**: The default quality. This is overridden by any format-specific options
- **blurredOptions**: Options for the low-resolution placeholder image. Set placeholder to "BLURRED" to use this
- **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.
Default is `[ 0.25, 0.5, 1, 2 ]`, for `CONSTRAINED` images, and `[ 1, 2 ]` for `FIXED`. Ignored for `FULL_WIDTH`, which uses `breakpoints` instead.
- **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.
- **blurredOptions**: Options for the low-resolution placeholder image. Set placeholder to `BLURRED` to use this
- width
- toFormat
- **tracedSVGOptions**: Options for traced placeholder SVGs. You also should set placeholder to "SVG".
- **tracedSVGOptions**: Options for traced placeholder SVGs. You also should set placeholder to `TRACED_SVG`.
- **jpgOptions**: Options to pass to sharp when generating JPG images.
- quality
- progressive
Expand Down
15 changes: 11 additions & 4 deletions packages/gatsby-plugin-image/src/__tests__/image-utils.ts
Expand Up @@ -37,6 +37,11 @@ const args: IGatsbyImageHelperArgs = {

const fluidArgs: IGatsbyImageHelperArgs = {
...args,
sourceMetadata: {
width: 2000,
height: 1500,
format: `jpg`,
},
layout: `fullWidth`,
}

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

expect(data.images?.sources?.[0].srcSet).toEqual(
`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`
)
expect(data.images?.sources?.[0].srcSet)
.toEqual(`https://example.com/afile.jpg/750/563/image.webp 750w,
https://example.com/afile.jpg/1080/810/image.webp 1080w,
https://example.com/afile.jpg/1366/1025/image.webp 1366w,
https://example.com/afile.jpg/1920/1440/image.webp 1920w`)
})

it(`converts to PNG if requested`, () => {
Expand Down
28 changes: 24 additions & 4 deletions packages/gatsby-plugin-image/src/image-utils.ts
Expand Up @@ -4,6 +4,7 @@ import { IGatsbyImageData } from "."
import type sharp from "gatsby-plugin-sharp/safe-sharp"

const DEFAULT_PIXEL_DENSITIES = [0.25, 0.5, 1, 2]
const DEFAULT_BREAKPOINTS = [750, 1080, 1366, 1920]
const DEFAULT_FLUID_WIDTH = 800
const DEFAULT_FIXED_WIDTH = 400

Expand Down Expand Up @@ -38,6 +39,7 @@ export interface ISharpGatsbyImageArgs {
webpOptions?: Record<string, unknown>
avifOptions?: Record<string, unknown>
blurredOptions?: { width?: number; toFormat?: ImageFormat }
breakpoints?: Array<number>
}

export interface IImageSizeArgs {
Expand All @@ -46,6 +48,7 @@ export interface IImageSizeArgs {
layout?: Layout
filename: string
outputPixelDensities?: Array<number>
breakpoints?: Array<number>
fit?: Fit
reporter?: IReporter
sourceMetadata: { width: number; height: number }
Expand Down Expand Up @@ -89,6 +92,7 @@ export interface IGatsbyImageHelperArgs {
sourceMetadata?: { width: number; height: number; format: ImageFormat }
fit?: Fit
options?: Record<string, unknown>
breakpoints?: Array<number>
}

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

if (layout === `fixed`) {
return fixedImageSizes(args)
} else if (layout === `fullWidth` || layout === `constrained`) {
} else if (layout === `constrained`) {
return responsiveImageSizes(args)
} else if (layout === `fullWidth`) {
return responsiveImageSizes({ breakpoints: DEFAULT_BREAKPOINTS, ...args })
} else {
reporter.warn(
`No valid layout was provided for the image at ${filename}. Valid image layouts are fixed, fullWidth, and constrained.`
Expand Down Expand Up @@ -386,6 +392,8 @@ export function responsiveImageSizes({
height,
fit = `cover`,
outputPixelDensities = DEFAULT_PIXEL_DENSITIES,
breakpoints,
layout,
}: IImageSizeArgs): IImageSizes {
let sizes
let aspectRatio = imgDimensions.width / imgDimensions.height
Expand Down Expand Up @@ -430,11 +438,23 @@ export function responsiveImageSizes({

width = Math.round(width)

sizes = densities.map(density => Math.round(density * (width as number)))
sizes = sizes.filter(size => size <= imgDimensions.width)
if (breakpoints?.length > 0) {
sizes = breakpoints.filter(size => size <= imgDimensions.width)

// If a larger breakpoint has been filtered-out, add the actual image width instead
if (
sizes.length < breakpoints.length &&
!sizes.includes(imgDimensions.width)
) {
sizes.push(imgDimensions.width)
}
} else {
sizes = densities.map(density => Math.round(density * (width as number)))
sizes = sizes.filter(size => size <= imgDimensions.width)
}

// ensure that the size passed in is included in the final output
if (!sizes.includes(width)) {
if (layout === `constrained` && !sizes.includes(width)) {
sizes.push(width)
}
sizes = sizes.sort(sortNumeric)
Expand Down
74 changes: 56 additions & 18 deletions packages/gatsby-plugin-sharp/src/__tests__/utils.js
Expand Up @@ -103,7 +103,7 @@ describe(`calculateImageSizes (fixed)`, () => {
imgDimensions,
}
const { sizes } = calculateImageSizes(args)
expect(sizes).toEqual(expect.arrayContaining([120, 240]))
expect(sizes).toEqual([120, 240])
})

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

Expand Down Expand Up @@ -172,7 +172,7 @@ describe(`calculateImageSizes (fullWidth & constrained)`, () => {
imgDimensions,
}
const { sizes } = calculateImageSizes(args)
expect(sizes).toEqual(expect.arrayContaining([80, 160, 320, 640]))
expect(sizes).toEqual([80, 160, 320, 640])
})

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

it(`should return sizes of provided srcSetBreakpoints`, () => {
const srcSetBreakpoints = [50, 70, 150, 250, 300]
it(`should return sizes of provided breakpoints in fullWidth`, () => {
const breakpoints = [50, 70, 150, 250, 300]
const width = 500
const args = {
layout: `fullWidth`,
width,
srcSetBreakpoints,
breakpoints,
file,
imgDimensions,
reporter,
}

const { sizes } = calculateImageSizes(args)
expect(sizes).toEqual(expect.arrayContaining([50, 70, 150, 250, 300, 500]))
expect(sizes).toEqual([50, 70, 150, 250, 300])
})

it(`should reject any srcSetBreakpoints larger than the original width`, () => {
const srcSetBreakpoints = [
it(`should include provided width along with breakpoints in constrained`, () => {
const breakpoints = [50, 70, 150, 250, 300]
const width = 500
const args = {
layout: `constrained`,
width,
breakpoints,
file,
imgDimensions,
reporter,
}

const { sizes } = calculateImageSizes(args)
expect(sizes).toEqual([50, 70, 150, 250, 300, 500])
})

it(`should reject any breakpoints larger than the original width`, () => {
const breakpoints = [
50,
70,
150,
250,
1250, // shouldn't be included, larger than original width
1200,
1800, // shouldn't be included, larger than original width
]
const width = 1500 // also shouldn't be included
const args = {
layout: `fullWidth`,
width,
srcSetBreakpoints,
breakpoints,
file,
imgDimensions,
reporter,
}

const { sizes } = calculateImageSizes(args)
expect(sizes).toEqual([50, 70, 150, 250, 1200])
})

it(`should add the original width instead of larger breakpoints`, () => {
const breakpoints = [
50,
70,
150,
250,
1800, // shouldn't be included, larger than original width
]
const width = 1300
const args = {
layout: `fullWidth`,
width,
breakpoints,
file,
imgDimensions,
reporter,
}

const { sizes } = calculateImageSizes(args)
expect(sizes).toEqual(expect.arrayContaining([50, 70, 150, 250]))
expect(sizes).toEqual(expect.not.arrayContaining([1250, 1500]))
expect(sizes).toEqual([50, 70, 150, 250, 1200])
})

it(`should only uses sizes from srcSetBreakpoints when outputPixelDensities are also passed in`, () => {
const srcSetBreakpoints = [400, 800] // should find these
it(`should ignore outputPixelDensities when breakpoints are passed in`, () => {
const breakpoints = [400, 800] // should find these
const width = 500
const args = {
layout: `fullWidth`,
width,
outputPixelDensities: [2, 4], // and ignore these, ie [1000, 2000]
srcSetBreakpoints,
breakpoints,
file,
imgDimensions,
reporter,
}

const { sizes } = calculateImageSizes(args)
expect(sizes).toEqual(expect.arrayContaining([400, 500, 800]))
expect(sizes).toEqual([400, 800])
})

it(`should adjust fullWidth sizes according to fit type`, () => {
Expand Down
25 changes: 15 additions & 10 deletions packages/gatsby-plugin-sharp/src/image-data.ts
Expand Up @@ -9,6 +9,8 @@ import { createTransformObject } from "./plugin-options"

const DEFAULT_BLURRED_IMAGE_WIDTH = 20

const DEFAULT_BREAKPOINTS = [750, 1080, 1366, 1920]

type ImageFormat = "jpg" | "png" | "webp" | "avif" | "" | "auto"
export type FileNode = Node & {
absolutePath?: string
Expand Down Expand Up @@ -84,14 +86,20 @@ export async function generateImageData({
}: IImageDataArgs): Promise<IGatsbyImageData | undefined> {
const {
layout = `constrained`,
placeholder = `blurred`,
placeholder = `dominantColor`,
tracedSVGOptions = {},
transformOptions = {},
quality,
} = args

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

if (layout === `fullWidth`) {
args.breakpoints = args.breakpoints?.length
? args.breakpoints
: DEFAULT_BREAKPOINTS
}

const {
fit = `cover`,
cropFocus = sharp.strategy.attention,
Expand All @@ -103,16 +111,12 @@ export async function generateImageData({
reporter.warn(
`Specifying fullWidth images will ignore the width and height arguments, you may want a constrained image instead. Otherwise, use the breakpoints argument.`
)
args.width = undefined
args.width = metadata.width
args.height = undefined
}

if (!args.width && !args.height && metadata.width) {
if (layout === `fullWidth`) {
args.width = Math.round(metadata.width / 2)
} else {
args.width = metadata.width
}
args.width = metadata.width
}

if (args.aspectRatio) {
Expand Down Expand Up @@ -206,9 +210,10 @@ export async function generateImageData({

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

const primaryIndex = imageSizes.sizes.findIndex(
size => size === imageSizes.unscaledWidth
)
const primaryIndex =
layout === `fullWidth`
? imageSizes.sizes.length - 1 // The largest image
: imageSizes.sizes.findIndex(size => size === imageSizes.unscaledWidth)

if (primaryIndex === -1) {
reporter.error(
Expand Down

0 comments on commit 9bcc12c

Please sign in to comment.