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

Add custom sizes for fluid images #8825

Merged
merged 17 commits into from
Oct 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/gatsby-plugin-sharp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ rendered markdown file is 800px, the sizes would then be: 200, 400, 800, 1200,
1600, 2400 – enough to provide close to the optimal image size for every device
size / screen resolution.

If you want more control over which sizes are output you can use the `srcSetBreakpoints`
parameter. For example, if you want images that are 200, 340, 520, and 890 wide you
can add `srcSetBreakpoints: [ 200, 340, 520, 890 ]` as a parameter. You will also get
`maxWidth` as a breakpoint (which is 800 by default), so you will actually get
`[ 200, 340, 520, 800, 890 ]` as breakpoints.

On top of that, `fluid` returns everything else (namely aspectRatio and
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
Expand All @@ -83,6 +89,7 @@ plugin for Markdown content as gatsby-remark-images).
- `maxHeight` (int)
- `quality` (int, default: 50)
- `sizeByPixelDensity` (bool, default: false)
- `srcSetBreakpoints` (array of int, default: [])

#### Returns

Expand Down
142 changes: 142 additions & 0 deletions packages/gatsby-plugin-sharp/src/__tests__/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ describe(`gatsby-plugin-sharp`, () => {
const absolutePath = path.join(__dirname, `images/test.png`)
const file = getFileObject(absolutePath)

// used to find all breakpoints in a srcSet string
const findAllBreakpoints = (srcSet) => {
// RegEx to find all occurrences of 'Xw', where 'X' can be any int
const regEx = /[0-9]+w/g
return srcSet.match(regEx)
}

describe(`queueImageResizing`, () => {
it(`should round height when auto-calculated`, () => {
// Resize 144-density.png (281x136) with a 3px width
Expand Down Expand Up @@ -107,6 +114,141 @@ describe(`gatsby-plugin-sharp`, () => {

expect(result.presentationWidth).toEqual(41)
})

it(`should throw if maxWidth is less than 1`, async () => {
const args = { maxWidth: 0 }
const result = fluid({
file: getFileObject(path.join(__dirname, `images/144-density.png`)),
args,
})

await expect(result).rejects.toThrow()
})

it(`accepts srcSet breakpoints`, async () => {
const srcSetBreakpoints = [
50,
70,
150,
250,
]
const args = { srcSetBreakpoints }
const result = await fluid({
file: getFileObject(path.join(__dirname, `images/144-density.png`)),
args,
})

// width of the image tested
const originalWidth = 281
const expected = srcSetBreakpoints
.map((size) => `${size}w`)
// add the original size of `144-density.png`
expected.push(`${originalWidth}w`)

const actual = findAllBreakpoints(result.srcSet)
// should contain all requested sizes as well as the original size
expect(actual).toEqual(expect.arrayContaining(expected))
})

it(`should throw on srcSet breakpoints less than 1`, async () => {
const srcSetBreakpoints = [
50,
0,
]
const args = { srcSetBreakpoints }
const result = fluid({
file: getFileObject(path.join(__dirname, `images/144-density.png`)),
args,
})

await expect(result).rejects.toThrow()
})

it(`ensure maxWidth is in srcSet breakpoints`, async () => {
const srcSetBreakpoints = [
50,
70,
150,
]
const maxWidth = 200
const args = {
maxWidth,
srcSetBreakpoints,
}
const result = await fluid({
file: getFileObject(path.join(__dirname, `images/144-density.png`)),
args,
})

expect(result.srcSet).toEqual(expect.stringContaining(`${maxWidth}w`))
})

it(`reject any breakpoints larger than the original width`, async () => {
const srcSetBreakpoints = [
50,
70,
150,
250,
300, // this shouldn't be in the output as it's wider than the original
]
const maxWidth = 500 // this also shouldn't be in the output
const args = {
maxWidth,
srcSetBreakpoints,
}
const result = await fluid({
file: getFileObject(path.join(__dirname, `images/144-density.png`)),
args,
})

// width of the image tested
const originalWidth = 281
const expected = srcSetBreakpoints
// filter out the widths that are larger than the source image width
.filter((size) => size < originalWidth)
.map((size) => `${size}w`)
// add the original size of `144-density.png`
expected.push(`${originalWidth}w`)

const actual = findAllBreakpoints(result.srcSet)
// should contain all requested sizes as well as the original size
expect(actual).toEqual(expect.arrayContaining(expected))
// should contain no other sizes
expect(actual.length).toEqual(expected.length)
})

it(`prevents duplicate breakpoints`, async () => {
const srcSetBreakpoints = [
50,
50,
100,
100,
100,
250,
250,
]
const maxWidth = 100
const args = {
maxWidth,
srcSetBreakpoints,
}
const result = await fluid({
file: getFileObject(path.join(__dirname, `images/144-density.png`)),
args,
})

const originalWidth = 281
const expected = [
`50w`,
`100w`,
`250w`,
`${originalWidth}w`,
]

const actual = findAllBreakpoints(result.srcSet)
expect(actual).toEqual(expect.arrayContaining(expected))
expect(actual.length).toEqual(expected.length)
})
})

describe(`fixed`, () => {
Expand Down
39 changes: 29 additions & 10 deletions packages/gatsby-plugin-sharp/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,10 @@ async function fluid({ file, args = {}, reporter }) {
const fixedDimension =
options.maxWidth === undefined ? `maxHeight` : `maxWidth`

if (options[fixedDimension] < 1) {
throw new Error(`${fixedDimension} has to be a positive int larger than zero (> 0), now it's ${options[fixedDimension]}`)
}

let presentationWidth, presentationHeight
if (fixedDimension === `maxWidth`) {
presentationWidth = Math.min(
Expand All @@ -540,21 +544,36 @@ async function fluid({ file, args = {}, reporter }) {
options.sizes = `(max-width: ${presentationWidth}px) 100vw, ${presentationWidth}px`
}

// Create sizes (in width) for the image. If the max width of the container
// for the rendered markdown file is 800px, the sizes would then be: 200,
// 400, 800, 1200, 1600, 2400.
// Create sizes (in width) for the image if no custom breakpoints are
// provided. If the max width of the container for the rendered markdown file
// is 800px, the sizes would then be: 200, 400, 800, 1200, 1600, 2400.
//
// This is enough sizes to provide close to the optimal image size for every
// device size / screen resolution while (hopefully) not requiring too much
// image processing time (Sharp has optimizations thankfully for creating
// multiple sizes of the same input file)
const fluidSizes = []
fluidSizes.push(options[fixedDimension] / 4)
fluidSizes.push(options[fixedDimension] / 2)
fluidSizes.push(options[fixedDimension])
fluidSizes.push(options[fixedDimension] * 1.5)
fluidSizes.push(options[fixedDimension] * 2)
fluidSizes.push(options[fixedDimension] * 3)
const fluidSizes = [
options[fixedDimension], // ensure maxWidth (or maxHeight) is added
]
// use standard breakpoints if no custom breakpoints are specified
if (!options.srcSetBreakpoints || !options.srcSetBreakpoints.length) {
fluidSizes.push(options[fixedDimension] / 4)
fluidSizes.push(options[fixedDimension] / 2)
fluidSizes.push(options[fixedDimension] * 1.5)
fluidSizes.push(options[fixedDimension] * 2)
fluidSizes.push(options[fixedDimension] * 3)
} else {
options.srcSetBreakpoints.forEach((breakpoint) => {
if (breakpoint < 1) {
throw new Error(`All ints in srcSetBreakpoints should be positive ints larger than zero (> 0), found ${breakpoint}`)
}
// ensure no duplicates are added
if (fluidSizes.includes(breakpoint)) {
return
}
fluidSizes.push(breakpoint)
})
}
const filteredSizes = fluidSizes.filter(
size => size < (fixedDimension === `maxWidth` ? width : height)
)
Expand Down
6 changes: 6 additions & 0 deletions packages/gatsby-transformer-sharp/src/extend-node-type.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const Promise = require(`bluebird`)
const {
GraphQLObjectType,
GraphQLList,
GraphQLBoolean,
GraphQLString,
GraphQLInt,
Expand Down Expand Up @@ -263,6 +264,11 @@ const fluidNodeType = ({
type: GraphQLString,
defaultValue: ``,
},
srcSetBreakpoints: {
type: GraphQLList(GraphQLInt),
defaultValue: [],
description: `A list of image widths to be generated. Example: [ 200, 340, 520, 890 ]`,
},
},
resolve: (image, fieldArgs, context) => {
const file = getNodeAndSavePathDependency(image.parent, context.path)
Expand Down
23 changes: 23 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -9690,6 +9690,15 @@ imageinfo@^1.0.4:
resolved "https://registry.yarnpkg.com/imageinfo/-/imageinfo-1.0.4.tgz#1dd2456ecb96fc395f0aa1179c467dfb3d5d7a2a"
integrity sha1-HdJFbsuW/DlfCqEXnEZ9+z1deio=

imagemin-mozjpeg@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/imagemin-mozjpeg/-/imagemin-mozjpeg-7.0.0.tgz#d926477fc6ef5f3a768a4222f7b2d808d3eba568"
integrity sha1-2SZHf8bvXzp2ikIi97LYCNPrpWg=
dependencies:
execa "^0.8.0"
is-jpg "^1.0.0"
mozjpeg "^5.0.0"

imagemin-pngquant@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/imagemin-pngquant/-/imagemin-pngquant-6.0.0.tgz#7c0c956338fa9a3a535deb63973c1c894519cc78"
Expand Down Expand Up @@ -10221,6 +10230,11 @@ is-ip@^2.0.0:
dependencies:
ip-regex "^2.0.0"

is-jpg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-jpg/-/is-jpg-1.0.1.tgz#296d57fdd99ce010434a7283e346ab9a1035e975"
integrity sha1-KW1X/dmc4BBDSnKD40armhA16XU=

is-lower-case@^1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-lower-case/-/is-lower-case-1.1.3.tgz#7e147be4768dc466db3bfb21cc60b31e6ad69393"
Expand Down Expand Up @@ -12829,6 +12843,15 @@ move-concurrently@^1.0.1:
rimraf "^2.5.4"
run-queue "^1.0.3"

mozjpeg@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/mozjpeg/-/mozjpeg-5.0.0.tgz#b8671c4924568a363de003ff2fd397ab83f752c5"
integrity sha1-uGccSSRWijY94AP/L9OXq4P3UsU=
dependencies:
bin-build "^2.2.0"
bin-wrapper "^3.0.0"
logalot "^2.0.0"

[email protected]:
version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
Expand Down