Erik Erik - 2 months ago 15
C# Question

Some images are being rotated when resized

In a nutshell the purpose of the following code is to resize an image based on the target size and the multiplier (1x, 2x, 3x). This works fine except for some reason I haven't determined some images are being rotated.

public void ResizeImage(TargetSize targetSize, ResizeMultiplier multiplier, Stream input, Stream output)
{
using (var image = Image.FromStream(input))
{
// Calculate the resize factor
var scaleFactor = targetSize.CalculateScaleFactor(image.Width, image.Height);
scaleFactor /= (int)multiplier; // Enum is effectively named constant with a value of 1, 2, or 3

var newWidth = (int)Math.Floor(image.Width / scaleFactor);
var newHeight = (int)Math.Floor(image.Height / scaleFactor);
using (var newBitmap = new Bitmap(newWidth, newHeight))
{
using (var imageScaler = Graphics.FromImage(newBitmap))
{
imageScaler.CompositingQuality = CompositingQuality.HighQuality;
imageScaler.SmoothingMode = SmoothingMode.HighQuality;
imageScaler.InterpolationMode = InterpolationMode.HighQualityBicubic;

var imageRectangle = new Rectangle(0, 0, newWidth, newHeight);
imageScaler.DrawImage(image, imageRectangle);

newBitmap.Save(output, image.RawFormat);
}
}
}
}

// Class definition for the class used in the method above
public class TargetSize
{
/// <summary>
/// The _width
/// </summary>
private readonly int _width;

/// <summary>
/// The _height
/// </summary>
private readonly int _height;

/// <summary>
/// Initializes a new instance of the <see cref="TargetSize"/> class.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
public TargetSize(int width, int height)
{
_height = height;
_width = width;
}

/// <summary>
/// Calculates the scale factor.
/// </summary>
/// <param name="width">The width.</param>
/// <param name="height">The height.</param>
/// <returns></returns>
public decimal CalculateScaleFactor(int width, int height)
{
// Scale proportinately
var heightScaleFactor = decimal.Divide(height, _height);
var widthScaleFactor = decimal.Divide(width, _width);

// Use the smaller of the two as the final scale factor so the image is never undersized.
return widthScaleFactor > heightScaleFactor ? heightScaleFactor : widthScaleFactor;
}
}

// NUnit integration test case I'm using to exercise the above code
[Test]
public void ResizeImage_Persistant_Single()
{
// Read the image from disk
using (var fileStream = File.OpenRead(@"TestData\dog.jpg"))
{
using (var outputStream = new MemoryStream())
{
// Call the resize image method detailed above. ResizeMultiplier.Medium casts to 2.
_sut.ResizeImage(new TargetSize(200, 200), ResizeMultiplier.Medium, fileStream, outputStream);
using (var newImage = Image.FromStream(outputStream))
{
// Save the resized image to disk
newImage.Save(@"TestData\ImageResizerTests.ResizeImage_Persistant_Single.jpg");
}
}
}
}


For instance this image:

Good image

is scaled appropriately but this image:

bad image

is flipped upside down. It is worth mentioning that the image also appeared to be upside down when it was in the preview pane to upload it to this site. That fact (which I obviously just discovered) strongly makes me think something is funny with the image. Regardless my code needs to handle it.

Imgur "fixed" the file above (because it doesn't rotate now when I run it through my code) so I uploaded it to Google Drive. If you right click on the image (in FireFox I haven't tested other browsers) and click Save Image As... then the image doesn't rotate with my code above. If you click the download button in the header then the image does rotate with my code.... Here is another copy of the dog image that flips 180 degrees with my code. All of this is very bizarre, and I don't know what I'm doing wrong...

To be clear my goal is to resize the image without rotating the image.




Edits based on comments:

An image that rotates/flips will do so consistently, and in the same manner. For example this dog picture will always flip 180 degrees. Some pictures will lay on their side (rotate 90 or 270 degrees). I've verified that the
newWidth
,
newHeight
,
scaleFactor
,
targetSize
(private variables), and
image.Height/image.Width
variables are all positive when the dog picture flips 180 degrees.

