Skip to content

Commit b4fd913

Browse files
Apply updates to XY and Anchor positions based on EXIF metadata
1 parent 9a03ec8 commit b4fd913

4 files changed

Lines changed: 326 additions & 44 deletions

File tree

.github/CONTRIBUTING.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# How to contribute to ImageSharp.Web
1+
# How to contribute to SixLabors.ImageSharp.Web
22

33
#### **Did you find a bug?**
44

@@ -16,20 +16,18 @@
1616

1717
#### **Do you intend to add a new feature or change an existing one?**
1818

19-
* Suggest your change in the [ImageSharp Gitter Chat Room](https://gitter.im/ImageSharp/General) and start writing code.
19+
* Suggest your change in the [Ideas Discussions Channel](https://github.com/SixLabors/ImageSharp.Web/discussions?discussions_q=category%3AIdeas) and start writing code.
2020

2121
* Do not open an issue on GitHub until you have collected positive feedback about the change. GitHub issues are primarily intended for bug reports and fixes.
2222

2323
#### **Do you have questions about consuming the library or the source code?**
2424

25-
* Ask any question about how to use ImageSharp in the [ImageSharp Gitter Chat Room](https://gitter.im/ImageSharp/General).
25+
* Ask any question about how to use SixLabors.ImageSharp.Web in the [Help Discussions Channel](https://github.com/SixLabors/ImageSharp.Web/discussions?discussions_q=category%3AHelp).
2626

27-
#### Code of Conduct
27+
#### Code of Conduct
2828
This project has adopted the code of conduct defined by the [Contributor Covenant](https://contributor-covenant.org/) to clarify expected behavior in our community.
2929
For more information, see the [.NET Foundation Code of Conduct](https://dotnetfoundation.org/code-of-conduct).
3030

31-
And please remember. ImageSharp.Web is the work of a very, very, small number of developers who struggle balancing time to contribute to the project with family time and work commitments. We encourage you to pitch in and help make our vision of simple accessible imageprocessing available to all. Open Source can only exist with your help.
31+
And please remember. SixLabors.ImageSharp.Web is the work of a very, very, small number of developers who struggle balancing time to contribute to the project with family time and work commitments. We encourage you to pitch in and help make our vision of simple accessible image processing available to all. Open Source can only exist with your help.
3232

3333
Thanks for reading!
34-
35-
James Jackson-South :heart:

src/ImageSharp.Web/FormattedImage.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Runtime.CompilerServices;
77
using SixLabors.ImageSharp.Advanced;
88
using SixLabors.ImageSharp.Formats;
9+
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
910
using SixLabors.ImageSharp.PixelFormats;
1011

1112
namespace SixLabors.ImageSharp.Web
@@ -97,6 +98,45 @@ public static FormattedImage Load(Configuration configuration, Stream source)
9798
/// <param name="destination">The destination stream.</param>
9899
public void Save(Stream destination) => this.Image.Save(destination, this.encoder);
99100

101+
/// <summary>
102+
/// Returns a value indicating whether the source image contains EXIF metadata for <see cref="ExifTag.Orientation"/>
103+
/// indicating that the image is rotated (not flipped).
104+
/// </summary>
105+
/// <param name="orientation">The captured orientation. Use <see cref="ExifOrientationMode"/> for comparison.</param>
106+
/// <returns>The <see cref="bool"/> indicating whether the image contains EXIF metadata indicating that the image is rotated.</returns>
107+
public bool IsExifRotated(out ushort orientation)
108+
{
109+
orientation = ExifOrientationMode.Unknown;
110+
if (this.Image.Metadata.ExifProfile != null)
111+
{
112+
IExifValue<ushort> value = this.Image.Metadata.ExifProfile.GetValue(ExifTag.Orientation);
113+
if (value is null)
114+
{
115+
return false;
116+
}
117+
118+
if (value.DataType == ExifDataType.Short)
119+
{
120+
orientation = value.Value;
121+
}
122+
else
123+
{
124+
orientation = Convert.ToUInt16(value.Value);
125+
}
126+
127+
return orientation switch
128+
{
129+
ExifOrientationMode.LeftTop
130+
or ExifOrientationMode.RightTop
131+
or ExifOrientationMode.RightBottom
132+
or ExifOrientationMode.LeftBottom => true,
133+
_ => false,
134+
};
135+
}
136+
137+
return false;
138+
}
139+
100140
/// <summary>
101141
/// Performs application-defined tasks associated with freeing, releasing, or resetting
102142
/// unmanaged resources.

src/ImageSharp.Web/Processors/ResizeWebProcessor.cs

Lines changed: 112 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ public FormattedImage Process(
8080
CommandParser parser,
8181
CultureInfo culture)
8282
{
83-
ResizeOptions options = GetResizeOptions(image.Image, commands, parser, culture);
83+
ResizeOptions options = GetResizeOptions(image, commands, parser, culture);
8484

8585
if (options != null)
8686
{
@@ -90,8 +90,18 @@ public FormattedImage Process(
9090
return image;
9191
}
9292

93-
private static ResizeOptions GetResizeOptions(
94-
Image image,
93+
/// <summary>
94+
/// Parses the command collection returning the resize options.
95+
/// </summary>
96+
/// <param name="image">The image to process.</param>
97+
/// <param name="commands">The ordered collection containing the processing commands.</param>
98+
/// <param name="parser">The command parser use for parting commands.</param>
99+
/// <param name="culture">
100+
/// The <see cref="CultureInfo"/> to use as the current parsing culture.
101+
/// </param>
102+
/// <returns>The <see cref="ResizeOptions"/>.</returns>
103+
internal static ResizeOptions GetResizeOptions(
104+
FormattedImage image,
95105
CommandCollection commands,
96106
CommandParser parser,
97107
CultureInfo culture)
@@ -101,18 +111,20 @@ private static ResizeOptions GetResizeOptions(
101111
return null;
102112
}
103113

104-
Size size = ParseSize(image, commands, parser, culture);
114+
bool rotated = ShouldHandleExifRotation(image, commands, parser, culture, out ushort orientation);
115+
116+
Size size = ParseSize(rotated, commands, parser, culture);
105117

106118
if (size.Width <= 0 && size.Height <= 0)
107119
{
108120
return null;
109121
}
110122

111-
var options = new ResizeOptions
123+
ResizeOptions options = new()
112124
{
113125
Size = size,
114-
CenterCoordinates = GetCenter(commands, parser, culture),
115-
Position = GetAnchor(commands, parser, culture),
126+
CenterCoordinates = GetCenter(rotated, commands, parser, culture),
127+
Position = GetAnchor(orientation, commands, parser, culture),
116128
Mode = GetMode(commands, parser, culture),
117129
Compand = GetCompandMode(commands, parser, culture),
118130
};
@@ -124,43 +136,19 @@ private static ResizeOptions GetResizeOptions(
124136
}
125137

126138
private static Size ParseSize(
127-
Image image,
139+
bool rotated,
128140
CommandCollection commands,
129141
CommandParser parser,
130142
CultureInfo culture)
131143
{
132144
// The command parser will reject negative numbers as it clamps values to ranges.
133145
int width = (int)parser.ParseValue<uint>(commands.GetValueOrDefault(Width), culture);
134146
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-
ExifOrientationMode.LeftTop
152-
or ExifOrientationMode.RightTop
153-
or ExifOrientationMode.RightBottom
154-
or ExifOrientationMode.LeftBottom => new Size(height, width),
155-
_ => new Size(width, height),
156-
};
157-
}
158-
}
159-
160-
return new Size(width, height);
147+
return rotated ? new Size(height, width) : new Size(width, height);
161148
}
162149

163150
private static PointF? GetCenter(
151+
bool rotated,
164152
CommandCollection commands,
165153
CommandParser parser,
166154
CultureInfo culture)
@@ -172,7 +160,7 @@ or ExifOrientationMode.RightBottom
172160
return null;
173161
}
174162

175-
return new PointF(coordinates[0], coordinates[1]);
163+
return rotated ? new PointF(coordinates[1], coordinates[0]) : new PointF(coordinates[0], coordinates[1]);
176164
}
177165

178166
private static ResizeMode GetMode(
@@ -182,10 +170,79 @@ private static ResizeMode GetMode(
182170
=> parser.ParseValue<ResizeMode>(commands.GetValueOrDefault(Mode), culture);
183171

184172
private static AnchorPositionMode GetAnchor(
173+
ushort orientation,
185174
CommandCollection commands,
186175
CommandParser parser,
187176
CultureInfo culture)
188-
=> parser.ParseValue<AnchorPositionMode>(commands.GetValueOrDefault(Anchor), culture);
177+
{
178+
AnchorPositionMode anchor = parser.ParseValue<AnchorPositionMode>(commands.GetValueOrDefault(Anchor), culture);
179+
if (orientation is >= ExifOrientationMode.Unknown and <= ExifOrientationMode.TopLeft)
180+
{
181+
return anchor;
182+
}
183+
184+
return anchor switch
185+
{
186+
AnchorPositionMode.Center => anchor,
187+
AnchorPositionMode.Top => orientation switch
188+
{
189+
ExifOrientationMode.BottomLeft or ExifOrientationMode.BottomRight => AnchorPositionMode.Bottom,
190+
ExifOrientationMode.LeftTop or ExifOrientationMode.RightTop => AnchorPositionMode.Left,
191+
ExifOrientationMode.LeftBottom or ExifOrientationMode.RightBottom => AnchorPositionMode.Right,
192+
_ => anchor,
193+
},
194+
AnchorPositionMode.Bottom => orientation switch
195+
{
196+
ExifOrientationMode.BottomLeft or ExifOrientationMode.BottomRight => AnchorPositionMode.Top,
197+
ExifOrientationMode.LeftTop or ExifOrientationMode.RightTop => AnchorPositionMode.Right,
198+
ExifOrientationMode.LeftBottom or ExifOrientationMode.RightBottom => AnchorPositionMode.Left,
199+
_ => anchor,
200+
},
201+
AnchorPositionMode.Left => orientation switch
202+
{
203+
ExifOrientationMode.TopRight or ExifOrientationMode.BottomRight => AnchorPositionMode.Right,
204+
ExifOrientationMode.LeftTop or ExifOrientationMode.LeftBottom => AnchorPositionMode.Bottom,
205+
ExifOrientationMode.RightTop or ExifOrientationMode.RightBottom => AnchorPositionMode.Top,
206+
_ => anchor,
207+
},
208+
AnchorPositionMode.Right => orientation switch
209+
{
210+
ExifOrientationMode.TopRight or ExifOrientationMode.BottomRight => AnchorPositionMode.Left,
211+
ExifOrientationMode.LeftTop or ExifOrientationMode.LeftBottom => AnchorPositionMode.Top,
212+
ExifOrientationMode.RightTop or ExifOrientationMode.RightBottom => AnchorPositionMode.Bottom,
213+
_ => anchor,
214+
},
215+
AnchorPositionMode.TopLeft => orientation switch
216+
{
217+
ExifOrientationMode.BottomLeft or ExifOrientationMode.LeftTop => AnchorPositionMode.BottomLeft,
218+
ExifOrientationMode.TopRight or ExifOrientationMode.RightBottom => AnchorPositionMode.TopRight,
219+
ExifOrientationMode.BottomRight or ExifOrientationMode.LeftBottom => AnchorPositionMode.BottomRight,
220+
_ => anchor,
221+
},
222+
AnchorPositionMode.TopRight => orientation switch
223+
{
224+
ExifOrientationMode.TopRight or ExifOrientationMode.LeftTop => AnchorPositionMode.TopLeft,
225+
ExifOrientationMode.BottomLeft or ExifOrientationMode.RightBottom => AnchorPositionMode.BottomRight,
226+
ExifOrientationMode.BottomRight or ExifOrientationMode.RightTop => AnchorPositionMode.BottomLeft,
227+
_ => anchor,
228+
},
229+
AnchorPositionMode.BottomRight => orientation switch
230+
{
231+
ExifOrientationMode.TopRight or ExifOrientationMode.RightBottom => AnchorPositionMode.BottomLeft,
232+
ExifOrientationMode.BottomLeft or ExifOrientationMode.LeftTop => AnchorPositionMode.TopRight,
233+
ExifOrientationMode.BottomRight or ExifOrientationMode.LeftBottom => AnchorPositionMode.TopLeft,
234+
_ => anchor,
235+
},
236+
AnchorPositionMode.BottomLeft => orientation switch
237+
{
238+
ExifOrientationMode.TopRight or ExifOrientationMode.LeftTop => AnchorPositionMode.BottomRight,
239+
ExifOrientationMode.BottomLeft or ExifOrientationMode.RightBottom => AnchorPositionMode.TopLeft,
240+
ExifOrientationMode.BottomRight or ExifOrientationMode.RightTop => AnchorPositionMode.TopRight,
241+
_ => anchor,
242+
},
243+
_ => anchor,
244+
};
245+
}
189246

190247
private static bool GetCompandMode(
191248
CommandCollection commands,
@@ -222,5 +279,24 @@ private static IResampler GetSampler(CommandCollection commands)
222279

223280
return KnownResamplers.Bicubic;
224281
}
282+
283+
private static bool ShouldHandleExifRotation(FormattedImage image, CommandCollection commands, CommandParser parser, CultureInfo culture, out ushort orientation)
284+
{
285+
// Browsers now implement 'image-orientation: from-image' by default.
286+
// https://developer.mozilla.org/en-US/docs/web/css/image-orientation
287+
// This makes orientation handling confusing for users who expect images to be resized in accordance
288+
// to what they observe rather than pure (and correct) methods.
289+
//
290+
// To accomodate this we parse the dimensions to use based upon decoded EXIF orientation values, switching
291+
// the width/height parameters when images are rotated (not flipped).
292+
// We default to 'true' for EXIF orientation handling. By passing 'false' it can be turned off.
293+
if (commands.Contains(Orient) && !parser.ParseValue<bool>(commands.GetValueOrDefault(Orient), culture))
294+
{
295+
orientation = ExifOrientationMode.Unknown;
296+
return false;
297+
}
298+
299+
return image.IsExifRotated(out orientation);
300+
}
225301
}
226302
}

0 commit comments

Comments
 (0)