diff --git a/src/global-options.js b/src/global-options.js index 30d80a6..f3d1eff 100644 --- a/src/global-options.js +++ b/src/global-options.js @@ -28,6 +28,7 @@ export const getDefaults = () => ({ sharpPngOptions: {}, // options passed to the Sharp png output method sharpJpegOptions: {}, // options passed to the Sharp jpeg output method sharpAvifOptions: {}, // options passed to the Sharp avif output method + sharpResizeOptions: {}, // options passed to the Sharp resize method formatHooks: { svg: svgHook, diff --git a/src/image.js b/src/image.js index 396dce3..7dc9a03 100644 --- a/src/image.js +++ b/src/image.js @@ -358,6 +358,24 @@ export default class Image { return {}; } + getSharpResizeOptions(stat, metadata) { + let resizeOptions = { + ...this.options.sharpResizeOptions, + }; + + // Eleventy Image owns output dimensions. Allowing these here would make + // returned metadata disagree with the output image. + delete resizeOptions.width; + delete resizeOptions.height; + resizeOptions.width = stat.width; + + if(!("withoutEnlargement" in resizeOptions) && (metadata.format !== "svg" || !this.options.svgAllowUpscale)) { + resizeOptions.withoutEnlargement = true; + } + + return resizeOptions; + } + async getInput() { // internal cache if(!this.#input) { @@ -415,14 +433,21 @@ export default class Image { "sharpWebpOptions", "sharpPngOptions", "sharpJpegOptions", - "sharpAvifOptions" + "sharpAvifOptions", + "sharpResizeOptions", ].sort(); let hashObject = {}; // The code currently assumes are keysToKeep are Object literals (see Util.getSortedObject) for(let key of keysToKeep) { - if(this.options[key]) { - hashObject[key] = Util.getSortedObject(this.options[key]); + let options = this.options[key]; + + if(key === "sharpResizeOptions" && Object.keys(options).length === 0) { + continue; + } + + if(options) { + hashObject[key] = Util.getSortedObject(options); } } @@ -710,15 +735,7 @@ export default class Image { if(!isTransformResize) { if(stat.width < sharpMetadata.width || (this.options.svgAllowUpscale && sharpMetadata.format === "svg")) { - let resizeOptions = { - width: stat.width - }; - - if(sharpMetadata.format !== "svg" || !this.options.svgAllowUpscale) { - resizeOptions.withoutEnlargement = true; - } - - sharpInstance.resize(resizeOptions); + sharpInstance.resize(this.getSharpResizeOptions(stat, sharpMetadata)); } } @@ -927,4 +944,3 @@ export default class Image { return img.statsByDimensionsSync(width, height); } } - diff --git a/test/test.js b/test/test.js index 672e085..2cbad4b 100644 --- a/test/test.js +++ b/test/test.js @@ -721,6 +721,81 @@ test("widths array should be ignored in hashing", t => { t.is(stats2.jpeg[1].url, "/img/KkPMmHd3hP-600.jpeg"); }); +test("empty sharpResizeOptions should be ignored in hashing", t => { + let stats = eleventyImage.statsSync("./test/bio-2017.jpg", { + widths: [1280], + sharpResizeOptions: {} + }); + + t.is(stats.jpeg[0].url, "/img/KkPMmHd3hP-1280.jpeg"); +}); + +test("dimension sharpResizeOptions should be included in hashing", t => { + let stats = eleventyImage.statsSync("./test/bio-2017.jpg", { + widths: [1280], + sharpResizeOptions: { + width: 10, + height: 10, + withoutEnlargement: true, + withoutReduction: true, + } + }); + + t.not(stats.jpeg[0].url, "/img/KkPMmHd3hP-1280.jpeg"); +}); + +test("sharpResizeOptions should apply to resize output and hash", async t => { + let defaultStats = await eleventyImage("./test/bio-2017.jpg", { + widths: [64], + formats: ["jpeg"], + outputDir: "./test/img/", + dryRun: true, + useCache: false, + }); + + let stats = await eleventyImage("./test/bio-2017.jpg", { + widths: [64], + formats: ["jpeg"], + outputDir: "./test/img/", + dryRun: true, + useCache: false, + sharpResizeOptions: { + kernel: "nearest", + }, + }); + + t.is(stats.jpeg[0].width, 64); + t.is(stats.jpeg[0].height, 42); + t.not(stats.jpeg[0].outputPath, path.join("test/img/KkPMmHd3hP-64.jpeg")); + + let defaultRaw = await sharp(defaultStats.jpeg[0].buffer).ensureAlpha().toFormat(sharp.format.raw).toBuffer(); + let nearestRaw = await sharp(stats.jpeg[0].buffer).ensureAlpha().toFormat(sharp.format.raw).toBuffer(); + + t.true(pixelmatch(defaultRaw, nearestRaw, null, stats.jpeg[0].width, stats.jpeg[0].height, { threshold: 0.15 }) > 0); +}); + +test("sharpResizeOptions should not override Eleventy Image dimensions", async t => { + let stats = await eleventyImage("./test/bio-2017.jpg", { + widths: [300], + formats: ["jpeg"], + outputDir: "./test/img/", + dryRun: true, + useCache: false, + sharpResizeOptions: { + width: 10, + height: 10, + kernel: "nearest", + }, + }); + + let outputMetadata = await sharp(stats.jpeg[0].buffer).metadata(); + + t.is(stats.jpeg[0].width, 300); + t.is(stats.jpeg[0].height, 199); + t.is(outputMetadata.width, 300); + t.true(outputMetadata.height > 10); +}); + test("statsSync and eleventyImage output comparison", async t => { let statsSync = eleventyImage.statsSync("./test/bio-2017.jpg", { widths: [399],