4

I have build a program which allows me to insert comment and the title of an Image through System.Image.Drawing so right now, I have trouble trying to overwrite the existing Jpeg file with the one that has comment and title added into it, but I am encountering error while deleting the file, so I'm not sure what to do as I have tried disposing the file but I cant saved it in that case, due to the fact that I disposed it too early, but I cant saved it because the existing file name is not deleted so I'm kinda stuck in the middle right now.

Here are my codes for it:

public string EditComment(string OriginalFilepath, string newFilename)
{
       
    image = System.Drawing.Image.FromFile(OriginalFilepath);
    PropertyItem propItem = image.PropertyItems[0];
    using (var file = System.Drawing.Image.FromFile(OriginalFilepath))
    {
        propItem.Id = 0x9286;  // this is the id for 'UserComment'
        propItem.Type = 2;
        propItem.Value = System.Text.Encoding.UTF8.GetBytes("HelloWorld\0");
        propItem.Len = propItem.Value.Length;
        file.SetPropertyItem(propItem);
        PropertyItem propItem1 = file.PropertyItems[file.PropertyItems.Count() - 1];
        file.Dispose();
        image.Dispose();
        string filepath = Filepath;
        if (File.Exists(@"C:\Desktop\Metadata"))
        {
            System.IO.File.Delete(@"C:\Desktop\Metadata");
        }
        string newFilepath = filepath + newFilename;
        file.Save(newFilepath, ImageFormat.Jpeg);//error appears here
        return filepath;      
     }
}

The Error shown are:

An exception of type 'System.ArgumentException' occurred in System.Drawing.dll but was not handled in user code

Additional information: Parameter is not valid.

Community
  • 1
  • 1
Marko233
  • 65
  • 9
  • Technically a duplicate, but I won't mark it since this specific case is different from most in that it does not require a returned usable image object. Also, finding actual correct answers even in those duplicate questions is a mess. – Nyerguds Feb 02 '18 at 09:42
  • by the way, to avoid potential issues with missing backslashes between paths and files, use `Path.Combine(folder, file)` to build full paths. It's much safer, with less hassle and checks. – Nyerguds Feb 02 '18 at 10:19
  • Also do note there may be a better way to modify jpeg comments than this; re-saving a jpeg severely lowers the quality of the image each time. I'd advise you to look up the jpeg format specifications to see if you can parse and edit the actual file format without touching the image data. – Nyerguds Feb 08 '18 at 10:52
  • @Nyerguds I'm not sure though, this seems the most reliable to me, and I copy the file and all i did was edit the metadata of it and saved it as a new file and delete the original one – Marko233 Feb 09 '18 at 03:12
  • @Nyerguds Do you by chance know the Id for the subject cause I have tried all the ID that has the subject tag but it doesn't work for me – Marko233 Feb 09 '18 at 03:14
  • Re-saving a jpeg _is not just copying._ It is unpacking the compressed data to image, and then recompressing that data with the lossy jpeg algorithm _again._ This will _always_ cause quality loss. You [can set the quality level](https://docs.microsoft.com/en-us/dotnet/framework/winforms/advanced/how-to-set-jpeg-compression-level), but by default [it always seems to save at 75%](https://stackoverflow.com/a/3959115/395685). Note that even setting that it to 100% [does not make it lossless](https://stackoverflow.com/a/1669870/395685). Just big. – Nyerguds Feb 09 '18 at 07:53
  • As for the jpeg specs, I don't know them (though I _have_ looked into the png format), but I'm fairly sure they must be available on the web somewhere. Your google is as good as mine. – Nyerguds Feb 09 '18 at 07:55
  • There's [an answer here on SO](https://stackoverflow.com/a/1038338/395685) which has useful links, though. – Nyerguds Feb 09 '18 at 08:28

1 Answers1

7

The problem is that opening an image from file locks the file. You can get around that by reading the file into a byte array, creating a memory stream from that, and then opening the image from that stream:

public string EditComment(string originalFilepath, string newFilename)
{
    Byte[] bytes = File.ReadAllBytes(originalFilepath);
    using (MemoryStream stream = new MemoryStream(bytes))
    using (Bitmap image = new Bitmap(stream))
    {
        PropertyItem propItem = image.PropertyItems[0];
        // Processing code
        propItem.Id = 0x9286;  // this is the id for 'UserComment'
        propItem.Type = 2;
        propItem.Value = System.Text.Encoding.UTF8.GetBytes("HelloWorld\0");
        propItem.Len = propItem.Value.Length;
        image.SetPropertyItem(propItem);
        // Not sure where your FilePath comes from but I'm just
        // putting it in the same folder with the new name.
        String newFilepath;
        if (newFilename == null)
            newFilepath = originalFilePath;
        else
            newFilepath = Path.Combine(Path.GetDirectory(originalFilepath), newFilename);
        image.Save(newFilepath, ImageFormat.Jpeg);
        return newFilepath;
    }
}

Make sure you do not dispose your image object inside the using block as you did in your test code. Not only does the using block exist exactly so you don't have to dispose manually, but it's also rather hard to save an image to disk that no longer exists in memory. Similarly, you seem to open the image from file twice. I'm just going to assume all of those were experiments to try to get around the problem, but do make sure to clean those up.

The basic rules to remember when opening images are these:

Contrary to what some people believe, a basic .Clone() call on the image object will not change this behaviour. The cloned object will still keep the reference to the original source.


Note, if you actually want a usable image object that is not contained in a using block, you can use LockBits and Marshal.Copy to copy the byte data of the image object into a new image with the same dimensions and the same PixelFormat, effectively making a full data clone of the original image. (Note: I don't think this works on animated GIF files) After doing that, you can safely dispose the original and just use the new cleanly-cloned version.

There are some other workarounds for actually getting the image out, but most of them I've seen aren't optimal. These are the two most common other valid workarounds for the locking problem:

  • Create a new Bitmap from an image loaded from file using the Bitmap(Image image) constructor. This new object will not have the link to that file, leaving you free to dispose the one that's locking the file. This works perfectly, but it changes the image's colour depth to 32-bit ARGB, which might not be desired. If you just need to show an image on the UI, this is an excellent solution, though.
  • Create a MemoryStream as shown in my code, but not in a using block, leaving the stream open as required. Leaving streams open doesn't really seem like a good idea to me. Though some people have said that, since a MemoryStream is just backed by a simple array, and not some external resource, the garbage collector apparently handles this case fine...

I've also seen some people use System.Drawing.ImageConverter to convert from bytes, but I looked into the internals of that process, and what it does is actually identical to the last method here, which leaves a memory stream open.

Nyerguds
  • 4,591
  • 1
  • 27
  • 52
  • Edited - a file stream would probably lock the file as well. Reading in advance fixes that. – Nyerguds Feb 02 '18 at 09:45
  • Problem right now is I cant dispose the file outside of using block, as Bitmap file is declare inside the block, so how do i do that? – Marko233 Feb 05 '18 at 03:01
  • You don't have to dispose any file. The only line in this code that _uses_ the file is `File.ReadAllBytes(OriginalFilepath)`; it isn't even inside the `using` block. The only moment the file is locked is _during_ that reading; after the reading is finished, the file is unlocked right away. Make sure you remove _all_ `Image.FromFile` calls in your code. – Nyerguds Feb 05 '18 at 13:32