Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bake input orientation into output image #194

Merged
merged 4 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 14 additions & 15 deletions img.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -541,21 +539,22 @@ 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
};
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) {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Binary file added test/exif-Landscape_15.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/exif-Landscape_3-bakedOrientation-200.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/exif-Landscape_3-bakedOrientation.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added test/exif-Landscape_3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 42 additions & 1 deletion test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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);
Expand Down