Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add new image resolvers #27443

Merged
merged 174 commits into from
Oct 29, 2020
Merged

feat: Add new image resolvers #27443

merged 174 commits into from
Oct 29, 2020

Conversation

ascorbic
Copy link
Contributor

@ascorbic ascorbic commented Oct 14, 2020

This PR adds a new GraphQL API for images, for use with the new gatsby-plugin-image. It adds a new resolver to the ImageSharp node, with a single field imageData. Unlike the existing fixed and fluid resolvers, this returns a JSON type, meaning you don't specify the individual fields, but are instead given the whole object. This is because the object is then passed in to the <GatsbyImage> component. The API is like this:

coverImage: file(relativePath: { eq: "plant.jpg" }) {
  childImageSharp {
    gatsbyImage(maxWidth: 720, layout: FLUID, placeholder: TRACED_SVG) {
      imageData
    }
  }
}
import { GatsbyImage, getImage } from "gatsby-plugin-image"

export function Plant({ data }) {
    const imageData = getImage(data.coverImage)
    return <GatsbyImage image={imageData} />
}

The helper function getImage takes a file node and returns file?.childImageSharp?.gatsbyImage?.imageData

Because this no longer uses fragments to specify which fields to return, it instead uses arguments passed to the resolver. These include:

  • placeholder: Format of generated placeholder image.
    DOMINANT_COLOR: a solid color, calculated from the dominant color of the image. (default) Currently disabled until sharp is updated
    BLURRED: a blurred, low resolution image, encoded as a base64 data URI
    TRACED_SVG: a low-resolution traced SVG of the image.
    NONE: no placeholder. Set "background" to use a fixed background color.

  • layout: The layout for the image.
    FIXED: A static image sized, that does not resize according to the screen width
    FLUID: The image resizes to fit its container. Pass a "sizes" option if it isn't going to be the full width of the screen.
    CONSTRAINED: Resizes to fit its container, up to a maximum width, at which point it will remain fixed in size.

  • 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 a 1x image.
    Default is [ 0.25, 0.5, 1, 2 ], for fluid/constrained images, and [ 1, 2 ] for fixed. In this case, an image with a fluid layout and maxWidth = 400 would generate images at 100, 200, 400 and 800px wide

  • sizes: The "sizes" property, 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.

This PR now also uses the same functions for static images, so they have a similar API.

[ch16945]
[ch16946]

Copy link
Contributor

@wardpeet wardpeet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added some api questions. I don't think we need all options that we currently have in fixed & fluid. We can always add them later.

This only applies when layout = FLUID or CONSTRAINED. For other layout types, use "width"`,
},
maxHeight: {
type: GraphQLInt,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we add description here too?

`,
},
height: {
type: GraphQLInt,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

Comment on lines +444 to +448
webP: {
type: GraphQLBoolean,
defaultValue: true,
description: `Generate images in WebP format as well as matching the input format. This is the default (and strongly recommended), but will add to processing time.`,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason why we want this option? :) I don't immediately see the need to disable it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doubles the processing time, so some sites where that's a big problem might want to

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Or maybe not doubles, but increases signficantly)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The biggest reason why I wouldn't want people to disable it in here is that they value DX over UX. With lazy images and query-on-demand, this becomes less of an issue IMO but I do get your point.

What if we change the description to talk more about performance and not the built time.

description: `Generate images in WebP format as well as matching the input format. This is the default (and strongly recommended), it improves your website performance.`,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the default was false during gatsby develop (i.e. NODE_ENV === development) for better DX perf, but true during gatsby build (i.e. NODE_ENV === production) for better UX?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll fix this with lazy images #27603

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build time is still important in production. My last job had a site that wouldn't build within the Netlify time limit if we had WebP enabled. We moved it to AWS, but it was still something we needed to be able to disable.

My plan for the new API will be to allow the user to pass an array of formats, with the default being [AUTO, WEBP]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build times are never more important than users. That's why I want this description to be different.

