diff --git a/.circleci/config.yml b/.circleci/config.yml index 5cc28f2f5c0f9..b1c1c29d5af16 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -23,7 +23,7 @@ executors: aliases: e2e-executor: &e2e-executor docker: - - image: cypress/browsers:node10.16.0-chrome76 + - image: cypress/browsers:node12.18.3-chrome87-ff82 restore_cache: &restore_cache restore_cache: diff --git a/e2e-tests/visual-regression/cypress/integration/image.js b/e2e-tests/visual-regression/cypress/integration/image.js index 0f68dee9c5e60..5c29ae4f32377 100644 --- a/e2e-tests/visual-regression/cypress/integration/image.js +++ b/e2e-tests/visual-regression/cypress/integration/image.js @@ -3,12 +3,14 @@ const testCases = [ ["fixed image smaller than requested size", "/images/fixed-too-big"], ["fluid image", "/images/fluid"], ["constrained image", "/images/constrained"], + ["avif format", "/images/avif"], ] const staticImageTestCases = [ ["fixed image", "/static-images/fixed"], ["fixed image smaller than requested size", "/static-images/fixed-too-big"], ["fluid image", "/static-images/fluid"], ["constrained image", "/static-images/constrained"], + ["avif format", "/static-images/avif"], ] const sizes = [["iphone-6"], ["ipad-2"], [1027, 768]] diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on 1027x768.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on 1027x768.snap.png new file mode 100644 index 0000000000000..62c5ed33ec1a5 Binary files /dev/null and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on 1027x768.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on ipad-2.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on ipad-2.snap.png new file mode 100644 index 0000000000000..5e5eb7794293d Binary files /dev/null and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on ipad-2.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on iphone-6.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on iphone-6.snap.png new file mode 100644 index 0000000000000..0518786b4baf2 Binary files /dev/null and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- avif format -- renders correctly on iphone-6.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on 1027x768.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on 1027x768.snap.png new file mode 100644 index 0000000000000..62c5ed33ec1a5 Binary files /dev/null and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on 1027x768.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on ipad-2.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on ipad-2.snap.png new file mode 100644 index 0000000000000..5e5eb7794293d Binary files /dev/null and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on ipad-2.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on iphone-6.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on iphone-6.snap.png new file mode 100644 index 0000000000000..0518786b4baf2 Binary files /dev/null and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- avif format -- renders correctly on iphone-6.snap.png differ diff --git a/e2e-tests/visual-regression/src/pages/images/avif.js b/e2e-tests/visual-regression/src/pages/images/avif.js new file mode 100644 index 0000000000000..509713df65b1f --- /dev/null +++ b/e2e-tests/visual-regression/src/pages/images/avif.js @@ -0,0 +1,28 @@ +import * as React from "react" +import { GatsbyImage, getImage } from "gatsby-plugin-image" +import { useStaticQuery, graphql } from "gatsby" +import { TestWrapper } from "../../components/test-wrapper" +import Layout from "../../components/layout" + +const Page = () => { + const data = useStaticQuery(graphql` + query { + file(relativePath: { eq: "cornwall.jpg" }) { + childImageSharp { + gatsbyImageData(maxWidth: 1024, layout: CONSTRAINED, formats: [AVIF]) + } + } + } + `) + + return ( + +

AVIF

+ + + +
+ ) +} + +export default Page diff --git a/e2e-tests/visual-regression/src/pages/static-images/avif.js b/e2e-tests/visual-regression/src/pages/static-images/avif.js new file mode 100644 index 0000000000000..5bf2993b4a56c --- /dev/null +++ b/e2e-tests/visual-regression/src/pages/static-images/avif.js @@ -0,0 +1,22 @@ +import * as React from "react" +import { StaticImage } from "gatsby-plugin-image" +import { TestWrapper } from "../../components/test-wrapper" +import Layout from "../../components/layout" + +const Page = () => { + return ( + +

AVIF

+ + + +
+ ) +} + +export default Page diff --git a/packages/gatsby-plugin-image/src/babel-helpers.ts b/packages/gatsby-plugin-image/src/babel-helpers.ts index 9723d4d88f2a8..f1687558126e1 100644 --- a/packages/gatsby-plugin-image/src/babel-helpers.ts +++ b/packages/gatsby-plugin-image/src/babel-helpers.ts @@ -11,6 +11,7 @@ export const SHARP_ATTRIBUTES = new Set([ `maxWidth`, `maxHeight`, `quality`, + `avifOptions`, `jpegOptions`, `pngOptions`, `webpOptions`, diff --git a/packages/gatsby-plugin-image/src/components/static-image.server.tsx b/packages/gatsby-plugin-image/src/components/static-image.server.tsx index e0d27b71746cc..3f9ab43849acd 100644 --- a/packages/gatsby-plugin-image/src/components/static-image.server.tsx +++ b/packages/gatsby-plugin-image/src/components/static-image.server.tsx @@ -22,6 +22,7 @@ export interface IStaticImageProps extends Omit { jpgOptions?: Record pngOptions?: Record webpOptions?: Record + avifOptions?: Record blurredOptions?: Record } diff --git a/packages/gatsby-plugin-image/src/resolver-utils.ts b/packages/gatsby-plugin-image/src/resolver-utils.ts index d86f906dd3d70..d02d3c366eccd 100644 --- a/packages/gatsby-plugin-image/src/resolver-utils.ts +++ b/packages/gatsby-plugin-image/src/resolver-utils.ts @@ -20,6 +20,7 @@ export const ImageFormatType = new GraphQLEnumType({ JPG: { value: `jpg` }, PNG: { value: `png` }, WEBP: { value: `webp` }, + AVIF: { value: `avif` }, }, }) @@ -99,10 +100,10 @@ export function getGatsbyImageFieldConfig( formats: { type: GraphQLList(ImageFormatType), description: stripIndent` - The image formats to generate. Valid values are "AUTO" (meaning the same format as the source image), "JPG", "PNG" and "WEBP". + The image formats to generate. Valid values are AUTO (meaning the same format as the source image), JPG, PNG, WEBP and AVIF. The default value is [AUTO, WEBP], and you should rarely need to change this. Take care if you specify JPG or PNG when you do not know the formats of the source images, as this could lead to unwanted results such as converting JPEGs to PNGs. Specifying - both PNG and JPG is not supported and will be ignored. + both PNG and JPG is not supported and will be ignored. AVIF support is currently experimental. `, defaultValue: [`auto`, `webp`], }, diff --git a/packages/gatsby-plugin-manifest/package.json b/packages/gatsby-plugin-manifest/package.json index b4f61c265faef..f34d7411545b2 100644 --- a/packages/gatsby-plugin-manifest/package.json +++ b/packages/gatsby-plugin-manifest/package.json @@ -11,7 +11,7 @@ "gatsby-core-utils": "^1.8.0-next.0", "gatsby-plugin-utils": "^0.7.0-next.1", "semver": "^7.3.2", - "sharp": "^0.26.3" + "sharp": "^0.27.0" }, "devDependencies": { "@babel/cli": "^7.12.1", diff --git a/packages/gatsby-plugin-sharp/package.json b/packages/gatsby-plugin-sharp/package.json index a8305d9da927e..f96cbfa1666cd 100644 --- a/packages/gatsby-plugin-sharp/package.json +++ b/packages/gatsby-plugin-sharp/package.json @@ -23,7 +23,7 @@ "probe-image-size": "^5.0.0", "progress": "^2.0.3", "semver": "^7.3.4", - "sharp": "^0.26.3", + "sharp": "^0.27.0", "svgo": "1.3.2", "uuid": "3.4.0" }, diff --git a/packages/gatsby-plugin-sharp/src/image-data.ts b/packages/gatsby-plugin-sharp/src/image-data.ts index 243cdf69e0490..bb04bc1335bb5 100644 --- a/packages/gatsby-plugin-sharp/src/image-data.ts +++ b/packages/gatsby-plugin-sharp/src/image-data.ts @@ -9,7 +9,7 @@ import { createTransformObject } from "./plugin-options" const DEFAULT_BLURRED_IMAGE_WIDTH = 20 -type ImageFormat = "jpg" | "png" | "webp" | "" | "auto" +type ImageFormat = "jpg" | "png" | "webp" | "avif" | "" | "auto" export interface ISharpGatsbyImageArgs { layout?: "fixed" | "fluid" | "constrained" formats?: Array @@ -28,6 +28,7 @@ export interface ISharpGatsbyImageArgs { jpgOptions: Record pngOptions: Record webpOptions: Record + avifOptions: Record blurredOptions: { width?: number; toFormat?: ImageFormat } } export type FileNode = Node & { @@ -159,6 +160,12 @@ export async function generateImageData({ } else if (formats.has(`webp`)) { primaryFormat = `webp` options = args.webpOptions + } else if (formats.has(`avif`)) { + reporter.warn( + `Image ${file.relativePath} specified only AVIF format, with no fallback format. This will mean your site cannot be viewed in all browsers.` + ) + primaryFormat = `avif` + options = args.webpOptions } const imageSizes: { @@ -232,6 +239,35 @@ export async function generateImageData({ }, } + if (primaryFormat !== `avif` && formats.has(`avif`)) { + const transforms = imageSizes.sizes.map(outputWidth => { + const width = Math.round(outputWidth) + const transform = createTransformObject({ + quality, + ...transformOptions, + fit, + cropFocus, + ...args.avifOptions, + width, + height: Math.round(width / imageSizes.aspectRatio), + toFormat: `avif`, + }) + return transform + }) + + const avifImages = batchQueueImageResizing({ + file, + transforms, + reporter, + }) + + imageProps.images.sources?.push({ + srcSet: getSrcSet(avifImages), + type: `image/avif`, + sizes, + }) + } + if (primaryFormat !== `webp` && formats.has(`webp`)) { const transforms = imageSizes.sizes.map(outputWidth => { const width = Math.round(outputWidth) @@ -253,10 +289,9 @@ export async function generateImageData({ transforms, reporter, }) - const webpSrcSet = getSrcSet(webpImages) imageProps.images.sources?.push({ - srcSet: webpSrcSet, + srcSet: getSrcSet(webpImages), type: `image/webp`, sizes, }) diff --git a/packages/gatsby-remark-images-contentful/package.json b/packages/gatsby-remark-images-contentful/package.json index e0d13f1fffc47..4dddbdd4ce2ad 100644 --- a/packages/gatsby-remark-images-contentful/package.json +++ b/packages/gatsby-remark-images-contentful/package.json @@ -22,7 +22,7 @@ "is-relative-url": "^3.0.0", "lodash": "^4.17.20", "semver": "^7.3.2", - "sharp": "^0.26.3", + "sharp": "^0.27.0", "unist-util-select": "^1.5.0" }, "devDependencies": { diff --git a/packages/gatsby-transformer-sharp/package.json b/packages/gatsby-transformer-sharp/package.json index aa50dfb985dac..21ec95411f1eb 100644 --- a/packages/gatsby-transformer-sharp/package.json +++ b/packages/gatsby-transformer-sharp/package.json @@ -13,7 +13,7 @@ "potrace": "^2.1.8", "probe-image-size": "^5.0.0", "semver": "^7.3.4", - "sharp": "^0.26.3" + "sharp": "^0.27.0" }, "devDependencies": { "@babel/cli": "^7.12.1", diff --git a/packages/gatsby-transformer-sharp/src/customize-schema.js b/packages/gatsby-transformer-sharp/src/customize-schema.js index 54421feca3dbe..583d6bdbd8cab 100644 --- a/packages/gatsby-transformer-sharp/src/customize-schema.js +++ b/packages/gatsby-transformer-sharp/src/customize-schema.js @@ -39,6 +39,7 @@ const { WebPOptionsType, BlurredOptionsType, TransformOptionsType, + AVIFOptionsType, } = require(`./types`) const { stripIndent } = require(`common-tags`) const { prefixId, CODES } = require(`./error-utils`) @@ -493,6 +494,10 @@ const imageNodeType = ({ type: WebPOptionsType, description: `Options to pass to sharp when generating WebP images.`, }, + avifOptions: { + type: AVIFOptionsType, + description: `Options to pass to sharp when generating AVIF images.`, + }, transformOptions: { type: TransformOptionsType, description: `Options to pass to sharp to control cropping and other image manipulations.`, diff --git a/packages/gatsby-transformer-sharp/src/types.ts b/packages/gatsby-transformer-sharp/src/types.ts index 0ca018fad952a..7cfb0f37f450d 100644 --- a/packages/gatsby-transformer-sharp/src/types.ts +++ b/packages/gatsby-transformer-sharp/src/types.ts @@ -22,6 +22,7 @@ export const ImageFormatType = new GraphQLEnumType({ JPG: { value: `jpg` }, PNG: { value: `png` }, WEBP: { value: `webp` }, + AVIF: { value: `avif` }, }, }) @@ -129,6 +130,23 @@ export const WebPOptionsType = new GraphQLInputObjectType({ }, }) +export const AVIFOptionsType = new GraphQLInputObjectType({ + name: `AVIFOptions`, + fields: (): GraphQLInputFieldConfigMap => { + return { + quality: { + type: GraphQLInt, + }, + lossless: { + type: GraphQLBoolean, + }, + speed: { + type: GraphQLInt, + }, + } + }, +}) + export const DuotoneGradientType = new GraphQLInputObjectType({ name: `DuotoneGradient`, fields: (): GraphQLInputFieldConfigMap => { diff --git a/yarn.lock b/yarn.lock index 57b1904e300a2..ab427109c4266 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17524,10 +17524,10 @@ node-abi@^2.7.0: dependencies: semver "^5.4.1" -node-addon-api@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.0.2.tgz#04bc7b83fd845ba785bb6eae25bc857e1ef75681" - integrity sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg== +node-addon-api@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.1.0.tgz#98b21931557466c6729e51cb77cd39c965f42239" + integrity sha512-flmrDNB06LIl5lywUz7YlNGZH/5p0M7W28k8hzd9Lshtdh1wshD2Y+U4h9LD6KObOy1f+fEVdgprPrEymjM5uw== node-cleanup@^2.1.2: version "2.1.2" @@ -22782,18 +22782,18 @@ shallowequal@^1.1.0: resolved "https://registry.yarnpkg.com/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== -sharp@^0.26.3: - version "0.26.3" - resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.26.3.tgz#9de8577a986b22538e6e12ced1f7e8a53f9728de" - integrity sha512-NdEJ9S6AMr8Px0zgtFo1TJjMK/ROMU92MkDtYn2BBrDjIx3YfH9TUyGdzPC+I/L619GeYQc690Vbaxc5FPCCWg== +sharp@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.27.0.tgz#523fba913ba674985dcc688a6a5237384079d80f" + integrity sha512-II+YBCW3JuVWQZdpTEA2IBjJcYXPuoKo3AUqYuW+FK9Um93v2gPE2ihICCsN5nHTUoP8WCjqA83c096e8n//Rw== dependencies: array-flatten "^3.0.0" color "^3.1.3" detect-libc "^1.0.3" - node-addon-api "^3.0.2" + node-addon-api "^3.1.0" npmlog "^4.1.2" prebuild-install "^6.0.0" - semver "^7.3.2" + semver "^7.3.4" simple-get "^4.0.0" tar-fs "^2.1.1" tunnel-agent "^0.6.0"