From a82362267d2c842aa7dd3a65c8ffb8d51183c9eb Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Thu, 28 Jan 2021 15:35:09 +0000 Subject: [PATCH] feat(gatsby-source-contentful): Add gatsbyImageData resolver (#28236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add types for resolver utils * Fix * fix(gatsby-plugin-styled-components): add `namespace` option (#29095) * chore(docs): Netlify CMS added branch to backend settings (#29162) * docs: fix broken link (#29163) * chore(docs): Update debugging-the-build-process (#29067) Co-authored-by: gatsbybot Co-authored-by: Lennart * chore(docs): Add cassandra to list of database sources (#29137) Co-authored-by: Lennart * feat(contentful): add support for gatsby-plugin-image * make gatsby-plugin-image a dependency again and warn users about the beta feature * WIP - support traced svgs again * Update packages/gatsby-source-contentful/src/extend-node-type.js Co-authored-by: Matt Kane * fix: set progressive jpg parameter only when format is forced to jpg * feat: add support for contentful backgorund parameter * feat: add support for dominant color placeholder * Error handling, and update api * Use helper * Remove gratuitous parseInt on a number * Destructured import Co-authored-by: Nathan Chu <63111210+nathanchu@users.noreply.github.com> Co-authored-by: Himanshu Bisht <32536536+Himanshu-27@users.noreply.github.com> Co-authored-by: Yuki Takemoto Co-authored-by: yonatanLehman <32838532+yonatanLehman@users.noreply.github.com> Co-authored-by: gatsbybot Co-authored-by: Lennart Co-authored-by: Alex Leventer Co-authored-by: Benedikt Rötsch Co-authored-by: Benedikt Rötsch --- packages/gatsby-plugin-sharp/src/index.js | 3 +- packages/gatsby-plugin-sharp/src/utils.js | 21 +++ .../gatsby-source-contentful/package.json | 4 +- .../src/extend-node-type.js | 171 ++++++++++++++++-- 4 files changed, 183 insertions(+), 16 deletions(-) diff --git a/packages/gatsby-plugin-sharp/src/index.js b/packages/gatsby-plugin-sharp/src/index.js index 6a6cd286a6c51..36fadb06d23da 100644 --- a/packages/gatsby-plugin-sharp/src/index.js +++ b/packages/gatsby-plugin-sharp/src/index.js @@ -20,7 +20,7 @@ const { memoizedTraceSVG, notMemoizedtraceSVG } = require(`./trace-svg`) const duotone = require(`./duotone`) const { IMAGE_PROCESSING_JOB_NAME } = require(`./gatsby-worker`) const { getDimensionsAndAspectRatio } = require(`./utils`) -// const { rgbToHex } = require(`./utils`) +const { getDominantColor } = require(`./utils`) const imageSizeCache = new Map() @@ -777,6 +777,7 @@ exports.fluid = fluid exports.fixed = fixed exports.getImageSize = getImageSize exports.getImageSizeAsync = getImageSizeAsync +exports.getDominantColor = getDominantColor exports.stats = stats exports._unstable_createJob = createJob exports._lazyJobsEnabled = lazyJobsEnabled diff --git a/packages/gatsby-plugin-sharp/src/utils.js b/packages/gatsby-plugin-sharp/src/utils.js index 1064afe7bd3fe..4197b810c3de2 100644 --- a/packages/gatsby-plugin-sharp/src/utils.js +++ b/packages/gatsby-plugin-sharp/src/utils.js @@ -361,3 +361,24 @@ export function getDimensionsAndAspectRatio(dimensions, options) { aspectRatio: width / height, } } + +const dominantColorCache = new Map() + +export const getDominantColor = async absolutePath => { + let dominantColor = dominantColorCache.get(absolutePath) + if (dominantColor) { + return dominantColor + } + + const pipeline = sharp(absolutePath) + const { dominant } = await pipeline.stats() + + // Fallback in case sharp doesn't support dominant + dominantColor = dominant + ? rgbToHex(dominant.r, dominant.g, dominant.b) + : `rgba(0,0,0,0.5)` + + dominantColorCache.set(absolutePath, dominantColor) + + return dominantColor +} diff --git a/packages/gatsby-source-contentful/package.json b/packages/gatsby-source-contentful/package.json index b2a69787ff39c..e8778d9bdb166 100644 --- a/packages/gatsby-source-contentful/package.json +++ b/packages/gatsby-source-contentful/package.json @@ -16,6 +16,7 @@ "contentful": "^7.14.12", "fs-extra": "^9.0.1", "gatsby-core-utils": "^1.10.0-next.0", + "gatsby-plugin-image": "^0.7.0-next.0", "gatsby-plugin-utils": "^0.9.0-next.0", "gatsby-source-filesystem": "^2.11.0-next.0", "is-online": "^8.5.1", @@ -40,7 +41,8 @@ "license": "MIT", "peerDependencies": { "gatsby": "^2.12.1", - "gatsby-plugin-sharp": "^2.6.14" + "gatsby-plugin-sharp": "^2.6.14", + "sharp": "^0.26.0" }, "repository": { "type": "git", diff --git a/packages/gatsby-source-contentful/src/extend-node-type.js b/packages/gatsby-source-contentful/src/extend-node-type.js index fa18a93e5a271..834a81eae72b6 100644 --- a/packages/gatsby-source-contentful/src/extend-node-type.js +++ b/packages/gatsby-source-contentful/src/extend-node-type.js @@ -1,7 +1,9 @@ +// @ts-check const fs = require(`fs`) const path = require(`path`) const crypto = require(`crypto`) +const sortBy = require(`lodash/sortBy`) const axios = require(`axios`) const { GraphQLObjectType, @@ -12,6 +14,11 @@ const { GraphQLNonNull, } = require(`gatsby/graphql`) const qs = require(`qs`) +const { generateImageData } = require(`gatsby-plugin-image`) +const { + getGatsbyImageFieldConfig, +} = require(`gatsby-plugin-image/graphql-utils`) +const { stripIndent } = require(`common-tags`) const cacheImage = require(`./cache-image`) @@ -157,7 +164,10 @@ const createUrl = (imgUrl, options = {}) => { const urlArgs = { w: options.width || undefined, h: options.height || undefined, - fl: options.jpegProgressive ? `progressive` : undefined, + fl: + options.toFormat === `jpg` && options.jpegProgressive + ? `progressive` + : undefined, q: options.quality || undefined, fm: options.toFormat || undefined, fit: options.resizingBehavior || undefined, @@ -170,6 +180,37 @@ const createUrl = (imgUrl, options = {}) => { } exports.createUrl = createUrl +const generateImageSource = ( + filename, + width, + height, + toFormat, + _fit, // We use resizingBehavior instead + { jpegProgressive, quality, cropFocus, backgroundColor, resizingBehavior } +) => { + const src = createUrl(filename, { + width, + height, + toFormat, + resizingBehavior, + background: backgroundColor?.replace(`#`, `rgb:`), + quality, + jpegProgressive, + cropFocus, + }) + return { width, height, format: toFormat, src } +} + +exports.generateImageSource = generateImageSource + +const fitMap = new Map([ + [`pad`, `contain`], + [`fill`, `cover`], + [`scale`, `fill`], + [`crop`, `cover`], + [`thumb`, `cover`], +]) + const resolveFixed = (image, options) => { if (!isImage(image)) return null @@ -223,8 +264,11 @@ const resolveFixed = (image, options) => { ) }) + // Sort sizes for prettiness. + const sortedSizes = sortBy(filteredSizes) + // Create the srcSet. - const srcSet = filteredSizes + const srcSet = sortedSizes .map((size, i) => { let resolution switch (i) { @@ -328,17 +372,19 @@ const resolveFluid = (image, options) => { // Add the original image (if it isn't already in there) to ensure the largest image possible // is available for small images. - const pwidth = parseInt(width, 10) if ( - !filteredSizes.includes(pwidth) && - pwidth < CONTENTFUL_IMAGE_MAX_SIZE && - Math.round(pwidth / desiredAspectRatio) < CONTENTFUL_IMAGE_MAX_SIZE + !filteredSizes.includes(width) && + width < CONTENTFUL_IMAGE_MAX_SIZE && + Math.round(width / desiredAspectRatio) < CONTENTFUL_IMAGE_MAX_SIZE ) { - filteredSizes.push(pwidth) + filteredSizes.push(width) } + // Sort sizes for prettiness. + const sortedSizes = sortBy(filteredSizes) + // Create the srcSet. - const srcSet = filteredSizes + const srcSet = sortedSizes .map(width => { const h = Math.round(width / desiredAspectRatio) return `${createUrl(image.file.url, { @@ -423,7 +469,7 @@ const fixedNodeType = ({ name, getTracedSVG }) => { srcSet: { type: new GraphQLNonNull(GraphQLString) }, srcWebp: { type: GraphQLString, - resolve({ image, options, context }) { + resolve({ image, options }) { if ( image?.file?.contentType === `image/webp` || options.toFormat === `webp` @@ -440,7 +486,7 @@ const fixedNodeType = ({ name, getTracedSVG }) => { }, srcSetWebp: { type: GraphQLString, - resolve({ image, options, context }) { + resolve({ image, options }) { if ( image?.file?.contentType === `image/webp` || options.toFormat === `webp` @@ -516,7 +562,7 @@ const fluidNodeType = ({ name, getTracedSVG }) => { srcSet: { type: new GraphQLNonNull(GraphQLString) }, srcWebp: { type: GraphQLString, - resolve({ image, options, context }) { + resolve({ image, options }) { if ( image?.file?.contentType === `image/webp` || options.toFormat === `webp` @@ -533,7 +579,7 @@ const fluidNodeType = ({ name, getTracedSVG }) => { }, srcSetWebp: { type: GraphQLString, - resolve({ image, options, context }) { + resolve({ image, options }) { if ( image?.file?.contentType === `image/webp` || options.toFormat === `webp` @@ -595,7 +641,9 @@ const fluidNodeType = ({ name, getTracedSVG }) => { } } -exports.extendNodeType = ({ type, store, cache, getNodesByType }) => { +let warnedForBeta = false + +exports.extendNodeType = ({ type, store, reporter }) => { if (type.name !== `ContentfulAsset`) { return {} } @@ -627,6 +675,69 @@ exports.extendNodeType = ({ type, store, cache, getNodesByType }) => { }) } + const getDominantColor = async ({ image, options }) => { + try { + const absolutePath = await cacheImage(store, image, options) + + const pluginSharp = require(`gatsby-plugin-sharp`) + if (!(`getDominantColor` in pluginSharp)) { + console.error( + `[gatsby-source-contentful] Please upgrade gatsby-plugin-sharp` + ) + return `rgba(0,0,0,0.5)` + } + + return pluginSharp.getDominantColor(absolutePath) + } catch (e) { + console.error( + `[gatsby-source-contentful] Please install gatsby-plugin-sharp` + ) + return `rgba(0,0,0,0.5)` + } + } + + const resolveGatsbyImageData = async (image, options) => { + const { baseUrl, ...sourceMetadata } = getBasicImageProps(image, options) + + const imageProps = generateImageData({ + ...options, + pluginName: `gatsby-source-contentful`, + sourceMetadata, + filename: baseUrl, + generateImageSource, + fit: fitMap.get(options.resizingBehavior), + options, + }) + + let placeholderDataURI = null + + if (options.placeholder === `dominantColor`) { + imageProps.backgroundColor = await getDominantColor({ + image, + options, + }) + } + + if (options.placeholder === `blurred`) { + placeholderDataURI = await getBase64Image({ + baseUrl, + }) + } + + if (options.placeholder === `tracedSVG`) { + placeholderDataURI = await getTracedSVG({ + image, + options, + }) + } + + if (placeholderDataURI) { + imageProps.placeholder = { fallback: placeholderDataURI } + } + + return imageProps + } + // TODO: Remove resolutionsNode and sizesNode for Gatsby v3 const fixedNode = fixedNodeType({ name: `ContentfulFixed`, getTracedSVG }) const resolutionsNode = fixedNodeType({ @@ -639,11 +750,43 @@ exports.extendNodeType = ({ type, store, cache, getNodesByType }) => { const sizesNode = fluidNodeType({ name: `ContentfulSizes`, getTracedSVG }) sizesNode.deprecationReason = `Sizes was deprecated in Gatsby v2. It's been renamed to "fluid" https://example.com/write-docs-and-fix-this-example-link` + // gatsby-plugin-image + const getGatsbyImageData = () => { + if (!warnedForBeta) { + reporter.warn( + stripIndent` + Thank you for trying the beta version of the \`gatsbyImageData\` API. Please provide feedback and report any issues at: https://github.com/gatsbyjs/gatsby/discussions/27950` + ) + warnedForBeta = true + } + + return getGatsbyImageFieldConfig(resolveGatsbyImageData, { + jpegProgressive: { + type: GraphQLBoolean, + defaultValue: true, + }, + resizingBehavior: { + type: ImageResizingBehavior, + }, + cropFocus: { + type: ImageCropFocusType, + }, + quality: { + type: GraphQLInt, + defaultValue: 50, + }, + backgroundColor: { + type: GraphQLString, + }, + }) + } + return { fixed: fixedNode, resolutions: resolutionsNode, fluid: fluidNode, sizes: sizesNode, + gatsbyImageData: getGatsbyImageData(), resize: { type: new GraphQLObjectType({ name: `ContentfulResize`, @@ -693,7 +836,7 @@ exports.extendNodeType = ({ type, store, cache, getNodesByType }) => { defaultValue: null, }, }, - resolve(image, options, context) { + resolve(image, options) { return resolveResize(image, options) }, },