diff --git a/docs/docs/gatsby-image.md b/docs/docs/gatsby-image.md index 98a91b9c060c4..dc5e1e1a8e075 100644 --- a/docs/docs/gatsby-image.md +++ b/docs/docs/gatsby-image.md @@ -129,7 +129,7 @@ file(relativePath: { eq: "images/default.jpg" }) { } ``` -Read more in the [gatsby-plugin-sharp](/packages/gatsby-plugin-sharp/?=#fixed) README. +Read more about fixed image queries in the [gatsby-plugin-sharp](/packages/gatsby-plugin-sharp/#fixed) README. ### Images that stretch across a _fluid_ container @@ -177,18 +177,17 @@ In a query, you can specify options for fluid images. - `maxHeight`(int) - `quality` (int, default: 50) - `srcSetBreakpoints` (array of int, default: []) -- `fit` (string, default: `[sharp.fit.cover][6]`) - `background` (string, default: `rgba(0,0,0,1)`) #### Returns - `base64` (string) -- `src` (string) -- `width` (int) -- `height` (int) - `aspectRatio` (float) - `src` (string) - `srcSet` (string) +- `srcSetType` (string) +- `sizes` (string) +- `originalImg` (string) This is where fragments like `GatsbyImageSharpFluid` come in handy, as they'll return all the above items in one line without having to type them all out: @@ -204,7 +203,7 @@ file(relativePath: { eq: "images/default.jpg" }) { } ``` -Read more in the [gatsby-plugin-sharp](/packages/gatsby-plugin-sharp/?=#fluid) README. +Read more about fluid image queries in the [gatsby-plugin-sharp](/packages/gatsby-plugin-sharp/#fluid) README. ### Resized images @@ -240,15 +239,19 @@ allImageSharp { } ``` +Read more about resized image queries in the [gatsby-plugin-sharp](/packages/gatsby-plugin-sharp/#resize) README. + ### Shared query parameters -In addition to `gatsby-plugin-sharp` settings in `gatsby-config.js`, there are additional query options that apply to both _fluid_ and _fixed_ images: +In addition to `gatsby-plugin-sharp` settings in `gatsby-config.js`, there are additional query options that apply to _fluid_, _fixed_, and _resized_ images: -- `grayscale` (bool, default: false) -- `duotone` (bool|obj, default: false) -- `toFormat` (string, default: \`\`) -- `cropFocus` (string, default: `[sharp.strategy.attention][6]`) -- `pngCompressionSpeed` (int, default: 4) +- [`grayscale`](/packages/gatsby-plugin-sharp/#grayscale) (bool, default: false) +- [`duotone`](/packages/gatsby-plugin-sharp/#duotone) (bool|obj, default: false) +- [`toFormat`](/packages/gatsby-plugin-sharp/#toformat) (string, default: \`\`) +- [`cropFocus`](/packages/gatsby-plugin-sharp/#cropfocus) (string, default: `ATTENTION`) +- [`fit`](/packages/gatsby-plugin-sharp/#fit) (string, default: `COVER`) +- [`pngCompressionSpeed`](/packages/gatsby-plugin-sharp/#pngcompressionspeed) (int, default: 4) +- [`rotate`](/packages/gatsby-plugin-sharp/#rotate) (int, default: 0) Here's an example of using the `duotone` option with a fixed image: @@ -286,13 +289,13 @@ fixed(
Grayscale | Before - After
-Read more in the [`gatsby-plugin-sharp`](/packages/gatsby-plugin-sharp) README. +Read more about shared image query parameters in the [`gatsby-plugin-sharp`](/packages/gatsby-plugin-sharp/#shared-options) README. ## Image query fragments GraphQL includes a concept called "query fragments", which are a part of a query that can be reused. To ease building with `gatsby-image`, Gatsby image processing plugins which support `gatsby-image` ship with fragments which you can easily include in your queries. -> Note: using fragments in your queries depends on which data source(s) you have configured. Read more in the [gatsby-image](/packages/gatsby-image#fragments) README. +> Note: using fragments in your queries depends on which data source(s) you have configured. Read more about image query fragments in the [gatsby-image](/packages/gatsby-image/#fragments) README. ### Common fragments with `gatsby-transformer-sharp` diff --git a/packages/gatsby-plugin-sharp/README.md b/packages/gatsby-plugin-sharp/README.md index a17ea0f20490c..42d33d6b3f335 100644 --- a/packages/gatsby-plugin-sharp/README.md +++ b/packages/gatsby-plugin-sharp/README.md @@ -101,11 +101,10 @@ a base64 image to use as a placeholder) you need to implement the "blur up" technique popularized by Medium and Facebook (and also available as a Gatsby plugin for Markdown content as gatsby-remark-images). -When both a `maxWidth` and `maxHeight` are provided, sharp will use `COVER` as a fit strategy by default. This might not be ideal so you can now choose between `COVER`, `CONTAIN` and `FILL` as a fit strategy. To see them in action the [CSS property object-fit](https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit) comes close to its implementation. - -#### Note - -fit strategies `CONTAIN` and `FILL` will not work when `cropFocus` is assigned to [sharp.strategy][6]. The `cropFocus` option cannot be `ENTROPY` or `ATTENTION` +When both a `maxWidth` and `maxHeight` are provided, sharp will [resize the image][6] using +`COVER` as a fit strategy by default. You can choose between `COVER`, `CONTAIN`, `FILL`, +`INSIDE`, and `OUTSIDE` as a fit strategy. See the [fit parameter below](#fit) +for more details. #### Parameters @@ -116,7 +115,6 @@ fit strategies `CONTAIN` and `FILL` will not work when `cropFocus` is assigned t - `pngQuality` (int) - `webpQuality` (int) - `srcSetBreakpoints` (array of int, default: []) -- `fit` (string, default: '[sharp.fit.cover][6]') - `background` (string, default: 'rgba(0,0,0,1)') - [deprecated] `sizeByPixelDensity` (bool, default: false) - Pixel density is only used in vector images, which Gatsby’s implementation of Sharp doesn’t support. This option is currently a no-op and will be removed in the next major version of Gatsby. @@ -139,8 +137,10 @@ following: - `grayscale` (bool, default: false) - `duotone` (bool|obj, default: false) - `toFormat` (string, default: '') -- `cropFocus` (string, default: '[sharp.strategy.attention][6]') +- `cropFocus` (string, default: 'ATTENTION') +- `fit` (string, default: 'COVER') - `pngCompressionSpeed` (int, default: 4) +- `rotate` (int, default: 0) #### toFormat @@ -151,7 +151,25 @@ Convert the source image to one of the following available options: `NO_CHANGE`, Change the cropping focus. Available options: `CENTER`, `NORTH`, `NORTHEAST`, `EAST`, `SOUTHEAST`, `SOUTH`, `SOUTHWEST`, `WEST`, `NORTHWEST`, `ENTROPY`, -`ATTENTION`. See Sharp's [crop][6]. +`ATTENTION`. See Sharp's [resize][6]. + +#### fit + +Select the fit strategy for sharp to use when resizing images. Available options +are: `COVER`, `CONTAIN`, `FILL`, `INSIDE`, `OUTSIDE`. See Sharp's [resize][6]. + +**Note:** The fit strategies `CONTAIN` and `FILL` will not work when `cropFocus` is +set to `ENTROPY` or `ATTENTION`. + +The following image shows the effects of each fit option. You can see that the +`INSIDE` option results in one dimension being smaller than requested, while +the `OUTSIDE` option results in one dimension being larger than requested. +![Sharp transform fit options](./sharp-transform-fit-options.png) + +#### pngCompressionSpeed + +Change the speed/quality tradeoff for PNG compression from 1 (brute-force) to +10 (fastest). See pngquant's [options][19]. #### rotate @@ -357,3 +375,4 @@ If updating these doesn't fix the issue, your project probably uses other plugin [16]: https://github.com/mozilla/mozjpeg [17]: https://www.sno.phy.queensu.ca/~phil/exiftool/ [18]: https://www.npmjs.com/package/color +[19]: https://pngquant.org/#options diff --git a/packages/gatsby-plugin-sharp/sharp-transform-fit-options.png b/packages/gatsby-plugin-sharp/sharp-transform-fit-options.png new file mode 100644 index 0000000000000..4f36b04f788ce Binary files /dev/null and b/packages/gatsby-plugin-sharp/sharp-transform-fit-options.png differ diff --git a/packages/gatsby-plugin-sharp/src/index.js b/packages/gatsby-plugin-sharp/src/index.js index 6f0209f86f281..aed3831a633d7 100644 --- a/packages/gatsby-plugin-sharp/src/index.js +++ b/packages/gatsby-plugin-sharp/src/index.js @@ -45,6 +45,67 @@ exports.setBoundActionCreators = actions => { boundActionCreators = actions } +function calculateImageDimensionsAndAspectRatio(file, options) { + // Calculate the eventual width/height of the image. + const dimensions = getImageSize(file) + const imageAspectRatio = dimensions.width / dimensions.height + + let width = options.width + let height = options.height + + switch (options.fit) { + case sharp.fit.fill: { + width = options.width ? options.width : dimensions.width + height = options.height ? options.height : dimensions.height + break + } + case sharp.fit.inside: { + const widthOption = options.width + ? options.width + : Number.MAX_SAFE_INTEGER + const heightOption = options.height + ? options.height + : Number.MAX_SAFE_INTEGER + + width = Math.min(widthOption, Math.round(heightOption * imageAspectRatio)) + height = Math.min( + heightOption, + Math.round(widthOption / imageAspectRatio) + ) + break + } + case sharp.fit.outside: { + const widthOption = options.width ? options.width : 0 + const heightOption = options.height ? options.height : 0 + + width = Math.max(widthOption, Math.round(heightOption * imageAspectRatio)) + height = Math.max( + heightOption, + Math.round(widthOption / imageAspectRatio) + ) + break + } + + default: { + if (options.width && !options.height) { + width = options.width + height = Math.round(options.width / imageAspectRatio) + } + + if (options.height && !options.width) { + width = Math.round(options.height * imageAspectRatio) + height = options.height + } + } + } + + return { + width, + height, + aspectRatio: width / height, + } +} + function prepareQueue({ file, args }) { const { pathPrefix, ...options } = args const argsDigestShort = createArgsDigest(options) @@ -60,30 +121,10 @@ function prepareQueue({ file, args }) { // make sure outputDir is created fs.ensureDirSync(outputDir) - let width - let height - // Calculate the eventual width/height of the image. - const dimensions = getImageSize(file) - let aspectRatio = dimensions.width / dimensions.height - - // If the width/height are both set, we're cropping so just return - // that. - if (options.width && options.height) { - width = options.width - height = options.height - // Recalculate the aspectRatio for the cropped photo - aspectRatio = width / height - } else if (options.width) { - // Use the aspect ratio of the image to calculate what will be the resulting - // height. - width = options.width - height = Math.round(options.width / aspectRatio) - } else { - // Use the aspect ratio of the image to calculate what will be the resulting - // width. - height = options.height - width = Math.round(options.height * aspectRatio) - } + const { width, height, aspectRatio } = calculateImageDimensionsAndAspectRatio( + file, + options + ) // encode the file name for URL const encodedImgSrc = `/${encodeURIComponent(file.name)}.${options.toFormat}` @@ -263,9 +304,21 @@ async function generateBase64({ file, args = {}, reporter }) { args.toFormat = forceBase64Format } + console.log({ + src: file.absolutePath, + width: options.width, + height: options.height, + position: options.cropFocus, + fit: options.fit, + background: options.background, + }) pipeline - .resize(options.width, options.height, { + .resize({ + width: options.width, + height: options.height, position: options.cropFocus, + fit: options.fit, + background: options.background, }) .png({ compressionLevel: options.pngCompressionLevel, @@ -508,7 +561,10 @@ async function fluid({ file, args = {}, reporter, cache }) { let base64Image if (options.base64) { const base64Width = options.base64Width || defaultBase64Width() - const base64Height = Math.max(1, Math.round((base64Width * height) / width)) + const base64Height = Math.max( + 1, + Math.round(base64Width / images[0].aspectRatio) + ) const base64Args = { duotone: options.duotone, grayscale: options.grayscale, @@ -516,6 +572,8 @@ async function fluid({ file, args = {}, reporter, cache }) { trim: options.trim, toFormat: options.toFormat, toFormatBase64: options.toFormatBase64, + cropFocus: options.cropFocus, + fit: options.fit, width: base64Width, height: base64Height, } @@ -626,16 +684,23 @@ async function fixed({ file, args = {}, reporter, cache }) { let base64Image if (options.base64) { + const base64Width = options.base64Width || defaultBase64Width() + const base64Height = Math.max( + 1, + Math.round(base64Width / images[0].aspectRatio) + ) const base64Args = { - // height is adjusted accordingly with respect to the aspect ratio - width: options.base64Width, duotone: options.duotone, grayscale: options.grayscale, rotate: options.rotate, + trim: options.trim, toFormat: options.toFormat, toFormatBase64: options.toFormatBase64, + cropFocus: options.cropFocus, + fit: options.fit, + width: base64Width, + height: base64Height, } - // Get base64 version base64Image = await base64({ file, diff --git a/packages/gatsby-transformer-sharp/src/types.js b/packages/gatsby-transformer-sharp/src/types.js index e63891e0f9959..243d5fced00e5 100644 --- a/packages/gatsby-transformer-sharp/src/types.js +++ b/packages/gatsby-transformer-sharp/src/types.js @@ -26,6 +26,8 @@ const ImageFitType = new GraphQLEnumType({ COVER: { value: sharp.fit.cover }, CONTAIN: { value: sharp.fit.contain }, FILL: { value: sharp.fit.fill }, + INSIDE: { value: sharp.fit.inside }, + OUTSIDE: { value: sharp.fit.outside }, }, })