Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OutOfMemoryException: Out of memory - System.Drawing.Graphics.FromImage

I get Out of Memory exception when using System.Drawing.Graphics.FromImage (using latest versions of .NET software on Windows 2012 server), ONLY on a very few specific image files. Most of the time the code works fine.

Typical answers to above issue indicate that certain resources are not being released.

Please consider the following before answering:-

  • This specific image is 34KB in size, is a .JPG image. Server is idle and has over 32GB RAM.
  • If I look at properties of this jpg file, using windows explorer, by right-clicking on file, Windows says: 96 dpi and 32 bit depth.
  • BUT, if I open this jpg file using any graphics program (e.g. photoshop), the file properties show as: 72 dpi and 24 bit depth.
  • So, there is a mis-match between what I think file header properties say and what the file actually contains.
  • Further, if I open the jpg file using a graphics program and just re-save without changing anything, the file properties in windows explorer now match/read correct (72 dpi and 24 bit depth); and the file is processed by System.Drawing.Graphics correctly, without throwing exception.

Due to my limited knowledge of the subject, I don't know if the file header of an image file can contain different data from actual file contents.

Questions:

How can I fix this problem? Or how can I tell System.Drawing.Graphics to ignore file header data and just look at actual image file contents? (as all graphics programs such as photoshop appear to do).

Thanks!

like image 663
Mike Avatar asked Sep 08 '14 12:09

Mike


3 Answers

While I'm not a guru on the JPEG file format i did some research on the subject and here's what i found that could help you with your problem/questions.

Note that this answer will assume rather than specifically pinpoint the source of your problem due to the lack of an example file to inspect and tell what differs it from what the .Net/GDI+ JPEG/JFIF decoder expects.

The JPEG/JFIF format

Starting off, you might want to have some insight into the JPEG/JFIF format itself. After all, you have just encountered a file that .Net/GDI+ cannot load/parse. Since i don't have the file you experience issues with i would suggest you load it up in a hex editor of choice... that has the capability to highlight the file based on a template/code/parser.

I used 010 Editor and the JPEG Template from Sweetscape's online template repository. 010 Editor comes with a 30-day free trial.

What you are specifically looking for is the SOFn identifier and data in your bad JPEG.

enter image description here

In the SOFn data i can see that my image is Y (154) pixels high and X (640) pixels wide with a precision of 8 bits per component using 3 components, making it 24 bits per pixel.

The JPEG/JFIF format is a huge mix of many different implementations/formats. Obviously, you won't find every variant of the format in any library that has been around since long long ago before the odd JPEG formats appeared. Which the GDI+ library has.

In your case, i suspect you have run into the commonly asked about CMYK color profile on your JPEG files.

The .Net implementation

You said you used System.Drawing.Graphics.FromImage so i will assume your code looks like one of the following:

Graphics.FromImage(Image.FromFile("nope.jpg"));
Graphics.FromImage(Image.FromFile("nope.jpg", true));
Graphics.FromImage(Image.FromStream(nopeJpegStream));

From those calls, you may get an OutOfMemoryException when the native gdiplus.dll calls...

  • GdipGetImageGraphicsContext
  • GdipLoadImageFromFile
  • GdipLoadImageFromFileICM (or their respective *Stream variants) or
  • GdipImageForceValidation

... returns code 3 or 5 (Out of memory or Insufficient buffer respectively)

Which i gathered from referencesource.microsoft.com looking through the .Net sources there.
In any case, this most likely isn't an issue with .Net but an issue with GDI+ (gdiplus.dll) which Microsoft doesn't provide source code for. Which also means that there is no way of controlling how the image loads using the .Net wrappers and there's no way to check WHY it fails. (though i still suspect your JPEG is saved with CMYK)

Unfortunately, you are going to find many many more of these strange exceptions/errors as you move along in GDI+ land. As the library is all but deprecated in favor of the Windows Presentation Framework (WPF) and the Windows Imaging Component. (WIC)

My own testing

Since you never provided an image or any additional details on the subject i attempted to reproduce your issue. Which was a task in of itself, Image.FromFile (GdipLoadImageFromFile) will fail on many different file formats. At least it doesn't care what the file extension is, which thankfully Photoshop does.

So with your information, i finally managed to reproduce a .jpg file that loads fine in Photoshop, shows DPI as 96 and bit depth as 32. Of course, if i knew more about the JPEG format i probably could have gotten to the solution right away.

Showing this file (which i had to set to CMYK color space in Photoshop) in 010 Editor gave me the following SOFn data: Y (154) pixels high and X (640) pixels wide with a precision of 8 bits per component using 4 components, making it 32 bits per pixel.

I suspect you would see the same on your "bad" file.
And yes, Image.FromFile now throws an OutOfMemoryException!

