9

I am using following code to put JPG's into a DataGridView's Image cell.

If strFileName.ToLower.EndsWith(".jpg") Then
     Dim inImg As Image = Image.FromFile(strFileName)
     DataGridView4.Rows.Add()
     DataGridView4.Rows(DataGridView4.Rows().Count - 1).Cells(0).Value = inImg
End If

The problem is that I need to save this file from within the program, but i get the message that the file is beeing used by another program.

So i tried to add inImg.Dispose() before the end if, but then the program doesnt display the images anymore in the DataGridView.

How can i add images in the DataGridView without locking them?

thanks

sharkyenergy
  • 3,842
  • 10
  • 46
  • 97
  • When you say "save" you mean doing something with the file which is incompatible with having it opened by a program? (examples: moving, deleting, renaming, etc.)? – varocarbas Aug 15 '13 at 11:01

5 Answers5

20

When you use the Image.FromFile(strFileName) method to create the Image, the method locks the file until you release the Image. The exact reason is explained below. And it's why you can't access more than one time to the same image file with this method.

You could instead:

  • use the Image.FromStream(stream) method.
  • that you use with a New FileStream or a MemoryStream that you create from the image file.

Here are possible implementation of a custom SafeImageFromFile method that doesn't lock the image file:

Public Shared Function SafeImageFromFile(path As String) As Image
    Using fs As New FileStream(path, FileMode.Open, FileAccess.Read)
        Dim img = Image.FromStream(fs)
        Return img
    End using
End Function

Or

Public Shared Function SafeImageFromFile(path As String) As Image
    Dim bytes = File.ReadAllBytes(path)
    Using ms As New MemoryStream(bytes)
        Dim img = Image.FromStream(ms)
        Return img
    End Using
End Function

Usage

If strFileName.ToLower.EndsWith(".jpg") Then
    Dim inImg As Image = SafeImageFromFile(strFileName)
    Dim index as integer = DataGridView4.Rows.Add()
    DataGridView4.Rows(index).Cells(0).Value = inImg
End If

Important note

Here I create the FileStream or a MemoryStream using a Using statement to make sure the stream is released. It works fine on my system and it seems it work for you too, though MSDN says about Image.FromStream(stream) method:

You must keep the stream open for the lifetime of the Image.

The reason of this sentence is explain here: KB814675 Bitmap and Image constructor dependencies

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.

To retain access to the source bits, GDI+ locks any source file, and forces the application to maintain the life of any source stream, for the life of the Bitmap or the Image object.

So know the code above could generate GDIexceptions because of releasing the stream using Using. It could happen when you save the image from the file or during the image creation. From this thread Loading an image from a stream without keeping the stream open and Hans Passant's comment they fixed several problems with indexed pixel formats in the Vista version of gdiplus.dll., it would happen only on XP.

To avoid this you need to keep the stream open. The methods would be:

Public Shared Function SafeImageFromFile(path As String) As Image
    Dim fs As New FileStream(path, FileMode.Open, FileAccess.Read)
    Dim img = Image.FromStream(fs)
    Return img
End Function

Or

Public Shared Function SafeImageFromFile(path As String) As Image
    Dim bytes = File.ReadAllBytes(path)
    Dim ms = New MemoryStream(bytes)
    Dim img = Image.FromStream(ms)
    Return img
End Function

But those last methods have some disadvantage like not releasing the stream (memory issue) and they violate rule CA2000 Dispose objects before losing scope .

The KB article gives some workarounds:

Create a Non-Indexed Image

This approach requires that the new image be in a non-indexed pixel format (more than 8 bits-per-pixel), even if the original image was in an indexed format. This workaround uses the Graphics.DrawImage() method to copy the image to a new Bitmap object:

  1. Construct the original Bitmap from the stream, from the memory, or from the file.
  2. Create a new Bitmap of the same size, with a pixel format of more than 8 bits-per-pixel (BPP).
  3. Use the Graphics.FromImage() method to obtain a Graphics object for the second Bitmap.
  4. Use Graphics.DrawImage() to draw the first Bitmap onto the second Bitmap.
  5. Use Graphics.Dispose() to dispose of the Graphics.
  6. Use Bitmap.Dispose() to dispose of the first Bitmap.

Create an Indexed Image

