Skip to content

Commit dd22b9e

Browse files
committed
Support manual resize in transform callback hook #52 Important for crops.
1 parent 0dd20c1 commit dd22b9e

File tree

2 files changed

+95
-20
lines changed

2 files changed

+95
-20
lines changed

src/image.js

+63-17
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ class Image {
142142
return JSON.stringify(opts, function(key, value) {
143143
// allows `transform` functions to be truthy for in-memory key
144144
if (typeof value === "function") {
145-
return "<fn>";
145+
return "<fn>" + (value.name || "");
146146
}
147147
return value;
148148
});
@@ -467,7 +467,7 @@ class Image {
467467
}
468468
}
469469

470-
let stats = {
470+
let statEntry = {
471471
format: outputFormat,
472472
width: width,
473473
height: height,
@@ -479,11 +479,11 @@ class Image {
479479
};
480480

481481
if(outputFilename) {
482-
stats.filename = outputFilename; // optional
483-
stats.outputPath = path.join(this.options.outputDir, outputFilename); // optional
482+
statEntry.filename = outputFilename; // optional
483+
statEntry.outputPath = path.join(this.options.outputDir, outputFilename); // optional
484484
}
485485

486-
return stats;
486+
return statEntry;
487487
}
488488

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

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

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

564562
results.push(this.getStat(outputFormat, width, height));
565563
}
@@ -569,6 +567,39 @@ class Image {
569567
return this.#transformRawFiles(results);
570568
}
571569

570+
static getDimensionsFromSharp(sharpInstance, stat) {
571+
let dims = {};
572+
if(sharpInstance.options.width > -1) {
573+
dims.width = sharpInstance.options.width;
574+
dims.resized = true;
575+
}
576+
if(sharpInstance.options.height > -1) {
577+
dims.height = sharpInstance.options.height;
578+
dims.resized = true;
579+
}
580+
581+
if(dims.width || dims.height) {
582+
if(!dims.width) {
583+
dims.width = Image.getAspectRatioWidth(stat, dims.height);
584+
}
585+
if(!dims.height) {
586+
dims.height = Image.getAspectRatioHeight(stat, dims.width);
587+
}
588+
}
589+
590+
return dims;
591+
}
592+
593+
static getAspectRatioWidth(originalDimensions, newHeight) {
594+
return Math.floor(newHeight * originalDimensions.width / originalDimensions.height);
595+
}
596+
597+
static getAspectRatioHeight(originalDimensions, newWidth) {
598+
// Warning: if this is a guess via statsByDimensionsSync and that guess is wrong
599+
// The aspect ratio will be wrong and any height/widths returned will be wrong!
600+
return Math.floor(newWidth * originalDimensions.height / originalDimensions.width);
601+
}
602+
572603
getOutputSize(contents, filePath) {
573604
if(contents) {
574605
if(this.options.svgCompressionSize === "br") {
@@ -643,12 +674,23 @@ class Image {
643674

644675
let sharpInstance = sharpInputImage.clone();
645676
let transform = this.options.transform;
677+
let isTransformResize = false;
678+
646679
if(transform) {
647680
if(typeof transform !== "function") {
648681
throw new Error("Expected `function` type in `transform` option. Received: " + transform);
649682
}
650683

651684
await transform(sharpInstance);
685+
686+
// Resized in a transform (maybe for a crop)
687+
let dims = Image.getDimensionsFromSharp(sharpInstance, stat);
688+
if(dims.resized) {
689+
isTransformResize = true;
690+
691+
// Overwrite current `stat` object with new sizes and file names
692+
stat = this.getStat(stat.format, dims.width, dims.height);
693+
}
652694
}
653695

654696
// https://github.com/11ty/eleventy-img/issues/244
@@ -661,15 +703,19 @@ class Image {
661703
if(this.options.fixOrientation || this.needsRotation(metadata.orientation)) {
662704
sharpInstance.rotate();
663705
}
664-
if(stat.width < metadata.width || (this.options.svgAllowUpscale && metadata.format === "svg")) {
665-
let resizeOptions = {
666-
width: stat.width
667-
};
668-
if(metadata.format !== "svg" || !this.options.svgAllowUpscale) {
669-
resizeOptions.withoutEnlargement = true;
670-
}
671706

672-
sharpInstance.resize(resizeOptions);
707+
if(!isTransformResize) {
708+
if(stat.width < metadata.width || (this.options.svgAllowUpscale && metadata.format === "svg")) {
709+
let resizeOptions = {
710+
width: stat.width
711+
};
712+
713+
if(metadata.format !== "svg" || !this.options.svgAllowUpscale) {
714+
resizeOptions.withoutEnlargement = true;
715+
}
716+
717+
sharpInstance.resize(resizeOptions);
718+
}
673719
}
674720

675721
// Format hooks take priority over Sharp processing.

test/transform-hooks-test.js

+32-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ const exifr = require("exifr");
33

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

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

17-
test("Transforms keep exif", async t => {
17+
test("Transform to keep exif", async t => {
1818
let stats = await eleventyImage("./test/exif-sample-large.jpg", {
1919
formats: ["auto"],
2020
// Keep exif metadata
21-
transform: (sharp) => {
21+
transform: function customNameForCacheKey1(sharp) {
2222
sharp.keepExif();
2323
},
2424
dryRun: true,
@@ -31,3 +31,32 @@ test("Transforms keep exif", async t => {
3131
t.is(exif.ApertureValue, 2);
3232
t.is(exif.BrightnessValue, 9.38);
3333
});
34+
35+
test("Transform to crop an image", async t => {
36+
let stats = await eleventyImage("./test/exif-sample-large.jpg", {
37+
formats: ["auto"],
38+
transform: function customNameForCacheKey2(sharp) {
39+
sharp.resize(300, 300);
40+
},
41+
dryRun: true,
42+
});
43+
44+
t.is(stats.jpeg[0].width, 300);
45+
t.is(stats.jpeg[0].height, 300);
46+
t.true(stats.jpeg[0].size < 50000);
47+
});
48+
49+
test("Resize in a transform an image takes precedence", async t => {
50+
let stats = await eleventyImage("./test/exif-sample-large.jpg", {
51+
formats: ["auto"],
52+
transform: function customNameForCacheKey3(sharp) {
53+
sharp.resize(400);
54+
},
55+
dryRun: true,
56+
});
57+
58+
t.is(stats.jpeg[0].width, 400);
59+
t.is(stats.jpeg[0].height, 300);
60+
t.true(stats.jpeg[0].size < 50000);
61+
});
62+

0 commit comments

Comments
 (0)