📜 ⬆️ ⬇️

Working with image metadata in WPF

image
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:


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:

  1. 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" );
  2. 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" );
  3. 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" );
  4. 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" );
  5. 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" );
  6. 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" );
  7. 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" );
  8. 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" );
  9. 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" );
  10. 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" );
  11. 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



  1. BitmapMetadata md = new BitmapMetadata ( "jpg" );
  2. md.SetQuery ( @ "/ xmp / <xmpalt> dc: title" , xmptitle);
  3. md.SetQuery ( @ "/ app1 / ifd / {ushort = 40091}" , exiftitle);
  4. md.SetQuery ( @ "/ app13 / irb / 8bimiptc / iptc / object name" , iptctitle);
  5. BitmapFrame frame = BitmapFrame.Create (decoder.Frames [ 0 ], decoder.Frames [ 0 ] .Thumbnail, md, decoder.Frames [ 0 ] .ColorContexts);
  6. BitmapEncoder encoder = new JpegBitmapEncoder ();
  7. encoder.Frames.Add (frame);
  8. FileStream of = File .Open ( "test2.jpg" , FileMode.Create, FileAccess.Write);
  9. encoder.Save (of);
  10. 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:

  1. InPlaceBitmapMetadataWriter writer;
  2. 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:

  1. BitmapFrame frame = (BitmapFrame) decoder.Frames [ 0 ] .Clone ();
  2. BitmapMetadata metadata = (BitmapMetadata) decoder.Frames [ 0 ] .Metadata.Clone ();
  3. metadata.SetQuery ( "/ app1 / ifd / PaddingSchema: Padding" , 2048 );
  4. metadata.SetQuery ( "/ app1 / ifd / exif / PaddingSchema: Padding" , 2048 );
  5. metadata.SetQuery ( "/ xmp / PaddingSchema: Padding" , 2048 );
  6. 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





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?

Source: https://habr.com/ru/post/93119/


All Articles