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

Image auto orientation method #836

Open
deivydas321 opened this issue May 2, 2019 · 14 comments
Open

Image auto orientation method #836

deivydas321 opened this issue May 2, 2019 · 14 comments

Comments

@deivydas321
Copy link

When uploading an image from mobile, it is always rotated to landscape view. But when you access the image in code you usually want to save it in correct orientation.

At the moment, there is no easy way to rotate the image. So it would be nice to have something like AutoOrient() which would correctly rotate the image based on its EXIF data.

@deivydas321 deivydas321 changed the title [FEATURE] Image auto orientation method Image auto orientation method May 2, 2019
@charlesroddie
Copy link

Does this feature exist in Skia?

@deivydas321
Copy link
Author

deivydas321 commented May 7, 2019

At the moment it doesn't exist. But it can be solved with this method.

private static SKBitmap AutoOrient(SKBitmap bitmap, SKEncodedOrigin origin)
        {
            SKBitmap rotated;
            switch (origin)
            {
                case SKEncodedOrigin.BottomRight:
                    using (var surface = new SKCanvas(bitmap))
                    {
                        surface.RotateDegrees(180, bitmap.Width / 2, bitmap.Height / 2);
                        surface.DrawBitmap(bitmap.Copy(), 0, 0);
                    }
                    return bitmap;
                case SKEncodedOrigin.RightTop:
                    rotated = new SKBitmap(bitmap.Height, bitmap.Width);
                    using (var surface = new SKCanvas(rotated))
                    {
                        surface.Translate(rotated.Width, 0);
                        surface.RotateDegrees(90);
                        surface.DrawBitmap(bitmap, 0, 0);
                    }
                    return rotated;
                case SKEncodedOrigin.LeftBottom:
                    rotated = new SKBitmap(bitmap.Height, bitmap.Width);
                    using (var surface = new SKCanvas(rotated))
                    {
                        surface.Translate(0, rotated.Height);
                        surface.RotateDegrees(270);
                        surface.DrawBitmap(bitmap, 0, 0);
                    }
                    return rotated;
                default:
                    return bitmap;
            }
        }

@Gillibald
Copy link
Contributor

I don't understand the issue here. Exif meta data just tells the consumer of that image how it should be displayed but doesn't effect the actual pixel data. That meta data isn't required nor is it guaranteed to work everywhere. You can't rely on it.

@deivydas321
Copy link
Author

@Gillibald, the problem is that when the image is taken from a mobile phone it is always saved in the orientation the device camera is built into the device(In most cases it is landscape view). So, if you want to read the image correctly you have to read EXIF Orientation data how the device was rotated when the picture was taken.
This AutoRotate feature would be very useful in this case and is provided in many other image processing libraries but is missing in SkiaSharp.
Some info about rotation problem in web app.

@Gillibald
Copy link
Contributor

It is not guaranteed that the camera driver is writing that meta data to the produced image. So you need to deal with that anyways.

@charlesroddie
Copy link

charlesroddie commented May 18, 2019

@deivyd321 I agree that this would be good to have, but if something doesn't exist in Skia then it's unlikely to end up in SkiaSharp. So using your posted code will be best until this ends up in Skia and subsequently gets taken into SkiaSharp.

@craigwi
Copy link

craigwi commented Jun 14, 2019

Short of doing the auto rotation, it would be helpful to store SKEncodedOrigin in SKBitmap. Given that SKBitmap uses SKCodec internally, it should be easy to retain the encoded origin when available.

@iainxt
Copy link

iainxt commented Feb 11, 2020

