Reading and Writing Tags for Photos in WPF
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).
Related posts:
Login
Categories
- Software Development (19)
- Video Games (13)
- Personal (7)
Common Tags
Feel Free to Comment
If you have any comments regarding my code (different approaches, improvements, missed opportunities...), don't hesitate to post! I'd love to see your point of view. However, try to keep it constructive. "This code sucks" doesn't help anyone.
If it's not about code, just try to keep it on topic.
Disclaimer
The views and opinions expressed herein are strictly those of the author and do not necessarily reflect the views or opinions of his employer or any of its affiliates.
All code is provided without warranty, expressed or implied.
April 1st, 2009 - 21:00
Small bug keys is not defined in the if statement for trysave.
April 2nd, 2009 - 10:29
Good catch. I’ve corrected it by setting keys = tags in the else statement.
April 24th, 2009 - 06:57
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
April 24th, 2009 - 12:10
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
May 24th, 2009 - 19:08
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.
May 24th, 2009 - 20:26
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.
May 26th, 2009 - 08:44
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?
May 26th, 2009 - 10:45
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!
May 30th, 2009 - 10:01
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).
June 1st, 2009 - 09:41
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
June 4th, 2009 - 11:32
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.
July 6th, 2009 - 14:19
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?
July 6th, 2009 - 17:07
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.
July 10th, 2009 - 02:26
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?
July 10th, 2009 - 02:58
http://msdn.microsoft.com/en-us/library/bb643802.aspx
July 11th, 2009 - 14:41
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?
July 17th, 2009 - 13:40
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
August 16th, 2009 - 18:27
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?
August 17th, 2009 - 08:42
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.
December 5th, 2009 - 19:13
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/).
January 4th, 2010 - 20:20
// 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?
January 5th, 2010 - 09:52
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).