Possible solutions

  • Use an external library for loading image files. (An exercise i leave to you but ImageMagick A.K.A Magick.NET seems like a good bet)
  • Make use of a command line tool (invoked when you get this exception) that can convert an image from one format to another. Or from JPEG to JPEG as it may be in this case. (Once again, ImageMagick's "convert" command line tool seems like a good bet)
  • Use the Windows Presentation Framework assemblies...

    public static Image ImageFromFileWpf(string filename) {
        /* Load the image into an encoder using the Presentation Framework.
         * This is done by adding a frame (which in laymans terms is a layer) to a class derived BitmapEncoder.
         * Only TIFF, Gif and JPEG XR supports multiple frames.
         * Since we are going to convert our image to a GDI+ resource we won't support this as GDI+ doesn't (really) support it either.
         * If you want/need support for layers/animated Gif files, create a similar method to this one that takes a BitmapFrame as an argument and then...
         *  1. Instanciate the appropriate BitmapDecoder.
         *  2. Iterate over the BitmapDecoders frames, feeding them to the new method.
         *  3. Store the returned images in a collection of images.
         * 
         * Finally, i opted to use a PngBitmapEncoder here which supports image transparency.
         */
        var bitmapEncoder = new PngBitmapEncoder();
        bitmapEncoder.Frames.Add(BitmapFrame.Create(new Uri(filename)));
    
        // Use a memorystream as a handover from one file format to another.
        using (var memoryStream = new MemoryStream()) {
            bitmapEncoder.Save(memoryStream);
            /* We MUST create a copy of our image from stream, MSDN specifically states that the stream must remain
             * open throughout the lifetime of the image.
             * We cannot instanciate the Image class, so we instanciate a Bitmap from our temporary image instead.
             * Bitmaps are derived from Image anyways, so this is perfectly fine.
             */
            var tempImage = Image.FromStream(memoryStream);
            return new Bitmap(tempImage);
        }
    }
    

    Based on this answer...

... Which i would say is a good option as it keeps you within the .Net framework.
Please keep in mind that when the method returns, you do specifically get a PNG image back. If you call Image.Save(string) on it you WILL save a PNG file, no matter what extension you save it as.

There is an overload Image.Save(string, ImageFormat) that will save the file using the intended file format. However, using that overload with ImageFormat.Jpeg will cause a loss in quality in the resulting file on more than one level.

That can be somewhat remedied by using the third overload:

foreach (var encoder in ImageCodecInfo.GetImageEncoders()) {
    if (encoder.MimeType == "image/jpeg")
        image.Save(filename, encoder, new EncoderParameters { Param = new [] { new EncoderParameter(Encoder.Quality, 100L) }});
}

Which, at least, will save a JPEG with "almost" no compression. GDI+ still doesn't do a good job at it.
However, no matter how much you twist and turn it. GDI+ will not be as good as a proper image library, which once again would most likely be ImageMagick. The further away you can get from GDI+, the better off you will be.

Conclusion / TL:DR and other notes.

Q: Can i load these files in .Net?
A: Yes, with a bit of fiddling and not using GDI+ for the initial loading of the file as GDI+ doesn't support the CMYK color space in JPEG files.
And even so, GDI+ lacks support for many things which is why i would recommend an external image library over GDI+.

Q: Mismatch in DPI and bit depth for file between Windows and <insert photo app here>
A: This is just proof that Windows JPEG loading differs from other applications JPEG loading routines. Only applications that use GDI or GDI+ would see the same information that Windows does when showing image details.
If you are using Windows 7+ then it isn't using GDI+ to show the information nor the image. It is using WPF or WIC to do so which are somewhat more up to date.

Q: If I open the jpg file using a graphics program and just re-save without changing anything, the file properties in windows explorer now match/read correct (72 dpi and 24 bit depth)
A: If you are using Adobe Photoshop and you use "Save for web" then the JPEG image will not be saved in CMYK format. Use "Save As..." instead and you will find that the color space (and bit depth) stays the same.

However, i wasn't able to reproduce your discrepancy in DPI and bit depth when loading my file in Photoshop. They are reported as the same in both Windows and Photoshop.

like image 191
Cadde Avatar answered Oct 12 '22 12:10

Cadde


I had the same issue with this bug - seems as though the Graphics / Bitmap / Image library throws an exception with certain malformed images. Narrowing it down more than that, as Cadde shows, is difficult.

Following on from the great answer made by Cadde (which left using an external library as an exercise to the reader), I changed my code to the following using MagickNet which you can get here, or simply with NuGet: PM> Install-Package Magick.NET-Q16-x86.

The code tries to create a Graphics object from the image, and if it fails, uses ImageMagick to load the image again, convert to a Bitmap, and attempts to load from there.

Image bitmap = Bitmap.FromFile(filename, false);
Graphics graphics = null;
try
{
    graphics = Graphics.FromImage(bitmap);
}
catch (OutOfMemoryException oome)
{
    // Well, this looks like a buggy image. 
    // Try using alternate method    
    ImageMagick.MagickImage image = new ImageMagick.MagickImage(filename);
    image.Resize(image.Width, image.Height);
    image.Quality = 90;
    image.CompressionMethod = ImageMagick.CompressionMethod.JPEG;  
    graphics = Graphics.FromImage(image.ToBitmap());            
}
like image 29
dazbradbury Avatar answered Oct 12 '22 14:10

dazbradbury


I had the same problem. My jpg file was generated from Photoshop. A simple solution is to open the jpg file with Winodws Paint, and save as a new jpg file. Import the new jpg file to C# project and the problem will be disappear.

like image 31
Hao Deng Avatar answered Oct 12 '22 13:10

Hao Deng