This workaround creates a Bitmap object in an indexed format:

  1. Construct the original Bitmap from the stream, from the memory, or from the file.
  2. Create a new Bitmap with the same size and pixel format as the first Bitmap.
  3. Use the Bitmap.LockBits() method to lock the whole image for both Bitmap objects in their native pixel format.
  4. Use either the Marshal.Copy function or another memory copying function to copy the image bits from the first Bitmap to the second Bitmap.
  5. Use the Bitmap.UnlockBits() method to unlock both Bitmap objects. Use Bitmap.Dispose() to dispose of the first Bitmap.

Here is an implementation of Non-Indexed Image creation, based on KB article and this answer https://stackoverflow.com/a/7972963/2387010 Your best bet is creating a pixel-perfect replica of the image -- though YMMV (with certain types of images there may be more than one frame, or you may have to copy palette data as well.) But for most images, this works:

Private Shared Function SafeImageFromFile(path As String) As Bitmap
    Dim img As Bitmap = Nothing
    Using fs As New FileStream(path, FileMode.Open, FileAccess.Read)
        Using b As New Bitmap(fs)
            img = New Bitmap(b.Width, b.Height, b.PixelFormat)
            Using g As Graphics = Graphics.FromImage(img)
                g.DrawImage(b, Point.Empty)
                g.Flush()
            End Using
        End Using
    End Using
    Return img
End Function

Someone indicated that what is important is that the FileStream is opened in read mode (FileAccess.Read).

True, but it makes more sens if you don't use Using statement and so you don't release the stream, or in multi threads context: FileAccess.Write is inappropriate, and FileAccess.ReadWrite is not required, but open the stream with FileAccess.Read mode won't prevent to have an IO.Exception if another program (or yours in multi threads context) has opened the file with another mode than FileAccess.Read.


If you want to be able to display the image and at the same time be able to save data to the file, Since you don't lock the file with those methods, you should be able to save the image (delete/overwrite the previous file) using the Image.Save method.

Community
  • 1
  • 1
Chris
  • 8,527
  • 10
  • 34
  • 51
  • 1
    +1 I didn't realise about what your code was exactly doing when I firsly saw it: you are also adding a copy of the given image to the DataGridView! The big difference between your code and the one from the OP is not the "Using" part, but getting the image from the stream (opened in read-mode) and thus adding a "virtual copy" of it, instead what Image.FromFile(strFileName) does (if you add "Using" with Image.FromFile the image is not stored). – varocarbas Aug 15 '13 at 11:48
  • 1
    thanks, but what would you do then? i cant say that the program wont be used on older systems.. what would you do to display the image and at the same time be able to save data to the file? thank you! – sharkyenergy Aug 15 '13 at 18:12
  • @justMe What do you mean by *display the image and at the same time be able to save data to the file*? – Chris Aug 15 '13 at 18:40
  • i have a tabview with a datagrid with imagecells. these imagecells display the images. then i have some textboxes. when i hit a button, i write the metatags i have written into the textboxes to the files that i have selected in the datagridview. its a program for tagging pictures. but i need to display the picture, and at the same time be able to save the metatags into the file on the harddrive. – sharkyenergy Aug 15 '13 at 19:41
  • @JustMe Since you don't lock the file, you should be able to save the image (delete/overwrite the previous file). This makes sense? – Chris Aug 15 '13 at 19:56
  • yes it makes sense. and it is indeed working. what i dont understand is: `You must keep the stream open for the lifetime of the Image. So know it could be a problem on older systems.` – sharkyenergy Aug 15 '13 at 20:16
  • 2
    MUCH more sense! thank you very much Chris! the answer is just PERFECT! – sharkyenergy Aug 17 '13 at 11:42
  • would it be too much if i'd ask to edit the code in the last codebox in a way that it also resizes the image to 300 width? (height updated proportionally). i tried but it doesnt work. thanks! – sharkyenergy Aug 17 '13 at 14:36
  • i can post it as a new question if you want. the final code you posted ( `SafeImageFromFile(path As String)` ) is what i need. but to be even better i would need it to return a image that has a width of 300 px. is that possible? – sharkyenergy Aug 17 '13 at 14:44
  • @Justme I think this is a new question and I have no answer for this one now. You should first do some searchs about image resizing. – Chris Aug 17 '13 at 15:03
  • Very nice, what i was looking for! – cking24343 Apr 21 '15 at 16:56
  • 1
    @Chris I modified the `g.DrawImage(b, Point.Empty)` like this `g.DrawImage(b, 0, 0, b.width, b.height)` because my image was blured. – Oscar TJ Sep 12 '18 at 07:57
  • The problem with GDI+ still occurs in Visual Studio 2019, .NET Framework 4.7.2. Just load an AniGIF in your picture box with the first USING method and you get the `System.Runtime.InteropServices.ExternalException` from `System.Drawing`. If you use the last one, you can see the first picture from the AniGIF without error, but there is no animation or further pictures visible. – PeterCo Apr 05 '19 at 19:56
  • Just for others: the last bit of code was returning some corrupted images (cropped or shrunk images). I have no idea why... I reverted to the first bit and all seems to be working well. I can't discern from Chris' post whether difficulties with the first approach only happen on XP, but here's hoping... – stigzler Oct 07 '20 at 14:55
