Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Lossless rotation of a JPG image with Image.Save and EncoderParameters fails

I have to rotate JPG images lossless in .net (90°|180°|270°). The following articles show how to do it:

  • https://docs.microsoft.com/en-us/dotnet/api/system.drawing.imaging.encoder.transformation?view=netframework-4.7.2
  • https://www.codeproject.com/tips/64116/Using-GDIplus-code-in-a-WPF-application-for-lossle.aspx

The examples seem quite straightforward; however, I had no luck getting this to work. My source data comes as an array (various JPG files, from camera from internet etc.) and so I want to return the rotated images also as a byte array. Here the (simplified) code:

Image image;
using (var ms = new MemoryStream(originalImageData)) {
    image = System.Drawing.Image.FromStream(ms);
}

// If I don't copy the image into a new bitmap, every try to save the image fails with a general GDI+ exception. This seems to be another bug of GDI+.
var bmp = new Bitmap(image);    

// Creating the parameters for saving
var encParameters = new EncoderParameters(1);            
encParameters.Param[0] = new EncoderParameter(Encoder.Transformation, (long)EncoderValue.TransformRotate90);              
using (var ms = new MemoryStream()) {                
    // Now saving the image, what fails always with an ArgumentException from GDI+
    // There is no difference, if I try to save to a file or to a stream.
    bmp.Save(ms, GetJpgEncoderInfo(), encParameters);
    return ms.ToArray();
}

I always get an ArgumentException from GDI+ without any useful information:

The operation failed with the final exception [ArgumentException].
Source: System.Drawing

I tried an awful lot of things, however never got it working. The main code seems right, since if I change the EncoderParameter to Encoder.Quality, the code works fine:

encParameters.Param[0] = new EncoderParameter(Encoder.Quality, 50L);

I found some interesting posts about this problem in the internet, however no real solution. One particularly contains a statement from Hans Passant, that this seems to be really a bug, with a response from an MS employee, which I don't understand or which may be also simply weird:

https://social.msdn.microsoft.com/Forums/vstudio/en-US/de74ec2e-643d-41c7-9d04-254642a9775c/imagesave-quotparameter-is-not-validquot-in-windows-7?forum=netfxbcl

However this post is 10 years old and I cannot believe, that this is not fixed, especially since the transformation has an explicit example in the MSDN docs.

Does anyone have a hint, what I'm doing wrong, or, if this is really a bug, how can I circumvent it?

Please note that I have to make the transformation lossless (as far as the pixel-size allows it). Therefore, Image.RotateFlip is not an option.

Windows version is 10.0.17763, .Net is 4.7.2

like image 639
HCL Avatar asked Mar 12 '19 10:03

HCL


People also ask

How do you rotate a JPG and save it?

Flip a pictureClick Edit. Select either Flip Horizontal or Flip Vertical. If you want to keep the picture flipped in this way, click Save.

What is JPEG Lossless Rotator?

JPEG Lossless Rotator is a small, simple, yet effective bit of freeware that will rotate your JPEGs without decoding them, preserving the image quality, or, if the image can't be rotated without losing some pixels, it will ask you if you still want to rotate the image and tell you what it costs in pixels.

What is lossless rotation?

JPEG Lossless Rotator is a simple tool that you can use to rotate images without losing the original quality - gHacks Tech News.


1 Answers

using (var ms = new MemoryStream(originalImageData)) {
    image = System.Drawing.Image.FromStream(ms);
}

This is the root of all evil and made the first attempt fail. It violates the rule stipulated in the Remarks section of the documentation, You must keep the stream open for the lifetime of the Image. Violating the rule does not cause consistent trouble, note how Save() call failed but the Bitmap(image) constructor succeeded. GDI+ is somewhat lazy, you have very nice evidence that the JPEG codec indeed tries to avoid recompressing the image. But that can't work, the raw data in the stream is no longer accessible since the stream got disposed. The exception is lousy because the native GDI+ code doesn't know beans about a MemoryStream. The fix is simple, just move the closing } bracket after the Save() call.

From there it went wrong another way, triggered primarily by the new bmp object. Neither the image nor the bmp objects are being disposed. This consumes address space in a hurry, the GC can't run often enough to keep you out of trouble since the data for the bitmap is stored in unmanaged memory. Now the Save() call fails when the MemoryStream can't allocate memory anymore.

You must use the using statement on these objects so this can't happen.

Ought to solve the problems, do get rid of Bitmap workaround since that forces the JPEG to be recompressed. Technically you can still get into trouble when the images are large, suffering from address space fragmentation in a 32-bit process. Keep an eye on the "Private bytes" memory counter for the process, ideally it stays below a gigabyte. If not then use Project > Properties > Build tab, untick "Prefer 32-bit".

like image 167
Hans Passant Avatar answered Oct 18 '22 02:10

Hans Passant