
Recently I decided to familiarize myself with the .NET platform, the C # language and the Windows Presentation Foundation.
In the process of studying (and I am learning languages and technologies, I am always in the process of developing a pilot project) I encountered quite a few pitfalls and subtle points. I would like to share it with the habrasoobshchestvo (I suppose that many beginning WPF developers would be interested in this) all at once, but the volume of the resulting habratopik would be too large, so I decided to start with image metadata, because on this topic of information, even in the English-language Internet is not enough.
In general, metadata may be available for images of various formats, however, I will be telling on the example of a JPEG, since worked with him. I think for other formats the difference will be small.
')
Types of metadata
First, let's look at what types of metadata can be in the image. All the most likely it is so know, but just in case I will tell:
- EXIF (Exchangeable Image File Format) is a standard for storing metadata in an image that is used by digital cameras to store information about shutter speed, aperture, and other shooting parameters. EXIF metadata can be stored in JPEG, TIFF and RIFF WAV files. According to the standard, only description (Description tag) and comment (User Comment tag) can be stored in the EXIF user descriptive metadata, but Windows Explorer also uses several additional tags (XPTitle, XPSubject, XPAuthor, XPComment, XPKeywords). Windows Explorer ignores the XPTitle tag when there is a standard Description tag.
- IPTC (International Press Telecommunications Council) - the name rather of the organization that developed the standard. The metadata standard itself is called the Information Interchange Model (IIM). The oldest of the described standards. In the original version of the standard, the metadata was stored in such a way that software that was not aware of the existence of IPTC could not work with image files that contained such metadata. However, later Adobe expanded the standard by transferring metadata to an APP13 block of a JPEG file, which allowed software that does not know about the standard to successfully read a JPEG file, ignoring unknown metadata. Descriptive fields such as ObjectName (title), Keywords (keywords), Caption (description, there are several tag variations) can be stored in IPTC metadata.
- XMP (eXtensible Metadata Platform) is a standard developed by Adobe. The metadata is stored in the RDF model in XML format, allowing you to include any necessary information in the image file. This format prefers to use WIC (Windows Imaging Component) in Windows Vista / 7.
Principles of working with metadata in WPF
WPF uses the BitmapEncoder, BitmapDecoder, BitmapSource, BitmapFrame, BitmapMetadata, InPlaceMetadataWriter classes to work with metadata.
The BitmapEncoder and BitmapDecoder classes have descendants that allow you to work with specific image formats. In my case - JpegBitmapEncoder and JpegBitmapDecoder.
The InPlaceMetadataWriter class is used to modify metadata on the spot, without recoding a file.
Data can be read and written by two methods — either using the GetQuery / SetQuery functions, which operate with hierarchical metadata tag names, or using the BitmapMetadata class fields, which make it easy to access metadata.
When accessing metadata through the fields of the BitmapMetadata class, the WIC tries to find the corresponding fields in the metadata of different standards in the following order: first XMP, then IPTC and EXIF. When writing tags through the fields of the BitmapMetadata class, WIC records them in XMP format.
Read metadata
Here is a ready-made example of a function with which you can read metadata from a file:
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
FileStream f = File .Open( "test.jpg" , FileMode.Open); BitmapDecoder decoder = JpegBitmapDecoder.Create(f, BitmapCreateOptions.IgnoreColorProfile, BitmapCacheOption.Default); BitmapMetadata metadata = (BitmapMetadata)decoder.Frames[ 0 ].Metadata; // string title = metadata.Title; // XMP string xmptitle = ( string )metadata.GetQuery( @"/xmp/<xmpalt>dc:title" ); // EXIF string exiftitle = ( string )metadata.GetQuery( @"/app1/ifd/{ushort=40091}" ); // IPTC string iptctitle = ( string )metadata.GetQuery( @"/app13/irb/8bimiptc/iptc/object name" );
Everything is quite simple and transparent, so we will go straight to the record.
Metadata Recording
- BitmapMetadata md = new BitmapMetadata ( "jpg" );
- md.SetQuery ( @ "/ xmp / <xmpalt> dc: title" , xmptitle);
- md.SetQuery ( @ "/ app1 / ifd / {ushort = 40091}" , exiftitle);
- md.SetQuery ( @ "/ app13 / irb / 8bimiptc / iptc / object name" , iptctitle);
- BitmapFrame frame = BitmapFrame.Create (decoder.Frames [ 0 ], decoder.Frames [ 0 ] .Thumbnail, md, decoder.Frames [ 0 ] .ColorContexts);
- BitmapEncoder encoder = new JpegBitmapEncoder ();
- encoder.Frames.Add (frame);
- FileStream of = File .Open ( "test2.jpg" , FileMode.Create, FileAccess.Write);
- encoder.Save (of);
- of.Close ();
The code goes like a continuation of a fragment reading metadata. We create a copy of the original file by writing in its metadata titles in all three metadata formats.
Editing in-place metadata
So far I have told in general fairly well-documented and simple things, but everything is more complicated here. The example in the official documentation (MSDN) is incorrect and generally opposite in meaning to the real state of things.
To edit metadata in place, you need to create an object of the InPlaceBitmapMetadataWriter class:
- InPlaceBitmapMetadataWriter writer;
- writer = decoder.Frames [ 0 ] .CreateInPlaceBitmapMetadataWriter ();
After that, you can work with it as with ordinary BitmapMetadata, by calling SetQuery to set the necessary metadata.
To save changes, you need to call the TrySave () method, which tries to save the changes to the original stream. Attempting to write may or may not be successful. If successful, the method returns true; on error, it returns false.
The most common mistake that can prevent you from writing changes is that there is not enough free space in the metadata. As a rule, all the freshly photographed photos do not contain enough space in the metadata, therefore, in order to start using metadata editing in place, you should make a copy of the file once, adding metadata in it with special padding fields that leave space for subsequent changes. To do this, the file is opened, the necessary frame and its metadata are cloned, and several queries are executed:
- BitmapFrame frame = (BitmapFrame) decoder.Frames [ 0 ] .Clone ();
- BitmapMetadata metadata = (BitmapMetadata) decoder.Frames [ 0 ] .Metadata.Clone ();
- metadata.SetQuery ( "/ app1 / ifd / PaddingSchema: Padding" , 2048 );
- metadata.SetQuery ( "/ app1 / ifd / exif / PaddingSchema: Padding" , 2048 );
- metadata.SetQuery ( "/ xmp / PaddingSchema: Padding" , 2048 );
- BitmapFrame newframe = BitmapFrame.Create (frame, frame.Thumbnail, metadata, original.Frames [ 0 ] .ColorContexts);
After that, the frame is sufficient to encode the encoder and write to the desired stream, with the result that in the image there will be free space for editing the metadata on the spot afterwards.
A padding value of 2048 bytes is usually sufficient. If you need more - you can specify a larger value.
Query strings
I think everyone has a reasonable question when studying SetQuery / GetQuery methods - where do you get all these query strings that you can't call simple and intuitive?
After a long search in MSDN found the appropriate
list . There are probably all the necessary requests. Missing ones can in principle be made up by analogy, there are plenty of examples :)
Subtleties and pitfalls
- WIC versions in Windows XP and Windows Vista can be buggy if the STAThread attribute is not specified for the calling JpegBitmapEncoder.Save () stream (by default, all threads created in the application receive the MTAThread attribute if not specified otherwise).
- The WIC version in Windows 7 saves the EXIF UserComment tag's default values in Unicode, while in Windows XP and Windows Vista it is in the encoding of the current system language (CP1251 for Russian). The format for writing UTF-8 parameters is as follows: the tag value itself is not stored as a string, but as an array of bytes. The first 7 bytes is the ASCII string "UNICODE", after which the Unicode-encoded tag character sequence begins.
- The BitmapCacheOptions parameter should be treated carefully. The OnLoad value caches all uncompressed image data in RAM, so if you open 20 large format JPEGs with this option, the free memory will be eaten very quickly. This memory is not released when deleting the image classes themselves (BitmapFrame, BitmapDecoder, etc.) and processing them by the garbage collector. In addition, to use the InPlaceBitmapMetadataWriter, open the image with BitmapCacheOptions = OnDemand or Default.
- In the example, I open the image with the IgnoreColorProfile flag, because without it, on some images, BitmapDecoder throws an exception.
Conclusion
In general, working with metadata using WPF seemed to me rather complicated and confusing. Almost all of the described pitfalls cost me several hours of debugging and googling, information about this is nowhere, and the symptoms are sometimes very strange. Official documentation (MSDN) covers this issue badly, and in some places is completely wrong.
I hope that this collected information will help those who need to work with metadata through WPF, and save them a few hours of time :)
PS I would be happy to see comments in comments (if I was mistaken somewhere) and descriptions of pitfalls that I haven’t met or forgot to mention.
PPS Should I continue to write about WPF, or am I writing long-known things?