Skip to content

Commit

Permalink
Support manual resize in transform callback hook #52 Important for …
Browse files Browse the repository at this point in the history
…crops.
  • Loading branch information
zachleat committed Jan 8, 2025
1 parent 0dd20c1 commit dd22b9e
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 20 deletions.
80 changes: 63 additions & 17 deletions src/image.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ class Image {
return JSON.stringify(opts, function(key, value) {
// allows `transform` functions to be truthy for in-memory key
if (typeof value === "function") {
return "<fn>";
return "<fn>" + (value.name || "");
}
return value;
});
Expand Down Expand Up @@ -467,7 +467,7 @@ class Image {
}
}

let stats = {
let statEntry = {
format: outputFormat,
width: width,
height: height,
Expand All @@ -479,11 +479,11 @@ class Image {
};

if(outputFilename) {
stats.filename = outputFilename; // optional
stats.outputPath = path.join(this.options.outputDir, outputFilename); // optional
statEntry.filename = outputFilename; // optional
statEntry.outputPath = path.join(this.options.outputDir, outputFilename); // optional
}

return stats;
return statEntry;
}

// https://jdhao.github.io/2019/07/31/image_rotation_exif_info/
Expand Down Expand Up @@ -531,7 +531,7 @@ class Image {

for(let outputFormat of outputFormats) {
if(!outputFormat || outputFormat === "auto") {
throw new Error("When using statsSync or statsByDimensionsSync, `formats: [null | auto]` to use the native image format is not supported.");
throw new Error("When using statsSync or statsByDimensionsSync, `formats: [null | 'auto']` to use the native image format is not supported.");
}

if(outputFormat === "svg") {
Expand All @@ -557,9 +557,7 @@ class Image {
} else { // not outputting SVG (might still be SVG input though!)
let widths = Image.getValidWidths(metadata.width, this.options.widths, metadata.format === "svg" && this.options.svgAllowUpscale, this.options.minimumThreshold);
for(let width of widths) {
// Warning: if this is a guess via statsByDimensionsSync and that guess is wrong
// The aspect ratio will be wrong and any height/widths returned will be wrong!
let height = Math.floor(width * metadata.height / metadata.width);
let height = Image.getAspectRatioHeight(metadata, width);

results.push(this.getStat(outputFormat, width, height));
}
Expand All @@ -569,6 +567,39 @@ class Image {
return this.#transformRawFiles(results);
}

static getDimensionsFromSharp(sharpInstance, stat) {
let dims = {};
if(sharpInstance.options.width > -1) {
dims.width = sharpInstance.options.width;
dims.resized = true;
}
if(sharpInstance.options.height > -1) {
dims.height = sharpInstance.options.height;
dims.resized = true;
}

if(dims.width || dims.height) {
if(!dims.width) {
dims.width = Image.getAspectRatioWidth(stat, dims.height);
}
if(!dims.height) {
dims.height = Image.getAspectRatioHeight(stat, dims.width);
}
}

return dims;
}

static getAspectRatioWidth(originalDimensions, newHeight) {
return Math.floor(newHeight * originalDimensions.width / originalDimensions.height);
}

static getAspectRatioHeight(originalDimensions, newWidth) {
// Warning: if this is a guess via statsByDimensionsSync and that guess is wrong
// The aspect ratio will be wrong and any height/widths returned will be wrong!
return Math.floor(newWidth * originalDimensions.height / originalDimensions.width);
}

getOutputSize(contents, filePath) {
if(contents) {
if(this.options.svgCompressionSize === "br") {
Expand Down Expand Up @@ -643,12 +674,23 @@ class Image {

let sharpInstance = sharpInputImage.clone();
let transform = this.options.transform;
let isTransformResize = false;

if(transform) {
if(typeof transform !== "function") {
throw new Error("Expected `function` type in `transform` option. Received: " + transform);
}

await transform(sharpInstance);

// Resized in a transform (maybe for a crop)
let dims = Image.getDimensionsFromSharp(sharpInstance, stat);
if(dims.resized) {
isTransformResize = true;

// Overwrite current `stat` object with new sizes and file names
stat = this.getStat(stat.format, dims.width, dims.height);
}
}

// https://github.com/11ty/eleventy-img/issues/244
Expand All @@ -661,15 +703,19 @@ class Image {
if(this.options.fixOrientation || this.needsRotation(metadata.orientation)) {
sharpInstance.rotate();
}
if(stat.width < metadata.width || (this.options.svgAllowUpscale && metadata.format === "svg")) {
let resizeOptions = {
width: stat.width
};
if(metadata.format !== "svg" || !this.options.svgAllowUpscale) {
resizeOptions.withoutEnlargement = true;
}

sharpInstance.resize(resizeOptions);
if(!isTransformResize) {
if(stat.width < metadata.width || (this.options.svgAllowUpscale && metadata.format === "svg")) {
let resizeOptions = {
width: stat.width
};

if(metadata.format !== "svg" || !this.options.svgAllowUpscale) {
resizeOptions.withoutEnlargement = true;
}

sharpInstance.resize(resizeOptions);
}
}

// Format hooks take priority over Sharp processing.
Expand Down
35 changes: 32 additions & 3 deletions test/transform-hooks-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const exifr = require("exifr");

const eleventyImage = require("../img.js");

test("Transforms Empty", async t => {
test("Transform Empty", async t => {
let stats = await eleventyImage("./test/exif-sample-large.jpg", {
formats: ["auto"],
// transform: undefined,
Expand All @@ -14,11 +14,11 @@ test("Transforms Empty", async t => {
t.deepEqual(exif, undefined);
});

test("Transforms keep exif", async t => {
test("Transform to keep exif", async t => {
let stats = await eleventyImage("./test/exif-sample-large.jpg", {
formats: ["auto"],
// Keep exif metadata
transform: (sharp) => {
transform: function customNameForCacheKey1(sharp) {
sharp.keepExif();
},
dryRun: true,
Expand All @@ -31,3 +31,32 @@ test("Transforms keep exif", async t => {
t.is(exif.ApertureValue, 2);
t.is(exif.BrightnessValue, 9.38);
});

test("Transform to crop an image", async t => {
let stats = await eleventyImage("./test/exif-sample-large.jpg", {
formats: ["auto"],
transform: function customNameForCacheKey2(sharp) {
sharp.resize(300, 300);
},
dryRun: true,
});

t.is(stats.jpeg[0].width, 300);
t.is(stats.jpeg[0].height, 300);
t.true(stats.jpeg[0].size < 50000);
});

test("Resize in a transform an image takes precedence", async t => {
let stats = await eleventyImage("./test/exif-sample-large.jpg", {
formats: ["auto"],
transform: function customNameForCacheKey3(sharp) {
sharp.resize(400);
},
dryRun: true,
});

t.is(stats.jpeg[0].width, 400);
t.is(stats.jpeg[0].height, 300);
t.true(stats.jpeg[0].size < 50000);
});

0 comments on commit dd22b9e

Please sign in to comment.