I don't believe this is an artifact of a particular tool. I see the rotation via; the preview in Windows Explorer, Windows Image Viewer, the stock Macintosh image viewer, FireFox, etc. The issue was actually brought to my attention by an iOS dev in my company who saw it in our app. I think too many tools are seeing this for it to be a viewer problem.

Answer

Thanks to the excellent help by Chris Farmer1 and Mark Ransom2 I was able to answer my own question.

The core problem was the EXIF data on some of the images was altering the orientation of the images. When I resized the images all the EXIF data was stripped off. Since the EXIF data was stripped the viewers didn't know the image should be orientated differently. That caused me to think the resizer code was rotating some images. It is worth noting that this orientation information doesn't show up in the details view when you right click on the image in Windows Explorer. You need to search for it in the image property objects or use an online view like this one.

Using the data from here3 I was able to create the following named constants:

private const int OrientationKey = 0x0112;
private const int NotSpecified = 0;
private const int NormalOrientation = 1;
private const int MirrorHorizontal = 2;
private const int UpsideDown = 3;
private const int MirrorVertical = 4;
private const int MirrorHorizontalAndRotateRight = 5;
private const int RotateLeft = 6;
private const int MirorHorizontalAndRotateLeft = 7;
private const int RotateRight = 8;

Using those named constants I extended my ResizeImage method to read:

public void ResizeImage(TargetSize targetSize, ResizeMultiplier multiplier, Stream input, Stream output)
{
    using (var image = Image.FromStream(input))
    {
        // Calculate the resize factor
        var scaleFactor = targetSize.CalculateScaleFactor(image.Width, image.Height);
        scaleFactor /= (int)multiplier; 

        var newWidth = (int)Math.Floor(image.Width / scaleFactor);
        var newHeight = (int)Math.Floor(image.Height / scaleFactor);
        using (var newBitmap = new Bitmap(newWidth, newHeight))
        {
            using (var imageScaler = Graphics.FromImage(newBitmap))
            {
                imageScaler.CompositingQuality = CompositingQuality.HighQuality;
                imageScaler.SmoothingMode = SmoothingMode.HighQuality;
                imageScaler.InterpolationMode = InterpolationMode.HighQualityBicubic;

                var imageRectangle = new Rectangle(0, 0, newWidth, newHeight);
                imageScaler.DrawImage(image, imageRectangle);

                // Fix orientation if needed.
                if (image.PropertyIdList.Contains(OrientationKey))
                {
                    var orientation = (int)image.GetPropertyItem(OrientationKey).Value[0];
                    switch (orientation)
                    {
                        case NotSpecified: // Assume it is good.
                        case NormalOrientation:
                            // No rotation required.
                            break;
                        case MirrorHorizontal:
                            newBitmap.RotateFlip(RotateFlipType.RotateNoneFlipX);
                            break;
                        case UpsideDown:
                            newBitmap.RotateFlip(RotateFlipType.Rotate180FlipNone);
                            break;
                        case MirrorVertical:
                            newBitmap.RotateFlip(RotateFlipType.Rotate180FlipX);
                            break;
                        case MirrorHorizontalAndRotateRight:
                            newBitmap.RotateFlip(RotateFlipType.Rotate90FlipX);
                            break;
                        case RotateLeft:
                            newBitmap.RotateFlip(RotateFlipType.Rotate90FlipNone);
                            break;
                        case MirorHorizontalAndRotateLeft:
                            newBitmap.RotateFlip(RotateFlipType.Rotate270FlipX);
                            break;
                        case RotateRight:
                            newBitmap.RotateFlip(RotateFlipType.Rotate270FlipNone);
                            break;
                        default:
                            throw new NotImplementedException("An orientation of " + orientation + " isn't implemented.");
                    }
                }
                newBitmap.Save(output, image.RawFormat);
            }
        }
    }
}

An observant person would notice that most of the additional code was pulled from this answer to a related question.


1: Who was right on the money but I didn't understand what he was driving at.

2:Who showed me what Chris Farmer was trying to say.

3:The Orientation key value was confirmed here.