6

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

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

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
HCL
  • 36,053
  • 27
  • 163
  • 213
  • If the problem is as described - that some metadata might be loaded but not saved - then you could strip the metadata (separately, or if PropertyItems is mutable, remove the metadata from that) and then try to rotate the image. – ta.speot.is Mar 12 '19 at 10:31
  • The meta data is fully stripped off after the creation of the second bitmap (bmp). If I don't include this step and work with the initially loaded image directly, GDI+ fails colossal with a GDI+ general exception (even less verbatim than the ArgumentException). – HCL Mar 13 '19 at 06:54
  • Side note, but 'Lossless rotation of a JPG' makes no sense. You can rotate (or flip) it as a bitmap and then you have to re-encode. – H H Mar 13 '19 at 07:07
  • 1
    @Henk Holterman: JPEG data can be rotated without re-rendering as long as the pixel width and height is dividable by 16. Hence there is no reencoding necessary and no quality penalty occurs. GDI+ should do this when the Encoder.Transformation is used as a save parameter. – HCL Mar 13 '19 at 13:08
  • 1
    Perhaps it is possible to rotate the image by changing data metadata only? https://stackoverflow.com/questions/1755185/how-to-add-comments-to-a-jpeg-file-using-c-sharp – Magnus Mar 18 '19 at 07:36
  • @Magnus: Thanks, sadly, in my case this is not possible, since I have to embed the images in documents which do not interpret EXIF meta data. Therefore, the images are shown with the wrong orientation. But in other scenarios, that would be a good solution. – HCL Mar 18 '19 at 08:37

1 Answers1

5
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".

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536