diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on 1027x768.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on 1027x768.snap.png index 59f2f3f231a83..7c626be71a161 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on 1027x768.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on 1027x768.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on ipad-2.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on ipad-2.snap.png index 59f2f3f231a83..7888987db6b1f 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on ipad-2.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on ipad-2.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on iphone-6.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on iphone-6.snap.png index de7ef28c730bd..7fc266698764d 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on iphone-6.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image -- renders correctly on iphone-6.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on 1027x768.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on 1027x768.snap.png index 1e3afed9093bc..df8ee8e7d04f7 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on 1027x768.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on 1027x768.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on ipad-2.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on ipad-2.snap.png index 1e3afed9093bc..df8ee8e7d04f7 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on ipad-2.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on ipad-2.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on iphone-6.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on iphone-6.snap.png index d01b074d2d8e3..1fbf4afb870e0 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on iphone-6.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/GatsbyImage -- fixed image smaller than requested size -- renders correctly on iphone-6.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on 1027x768.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on 1027x768.snap.png index 9472385b8ef13..a7e39a84f91f8 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on 1027x768.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on 1027x768.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on ipad-2.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on ipad-2.snap.png index 9472385b8ef13..d4037fa022037 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on ipad-2.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on ipad-2.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on iphone-6.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on iphone-6.snap.png index 9472385b8ef13..a616c05413f0a 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on iphone-6.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image -- renders correctly on iphone-6.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on 1027x768.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on 1027x768.snap.png index 08bfd9bff8188..9a3de2a25400a 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on 1027x768.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on 1027x768.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on ipad-2.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on ipad-2.snap.png index 22ce726169026..9a3de2a25400a 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on ipad-2.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on ipad-2.snap.png differ diff --git a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on iphone-6.snap.png b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on iphone-6.snap.png index f4e9840afb828..5c7c5b17a936d 100644 Binary files a/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on iphone-6.snap.png and b/e2e-tests/visual-regression/cypress/snapshots/image.js/StaticImage -- fixed image smaller than requested size -- renders correctly on iphone-6.snap.png differ diff --git a/e2e-tests/visual-regression/src/pages/images/constrained.js b/e2e-tests/visual-regression/src/pages/images/constrained.js index 9181de216b8b3..4f0b8c92ad769 100644 --- a/e2e-tests/visual-regression/src/pages/images/constrained.js +++ b/e2e-tests/visual-regression/src/pages/images/constrained.js @@ -9,9 +9,7 @@ const Page = () => { query { file(relativePath: { eq: "cornwall.jpg" }) { childImageSharp { - gatsbyImage(maxWidth: 1024, layout: CONSTRAINED) { - imageData - } + gatsbyImageData(maxWidth: 1024, layout: CONSTRAINED) } } } diff --git a/e2e-tests/visual-regression/src/pages/images/fixed-too-big.js b/e2e-tests/visual-regression/src/pages/images/fixed-too-big.js index a0fb76294d1b9..b3abf9de705a2 100644 --- a/e2e-tests/visual-regression/src/pages/images/fixed-too-big.js +++ b/e2e-tests/visual-regression/src/pages/images/fixed-too-big.js @@ -9,9 +9,7 @@ const Page = () => { query { file(relativePath: { eq: "landsend.jpg" }) { childImageSharp { - gatsbyImage(width: 500, height: 500, layout: FIXED) { - imageData - } + gatsbyImageData(width: 500, height: 500, layout: FIXED) } } } diff --git a/e2e-tests/visual-regression/src/pages/images/fixed.js b/e2e-tests/visual-regression/src/pages/images/fixed.js index ba9268a6398e0..398819aa83de4 100644 --- a/e2e-tests/visual-regression/src/pages/images/fixed.js +++ b/e2e-tests/visual-regression/src/pages/images/fixed.js @@ -9,9 +9,7 @@ const Page = () => { query { file(relativePath: { eq: "cornwall.jpg" }) { childImageSharp { - gatsbyImage(width: 240, height: 100, layout: FIXED) { - imageData - } + gatsbyImageData(width: 240, height: 100, layout: FIXED) } } } diff --git a/e2e-tests/visual-regression/src/pages/images/fluid.js b/e2e-tests/visual-regression/src/pages/images/fluid.js index f603d4e2e775b..9ddd5503fc903 100644 --- a/e2e-tests/visual-regression/src/pages/images/fluid.js +++ b/e2e-tests/visual-regression/src/pages/images/fluid.js @@ -9,9 +9,7 @@ const Page = () => { query { file(relativePath: { eq: "cornwall.jpg" }) { childImageSharp { - gatsbyImage(maxWidth: 1024, layout: FLUID) { - imageData - } + gatsbyImageData(maxWidth: 1024, layout: FLUID) } } } diff --git a/e2e-tests/visual-regression/src/pages/static-images/constrained.js b/e2e-tests/visual-regression/src/pages/static-images/constrained.js index e2c04b0f58984..6b36de5496eef 100644 --- a/e2e-tests/visual-regression/src/pages/static-images/constrained.js +++ b/e2e-tests/visual-regression/src/pages/static-images/constrained.js @@ -13,8 +13,6 @@ const Page = () => { layout="constrained" alt="cornwall" maxWidth={1024} - placeholder="blurred" - webP /> diff --git a/packages/gatsby-plugin-image/README.md b/packages/gatsby-plugin-image/README.md index 950538fb0ec77..69ed52bb663ad 100644 --- a/packages/gatsby-plugin-image/README.md +++ b/packages/gatsby-plugin-image/README.md @@ -284,9 +284,7 @@ export const query = graphql` childImageSharp { # Specify the image processing specifications right in the query. # Makes it trivial to update as your page's design changes. - gatsbyImage(layout: FIXED, width: 125, height: 125) { - imageData - } + gatsbyImageData(layout: FIXED, width: 125, height: 125) } } } diff --git a/packages/gatsby-plugin-image/src/babel-helpers.ts b/packages/gatsby-plugin-image/src/babel-helpers.ts index 154e2fe2bf25f..fbbc8101cab26 100644 --- a/packages/gatsby-plugin-image/src/babel-helpers.ts +++ b/packages/gatsby-plugin-image/src/babel-helpers.ts @@ -9,29 +9,17 @@ export const SHARP_ATTRIBUTES = new Set([ `maxWidth`, `maxHeight`, `quality`, - `jpegQuality`, - `pngQuality`, - `webpQuality`, - `grayscale`, - `toFormat`, - `cropFocus`, - `pngCompressionSpeed`, - `rotate`, - `duotone`, + `jpegOptions`, + `pngOptions`, + `webpOptions`, + `blurredOptions`, + `transformOptions`, `width`, `height`, `placeholder`, `tracedSVGOptions`, - `webP`, - `outputPixelDensities`, `sizes`, - `fit`, `background`, - `base64Width`, - `jpegProgressive`, - `toFormatBase64`, - `trim`, - `srcSetBreakpoints`, ]) export function evaluateImageAttributes( diff --git a/packages/gatsby-plugin-image/src/components/hooks.ts b/packages/gatsby-plugin-image/src/components/hooks.ts index f778f8bbc8161..6d6d4fc4cae45 100644 --- a/packages/gatsby-plugin-image/src/components/hooks.ts +++ b/packages/gatsby-plugin-image/src/components/hooks.ts @@ -32,14 +32,12 @@ export function hasImageLoaded(cacheKey: string): boolean { export type FileNode = Node & { childImageSharp?: Node & { - gatsbyImage?: Node & { - imageData: ISharpGatsbyImageData - } + gatsbyImageData?: ISharpGatsbyImageData } } export const getImage = (file: FileNode): ISharpGatsbyImageData | undefined => - file?.childImageSharp?.gatsbyImage?.imageData + file?.childImageSharp?.gatsbyImageData export function getWrapperProps( width: number, diff --git a/packages/gatsby-plugin-image/src/utils.ts b/packages/gatsby-plugin-image/src/utils.ts index fe088599f5aee..50573851a25aa 100644 --- a/packages/gatsby-plugin-image/src/utils.ts +++ b/packages/gatsby-plugin-image/src/utils.ts @@ -30,7 +30,6 @@ export interface ICommonImageProps { export interface IFluidImageProps extends ICommonImageProps { maxWidth?: number maxHeight?: number - srcSetBreakpoints?: Array fit?: number background?: number } diff --git a/packages/gatsby-plugin-sharp/package.json b/packages/gatsby-plugin-sharp/package.json index f2e866c3d94e4..a61882cd0719f 100644 --- a/packages/gatsby-plugin-sharp/package.json +++ b/packages/gatsby-plugin-sharp/package.json @@ -29,6 +29,7 @@ "devDependencies": { "@babel/cli": "^7.11.6", "@babel/core": "^7.11.6", + "@types/sharp": "^0.26.0", "babel-preset-gatsby-package": "^0.6.0-next.0", "cross-env": "^7.0.2", "gatsby-plugin-image": "^0.1.0-next.0" diff --git a/packages/gatsby-plugin-sharp/src/__tests__/utils.js b/packages/gatsby-plugin-sharp/src/__tests__/utils.js index 74e143bfe168e..ecadb82811276 100644 --- a/packages/gatsby-plugin-sharp/src/__tests__/utils.js +++ b/packages/gatsby-plugin-sharp/src/__tests__/utils.js @@ -292,28 +292,52 @@ describe(`calculateImageSizes (fluid & constrained)`, () => { const testsCases = [ { args: { maxWidth: 20, maxHeight: 20 }, result: [20, 20] }, { - args: { maxWidth: 20, maxHeight: 20, fit: sharp.fit.fill }, + args: { + maxWidth: 20, + maxHeight: 20, + transformOptions: { fit: sharp.fit.fill }, + }, result: [20, 20], }, { - args: { maxWidth: 20, maxHeight: 20, fit: sharp.fit.inside }, + args: { + maxWidth: 20, + maxHeight: 20, + transformOptions: { fit: sharp.fit.inside }, + }, result: [20, 10], }, { - args: { maxWidth: 20, maxHeight: 20, fit: sharp.fit.outside }, + args: { + maxWidth: 20, + maxHeight: 20, + transformOptions: { fit: sharp.fit.outside }, + }, result: [41, 20], }, { args: { maxWidth: 200, maxHeight: 200 }, result: [200, 200] }, { - args: { maxWidth: 200, maxHeight: 200, fit: sharp.fit.fill }, + args: { + maxWidth: 200, + maxHeight: 200, + transformOptions: { fit: sharp.fit.fill }, + }, result: [200, 200], }, { - args: { maxWidth: 200, maxHeight: 200, fit: sharp.fit.inside }, + args: { + maxWidth: 200, + maxHeight: 200, + transformOptions: { fit: sharp.fit.inside }, + }, result: [200, 97], }, { - args: { maxWidth: 200, maxHeight: 200, fit: sharp.fit.outside }, + args: { + maxWidth: 200, + maxHeight: 200, + transformOptions: { fit: sharp.fit.outside }, + }, result: [413, 200], }, ] @@ -341,28 +365,52 @@ describe(`calculateImageSizes (fluid & constrained)`, () => { const testsCases = [ { args: { width: 20, height: 20 }, result: [20, 20] }, { - args: { width: 20, height: 20, fit: sharp.fit.fill }, + args: { + width: 20, + height: 20, + transformOptions: { fit: sharp.fit.fill }, + }, result: [20, 20], }, { - args: { width: 20, height: 20, fit: sharp.fit.inside }, + args: { + width: 20, + height: 20, + transformOptions: { fit: sharp.fit.inside }, + }, result: [20, 10], }, { - args: { width: 20, height: 20, fit: sharp.fit.outside }, + args: { + width: 20, + height: 20, + transformOptions: { fit: sharp.fit.outside }, + }, result: [41, 20], }, { args: { width: 200, height: 200 }, result: [200, 200] }, { - args: { width: 200, height: 200, fit: sharp.fit.fill }, + args: { + width: 200, + height: 200, + transformOptions: { fit: sharp.fit.fill }, + }, result: [200, 200], }, { - args: { width: 200, height: 200, fit: sharp.fit.inside }, + args: { + width: 200, + height: 200, + transformOptions: { fit: sharp.fit.inside }, + }, result: [200, 97], }, { - args: { width: 200, height: 200, fit: sharp.fit.outside }, + args: { + width: 200, + height: 200, + transformOptions: { fit: sharp.fit.outside }, + }, result: [413, 200], }, ] diff --git a/packages/gatsby-plugin-sharp/src/image-data.ts b/packages/gatsby-plugin-sharp/src/image-data.ts index 530d75e1e6a75..4095cdce76764 100644 --- a/packages/gatsby-plugin-sharp/src/image-data.ts +++ b/packages/gatsby-plugin-sharp/src/image-data.ts @@ -4,29 +4,42 @@ import { GatsbyCache, Node } from "gatsby" import { Reporter } from "gatsby-cli/lib/reporter/reporter" import { rgbToHex, calculateImageSizes, getSrcSet, getSizes } from "./utils" import { traceSVG, getImageSizeAsync, base64, batchQueueImageResizing } from "." -import type { SharpInstance } from "sharp" import sharp from "./safe-sharp" import { createTransformObject } from "./plugin-options" + +const DEFAULT_BLURRED_IMAGE_WIDTH = 20 + +type ImageFormat = "jpg" | "png" | "webp" | "" | "auto" export interface ISharpGatsbyImageArgs { layout?: "fixed" | "fluid" | "constrained" + formats?: Array placeholder?: "tracedSVG" | "dominantColor" | "blurred" | "none" tracedSVGOptions?: Record width?: number height?: number maxWidth?: number maxHeight?: number - fit?: "contain" | "cover" | "fill" | "inside" | "outside" - [key: string]: unknown + sizes?: string + quality?: number + transformOptions: { + fit?: "contain" | "cover" | "fill" | "inside" | "outside" + cropFocus?: typeof sharp.strategy | typeof sharp.gravity | string + } + jpgOptions: Record + pngOptions: Record + webpOptions: Record + blurredOptions: { width?: number; toFormat?: ImageFormat } } export type FileNode = Node & { absolutePath?: string + extension: string } export interface IImageMetadata { - width: number - height: number - format: string - density?: string + width?: number + height?: number + format?: string + density?: number dominantColor?: string } @@ -45,7 +58,7 @@ export async function getImageMetadata( if (metadata && process.env.NODE_ENV !== `test`) { return metadata } - const pipeline: SharpInstance = sharp(file.absolutePath) + const pipeline = sharp(file.absolutePath) const { width, height, density, format } = await pipeline.metadata() @@ -75,6 +88,13 @@ export interface IImageDataArgs { reporter: Reporter } +function normalizeFormat(format: string): ImageFormat { + if (format === `jpeg`) { + return `jpg` + } + return format as ImageFormat +} + export async function generateImageData({ file, args, @@ -84,18 +104,50 @@ export async function generateImageData({ }: IImageDataArgs): Promise { const { layout = `fixed`, - placeholder = `dominantColor`, + placeholder = `blurred`, tracedSVGOptions = {}, + transformOptions = {}, + quality, } = args + const { + fit = `cover`, + cropFocus = sharp.strategy.attention, + } = transformOptions + + const formats = new Set(args.formats) + let useAuto = formats.has(``) || formats.has(`auto`) || formats.size === 0 + + if (formats.has(`jpg`) && formats.has(`png`)) { + reporter.warn( + `Specifying both "jpg" and "png" formats is not supported and will be ignored. Please use "auto" instead.` + ) + useAuto = true + } + const metadata = await getImageMetadata(file, placeholder === `dominantColor`) + let primaryFormat: ImageFormat | undefined + let options: Record | undefined + if (useAuto) { + primaryFormat = normalizeFormat(metadata.format || file.extension) + } else if (formats.has(`png`)) { + primaryFormat = `png` + options = args.pngOptions + } else if (formats.has(`jpg`)) { + primaryFormat = `jpg` + options = args.jpgOptions + } else if (formats.has(`webp`)) { + primaryFormat = `webp` + options = args.webpOptions + } + const imageSizes: { sizes: Array presentationWidth: number presentationHeight: number aspectRatio: number - isTopSizeOverriden: boolean + unscaledWidth: number } = calculateImageSizes({ file, layout, @@ -107,10 +159,15 @@ export async function generateImageData({ const transforms = imageSizes.sizes.map(outputWidth => { const width = Math.round(outputWidth) const transform = createTransformObject({ - ...args, + quality, + ...transformOptions, + fit, + cropFocus, + ...options, + tracedSVGOptions, width, height: Math.round(width / imageSizes.aspectRatio), - toFormat: args.toFormat || metadata.format, + toFormat: primaryFormat, }) if (pathPrefix) transform.pathPrefix = pathPrefix @@ -125,20 +182,17 @@ export async function generateImageData({ const srcSet = getSrcSet(images) - const widthOfMaxSize = imageSizes.isTopSizeOverriden - ? metadata.width - : imageSizes.presentationWidth - - const sizes = args.sizes || getSizes(widthOfMaxSize, layout) + const sizes = args.sizes || getSizes(imageSizes.unscaledWidth, layout) const primaryIndex = imageSizes.sizes.findIndex( - size => size === widthOfMaxSize + size => size === imageSizes.unscaledWidth ) if (primaryIndex === -1) { - reporter.panic( + reporter.error( `No image of the specified size found. Images may not have been processed correctly.` ) + return undefined } const primaryImage = images[primaryIndex] @@ -159,11 +213,14 @@ export async function generateImageData({ }, } - if (args.webP) { + if (primaryFormat !== `webp` && formats.has(`webp`)) { const transforms = imageSizes.sizes.map(outputWidth => { const width = Math.round(outputWidth) const transform = createTransformObject({ - ...args, + quality, + ...args.transformOptions, + ...args.webpOptions, + tracedSVGOptions, width, height: Math.round(width / imageSizes.aspectRatio), toFormat: `webp`, @@ -186,9 +243,17 @@ export async function generateImageData({ } if (placeholder === `blurred`) { + const placeholderWidth = + args.blurredOptions?.width || DEFAULT_BLURRED_IMAGE_WIDTH const { src: fallback } = await base64({ file, - args: { ...args, width: args.base64Width, height: args.base64Height }, + args: { + ...options, + ...args.transformOptions, + toFormatBase64: args.blurredOptions?.toFormat, + width: placeholderWidth, + height: Math.round(placeholderWidth / imageSizes.aspectRatio), + }, reporter, }) imageProps.placeholder = { diff --git a/packages/gatsby-plugin-sharp/src/safe-sharp.d.ts b/packages/gatsby-plugin-sharp/src/safe-sharp.d.ts new file mode 100644 index 0000000000000..d460ec32c7efa --- /dev/null +++ b/packages/gatsby-plugin-sharp/src/safe-sharp.d.ts @@ -0,0 +1,2 @@ +import sharp from "sharp" +export default sharp diff --git a/packages/gatsby-plugin-sharp/src/utils.js b/packages/gatsby-plugin-sharp/src/utils.js index a483403fc7ec6..d4ade83da8108 100644 --- a/packages/gatsby-plugin-sharp/src/utils.js +++ b/packages/gatsby-plugin-sharp/src/utils.js @@ -144,13 +144,13 @@ export function fixedImageSizes({ maxWidth, height, maxHeight, - fit = `cover`, + transformOptions = {}, outputPixelDensities = DEFAULT_PIXEL_DENSITIES, srcSetBreakpoints, reporter, }) { - let sizes let aspectRatio = imgDimensions.width / imgDimensions.height + const { fit = `cover` } = transformOptions // Sort, dedupe and ensure there's a 1 const densities = dedupeAndSortDensities(outputPixelDensities) @@ -182,7 +182,6 @@ export function fixedImageSizes({ } const originalWidth = width // will use this for presentationWidth, don't want to lose it - const isTopSizeOverriden = imgDimensions.width < width || imgDimensions.height < height @@ -201,11 +200,16 @@ export function fixedImageSizes({ If possible, replace the current image with a larger one. `) - width = imgDimensions.width - height = imgDimensions.height + if (fixedDimension === `width`) { + width = imgDimensions.width + height = Math.round(width / aspectRatio) + } else { + height = imgDimensions.height + width = height * aspectRatio + } } - sizes = densities + const sizes = densities .filter(size => size >= 1) // remove smaller densities because fixed images don't need them .map(density => Math.round(density * width)) .filter(size => size <= imgDimensions.width) @@ -215,7 +219,7 @@ export function fixedImageSizes({ aspectRatio, presentationWidth: originalWidth, presentationHeight: Math.round(originalWidth / aspectRatio), - isTopSizeOverriden, + unscaledWidth: width, } } @@ -225,12 +229,14 @@ export function fluidImageSizes({ width, maxWidth, height, - fit, + transformOptions = {}, maxHeight, outputPixelDensities = DEFAULT_PIXEL_DENSITIES, srcSetBreakpoints, reporter, }) { + const { fit = `cover` } = transformOptions + // warn if ignored parameters are passed in warnForIgnoredParameters( `fluid and constrained`, @@ -270,8 +276,8 @@ export function fluidImageSizes({ maxWidth = maxHeight * aspectRatio } - let originalMaxWidth = maxWidth - let isTopSizeOverriden = + const originalMaxWidth = maxWidth + const isTopSizeOverriden = imgDimensions.width < maxWidth || imgDimensions.height < maxHeight if (isTopSizeOverriden) { maxWidth = imgDimensions.width @@ -306,7 +312,7 @@ export function fluidImageSizes({ aspectRatio, presentationWidth: originalMaxWidth, presentationHeight: Math.round(originalMaxWidth / aspectRatio), - isTopSizeOverriden, + unscaledWidth: maxWidth, } } diff --git a/packages/gatsby-transformer-sharp/package.json b/packages/gatsby-transformer-sharp/package.json index 169515efee082..9eed69b753e51 100644 --- a/packages/gatsby-transformer-sharp/package.json +++ b/packages/gatsby-transformer-sharp/package.json @@ -18,6 +18,7 @@ "devDependencies": { "@babel/cli": "^7.11.6", "@babel/core": "^7.11.6", + "@types/sharp": "^0.26.0", "babel-preset-gatsby-package": "^0.6.0-next.0", "cross-env": "^7.0.2" }, @@ -39,9 +40,9 @@ "directory": "packages/gatsby-transformer-sharp" }, "scripts": { - "build": "babel src --out-dir . --ignore \"**/__tests__\"", + "build": "babel src --out-dir . --ignore \"**/__tests__\" --extensions \".ts,.js\"", "prepare": "cross-env NODE_ENV=production npm run build", - "watch": "babel -w src --out-dir . --ignore \"**/__tests__\"" + "watch": "babel -w src --out-dir . --ignore \"**/__tests__\" --extensions \".ts,.js\"" }, "engines": { "node": ">=10.13.0" diff --git a/packages/gatsby-transformer-sharp/src/customize-schema.js b/packages/gatsby-transformer-sharp/src/customize-schema.js index 4b0344f646317..12e1bdb871bbd 100644 --- a/packages/gatsby-transformer-sharp/src/customize-schema.js +++ b/packages/gatsby-transformer-sharp/src/customize-schema.js @@ -34,6 +34,11 @@ const { ImageFitType, ImageLayoutType, ImagePlaceholderType, + JPGOptionsType, + PNGOptionsType, + WebPOptionsType, + BlurredOptionsType, + TransformOptionsType, } = require(`./types`) const { stripIndent } = require(`common-tags`) const { prefixId, CODES } = require(`./error-utils`) @@ -380,6 +385,8 @@ const fluidNodeType = ({ } } +let warnedForAlpha = false + const imageNodeType = ({ pathPrefix, getNodeAndSavePathDependency, @@ -387,14 +394,7 @@ const imageNodeType = ({ cache, }) => { return { - type: new GraphQLObjectType({ - name: `ImageSharpGatsbyImage`, - fields: { - imageData: { - type: new GraphQLNonNull(GraphQLJSON), - }, - }, - }), + type: new GraphQLNonNull(GraphQLJSON), args: { layout: { type: ImageLayoutType, @@ -415,6 +415,10 @@ const imageNodeType = ({ }, maxHeight: { type: GraphQLInt, + description: stripIndent` + If set, the generated image is a maximum of this height, cropping if necessary. + If the image layout is "constrained" then the image will be limited to this height. + If the aspect ratio of the image is different than the source, then the image will be cropped.`, }, width: { type: GraphQLInt, @@ -426,31 +430,42 @@ const imageNodeType = ({ }, height: { type: GraphQLInt, + description: stripIndent` + 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.`, }, placeholder: { type: ImagePlaceholderType, defaultValue: `blurred`, description: stripIndent` - Format of generated placeholder image. + Format of generated placeholder image, displayed while the main image loads. + BLURRED: a blurred, low resolution image, encoded as a base64 data URI (default) DOMINANT_COLOR: a solid color, calculated from the dominant color of the image. - BASE64: 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.`, }, + blurredOptions: { + type: BlurredOptionsType, + description: `Options for the low-resolution placeholder image. Set placeholder to "BLURRED" to use this`, + }, tracedSVGOptions: { type: PotraceType, defaultValue: false, - description: `Options for traced placeholder SVGs. You also should set placeholder to SVG.`, + description: `Options for traced placeholder SVGs. You also should set placeholder to "SVG".`, }, - 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.`, + 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 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. + `, + defaultValue: [`auto`, `webp`], }, outputPixelDensities: { type: GraphQLList(GraphQLFloat), 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. + A list of image pixel densities to generate. It will never generate images larger than the source, and will always include 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`, }, sizes: { @@ -462,72 +477,30 @@ const imageNodeType = ({ container will be the full width of the screen. In these cases we will generate an appropriate value. `, }, - base64Width: { - type: GraphQLInt, - }, - grayscale: { - type: GraphQLBoolean, - defaultValue: false, - }, - jpegProgressive: { - type: GraphQLBoolean, - defaultValue: true, - }, - pngCompressionSpeed: { - type: GraphQLInt, - defaultValue: DEFAULT_PNG_COMPRESSION_SPEED, - }, - duotone: { - type: DuotoneGradientType, - defaultValue: false, - }, quality: { type: GraphQLInt, + description: `The default quality. This is overriden by any format-specific options`, }, - jpegQuality: { - type: GraphQLInt, + jpgOptions: { + type: JPGOptionsType, + description: `Options to pass to sharp when generating JPG images.`, }, - pngQuality: { - type: GraphQLInt, + pngOptions: { + type: PNGOptionsType, + description: `Options to pass to sharp when generating PNG images.`, }, - webpQuality: { - type: GraphQLInt, - }, - toFormat: { - type: ImageFormatType, - defaultValue: ``, - }, - toFormatBase64: { - type: ImageFormatType, - defaultValue: ``, - description: `Force output format. Default is to use the same as the input format`, + webpOptions: { + type: WebPOptionsType, + description: `Options to pass to sharp when generating WebP images.`, }, - cropFocus: { - type: ImageCropFocusType, - defaultValue: sharp.strategy.attention, - }, - fit: { - type: ImageFitType, - defaultValue: sharp.fit.cover, + transformOptions: { + type: TransformOptionsType, + description: `Options to pass to sharp to control cropping and other image manipulations.`, }, background: { type: GraphQLString, - defaultValue: `rgba(0,0,0,1)`, - }, - rotate: { - type: GraphQLInt, - defaultValue: 0, - }, - trim: { - type: GraphQLFloat, - defaultValue: false, - }, - srcSetBreakpoints: { - type: GraphQLList(GraphQLInt), - defaultValue: [], - description: stripIndent`\ - A list of image widths to be generated. Example: [ 200, 340, 520, 890 ]. - You should usually leave this blank and allow it to be generated from the width/maxWidth and outputPixelDensities`, + defaultValue: `rgba(0,0,0,0)`, + description: `Background color applied to the wrapper. Also passed to sharp to use as a background when "letterboxing" an image to another aspect ratio.`, }, }, resolve: async (image, fieldArgs, context) => { @@ -538,11 +511,14 @@ const imageNodeType = ({ reporter.warn(`Please upgrade gatsby-plugin-sharp`) return null } - reporter.warn( - stripIndent` - You are using the alpha version of the \`gatsbyImage\` sharp API, which is unstable and will change without notice. + if (!warnedForAlpha) { + reporter.warn( + stripIndent` + You are using the alpha version of the \`gatsbyImageData\` sharp API, which is unstable and will change without notice. Please do not use it in production.` - ) + ) + warnedForAlpha = true + } const imageData = await generateImageData({ file, args, @@ -550,12 +526,7 @@ const imageNodeType = ({ cache, }) - return { - imageData, - fieldArgs: args, - image, - file, - } + return imageData }, } } @@ -598,7 +569,7 @@ const createFields = ({ resolutions: resolutionsNode, fluid: fluidNode, sizes: sizesNode, - gatsbyImage: imageNode, + gatsbyImageData: imageNode, original: { type: new GraphQLObjectType({ name: `ImageSharpOriginal`, diff --git a/packages/gatsby-transformer-sharp/src/types.js b/packages/gatsby-transformer-sharp/src/types.ts similarity index 54% rename from packages/gatsby-transformer-sharp/src/types.js rename to packages/gatsby-transformer-sharp/src/types.ts index f05a5d40c65d6..0ca018fad952a 100644 --- a/packages/gatsby-transformer-sharp/src/types.js +++ b/packages/gatsby-transformer-sharp/src/types.ts @@ -1,4 +1,4 @@ -const { +import { GraphQLInputObjectType, GraphQLBoolean, GraphQLString, @@ -6,14 +6,19 @@ const { GraphQLFloat, GraphQLEnumType, GraphQLNonNull, -} = require(`gatsby/graphql`) -const sharp = require(`./safe-sharp`) -const { Potrace } = require(`potrace`) + GraphQLInputFieldConfigMap, +} from "gatsby/graphql" +import { Potrace } from "potrace" +import type Sharp from "sharp" + +const sharp: typeof Sharp = require(`./safe-sharp`) +const DEFAULT_PNG_COMPRESSION_SPEED = 4 export const ImageFormatType = new GraphQLEnumType({ name: `ImageFormat`, values: { NO_CHANGE: { value: `` }, + AUTO: { value: `` }, JPG: { value: `jpg` }, PNG: { value: `png` }, WEBP: { value: `webp` }, @@ -34,7 +39,7 @@ export const ImagePlaceholderType = new GraphQLEnumType({ values: { DOMINANT_COLOR: { value: `dominantColor` }, TRACED_SVG: { value: `tracedSVG` }, - BASE64: { value: `base64` }, + BLURRED: { value: `blurred` }, NONE: { value: `none` }, }, }) @@ -67,9 +72,66 @@ export const ImageCropFocusType = new GraphQLEnumType({ }, }) +export const PNGOptionsType = new GraphQLInputObjectType({ + name: `PNGOptions`, + fields: (): GraphQLInputFieldConfigMap => { + return { + quality: { + type: GraphQLInt, + }, + compressionSpeed: { + type: GraphQLInt, + defaultValue: DEFAULT_PNG_COMPRESSION_SPEED, + }, + } + }, +}) + +export const JPGOptionsType = new GraphQLInputObjectType({ + name: `JPGOptions`, + fields: (): GraphQLInputFieldConfigMap => { + return { + quality: { + type: GraphQLInt, + }, + progressive: { + type: GraphQLBoolean, + defaultValue: true, + }, + } + }, +}) + +export const BlurredOptionsType = new GraphQLInputObjectType({ + name: `BlurredOptions`, + fields: (): GraphQLInputFieldConfigMap => { + return { + width: { + type: GraphQLInt, + description: `Width of the generated low-res preview. Default is 20px`, + }, + toFormat: { + type: ImageFormatType, + description: `Force the output format for the low-res preview. Default is to use the same format as the input. You should rarely need to change this`, + }, + } + }, +}) + +export const WebPOptionsType = new GraphQLInputObjectType({ + name: `WebPOptions`, + fields: (): GraphQLInputFieldConfigMap => { + return { + quality: { + type: GraphQLInt, + }, + } + }, +}) + export const DuotoneGradientType = new GraphQLInputObjectType({ name: `DuotoneGradient`, - fields: () => { + fields: (): GraphQLInputFieldConfigMap => { return { highlight: { type: new GraphQLNonNull(GraphQLString) }, shadow: { type: new GraphQLNonNull(GraphQLString) }, @@ -92,7 +154,7 @@ export const PotraceTurnPolicyType = new GraphQLEnumType({ export const PotraceType = new GraphQLInputObjectType({ name: `Potrace`, - fields: () => { + fields: (): GraphQLInputFieldConfigMap => { return { turnPolicy: { type: PotraceTurnPolicyType, @@ -108,3 +170,35 @@ export const PotraceType = new GraphQLInputObjectType({ } }, }) + +export const TransformOptionsType = new GraphQLInputObjectType({ + name: `TransformOptions`, + fields: (): GraphQLInputFieldConfigMap => { + return { + grayscale: { + type: GraphQLBoolean, + defaultValue: false, + }, + duotone: { + type: DuotoneGradientType, + defaultValue: false, + }, + rotate: { + type: GraphQLInt, + defaultValue: 0, + }, + trim: { + type: GraphQLFloat, + defaultValue: false, + }, + cropFocus: { + type: ImageCropFocusType, + defaultValue: sharp.strategy.attention, + }, + fit: { + type: ImageFitType, + defaultValue: sharp.fit.cover, + }, + } + }, +}) diff --git a/yarn.lock b/yarn.lock index d2e3fd468b7d0..72cf04c69a21f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4083,6 +4083,13 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" +"@types/sharp@^0.26.0": + version "0.26.0" + resolved "https://registry.yarnpkg.com/@types/sharp/-/sharp-0.26.0.tgz#2fa8419dbdaca8dd38f73888b27b207f188a8669" + integrity sha512-oJrR8eiwpL7qykn2IeFRduXM4za7z+7yOUEbKVtuDQ/F6htDLHYO6IbzhaJQHV5n6O3adIh4tJvtgPyLyyydqg== + dependencies: + "@types/node" "*" + "@types/signal-exit@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/signal-exit/-/signal-exit-3.0.0.tgz#75e3b17660cf1f6c6cb8557675b4e680e43bbf36"