Skip to content

Commit 50787f9

Browse files
Respect EXIF orientation when resizing
1 parent c99f07c commit 50787f9

2 files changed

Lines changed: 159 additions & 41 deletions

File tree

src/ImageSharp.Web/Processors/ResizeWebProcessor.cs

Lines changed: 56 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using System.Globalization;
66
using Microsoft.Extensions.Logging;
7+
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
78
using SixLabors.ImageSharp.Processing;
89
using SixLabors.ImageSharp.Processing.Processors.Transforms;
910
using SixLabors.ImageSharp.Web.Commands;
@@ -41,10 +42,15 @@ public class ResizeWebProcessor : IImageWebProcessor
4142
public const string Sampler = "rsampler";
4243

4344
/// <summary>
44-
/// The command constant for the resize sampler.
45+
/// The command constant for the resize anchor position.
4546
/// </summary>
4647
public const string Anchor = "ranchor";
4748

49+
/// <summary>
50+
/// The command constant for the resize orientation handling mode.
51+
/// </summary>
52+
public const string Orient = "rorient";
53+
4854
/// <summary>
4955
/// The command constant for the resize compand mode.
5056
/// </summary>
@@ -59,7 +65,8 @@ private static readonly IEnumerable<string> ResizeCommands
5965
Mode,
6066
Sampler,
6167
Anchor,
62-
Compand
68+
Compand,
69+
Orient
6370
};
6471

