tags:

views:

476

answers:

8

I have this jpeg, that opens fine in picasa, photoshop, web browser etc. but in .Net it just refuses to work.

 Image image = Image.FromFile(@"myimage.jpg");
 image.Save(@"myimage2.jpg");
 // ExternalException - A generic error occurred in GDI+. 

Is there any way to recover it in .Net so I can work with it (I need to resize it), without fixing the problem at the source?

Full exception details:

source: System.Drawing 
type: System.Runtime.InteropServices.ExternalException 
message: A generic error occurred in GDI+. 
stack trace:    
at System.Drawing.Image.Save(String filename, ImageCodecInfo encoder, EncoderParameters encoderParams)
   at System.Drawing.Image.Save(String filename, ImageFormat format)
   at System.Drawing.Image.Save(String filename)
   at ConsoleApplication20.Program.Main(String[] args) in C:\Users\sam\Desktop\S
ource\ConsoleApplication20\ConsoleApplication20\Program.cs:line 16

This issue is reproducible on Windows 7.

A: 

Try checking your permissions. Not being able to save can be caused by not having the right permission to write/modify, and could generate this error.

Pbirkoff
Its not permissions. I tested with another image and it works fine. Its just this particular image that has something wrong with it.
Sam Saffron
+4  A: 

Try explicitly specifying the format:

using (Image image = Image.FromFile(@"test.jpg"))
{
    image.Save(@"myimage2.gif", ImageFormat.Gif);
}

All ImageFormat.Png, ImageFormat.Bmp and ImageFormat.Gif work fine. ImageFormat.Jpeg throws an exception.

The original image is in the JFIF format as it starts with FF D8 FF E0 bytes.

Darin Dimitrov
yerp that was my workaround, I think it is fair from trial and error the condition of having problem jpegs is kind of rare, so switching formats is trivial and not that expensive.
Sam Saffron
so is JFIF not fully supported in .Net?
Sam Saffron
@Sam Saffron: JFIF is the actual file format used for JPEG-encoded images. It is supported by GDI+, and therefore supported by the .NET `Image` and related classes, which are just wrappers around GDI+.
Aaronaught
+9  A: 

This seems to work:

    using (Image image = Image.FromFile(@"c:\dump\myimage.jpg"))
    using (Image clone = new Bitmap(image))
    {
        clone.Save(@"c:\dump\myimage2.jpg", ImageFormat.Jpeg);
    }

image is actually a Bitmap anyway, so it should be similar. Oddly myimage2 is 5k smaller - the joys of jpeg ;-p

A nice thing about this is that you can resize at the same time (your actual intent):

    using (Image image = Image.FromFile(@"c:\dump\myimage.jpg"))
    using (Image clone = new Bitmap(image,
        new Size(image.Size.Width / 2, image.Size.Height / 2)))
    {

        clone.Save(@"c:\dump\myimage2.jpg", ImageFormat.Jpeg);
    }
Marc Gravell
Sam Saffron
I guess I could fit in your fix, still do you think this is a legit bug in .net? Did you also get the error?
Sam Saffron
@Sam - yes I got the error with the original code, and yes I think this is a GDI bug. And no, I wouldn't expect it to be fixed any time soon.
Marc Gravell
I was tempted to log a call with MS, but after just spending half an hour talking to some monkey about another legit issue I do not think I could be bothered.
Sam Saffron
+2  A: 

Try using WPF's BitmapSource instead of the WinForm's Image, it supports more pixel, image and file formats.

Danny Varod
A: 

That error occurrs when you either have

a. no permissions to write the file
b. you are trying to write an image that is locked 
   (often, by a UI control ie. picturebox)

You'll need to dispose the original, and then save over it with your resized clone as in Marc's answer. I just figured I'd tell you why it's happening.

SnOrfus
To the downvoter: Apparently copy/paste code is vastly superior than to knowing why the problem is happening in the first place. well done
SnOrfus
+3  A: 

It seems to work fine on Windows XP and Vista, but not Windows 7.

I was able to find this issue on Microsoft Connect. It's not identical to your issue but it looks very similar - an ExternalException occurring when trying to resave a JPEG file. Currently at 16 repros including some comments that the issue still exists in the final release.

So it looks not to be a bug in the .NET Framework, but a bug in Windows 7 - specifically, a bug in the GdipSaveImageToStream API.

The workaround, as others have mentioned, is to force a conversion to another format. That's what both Marc and Darin's answers are doing. There is obviously some "extra" information in the JPEG file that is triggering the bug in Win7. When you convert to a different format or make a bitmap copy, that information (EXIF maybe?) is eliminated.

