Skip to content

Commit da0b622

Browse files
oorestisimepieh
authored andcommitted
fix(gatsby-plugin-sharp): strip non related args when hashing resizing args (#12129)
So continuing on the work to make gatsby-plugin-sharp friendly on third-party packages (oorestisime/gatsby-remark-rehype-images#2) we now face this issue #12080 TL;DR images generated from transformer-sharp or and other plugins are the same (same sha256) but with different hash (depends on the available options). The problem is that all the plugins leak their options into the hash generation and even ones that are unrelated to the resizing processing (base64, generateTraceSVG, traceSVG etc) I've been discussing this with @pieh for the last 2-3 hours :D Proposed solution was to sanitize the args based on a whitelist, which is implemented on the PR. Which leads to the first question: - is it ok if after this release _all_ image hashes are changing? By doing so it can also potentially help lazy image processing @wardpeet by getting rid of unrelated options when persisting jobs. Anyway current state of the PR is that it kind of works, i ve tested this in websites using my plugin and other that not. The issue here is that there are some differences in the number of images created when this modification is on or not. I also verified that public folder doesn't contain duplicate images: `find public -regex ".*\.\(jpg\|webp\|png\|jpeg\)" -type f -exec sha256sum {} \; | cut -d ' ' -f 1 |sort| uniq -d | wc -l`
1 parent 85b8749 commit da0b622

File tree

6 files changed

+189
-49
lines changed

6 files changed

+189
-49
lines changed

packages/gatsby-plugin-sharp/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
"@babel/runtime": "^7.0.0",
1111
"async": "^2.1.2",
1212
"bluebird": "^3.5.0",
13-
"fs-exists-cached": "^1.0.0",
1413
"fs-extra": "^7.0.0",
1514
"imagemin": "^6.0.0",
1615
"imagemin-mozjpeg": "^8.0.0",

packages/gatsby-plugin-sharp/src/__tests__/__snapshots__/index.js.snap

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ Object {
1515
"aspectRatio": 2.0661764705882355,
1616
"base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABAklEQVQoz61R7WrCQBDM+7+Cv1qhlIogoUWMoH8KrfgOLVXRKDV4d7kkl6/pbsxpqBIo9mBuZ7N3k51bB7TKsrwZVsexyS2rKerwFhYFVI1DnkMQOHIuCaLB+ayuwXlMaApXgvMoxjTU8MIQE4rMx8SnNffUkU/qM/bbiPAeRfgw5n8tB+QgO1kmUvwROdm0kYHmUNoe+NoU2wZTdVjQH7Isg9YRpFIXor8vpGmKjb/FQQgslit8fi3wvQ/OHXLhxRvDHY7Qd5+x3e2udmNzTUN4fZvhoddH566L+8cn9AZuVcvpLR3e9kEAISWEkDAmbRXkGCcJ1r4PRRNPEkPu9Kn2Azs0CqJYU3JKAAAAAElFTkSuQmCC",
1717
"density": 144,
18-
"originalImg": "/static/1234/c81ba/test.png",
18+
"originalImg": "/static/1234/e681d/test.png",
1919
"originalName": undefined,
2020
"presentationHeight": 68,
2121
"presentationWidth": 141,
2222
"sizes": "(max-width: 141px) 100vw, 141px",
23-
"src": "/static/1234/c81ba/test.png",
24-
"srcSet": "/static/1234/e4832/test.png 200w,
25-
/static/1234/c81ba/test.png 281w",
23+
"src": "/static/1234/e681d/test.png",
24+
"srcSet": "/static/1234/9ec3c/test.png 200w,
25+
/static/1234/e681d/test.png 281w",
2626
"srcSetType": "image/png",
2727
"tracedSVG": undefined,
2828
}
@@ -33,14 +33,14 @@ Object {
3333
"aspectRatio": 2.0661764705882355,
3434
"base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAABYlAAAWJQFJUiTwAAABAklEQVQoz61R7WrCQBDM+7+Cv1qhlIogoUWMoH8KrfgOLVXRKDV4d7kkl6/pbsxpqBIo9mBuZ7N3k51bB7TKsrwZVsexyS2rKerwFhYFVI1DnkMQOHIuCaLB+ayuwXlMaApXgvMoxjTU8MIQE4rMx8SnNffUkU/qM/bbiPAeRfgw5n8tB+QgO1kmUvwROdm0kYHmUNoe+NoU2wZTdVjQH7Isg9YRpFIXor8vpGmKjb/FQQgslit8fi3wvQ/OHXLhxRvDHY7Qd5+x3e2udmNzTUN4fZvhoddH566L+8cn9AZuVcvpLR3e9kEAISWEkDAmbRXkGCcJ1r4PRRNPEkPu9Kn2Azs0CqJYU3JKAAAAAElFTkSuQmCC",
3535
"density": 144,
36-
"originalImg": "/static/1234/ec7d1/test.png",
36+
"originalImg": "/static/1234/e681d/test.png",
3737
"originalName": undefined,
3838
"presentationHeight": 136,
3939
"presentationWidth": 281,
4040
"sizes": "(max-width: 281px) 100vw, 281px",
41-
"src": "/static/1234/ec7d1/test.png",
42-
"srcSet": "/static/1234/dc107/test.png 200w,
43-
/static/1234/ec7d1/test.png 281w",
41+
"src": "/static/1234/e681d/test.png",
42+
"srcSet": "/static/1234/9ec3c/test.png 200w,
43+
/static/1234/e681d/test.png 281w",
4444
"srcSetType": "image/png",
4545
"tracedSVG": undefined,
4646
}
@@ -51,13 +51,13 @@ Object {
5151
"aspectRatio": 1,
5252
"base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAIAAAAC64paAAAACXBIWXMAAAsSAAALEgHS3X78AAABKklEQVQ4y8WUy07DMBBFrwMpr4aybjepUeqIFd/CAgmJsuIHgWXVL+Gx4C3xCdQJnpm0UDJNAQnVuouMM8dzY4+DcoA/C/8Fe9bv4ABMMpQOZc5yFPqfwEUQY499jHoY9/BsKQyTRTNMyzu8WAwTbBnI2DY438XbPr0qFsFCvlrkMTERYFgRL3HYUvh5OMdpQqkbBtPC9NDiINQPCZM6LGVvUzIpwNch4V5EGxHS/DeYtjfHZbcyXB8yGfaPimcafLEMHqmw2L5OsWkUUuY6Ee5V28WUP25TXjwPS3iWUFnfcFQPFna98rlmSGL4IOZucQvgT76Po53qeOTYTtp4qpFKe/qM29PhJsVVl3SXsluntLfS7rPvn10MP1Aau+lKBuA9I/kV/AyW6gNTIWTuA/r8lQAAAABJRU5ErkJggg==",
5353
"density": 72,
54-
"originalImg": "/static/1234/197d2/test.png",
54+
"originalImg": "/static/1234/c0399/test.png",
5555
"originalName": undefined,
5656
"presentationHeight": 100,
5757
"presentationWidth": 100,
5858
"sizes": "(max-width: 100px) 100vw, 100px",
59-
"src": "/static/1234/197d2/test.png",
60-
"srcSet": "/static/1234/197d2/test.png 100w",
59+
"src": "/static/1234/c0399/test.png",
60+
"srcSet": "/static/1234/c0399/test.png 100w",
6161
"srcSetType": "image/png",
6262
"tracedSVG": undefined,
6363
}
@@ -144,8 +144,8 @@ Object {
144144
"base64": undefined,
145145
"height": 100,
146146
"originalName": undefined,
147-
"src": "/static/1234/24ae5/test.png",
148-
"srcSet": "/static/1234/24ae5/test.png 1x",
147+
"src": "/static/1234/c0399/test.png",
148+
"srcSet": "/static/1234/c0399/test.png 1x",
149149
"tracedSVG": "data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3e%3cpath d='M41 24c-18 7-24 29-11 43 15 17 44 8 46-15 1-19-17-34-35-28' fill='red' fill-rule='evenodd'/%3e%3c/svg%3e",
150150
"width": 100,
151151
}
@@ -156,15 +156,15 @@ Object {
156156
"aspectRatio": 1,
157157
"base64": undefined,
158158
"density": 72,
159-
"originalImg": "/static/1234/9dd55/test.png",
159+
"originalImg": "/static/1234/c0399/test.png",
160160
"originalName": undefined,
161161
"presentationHeight": 100,
162162
"presentationWidth": 100,
163163
"sizes": "(max-width: 100px) 100vw, 100px",
164-
"src": "/static/1234/9dd55/test.png",
165-
"srcSet": "/static/1234/a516e/test.png 25w,
166-
/static/1234/208be/test.png 50w,
167-
/static/1234/9dd55/test.png 100w",
164+
"src": "/static/1234/c0399/test.png",
165+
"srcSet": "/static/1234/0f0dc/test.png 25w,
166+
/static/1234/bc08f/test.png 50w,
167+
/static/1234/c0399/test.png 100w",
168168
"srcSetType": "image/png",
169169
"tracedSVG": "data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3e%3cpath d='M41 24c-18 7-24 29-11 43 15 17 44 8 46-15 1-19-17-34-35-28' fill='red' fill-rule='evenodd'/%3e%3c/svg%3e",
170170
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
const { createArgsDigest } = require(`../process-file`)
2+
3+
describe(`createArgsDigest`, () => {
4+
const defaultArgsBaseline = {
5+
toFormat: `jpg`,
6+
width: 500,
7+
height: 500,
8+
cropFocus: 17,
9+
quality: 50,
10+
pngCompressionLevel: 4,
11+
jpegProgressive: false,
12+
grayscale: false,
13+
rotate: 0,
14+
duotone: null,
15+
}
16+
17+
describe(`changes hash if used args are different`, () => {
18+
const testHashDifferent = (label, change, extraBaselineOptions = {}) => {
19+
it(label, () => {
20+
const argsBaseline = {
21+
...defaultArgsBaseline,
22+
...extraBaselineOptions,
23+
}
24+
const baselineHash = createArgsDigest(argsBaseline)
25+
const outputHash = createArgsDigest({
26+
...defaultArgsBaseline,
27+
...change,
28+
})
29+
expect(baselineHash).not.toBe(outputHash)
30+
})
31+
}
32+
33+
testHashDifferent(`width change`, { width: defaultArgsBaseline.width + 1 })
34+
testHashDifferent(`height change`, {
35+
height: defaultArgsBaseline.height + 1,
36+
})
37+
testHashDifferent(`cropFocus change`, {
38+
cropFocus: defaultArgsBaseline.cropFocus + 1,
39+
})
40+
testHashDifferent(`format change`, { toFormat: `png` })
41+
testHashDifferent(`jpegProgressive change`, {
42+
jpegProgressive: !defaultArgsBaseline.jpegProgressive,
43+
})
44+
testHashDifferent(`grayscale change`, {
45+
grayscale: !defaultArgsBaseline.grayscale,
46+
})
47+
testHashDifferent(`rotate change`, {
48+
rotate: defaultArgsBaseline.rotate + 1,
49+
})
50+
testHashDifferent(`dutone change`, {
51+
duotone: {
52+
highlight: `#ff0000`,
53+
shadow: `#000000`,
54+
},
55+
})
56+
})
57+
58+
describe(`doesn't change hash if not used options are different`, () => {
59+
const testHashEqual = (label, change, extraBaselineOptions = {}) => {
60+
it(label, () => {
61+
const argsBaseline = {
62+
...defaultArgsBaseline,
63+
...extraBaselineOptions,
64+
}
65+
const baselineHash = createArgsDigest(argsBaseline)
66+
const outputHash = createArgsDigest({
67+
...defaultArgsBaseline,
68+
...change,
69+
})
70+
expect(baselineHash).toBe(outputHash)
71+
})
72+
}
73+
74+
testHashEqual(`unrelated argument`, { foo: `bar` })
75+
76+
testHashEqual(`png option change when using jpg`, {
77+
pngCompressionLevel: defaultArgsBaseline.pngCompressionLevel + 1,
78+
})
79+
testHashEqual(
80+
`jpg option change when using png`,
81+
{
82+
toFormat: `png`,
83+
jpegProgressive: !defaultArgsBaseline.jpegProgressive,
84+
},
85+
{
86+
toFormat: `png`,
87+
}
88+
)
89+
describe(`not used arguments`, () => {
90+
testHashEqual(`maxWidth`, { maxWidth: 500 })
91+
testHashEqual(`base64`, { base64: true })
92+
})
93+
})
94+
})

packages/gatsby-plugin-sharp/src/index.js

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const _ = require(`lodash`)
2525
const fs = require(`fs-extra`)
2626
const path = require(`path`)
2727
const { scheduleJob } = require(`./scheduler`)
28+
const { createArgsDigest } = require(`./process-file`)
2829

2930
const imageSizeCache = new Map()
3031
const getImageSize = file => {
@@ -148,33 +149,12 @@ const healOptions = (
148149

149150
function queueImageResizing({ file, args = {}, reporter }) {
150151
const options = healOptions(pluginOptions, args, file.extension)
151-
// Filter out false args, and args not for this extension and put width at
152-
// end (for the file path)
153-
const pairedArgs = _.toPairs(args)
154-
let filteredArgs
155-
// Remove non-true arguments
156-
filteredArgs = _.filter(pairedArgs, arg => arg[1])
157-
// Remove pathPrefix
158-
filteredArgs = _.filter(filteredArgs, arg => arg[0] !== `pathPrefix`)
159-
filteredArgs = _.filter(filteredArgs, arg => {
160-
if (file.extension.match(/^jp*/)) {
161-
return !_.includes(arg[0], `png`)
162-
} else if (file.extension.match(/^png/)) {
163-
return !arg[0].match(/^jp*/)
164-
}
165-
return true
166-
})
167-
const sortedArgs = _.sortBy(filteredArgs, arg => arg[0] === `width`)
168-
const fileExtension = options.toFormat ? options.toFormat : file.extension
169-
170-
const argsDigest = crypto
171-
.createHash(`md5`)
172-
.update(JSON.stringify(sortedArgs))
173-
.digest(`hex`)
174-
175-
const argsDigestShort = argsDigest.substr(argsDigest.length - 5)
152+
if (!options.toFormat) {
153+
options.toFormat = file.extension
154+
}
176155

177-
const imgSrc = `/${file.name}.${fileExtension}`
156+
const argsDigestShort = createArgsDigest(options)
157+
const imgSrc = `/${file.name}.${options.toFormat}`
178158
const dirPath = path.join(
179159
process.cwd(),
180160
`public`,
@@ -212,7 +192,7 @@ function queueImageResizing({ file, args = {}, reporter }) {
212192
}
213193

214194
// encode the file name for URL
215-
const encodedImgSrc = `/${encodeURIComponent(file.name)}.${fileExtension}`
195+
const encodedImgSrc = `/${encodeURIComponent(file.name)}.${options.toFormat}`
216196

217197
// Prefix the image src.
218198
const digestDirPrefix = `${file.internal.contentDigest}/${argsDigestShort}`

packages/gatsby-plugin-sharp/src/process-file.js

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const imagemin = require(`imagemin`)
66
const imageminMozjpeg = require(`imagemin-mozjpeg`)
77
const imageminPngquant = require(`imagemin-pngquant`)
88
const imageminWebp = require(`imagemin-webp`)
9+
const _ = require(`lodash`)
10+
const crypto = require(`crypto`)
911

1012
// Try to enable the use of SIMD instructions. Seems to provide a smallish
1113
// speedup on resizing heavy loads (~10%). Sharp disables this feature by
@@ -24,7 +26,49 @@ try {
2426
// doesn't support cpu-core-count utility.
2527
}
2628

27-
module.exports = (file, transforms, options = {}) => {
29+
/**
30+
* List of arguments used by `processFile` function.
31+
* This is used to generate args hash using only
32+
* arguments that affect output of that function.
33+
*/
34+
const argsWhitelist = [
35+
`height`,
36+
`width`,
37+
`cropFocus`,
38+
`toFormat`,
39+
`pngCompressionLevel`,
40+
`quality`,
41+
`jpegProgressive`,
42+
`grayscale`,
43+
`rotate`,
44+
`duotone`,
45+
]
46+
47+
/**
48+
* @typedef {Object} TransformArgs
49+
* @property {number} height
50+
* @property {number} width
51+
* @property {number} cropFocus
52+
* @property {string} toFormat
53+
* @property {number} pngCompressionLevel
54+
* @property {number} quality
55+
* @property {boolean} jpegProgressive
56+
* @property {boolean} grayscale
57+
* @property {number} rotate
58+
* @property {object} duotone
59+
*/
60+
61+
/**+
62+
* @typedef {Object} Transform
63+
* @property {string} outputPath
64+
* @property {TransformArgs} args
65+
*/
66+
67+
/**
68+
* @param {String} file
69+
* @param {Transform[]} transforms
70+
*/
71+
exports.processFile = (file, transforms, options = {}) => {
2872
let pipeline
2973
try {
3074
pipeline = sharp(file)
@@ -169,3 +213,26 @@ const compressWebP = (pipeline, outputPath, options) =>
169213
})
170214
.then(imageminBuffer => fs.writeFile(outputPath, imageminBuffer))
171215
)
216+
217+
exports.createArgsDigest = args => {
218+
const filtered = _.pickBy(args, (value, key) => {
219+
// remove falsy
220+
if (!value) return false
221+
if (args.toFormat.match(/^jp*/) && _.includes(key, `png`)) {
222+
return false
223+
} else if (args.toFormat.match(/^png/) && key.match(/^jp*/)) {
224+
return false
225+
}
226+
// after initial processing - get rid of unknown/unneeded fields
227+
return argsWhitelist.includes(key)
228+
})
229+
230+
const argsDigest = crypto
231+
.createHash(`md5`)
232+
.update(JSON.stringify(filtered, Object.keys(filtered).sort()))
233+
.digest(`hex`)
234+
235+
const argsDigestShort = argsDigest.substr(argsDigest.length - 5)
236+
237+
return argsDigestShort
238+
}

packages/gatsby-plugin-sharp/src/scheduler.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
const _ = require(`lodash`)
22
const ProgressBar = require(`progress`)
3-
const existsSync = require(`fs-exists-cached`).sync
3+
const { existsSync } = require(`fs`)
44
const queue = require(`async/queue`)
5-
const processFile = require(`./process-file`)
5+
const { processFile } = require(`./process-file`)
66

77
const toProcess = {}
88
let totalJobs = 0

0 commit comments

Comments
 (0)