diff --git a/img.js b/img.js index 3602afe..8b1d351 100644 --- a/img.js +++ b/img.js @@ -439,9 +439,10 @@ class Image { } // https://jdhao.github.io/2019/07/31/image_rotation_exif_info/ - // Orientation 5 to 8 means width/height are flipped - needsRotation(orientation) { - return orientation >= 5; + // Orientations 5 to 8 mean image is rotated ±90º (width/height are flipped) + isQuarterTurn(orientation) { + // Sharp's metadata API exposes undefined EXIF orientations >8 as 1 (normal) but check anyways + return orientation >= 5 && orientation <= 8; } // metadata so far: width, height, format @@ -450,11 +451,8 @@ class Image { let results = []; let outputFormats = Image.getFormatsArray(this.options.formats, metadata.format || this.options.overrideInputFormat, this.options.svgShortCircuit); - if (this.needsRotation(metadata.orientation)) { - let height = metadata.height; - let width = metadata.width; - metadata.width = height; - metadata.height = width; + if (this.isQuarterTurn(metadata.orientation)) { + [metadata.height, metadata.width] = [metadata.width, metadata.height]; } if(metadata.pageHeight) { @@ -541,6 +539,13 @@ class Image { } let sharpInstance = sharpImage.clone(); + // Output images do not include orientation metadata (https://github.com/11ty/eleventy-img/issues/52) + // Use sharp.rotate to bake orientation into the image (https://github.com/lovell/sharp/blob/v0.32.6/docs/api-operation.md#rotate): + // > If no angle is provided, it is determined from the EXIF data. Mirroring is supported and may infer the use of a flip operation. + // > The use of rotate without an angle will remove the EXIF Orientation tag, if any. + 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 @@ -548,14 +553,8 @@ class Image { if(metadata.format !== "svg" || !this.options.svgAllowUpscale) { resizeOptions.withoutEnlargement = true; } - if(this.options.fixOrientation || this.needsRotation(metadata.orientation)) { - sharpInstance.rotate(); - } + sharpInstance.resize(resizeOptions); - } else if (metadata.format !== "svg") { - if(this.options.fixOrientation || stat.width === metadata.width && this.needsRotation(metadata.orientation)) { - sharpInstance.rotate(); - } } if(!this.options.dryRun) { diff --git a/package.json b/package.json index 9ad2d44..c6891f0 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,8 @@ "@11ty/eleventy": "^2.0.1", "@11ty/eleventy-plugin-webc": "^0.11.1", "ava": "^5.3.1", - "eslint": "^8.52.0" + "eslint": "^8.52.0", + "pixelmatch": "^5.3.0" }, "ava": { "failFast": false, diff --git a/test/exif-Landscape_15.jpg b/test/exif-Landscape_15.jpg new file mode 100644 index 0000000..535d1cd Binary files /dev/null and b/test/exif-Landscape_15.jpg differ diff --git a/test/exif-Landscape_3-bakedOrientation-200.jpg b/test/exif-Landscape_3-bakedOrientation-200.jpg new file mode 100644 index 0000000..6819d13 Binary files /dev/null and b/test/exif-Landscape_3-bakedOrientation-200.jpg differ diff --git a/test/exif-Landscape_3-bakedOrientation.jpg b/test/exif-Landscape_3-bakedOrientation.jpg new file mode 100644 index 0000000..0a157de Binary files /dev/null and b/test/exif-Landscape_3-bakedOrientation.jpg differ diff --git a/test/exif-Landscape_3.jpg b/test/exif-Landscape_3.jpg new file mode 100644 index 0000000..f508052 Binary files /dev/null and b/test/exif-Landscape_3.jpg differ diff --git a/test/test.js b/test/test.js index 308a48e..b94569c 100644 --- a/test/test.js +++ b/test/test.js @@ -3,6 +3,8 @@ const test = require("ava"); const fs = require("fs"); const { URL } = require("url"); const eleventyImage = require("../"); +const sharp = require("sharp"); +const pixelmatch = require('pixelmatch'); // Remember that any outputPath tests must use path.join to work on Windows @@ -846,6 +848,32 @@ test("Maintains orientation #132", async t => { }); // Broken test cases from https://github.com/recurser/exif-orientation-examples +test("#158: Test EXIF orientation data landscape (3)", async t => { + let stats = await eleventyImage("./test/exif-Landscape_3.jpg", { + widths: [200, "auto"], + formats: ['auto'], + useCache: false, + dryRun: true, + }); + + t.is(stats.jpeg.length, 2); + t.is(stats.jpeg[0].width, 200); + t.is(stats.jpeg[1].width, 1800); + t.is(Math.floor(stats.jpeg[0].height), 133); + t.is(stats.jpeg[1].height, 1200); + + // This orientation (180º rotation) preserves image dimensions and requires an image diff + const readToRaw = async input => { + // pixelmatch requires 4 bytes/pixel, hence alpha + return sharp(input).ensureAlpha().toFormat(sharp.format.raw).toBuffer(); + }; + for (const [inSrc, outStat] of [["./test/exif-Landscape_3-bakedOrientation-200.jpg", stats.jpeg[0]], ["./test/exif-Landscape_3-bakedOrientation.jpg", stats.jpeg[1]]]) { + const inRaw = await readToRaw(inSrc); + const outRaw = await readToRaw(outStat.buffer); + t.is(pixelmatch(inRaw, outRaw, null, outStat.width, outStat.height, { threshold: 0.15 }), 0); + } +}); + test("#132: Test EXIF orientation data landscape (5)", async t => { let stats = await eleventyImage("./test/exif-Landscape_5.jpg", { widths: [400, "auto"], @@ -892,7 +920,20 @@ test("#132: Test EXIF orientation data landscape (8)", async t => { widths: [400], formats: ['auto'], outputDir: "./test/img/", - // dryRun: true, + dryRun: true, + }); + + t.is(stats.jpeg.length, 1); + t.is(stats.jpeg[0].width, 400); + t.is(Math.floor(stats.jpeg[0].height), 266); +}); + +test("#158: Test EXIF orientation data landscape (15)", async t => { + let stats = await eleventyImage("./test/exif-Landscape_15.jpg", { + widths: [400], + formats: ['auto'], + outputDir: "./test/img/", + dryRun: true, }); t.is(stats.jpeg.length, 1);