It took my brain awhile to catch up on it, so to save others time here is the code I am using. I have tested this on all origins and it works.

        public Stream FixImageOrientation(Stream stream, out int width, out int height)
        {
            try { stream.Position = 0; } catch (NotSupportedException) { }
            using (var inputStream = new SKManagedStream(stream))
            {
                using (var codec = SKCodec.Create(inputStream))
                {
                    using (var original = SKBitmap.Decode(codec))
                    {
                        var useWidth = original.Width;
                        var useHeight = original.Height;
                        Action<SKCanvas> transform = canvas => { };
                        switch (codec.EncodedOrigin)
                        {
                            case SKEncodedOrigin.TopLeft:
                                break;
                            case SKEncodedOrigin.TopRight:
                                // flip along the x-axis
                                transform = canvas => canvas.Scale(-1, 1, useWidth / 2, useHeight / 2);
                                break;
                            case SKEncodedOrigin.BottomRight:
                                transform = canvas => canvas.RotateDegrees(180, useWidth / 2, useHeight / 2);
                                break;
                            case SKEncodedOrigin.BottomLeft:
                                // flip along the y-axis
                                transform = canvas => canvas.Scale(1, -1, useWidth / 2, useHeight / 2);
                                break;
                            case SKEncodedOrigin.LeftTop:
                                useWidth = original.Height;
                                useHeight = original.Width;
                                transform = canvas =>
                                {
                                    // Rotate 90
                                    canvas.RotateDegrees(90, useWidth / 2, useHeight / 2);
                                    canvas.Scale(useHeight * 1.0f / useWidth, -useWidth * 1.0f / useHeight, useWidth / 2, useHeight / 2);
                                };
                                break;
                            case SKEncodedOrigin.RightTop:
                                useWidth = original.Height;
                                useHeight = original.Width;
                                transform = canvas =>
                                {
                                    // Rotate 90
                                    canvas.RotateDegrees(90, useWidth / 2, useHeight / 2);
                                    canvas.Scale(useHeight * 1.0f / useWidth, useWidth * 1.0f / useHeight, useWidth / 2, useHeight / 2);
                                };
                                break;
                            case SKEncodedOrigin.RightBottom:
                                useWidth = original.Height;
                                useHeight = original.Width;
                                transform = canvas =>
                                {
                                    // Rotate 90
                                    canvas.RotateDegrees(90, useWidth / 2, useHeight / 2);
                                    canvas.Scale(-useHeight * 1.0f / useWidth, useWidth * 1.0f / useHeight, useWidth / 2, useHeight / 2);
                                };
                                break;
                            case SKEncodedOrigin.LeftBottom:
                                useWidth = original.Height;
                                useHeight = original.Width;
                                transform = canvas =>
                                {
                                    // Rotate 90
                                    canvas.RotateDegrees(90, useWidth / 2, useHeight / 2);
                                    canvas.Scale(-useHeight * 1.0f / useWidth, -useWidth * 1.0f / useHeight, useWidth / 2, useHeight / 2);
                                };
                                break;
                            default:
                                break;
                        }
                        var info = new SKImageInfo(useWidth, useHeight);
                        using (var surface = SKSurface.Create(info))
                        {
                            using (var paint = new SKPaint())
                            {
                                // high quality with antialiasing
                                paint.IsAntialias = true;
                                paint.FilterQuality = SKFilterQuality.High;

                                // rotate according to origin
                                transform.Invoke(surface.Canvas);

                                // draw the bitmap to fill the surface
                                surface.Canvas.DrawBitmap(original, info.Rect, paint);
                                surface.Canvas.Flush();

                                using (var image = surface.Snapshot())
                                {
                                    var output = new MemoryStream();
                                    using (var data = image.Encode(SKEncodedImageFormat.Jpeg, _azureStorageConfig.OriginalImageQuality))
                                    {
                                        data.SaveTo(output);
                                        width = useWidth;
                                        height = useHeight;
                                        return output;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

@mattleibow
Copy link
Contributor

@iainxt thanks for that. I'm thinking there might be a cause to have a property/method somewhere that could at least create a matrix for this so that you can draw an image using the matrix. And then have some member that does this auto-rotate.

Definitely something to have, thanks for the work.

@ziriax
Copy link
Contributor

ziriax commented Jun 15, 2021

Please note:

  • SKImage applies the orientation during decoding
  • SKBitmap doesn't

So if you need to auto-orient your images, just use SKImage?

@ViniciusLucas14

This comment was marked as resolved.

@Hoopou
Copy link

Hoopou commented Mar 2, 2023

At the moment it doesn't exist. But it can be solved with this method.

private static SKBitmap AutoOrient(SKBitmap bitmap, SKEncodedOrigin origin)
        {
            SKBitmap rotated;
            switch (origin)
            {
                case SKEncodedOrigin.BottomRight:
                    using (var surface = new SKCanvas(bitmap))
                    {
                        surface.RotateDegrees(180, bitmap.Width / 2, bitmap.Height / 2);
                        surface.DrawBitmap(bitmap.Copy(), 0, 0);
                    }
                    return bitmap;
                case SKEncodedOrigin.RightTop:
                    rotated = new SKBitmap(bitmap.Height, bitmap.Width);
                    using (var surface = new SKCanvas(rotated))
                    {
                        surface.Translate(rotated.Width, 0);
                        surface.RotateDegrees(90);
                        surface.DrawBitmap(bitmap, 0, 0);
                    }
                    return rotated;
                case SKEncodedOrigin.LeftBottom:
                    rotated = new SKBitmap(bitmap.Height, bitmap.Width);
                    using (var surface = new SKCanvas(rotated))
                    {
                        surface.Translate(0, rotated.Height);
                        surface.RotateDegrees(270);
                        surface.DrawBitmap(bitmap, 0, 0);
                    }
                    return rotated;
                default:
                    return bitmap;
            }
        }

In the case SKEncodedOrigin.BottomRight , surface.DrawBitmap(bitmap.Copy(), 0, 0); leads to a memory leak since a copy of the SKBitmap is passed to the surface and not the actual Bitmap.
Quick fix, remove the .Copy() and everything works fine. surface.DrawBitmap(bitmap, 0, 0);
OR
I'm probably going to use the long way since I don't like this method some time returning a reference and other time a new object, so I will always create a new object if their is a rotation to do.

private static SKBitmap? AutoOrient(SKBitmap bitmap, SKEncodedOrigin origin)
        {
            SKBitmap rotated;
            switch (origin)
            {
                case SKEncodedOrigin.BottomRight:
                    rotated = new SKBitmap(bitmap.Width, bitmap.Height);
                    using (var surface = new SKCanvas(rotated))
                    {
                        surface.RotateDegrees(180, bitmap.Width / 2, bitmap.Height / 2);
                        surface.DrawBitmap(bitmap, 0, 0);
                    }
                    return rotated;
                case SKEncodedOrigin.RightTop:
                    rotated = new SKBitmap(bitmap.Height, bitmap.Width);
                    using (var surface = new SKCanvas(rotated))
                    {
                        surface.Translate(rotated.Width, 0);
                        surface.RotateDegrees(90);
                        surface.DrawBitmap(bitmap, 0, 0);
                    }
                    return rotated;
                case SKEncodedOrigin.LeftBottom:
                    rotated = new SKBitmap(bitmap.Height, bitmap.Width);
                    using (var surface = new SKCanvas(rotated))
                    {
                        surface.Translate(0, rotated.Height);
                        surface.RotateDegrees(270);
                        surface.DrawBitmap(bitmap, 0, 0);
                    }
                    return rotated;
                default:
                    return null;
            }
        }

I return null if there is no rotation to do, so in the calling method I can easilly know if I have to close the given SKBitmap after the execution of this method or not. for example:

 var newBitmap = AutoOrient(Bitmap, orientation);
 if (newBitmap != null)
 {
     Bitmap.Dispose();
     Bitmap = newBitmap;
 }

@ggolda
Copy link

ggolda commented Aug 2, 2023

Would be great if SKBitmap and SKImage supported an optional parameter that accepts a rotation angle or SKEncodedOrigin when consuming already decoded data from SKData or byte array.

Currently it is not supported and we either have to create an extra surface like people proposed above (which is a waste of memory and CPU, especially for 4K images) or manually apply rotation on byte array before creating image (which is complex to do correctly).

@fourix
Copy link

fourix commented Dec 21, 2023

OriginalImageQuality

How to get _azureStorageConfig.OriginalImageQuality ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: New
Development

No branches or pull requests