SkiaSharp
SkiaSharp copied to clipboard
Image auto orientation method
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.
Does this feature exist in Skia?
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;
}
}
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.
@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.
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.
@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.
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.
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;
}
}
}
}
}
}
}
}
@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.
Please note:
-
SKImage
applies the orientation during decoding -
SKBitmap
doesn't
So if you need to auto-orient your images, just use SKImage
?
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; } } } } } } } }
WORKS FINE
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;
}
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).
OriginalImageQuality
How to get _azureStorageConfig.OriginalImageQuality ?