Aaronaught
I have a suspicion that it's in the headers somewhere, but I can't test it right this second. I suggest that someone try using jhead to strip various headers from the image file, and see if the code in the OP succeeds without one or more of the headers. The tool is here: http://www.sentex.net/~mwandel/jhead/
Aaronaught
This is the most complete answer to my question now considering posting this to MS
Sam Saffron
+1  A: 

I tried yesterday on 32bit XP and cant reproduced the problem. Today I tried on 64bit Win7 and get exactly the error you described, which is great for my debugging.

I investigated the header and the JPEG is a standard JPEG but with EXIF header. From what I read that it is not uncommon that the EXIF header maybe corrupt and some programs will just ignore it. In .NET it allows reading and (may even allow writing at some point) but not writing. You can read more about it here.
http://blogs.msdn.com/calvin_hsia/archive/2005/07/24/442873.aspx

One way to remove it is to clone the image like suggested by Marc, which will create the new image without EXIF header, hence that explains why the file size is actually smaller. There are method of removing EXIF header programmatically, some has been suggested in StackOverflow like below: http://stackoverflow.com/questions/1005463/simple-way-to-remove-exif-data-from-a-jpeg-with-net

The suggested problem with reading the byte marker and skip the stream will not work well consistently as we are dealing with corrupted EXIF header. I tried using RemovePropertyItem is an option but it still doesnt work too, and my guess is because there are corrupted property item that is being referred (when I inspect the JPEG header there are 6 properties, .NET only loads 4). They are other library like exiv2net which can be explored but I suspect the outcome will be similar.

In short the answer will be to clone the image as suggested by Marc. This is perhaps not the solution but an insight to the problem.

Fadrian Sudaman
+2  A: 

This is a long-standing bug in the .NET framework itself that I don't expect to see fixed any time soon. There are several related bugs I've also come across that throw the 'generic error occurred in GDI+' including if you access the PropertyItem collection for some JPEGs (to examine the EXIF codes) then from that point on you won't be able to save the image. Also, for no rhyme or reason some JPEGs, like the one you have here, simply won't save at all. Note that just saving as a different format won't always work either, although it does in this case.

The workaround I've employed in an app of mine that consumes pictures from all over the web (and therefore sees more than its fair share of these types of problems) is to catch the ExternalException and copy the image into a new Bitmap as in one of the previous answers, however simply saving this as a new JPEG will drop the quality rather a lot so I use code much like that below to keep the quality high:

namespace ImageConversionTest
{
    using System.Drawing;
    using System.Runtime.InteropServices;
    using System.Drawing.Imaging;
    using System.Globalization;

    class Program
    {
        static void Main( string[] args )
        {
            using( Image im = Image.FromFile( @"C:\20128X.jpg" ) )
            {
                string saveAs = @"C:\output.jpg";

                EncoderParameters encoderParams = null;
                ImageCodecInfo codec = GetEncoderInfo( "image/jpeg" );
                if( codec != null )
                {
                    int quality = 100; // highest quality
                    EncoderParameter qualityParam = new EncoderParameter( 
                        System.Drawing.Imaging.Encoder.Quality, quality );
                    encoderParams = new EncoderParameters( 1 );
                    encoderParams.Param[0] = qualityParam;
                }

                try
                {
                    if( encoderParams != null )
                    {
                        im.Save( saveAs, codec, encoderParams );
                    }
                    else
                    {
                        im.Save( saveAs, ImageFormat.Jpeg );
                    }
                }
                catch( ExternalException )
                {
                    // copy and save separately
                    using( Image temp = new Bitmap( im ) )
                    {
                        if( encoderParams != null )
                        {
                            temp.Save( saveAs, codec, encoderParams );
                        }
                        else
                        {
                            temp.Save( saveAs, ImageFormat.Jpeg );
                        }
                    }
                }
            }
        }

        private static ImageCodecInfo GetEncoderInfo( string mimeType )
        {
            // Get image codecs for all image formats
            ImageCodecInfo[] codecs = ImageCodecInfo.GetImageEncoders();

            // Find the correct image codec
            foreach( ImageCodecInfo codec in codecs )
            {
                if( string.Compare( codec.MimeType, mimeType, true, CultureInfo.InvariantCulture ) == 0 )
                {
                    return codec;
                }
            }
            return null;
        }
    }
}

Note that you'll lose the EXIF information but I'll leave that for now (you'll generally still be able to read the EXIF codes, even when saving the source image fails). I have long since given up attempting to figure out what it is that .NET doesn't like about particular images (and have samples of various failure cases should anybody ever want to take up the challenge) but the approach above works, which is nice.

John Conners