Friday, February 11, 2011

Image.FromStream or Image.FromFile locking streams and files

You may want to create an image from a Stream or from a file on the system. You would want to use the static methods Image.FromStream or Image.FromFile to do it. However, you soon realize that this would not work if you immediately dispose the stream. Also, trying to delete the file would result in a locked file error. Could it be that some reference of the stream or file is kept in the resulting Image object? The answer is yes.

Microsoft explains it like this: GDI+, and therefore the System.Drawing namespace, may defer the decoding of raw image bits until the bits are required by the image. Additionally, even after the image has been decoded, GDI+ may determine that it is more efficient to discard the memory for a large Bitmap and to re-decode later. Therefore, GDI+ must have access to the source bits for the image for the life of the Bitmap or the Image object. And they have a fix to this, here: Bitmap and Image constructor dependencies

As you can see in the link above, their solution is different from indexed versus non indexed images. You can determine if an image is indexed or not by looking at the PixelFormat property. Also, you need to do some pretty weird stuff in the indexed case and there is no code on the Microsoft page. So here is a helper class I've cropped up to get images from Stream, File or byte array without locking any of the original resources:

/// <summary>
/// Helper for Image objects
/// </summary>
public static class ImageHelper
{
/// <summary>
/// Get an image from a byte array
/// </summary>
/// <param name="iconBytes"></param>
/// <returns></returns>
public static Image GetImageFromBytes(byte[] iconBytes)
{
if (iconBytes == null || iconBytes.Length == 0)
return null;
using (MemoryStream ms = new MemoryStream(iconBytes))
{
try
{
return GetImageFromStream(ms);
}
catch
{
return null;
}
}
}

/// <summary>
/// Get an image from a file without locking the file
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public static Image GetImageFromFile(string fileName)
{
try
{
using (var stream = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
return GetImageFromStream(stream);
}
}
catch
{
return null;
}
}

/// <summary>
/// Determines if an image is indexed (with a color palette)
/// </summary>
/// <param name="image"></param>
/// <returns></returns>
public static bool IsImageIndexed(Image image)
{
switch (image.PixelFormat)
{
case PixelFormat.Format1bppIndexed:
case PixelFormat.Format4bppIndexed:
case PixelFormat.Format8bppIndexed:
case PixelFormat.Indexed:
return true;
}
return false;
}

/// <summary>
/// Get an image from stream without locking the stream
/// </summary>
/// <param name="stream"></param>
/// <returns></returns>
public static Image GetImageFromStream(Stream stream)
{
try
{
using (var image = Image.FromStream(stream))
{
var srcBitmap = image as Bitmap;
var destBitmap = new Bitmap(image.Width, image.Height,image.PixelFormat);
if (IsImageIndexed(image)&&srcBitmap!=null)
{
Rectangle srcArea = new Rectangle(0, 0, image.Width, image.Height);
BitmapData srcData = srcBitmap.LockBits(srcArea, ImageLockMode.ReadOnly, image.PixelFormat);
Rectangle destArea = new Rectangle(0, 0, image.Width, image.Height);
BitmapData destData = destBitmap.LockBits(destArea, ImageLockMode.WriteOnly, image.PixelFormat);

IntPtr srcPtr = srcData.Scan0;
IntPtr destPtr = destData.Scan0;
byte[] buffer = new byte[srcData.Stride*image.Height];
Marshal.Copy(srcPtr, buffer, 0, buffer.Length);
Marshal.Copy(buffer, 0, destPtr, buffer.Length);
srcBitmap.UnlockBits(srcData);
destBitmap.UnlockBits(destData);
destBitmap.Palette = srcBitmap.Palette;
} else
{
using (var graphics = Graphics.FromImage(destBitmap))
{
graphics.DrawImage(image, 0, 0);
}
}
return destBitmap;
}
}
catch
{
return null;
}
}
}

2 comments:

Anonymous said...

Thanks for the article and code - it works well except that it does not keep the PropertyItems which I need in order to update EXIF and IPTC information. Working on a way to do this but no luck so far! Will post back here with an update when I find out how to do it.

Siderite said...

A comment I could learn from. I had no idea what EXIF and IPTC meant. Thanks!

By looking around I've found several resources. http://stackoverflow.com/questions/7435716/copying-image-propertyitems-using-c-sharp says that the managed way to set metadata properties misses some of them and the recommendation is to binary copy everything from one image to another.
http://stackoverflow.com/questions/58649/how-to-get-the-exif-data-from-a-file-using-c-sharp has a lot of resources.
http://www.bobpowell.net/discoverproperties.htm apparently has a way of reading the metadata directly from the binary stream and interpret it programatically.

Hope it helps.