0

@ Chris: Opening approximately 100 large (3400x2200) images with your final code, I was receiving an invalid argument crash on [img = new bitmap(...], I have seen this before opening an image of zero size, but that was not the case here. I added fs.dispose and successfully opened thousands of images of the same size of the same set as the first test without issue. I'm interested in your comments on this.

Private Function SafeImageFromFile(FilePath As String) As Image
    Dim img As Bitmap = Nothing
    Using fs As New FileStream(FilePath, FileMode.Open, FileAccess.Read)
        Using b As New Bitmap(fs)
            img = New Bitmap(b.Width, b.Height, b.PixelFormat)
            Using g As Graphics = Graphics.FromImage(img)
                g.DrawImage(b, Point.Empty)
                g.Flush()
            End Using
        End Using
        fs.Dispose()
    End Using
    Return img
End Function
Robert
  • 5,278
  • 43
  • 65
  • 115
Alan
  • 1
  • This works better, but in a larger test it still failed. the filestream leaks. – Alan May 21 '15 at 14:27
  • call `fs.Dispose()` won't change anything here because the next line (`End Using`) will automatically call `fs.Dispose()`. – Chris May 24 '15 at 12:42
0

This works without issue, ran 4189 images 3400x2200 through it (twice) without issue, this moves the filestream outside of the function and re-uses it. Im closing the file to release the write lock. Im pointing a picturebox at this image in a loop for my test.

Private fsIMG As FileStream
Private Function SafeImageFromFile(FilePath As String) As Image
    'Ref:  http://stackoverflow.com/questions/18250848/how-to-prevent-the-image-fromfile-method-to-lock-the-file
    Dim img As Bitmap = Nothing
    fsIMG = New FileStream(FilePath, FileMode.Open, FileAccess.Read)
    Using b As New Bitmap(fsIMG)
        img = New Bitmap(b.Width, b.Height, b.PixelFormat)
        Using g As Graphics = Graphics.FromImage(img)
            g.DrawImage(b, Point.Empty)
            g.Flush()
        End Using
    End Using
    fsIMG.Close()
    Return img
End Function
Alan
  • 1
0

After searching the internet for long time I found out I can use this code without any error.

  Private fsIMG As FileStream
Private Function SafeImageFromFile(FilePath As String) As Image
    'Ref:  http://stackoverflow.com/questions/18250848/how-to-prevent-the-image-fromfile-method-to-lock-the-file
    Dim img As Bitmap = Nothing
    fsIMG = New FileStream(FilePath, FileMode.Open, FileAccess.Read)
    Using b As New Bitmap(fsIMG)
        img = New Bitmap(b.Width, b.Height, b.PixelFormat)
        Using g As Graphics = Graphics.FromImage(img)
            g.DrawImage(b, Point.Empty)
            g.Flush()
        End Using
    End Using
    fsIMG.Close()
    Return img
End Function
Ɖiamond ǤeezeƦ
  • 3,223
  • 3
  • 28
  • 40
0

I encountered the same situation and used this code:

' Create memory stream from file
Dim ms As New MemoryStream()
' Open image file
Using fs As New FileStream(.FileName, FileMode.Open, FileAccess.Read)
    ' Save to memory stream
    fs.CopyTo(ms)
End Using

' Create image from the file's copy in memory
Dim img = Image.FromStream(ms)

I didn't dispose the memory stream because it allows to save the image later using exactly the same encoding as the original file, using this code:

img.Save(someOtherStream, img.RawFormat)