Comment on lines +465 to +467
base64Width: {
type: GraphQLInt,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we do the same as tracedSVGOptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? Combine all base64 options into a single object?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes so it's all in one option, I know we only have base64Width but than it's on par with tracedSVG

Comment on lines +472 to +475
jpegProgressive: {
type: GraphQLBoolean,
defaultValue: true,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if we need this option

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I just copied it from the current options

Comment on lines +484 to +495
quality: {
type: GraphQLInt,
},
jpegQuality: {
type: GraphQLInt,
},
pngQuality: {
type: GraphQLInt,
},
webpQuality: {
type: GraphQLInt,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we move these things into another inputType so it doesn't add up to args?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean create an options object for each format?

Comment on lines +500 to +504
toFormatBase64: {
type: ImageFormatType,
defaultValue: ``,
description: `Force output format. Default is to use the same as the input format`,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsure if this makes sense into gatsbyProps

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, maybe in an options object for the blurred preview

Comment on lines +505 to +524
cropFocus: {
type: ImageCropFocusType,
defaultValue: sharp.strategy.attention,
},
fit: {
type: ImageFitType,
defaultValue: sharp.fit.cover,
},
background: {
type: GraphQLString,
defaultValue: `rgba(0,0,0,1)`,
},
rotate: {
type: GraphQLInt,
defaultValue: 0,
},
trim: {
type: GraphQLFloat,
defaultValue: false,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to put this into an inputType as well?

const ProgressBar = require(`progress`)

import ProgressBar from "progress"
import sharp from "sharp"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use

Suggested change
import sharp from "sharp"
import sharp from "./safe-sharp"

@ascorbic
Copy link
Contributor Author

@wardpeet @laurieontech @gillkyle I'm going to create a doc with all the options and we can discuss them in more depth. I think it does make sense to put some time into getting this right.

@ascorbic
Copy link
Contributor Author

Here is the RFC with the new options proposal: #27668

wardpeet
wardpeet previously approved these changes Oct 29, 2020
Copy link
Contributor

@wardpeet wardpeet left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have anything really blocking. Some small nits and follow-ups but nothing to block this PR from being merged as long as we fix those follow ups.

Visual regression test is failing for me locally;
image
The visual regression test is a great addition! Thank you!

What's next?

  1. We'll need e2e test for disabled javascript and when nativeImageLazyLoading is not available.
  2. test if eager is actually working as expected
  3. move gatsby-static-image to gatsby-image e2e test
  4. Test gatsbyImage resolver in the e2e tests

"data-placeholder-image": ``,
style: {
opacity: isLoaded ? 0 : 1,
transition: `opacity 500ms linear`,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be 250 like the main image?

Suggested change
transition: `opacity 500ms linear`,
transition: `opacity 250ms linear`,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gillkyle Could you look at this?

Copy link
Contributor

@gillkyle gillkyle Oct 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the placeholder needs to fade out, I made the placholder 500ms so you won't have a really gradual fade in from the empty space as the main image fades in (in 250ms). You can see what this looks like in the current state vs the suggested:

250ms for placeholder, 500ms for main 500ms for placeholder, 500ms for main image
CleanShot 2020-10-29 at 09 46 40 CleanShot 2020-10-29 at 09 50 55

@wardpeet

}

return result
}

export type PlaceholderImageAttrs = ImgHTMLAttributes<HTMLImageElement> &
Pick<PlaceholderProps, "sources" | "fallback">
Pick<PlaceholderProps, "sources" | "fallback"> & {
"data-placeholder-image"?: string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up:
Let's move to data-gatsby-image-placeholder, let's do the same with main image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea

Comment on lines 10 to 28
export const Placeholder: FunctionComponent<PlaceholderProps> = function Placeholder({
fallback,
...props
}) {
return (
<Picture
{...props}
fallback={{
src: fallback,
}}
aria-hidden
alt=""
/>
)
if (fallback) {
return (
<Picture
{...props}
fallback={{
src: fallback,
}}
aria-hidden
alt=""
/>
)
} else {
return <div {...props}></div>
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up:
this is going to cause issue with composability when we want devs to create their own version. Can we add a background prop? This allows us to update proptypes to be fallback or background

Comment on lines +56 to +61
if (!image) {
if (process.env.NODE_ENV === `development`) {
console.warn(`[gatsby-plugin-image] Missing image prop`)
}
return null
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up:
Shouldn't this be handled by propTypes?

target.style.opacity = 1;
if (placeholder) {
placeholder.style.opacity = 0;
placeholder.style.transition = "opacity 500ms linear";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be 250?

Suggested change
placeholder.style.transition = "opacity 500ms linear";
placeholder.style.transition = "opacity 250ms linear";


const primaryImage = images[primaryIndex]

if (!images?.length) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

images should always be a [] unsure why ou need the ?

Suggested change
if (!images?.length) {
if (!images.length) {

Comment on lines +163 to +185
const transforms = imageSizes.sizes.map(outputWidth => {
const width = Math.round(outputWidth)
const transform = createTransformObject({
...args,
width,
height: Math.round(width / imageSizes.aspectRatio),
toFormat: `webp`,
})
return transform
})

const webpImages = batchQueueImageResizing({
file,
transforms,
reporter,
})
const webpSrcSet = getSrcSet(webpImages)

imageProps.images.sources?.push({
srcSet: webpSrcSet,
type: `image/webp`,
sizes,
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow up:
Any reason why we don't append webp to the previous batch? Wouldn't that speed up things? (less active jobs)

Or isn't it possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it should be possible. I'm goign to handle it differently with the updated API

Comment on lines +444 to +448
webP: {
type: GraphQLBoolean,
defaultValue: true,
description: `Generate images in WebP format as well as matching the input format. This is the default (and strongly recommended), but will add to processing time.`,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll fix this with lazy images #27603

Comment on lines 450 to 455
outputPixelDensities: {
type: GraphQLList(GraphQLInt),
description: stripIndent`
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 a 1x image.
Default is [ 1, 2 ] for fixed images, meaning 1x, 2x, 3x, and [0.25, 0.5, 1, 2] for fluid. In this case, an image with a fluid layout and width = 400 would generate images at 100, 200, 400 and 800px wide`,
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For fluid this isn't working, this needs to be GraphQLFloat

packages/gatsby-plugin-sharp/src/utils.js Show resolved Hide resolved
@wardpeet wardpeet changed the title Add new image resolvers feat: Add new image resolvers Oct 29, 2020
@ascorbic ascorbic merged commit e2022c2 into master Oct 29, 2020
@delete-merged-branch delete-merged-branch bot deleted the feat/image-resolvers branch October 29, 2020 11:58
@ascorbic ascorbic restored the feat/image-resolvers branch November 4, 2020 16:28
@LekoArts LekoArts deleted the feat/image-resolvers branch November 9, 2020 08:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: media Related to gatsby-plugin-image, or general image/media processing topics
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants