Tagging is starting to infiltrate all of our media. It makes organizing and using it easier, and is a valuable feature to support.
Reading metadata from an image is fairly simple, once you find the pieces. Vista uses the property Keywords to store tags, so we’ll use that as well. The BitmapMetadata object holds quite a few properties for common metadata, such as Author, Rating, and Date Taken.
public string[] GetTags(string filename) { // open a filestream for the file we wish to look at using (Stream fs = File.Open(filename, FileMode.Open, FileAccess.ReadWrite)) { // create a decoder to parse the file BitmapDecoder decoder = BitmapDecoder.Create(fs, BitmapCreateOptions.None, BitmapCacheOption.Default); // grab the bitmap frame, which contains the metadata BitmapFrame frame = decoder.Frames[0]; // get the metadata as BitmapMetadata BitmapMetadata metadata = frame.Metadata as BitmapMetadata; // close the stream before returning fs.Close(); // return a null array if keywords don't exist. otherwise, return a string array if (metadata != null && metadata.Keywords != null) return metadata.Keywords.ToArray(); else return null; } } |
If there is another metadata property you’d like to access, use the GetQuery method instead:
public string[] GetTags(string filename) { // open a filestream for the file we wish to look at using (Stream fs = File.Open(filename, FileMode.Open, FileAccess.ReadWrite)) { // create a decoder to parse the file BitmapDecoder decoder = BitmapDecoder.Create(fs, BitmapCreateOptions.None, BitmapCacheOption.Default); // grab the bitmap frame, which contains the metadata BitmapFrame frame = decoder.Frames[0]; // get the metadata as BitmapMetadata BitmapMetadata metadata = frame.Metadata as BitmapMetadata; // close the stream before returning fs.Close(); // System.Keywords is the same as using the above method. this particular metadata property // will return an array of strings, though we still have to cast it as such string[] tags = metadata.GetQuery("System.Keywords") as string[]; return tags; } } |
There is plenty that can go wrong here – the file could not exist, it could be in use, or not be an image at all. Be sure to handle the various exceptions accordingly.
Writing tags can actually get a little hairy, because space for metadata isn’t necessarily alotted to every file. In actuality, it isn’t there unless an application (which, notably, will be yours after you implement this) makes some room for it. When writing tags, you still need to get to the associated BitmapMetadata object. Where the method departs from reading is instantiating an InPlaceBitmapMetadataWriter object. This is what we use to write the data (assuming there is space for it). If that fails, then we address the issue of adding space for the metadata.
public static void AddTags(string filename, string[] tags) { // open a filestream for the file we wish to look at using (Stream fs = File.Open(filename, FileMode.Open, FileAccess.ReadWrite)) { // create a decoder to parse the file BitmapDecoder decoder = BitmapDecoder.Create(fs, BitmapCreateOptions.None, BitmapCacheOption.Default); // grab the bitmap frame, which contains the metadata BitmapFrame frame = decoder.Frames[0]; // get the metadata as BitmapMetadata BitmapMetadata metadata = frame.Metadata as BitmapMetadata; // instantiate InPlaceBitmapMetadataWriter to write the metadata to the file InPlaceBitmapMetadataWriter writer = frame.CreateInPlaceBitmapMetadataWriter(); string[] keys; if (metadata.Keywords != null) // tags exist - include them when saving { // build the complete list of tags - new and old keys = new string[metadata.Keywords.Count + tags.Length]; int i = 0; foreach (string keyword in metadata.Keywords) { keys[i] = keyword; i++; } foreach (string tag in tags) { keys[i] = tag; i++; } // associate the tags with the writer // the type of variable to pass (here, an array of strings) depends on // which metadata property you are using. Since we are modifying the // Keywords property, we use the array. If you use the author property, // it will simply be a string. writer.SetQuery("System.Keywords", keys); } else // no old tags - just use the new ones { keys = tags; // associate the tags with the writer // the type of variable to pass (here, an array of strings) depends on // which metadata property you are using. Since we are modifying the // Keywords property, we use the array. If you use the author property, // it will simply be a string. writer.SetQuery("System.Keywords", tags); } // try to save the metadata to the file if (!writer.TrySave()) { // if it fails, there is no room for the metadata to be written to. // we must add room to the file using SetUpMetadataOnImage (defined below) SetUpMetadataOnImage(filename, keys); } } } |
So, if we have space allotted for metadata in the file, this will work great. Again, be sure to add exception handling.
What if we need to add room for the metadata? Well, this requires creating a new file, which will overwrite the old one. Essentially, we open it up, copy the image data from the old file, add whatever metadata we want to add initially, and save the file. This is a lossless transcoding, so no quality is lost. This does mean that the file size grows, however.
private static void SetUpMetadataOnImage(string filename, string[] tags) { // padding amount, using 2Kb. don't need much here; metadata is rather small uint paddingAmount = 2048; // open image file to read using (Stream file = File.Open(filename, FileMode.Open, FileAccess.Read)) { // create the decoder for the original file. The BitmapCreateOptions and BitmapCacheOption denote // a lossless transocde. We want to preserve the pixels and cache it on load. Otherwise, we will lose // quality or even not have the file ready when we save, resulting in 0b of data written BitmapDecoder original = BitmapDecoder.Create(file, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None); // create an encoder for the output file JpegBitmapEncoder output = new JpegBitmapEncoder(); // add padding and tags to the file, as well as clone the data to another object if (original.Frames[0] != null && original.Frames[0].Metadata != null) { // Because the file is in use, the BitmapMetadata object is frozen. // So, we clone the object and add in the padding. BitmapFrame frameCopy = (BitmapFrame)original.Frames[0].Clone(); BitmapMetadata metadata = original.Frames[0].Metadata.Clone() as BitmapMetadata; // we use the same method described in AddTags() as saving tags to save an amount of padding metadata.SetQuery("/app1/ifd/PaddingSchema:Padding", paddingAmount); metadata.SetQuery("/app1/ifd/exif/PaddingSchema:Padding", paddingAmount); metadata.SetQuery("/xmp/PaddingSchema:Padding", paddingAmount); // we add the tags we want as well. Again, using the same method described above metadata.SetQuery("System.Keywords", tags); // finally, we create a new frame that has all of this new metadata, along with the data that was in the original message output.Frames.Add(BitmapFrame.Create(frameCopy, frameCopy.Thumbnail, metadata, frameCopy.ColorContexts));original.Frames[0].ColorContexts)); file.Close(); // close the file to ready for overwrite } // finally, save the new file over the old file using (Stream outputFile = File.Open(filename, FileMode.Create, FileAccess.Write)) { output.Save(outputFile); } } } |
There it is. Not entirely straight-forward, but it works nonetheless.
Updated: thanks to Thomas Manz and Frank Paris for bringing a couple flaws to my attention (see the comments).
Small bug keys is not defined in the if statement for trysave.
Good catch. I’ve corrected it by setting keys = tags in the else statement.
Pingback: TagFlo » Updates: » C#, WPF and Bitmap Metadata
Hi,
I just made some tests with your function SetUpMetadataOnImage() and I found out that it is NOT lossless, i. e. the jpeg seems to be encoded new.
I compared the jpegs “before” and “after” with Beyond Compare 3 (there is a picture and also a hex compare mode, you can test it for 30 days).
So why do you think this operation is lossless and (if you agree to me) do you have an idea how to add tags really lossless?
Thanks and regards
Thomas
Hey Thomas,
You’re right, it does incur some loss. After taking a look, I was able to verify that changing the BitmapCacheOption to BitmapCacheOption,None when creating the BitmapDecoder appears to fix this problem. Let me know if you find any other problems with this approach.
Thanks!
Andrew
I’ve been struggling with your SetUpMetadataOnImage() function for several days now. In every experiment I tried, the last statement of the function (the output.Save) causes an exception, “The image data generated and overflow during processing.” I have no idea what this means and Googling didn’t come up with anything. The same exception occurs if you simplify the BitmapFrame.Create to just the first parameter. This is very unfortunate for me, because your post is the only one I’ve been able to find that claims to have solved the problem of adding metadata to a image file in WPF, and it simply does not work.
I did some more experimenting and when I changed the BitmapCacheOption to OnLoad, the exception went away and I was able to create the new file with the added metadata. This makes me very happy, but I’m concerned about WikkaWikka’s discovery. My bitmap got 20% smaller using OnLoad. But I get that inexplicable exception when I set the option to None. I don’t understand why you don’t get the exception.
I found the solution where BitmapCacheOption.None can still be used and the “overflow” exception does not happen. You have to Clone() the original.Frames[0] before you add them to the encoder Frames. Also I don’t get the point of specifying the IgnoreColorProfile BitmapCreationOptions and I have dropped it in my code. By specifying IgnoreColorProfile, aren’t you throwing away the color profile in your final image?
Hey Frank,
That is interesting that you have been having exceptions when not cloning the original.Frames[0] – I haven’t had any problems with that. I’ll implement that in my solution to be sure there isn’t some corner case where that occurs that I haven’t witnessed yet. Thanks for updating me with your solution.
You’re right about using IgnoreColorProfile. This post includes some bits I’ve picked up from others, and that’s one line that I didn’t give much scrutiny. I’ll remove that for the benefit of others happening upon this post.
Thanks, Frank!
Hi, Andrew.
My problem is worse than original.Frames[0] not working. I’ve been struggling with this for weeks now and I finally got part way to the root of the problem, why it works sometimes and not others. It fails 100% of the time if the BitmapDecoder.Save() function is executed inside a worker thread and succeeds 100% of the time if it is executed inside the main application thread. But I have been unable to find out why. My problem is, I need to be in a worker thread so I can show a progress bar, since I may have hundreds to thousands of images whose metadata I want to change (e.g. the copyright notice).
Hi Frank,
Things are a bit busy right now for me, so I don’t have the time to look into the reason behind it not working off of the UI thread; however using the Dispatcher.BeginInvoke() or Dispatcher.Invoke() methods (as used in this post,though I never did start my more detailed threading post) may be able to help work around the issue. You can run your logic for updating the user in the separate thread, but call the UI thread with one of those methods to execute this code to ensure its success. So it would be something like this pseudocode:
// run in separate thread
methodforupdatinguserwhilewritingmetadata()
{
// do stuff in separate thread you need to do
Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Normal,
(ThreadStart)delegate ()
{
// run setup metadata
});
}
Hope that helps.
Andrew
Thanks for this example! The MS docs on this are horrible beyond belief. I would never have gotten my metadata writing working without your example.
hello,
first – thank you very much
this seem exactly like what I need and cannot find anywhere else
unfortunately.. the method SetUpMetadataOnImage throws an System.OverflowException 🙁 it is caused by the output.Save(outputFile);
any ideas what can be wrong?
Hi urza,
No problem – I had a lot of trouble finding information as well, which is why I was sure to write up a post regarding it.
Frank was having a problem with OverflowException on that same line. Here’s something he discovered:
Let me know if this doesn’t do it for you.
It seems to work now with the cloning.
Would you know what string do I need to pass to SetQuery if I would also like to change the image title and image description in metadata?
http://msdn.microsoft.com/en-us/library/bb643802.aspx
I am here again :/
I still have the owerflow exception. Even with the cloning. It only works with pictures that allready have some metadata. Thats why I tought it works – I tried it on pictures in windows vista MyPictures/Samples – all files there have ratings or other metadata.. but when I download photo or picture from internet. It has no metadata and the code throws up the exception. It also destroys the file. The file has size 0 B and is not readable.
any ideas? why is it only working with files that allready have metadata?
Hey urza,
Are you able to write the new image to a separate file? That is, replace:
// finally, save the new file over the old file
using (Stream outputFile = File.Open(filename, FileMode.Create, FileAccess.Write))
{
output.Save(outputFile);
}
with:
// finally, save the new file over the old file
using (Stream outputFile = File.Open(filename + “.tmp”, FileMode.Create, FileAccess.Write))
{
output.Save(outputFile);
}
If so, a workaround might be to save it to a temporary file (as shown) and override the original file afterwords. This way, you can check the newly created file before overriding the original and causing data loss.
Andrew
I was having the same issues with the encoder.Save throwing an exception, but that went away when I marked the program to use STAThread.
Also, I was having the issue of having the new file being smaller, and used the advice of marking the cache options to None. But I was wondering, I couldn’t see any differnce between the smaller and the larger file. Does someone know what I might have been missing?
Likely, the difference wasn’t great enough to be seen by the human eye (or, at the least, an untrained eye). If you do a bitwise comparison, you’ll find that the two files are not equal; however, the loss isn’t entirely great. While that is so, it is definitely better to avoid loss at all.
Interesting thread, I didn’t appreciate another consequence of the Onload vs OnDemand. I’ve published a pretty comprehensive metadata library on Codeplex if anyone is interested (http://fotofly.codeplex.com/).
// finally, save the new file over the old file
using (Stream outputFile = File.Open(filename, FileMode.Create, FileAccess.Write))
{
output.Save(outputFile);
}
On the line output.Save(outputFile);
I’m getting the error ‘Access is Denied’
Any idea what this means?
Sounds like the outputFile is currently in use, or the location is not accessible to the current user.
The outputFile might still be in use from this application, as the stream you opened for it earlier might not quite be closed. If that’s the case, you could go the route that I suggested above for urza, where you save a temporary file and copy it over. Alternatively, you could create a loop that waits for the file to be accessible, but be sure to have a cap for that in case it never becomes accessible.
If it is a permissions problem, you’ll have to require that your application be ran with administrator privileges, or notify the user that the file they are trying to modify is not accessible by them (I’d go with the latter).
Pingback: Scott Hanselman - Dealing with Images with Bad Metadata - Corrupted Color Profiles in WPF
Pingback: Dealing with Images with Bad Metadata – Corrupted Color Profiles in WPF | Programming
Hi Andrew,
Great article and really nice code…but does this work for .tiff format as well ? I tried the code on a 16 bit .tiff file and at the code where the output file is saved, I got an Overflow Exception.
I wonder if this has something to do with the padding value or is it the tiff format.
metadata.SetQuery(“/app1/ifd/PaddingSchema:Padding”, paddingAmount);
metadata.SetQuery(“/app1/ifd/exif/PaddingSchema:Padding”, paddingAmount);
metadata.SetQuery(“/xmp/PaddingSchema:Padding”, paddingAmount);
Can I use the above code for .tiff format as well ?
Wow !!! I finally got this thing to work….thanks to Andrew and all the previous commenters.
To all the guys above who found that the file size got smaller, I found the solution….
Just set the encoder compression to None…..for example, in my case I was using TiffBitmapEncoder.
All I had to do was set TiffCompressOption.None as the setting. That fixed it !!
Thanks, Koshy. I’m sure that’ll prove helpful to anyone supporting .tiff files in the future.
To help future readers, I’d like to point out some intricacies to remember when dealing with the WPF metadata writer:
1. Make sure that your method that deals with the metadata is invoked on an STA apartment thread. Otherwise, you get some nasty COM exceptions. You can check if your method is running on STA by using the Thread.CurrentThread.GetApartmentState() call.
2. If you find that the file is getting smaller after writing metadata, just make sure that your Encoder compression setting is “None”. (See my previous comment).
3. If you find it hard to read an image file and write metadata to it, do it the simple way. Create a temporary file that is a copy of the original image. Load the image decoder using the temporary file and after filling in the metadata, write the encoder output to the original file. Then, you can delete the temporary file.
Hope this helps !
In the SetUpMetadataOnImage sub you have the line:
output.Frames.Add(BitmapFrame.Create(frameCopy, frameCopy.Thumbnail, metadata, frameCopy.ColorContexts));original.Frames[0].ColorContexts));
which does not seem to be correct. Can you give me the correct way to write that line?