6572
/// <inheritdoc/>
@@ -73,7 +80,7 @@ public FormattedImage Process(
7380
CommandParser parser,
7481
CultureInfo culture)
7582
{
76-
ResizeOptions options = GetResizeOptions(commands, parser, culture);
83+
ResizeOptions options = GetResizeOptions(image.Image, commands, parser, culture);
7784

7885
if (options != null)
7986
{
@@ -84,6 +91,7 @@ public FormattedImage Process(
8491
}
8592

8693
private static ResizeOptions GetResizeOptions(
94+
Image image,
8795
CommandCollection commands,
8896
CommandParser parser,
8997
CultureInfo culture)
@@ -93,7 +101,7 @@ private static ResizeOptions GetResizeOptions(
93101
return null;
94102
}
95103

96-
Size size = ParseSize(commands, parser, culture);
104+
Size size = ParseSize(image, commands, parser, culture);
97105

98106
if (size.Width <= 0 && size.Height <= 0)
99107
{
@@ -116,15 +124,38 @@ private static ResizeOptions GetResizeOptions(
116124
}
117125

118126
private static Size ParseSize(
127+
Image image,
119128
CommandCollection commands,
120129
CommandParser parser,
121130
CultureInfo culture)
122131
{
123132
// The command parser will reject negative numbers as it clamps values to ranges.
124-
uint width = parser.ParseValue<uint>(commands.GetValueOrDefault(Width), culture);
125-
uint height = parser.ParseValue<uint>(commands.GetValueOrDefault(Height), culture);
133+
int width = (int)parser.ParseValue<uint>(commands.GetValueOrDefault(Width), culture);
134+
int height = (int)parser.ParseValue<uint>(commands.GetValueOrDefault(Height), culture);
135+
136+
// Browsers now implement 'image-orientation: from-image' by default.
137+
// https://developer.mozilla.org/en-US/docs/web/css/image-orientation
138+
// This makes orientation handling confusing for users who expect images to be resized in accordance
139+
// to what they observe rather than pure (and correct) methods.
140+
//
141+
// To accomodate this we parse the dimensions to use based upon decoded EXIF orientation values, switching
142+
// the width/height parameters when images are rotated (not flipped).
143+
// We default to 'true' for EXIF orientation handling. By passing 'false' it can be turned off.
144+
if (!commands.Contains(Orient) || parser.ParseValue<bool>(commands.GetValueOrDefault(Orient), culture))
145+
{
146+
if (image.Metadata.ExifProfile != null)
147+
{
148+
IExifValue<ushort> orientation = image.Metadata.ExifProfile.GetValue(ExifTag.Orientation);
149+
return orientation.Value switch
150+
{
151+
// LeftTop, RightTop, RightBottom, LeftBottom
152+
5 or 6 or 7 or 8 => new Size(height, width),
153+
_ => new Size(width, height),
154+
};
155+
}
156+
}
126157

127-
return new Size((int)width, (int)height);
158+
return new Size(width, height);
128159
}
129160

130161
private static PointF? GetCenter(
@@ -166,40 +197,25 @@ private static IResampler GetSampler(CommandCollection commands)
166197

167198
if (sampler != null)
168199
{
169-
switch (sampler.ToLowerInvariant())
200+
// No need to do a case test here. Parsed commands are automatically converted to lowercase.
201+
return sampler switch
170202
{
171-
case "nearest":
172-
case "nearestneighbor":
173-
return KnownResamplers.NearestNeighbor;
174-
case "box":
175-
return KnownResamplers.Box;
176-
case "mitchell":
177-
case "mitchellnetravali":
178-
return KnownResamplers.MitchellNetravali;
179-
case "catmull":
180-
case "catmullrom":
181-
return KnownResamplers.CatmullRom;
182-
case "lanczos2":
183-
return KnownResamplers.Lanczos2;
184-
case "lanczos3":
185-
return KnownResamplers.Lanczos3;
186-
case "lanczos5":
187-
return KnownResamplers.Lanczos5;
188-
case "lanczos8":
189-
return KnownResamplers.Lanczos8;
190-
case "welch":
191-
return KnownResamplers.Welch;
192-
case "robidoux":
193-
return KnownResamplers.Robidoux;
194-
case "robidouxsharp":
195-
return KnownResamplers.RobidouxSharp;
196-
case "spline":
197-
return KnownResamplers.Spline;
198-
case "triangle":
199-
return KnownResamplers.Triangle;
200-
case "hermite":
201-
return KnownResamplers.Hermite;
202-
}
203+
"nearest" or "nearestneighbor" => KnownResamplers.NearestNeighbor,
204+
"box" => KnownResamplers.Box,
205+
"mitchell" or "mitchellnetravali" => KnownResamplers.MitchellNetravali,
206+
"catmull" or "catmullrom" => KnownResamplers.CatmullRom,
207+
"lanczos2" => KnownResamplers.Lanczos2,
208+
"lanczos3" => KnownResamplers.Lanczos3,
209+
"lanczos5" => KnownResamplers.Lanczos5,
210+
"lanczos8" => KnownResamplers.Lanczos8,
211+
"welch" => KnownResamplers.Welch,
212+
"robidoux" => KnownResamplers.Robidoux,
213+
"robidouxsharp" => KnownResamplers.RobidouxSharp,
214+
"spline" => KnownResamplers.Spline,
215+
"triangle" => KnownResamplers.Triangle,
216+
"hermite" => KnownResamplers.Hermite,
217+
_ => KnownResamplers.Bicubic,
218+
};
203219
}
204220

205221
return KnownResamplers.Bicubic;

tests/ImageSharp.Web.Tests/Processors/ResizeWebProcessorTests.cs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Collections.Generic;
55
using System.Globalization;
66
using SixLabors.ImageSharp.Formats.Png;
7+
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
78
using SixLabors.ImageSharp.PixelFormats;
89
using SixLabors.ImageSharp.Processing;
910
using SixLabors.ImageSharp.Web.Commands;
@@ -50,7 +51,9 @@ public void ResizeWebProcessor_UpdatesSize(string resampler)
5051

5152
CommandCollection commands = new()
5253
{
53-
{ new(ResizeWebProcessor.Sampler, resampler) },
54+
// We only need to do this for the unit tests.
55+
// Commands generated via URL will automatically be converted to lowecase
56+
{ new(ResizeWebProcessor.Sampler, resampler.ToLowerInvariant()) },
5457
{ new(ResizeWebProcessor.Width, width.ToString()) },
5558
{ new(ResizeWebProcessor.Height, height.ToString()) },
5659
{ new(ResizeWebProcessor.Xy, "0,0") },
@@ -64,5 +67,104 @@ public void ResizeWebProcessor_UpdatesSize(string resampler)
6467
Assert.Equal(width, image.Width);
6568
Assert.Equal(height, image.Height);
6669
}
70+
71+
[Theory]
72+
[InlineData(0, false)]
73+
[InlineData(1, false)]
74+
[InlineData(2, false)]
75+
[InlineData(3, false)]
76+
[InlineData(4, false)]
77+
[InlineData(5, true)]
78+
[InlineData(6, true)]
79+
[InlineData(7, true)]
80+
[InlineData(8, true)]
81+
public void ResizeWebProcessor_RespectsOrientation(ushort orientation, bool rotated)
82+
{
83+
const int width = 4;
84+
const int height = 6;
85+
86+
var converters = new List<ICommandConverter>
87+
{
88+
new IntegralNumberConverter<uint>(),
89+
new ArrayConverter<float>(),
90+
new EnumConverter(),
91+
new SimpleCommandConverter<bool>(),
92+
new SimpleCommandConverter<float>()
93+
};
94+
95+
var parser = new CommandParser(converters);
96+
CultureInfo culture = CultureInfo.InvariantCulture;
97+
98+
CommandCollection commands = new()
99+
{
100+
{ new(ResizeWebProcessor.Width, width.ToString()) },
101+
{ new(ResizeWebProcessor.Height, height.ToString()) },
102+
{ new(ResizeWebProcessor.Mode, nameof(ResizeMode.Stretch)) }
103+
};
104+
105+
using var image = new Image<Rgba32>(1, 1);
106+
image.Metadata.ExifProfile = new();
107+
image.Metadata.ExifProfile.SetValue(ExifTag.Orientation, orientation);
108+
109+
using var formatted = new FormattedImage(image, PngFormat.Instance);
110+
new ResizeWebProcessor().Process(formatted, null, commands, parser, culture);
111+
112+
if (rotated)
113+
{
114+
Assert.Equal(height, image.Width);
115+
Assert.Equal(width, image.Height);
116+
}
117+
else
118+
{
119+
Assert.Equal(width, image.Width);
120+
Assert.Equal(height, image.Height);
121+
}
122+
}
123+
124+
[Theory]
125+
[InlineData(0)]
126+
[InlineData(1)]
127+
[InlineData(2)]
128+
[InlineData(3)]
129+
[InlineData(4)]
130+
[InlineData(5)]
131+
[InlineData(6)]
132+
[InlineData(7)]
133+
[InlineData(8)]
134+
public void ResizeWebProcessor_CanIgnoreOrientation(ushort orientation)
135+
{
136+
const int width = 4;
137+
const int height = 6;
138+
139+
var converters = new List<ICommandConverter>
140+
{
141+
new IntegralNumberConverter<uint>(),
142+
new ArrayConverter<float>(),
143+
new EnumConverter(),
144+
new SimpleCommandConverter<bool>(),
145+
new SimpleCommandConverter<float>()
146+
};
147+
148+
var parser = new CommandParser(converters);
149+
CultureInfo culture = CultureInfo.InvariantCulture;
150+
151+
CommandCollection commands = new()
152+
{
153+
{ new(ResizeWebProcessor.Width, width.ToString()) },
154+
{ new(ResizeWebProcessor.Height, height.ToString()) },
155+
{ new(ResizeWebProcessor.Mode, nameof(ResizeMode.Stretch)) },
156+
{ new(ResizeWebProcessor.Orient, bool.FalseString) }
157+
};
158+
159+
using var image = new Image<Rgba32>(1, 1);
160+
image.Metadata.ExifProfile = new();
161+
image.Metadata.ExifProfile.SetValue(ExifTag.Orientation, orientation);
162+
163+
using var formatted = new FormattedImage(image, PngFormat.Instance);
164+
new ResizeWebProcessor().Process(formatted, null, commands, parser, culture);
165+
166+
Assert.Equal(width, image.Width);
167+
Assert.Equal(height, image.Height);
168+
}
67169
}
68170
}

0 commit comments

Comments
 (0)