Introduction
Reading and writing bitmap image metadata tags is one of the thorniest areas within WPF. Metadata within an image file is data about the image data within the bitmap file, usually written by the hardware capture device: e.g. a digital camera or scanner. This is so called objective metadata that records historical facts about the image such as the f/stop the lens was set at when the picture was taken. But there are also what may be called subjective metadata tags, tags that are usually written by software tools after the image is "taken." Subjective metadata tags describe subjective features of the image, such as the subject matter or a quality rating in the opinion of the photographer. This article restricts itself to reading image metadata, and specifically with iterating through all of the metadata tags within an image file.
If you're familiar with how easy it is to do this in GDI+, you may be puzzled by a perusal of the methods and properties of the WPF classes dealing with images. It may not be at all obvious! But once you understand the hierarchy of the WPF imaging classes, everything becomes clear. It's because the technique is somewhat obscure that I chose to write this article. By the time you're finished with the article, you'll understand exactly how this is done and what you have to do to overcome the limitations in the format of the metadata "query strings" you'll be retrieving from the WPF classes.
Background
Metadata within an image is organized hierarchically within the metadata section of an image file. Therefore, a specific metadata tag can be addressed by something that looks like a file system folder path. For example, in a JPEG file, the path to the information containing the lens focal length is the following:
/app1/ifd/exif/{ushort=33434}
The above string is known to the WPF BitmapMetadata
class as a query string and can be used as input to the GetQuery()
method.
The first component in the path is the physical name of a segment within the metadata section of the image file. The second and third components are symbolic names of segments within the app1 segment. The fourth component in the example is the numeric ID of an atomic metadata item that happens to identify a tag that is given a symbolic name of ExposureTime
in the metadata specification standards, also called the shutter speed in familiar photographic parlance. But the physical numeric ID exists in the metadata as an unsigned 16-bit integer.
Associated with each numeric ID is a metadata value in a format defined in the standard. For example, the exposure time is specified by an unsigned 64-bit integer that is composed of a 32 bit unsigned numerator and a 32 bit unsigned denominator that together express the shutter speed in seconds, for example 2/5 for a shutter speed of 0.4 seconds.
Although in query strings, WPF programs will normally use symbolic expressions like the above example, internally within the metadata block all components after the first (app1 in the example) are expressed as unsigned 16-bit integers. Our example is physically identified by the following query string:
/app1/{ushort=0}/{ushort=34665}/{ushort33434}
Either the symbolic string above or this raw query string can be used as input to the BitmapMetadata.GetQuery()
method to get the shutter speed at which an image was taken. Normally the programmer would not use the raw query string to make a query, but would only use the symbolic query string. But the mechanism for iterating through all of the metadata items in an image file only returns raw query strings, which is why, if you want to perform this iteration, you need to know about this raw query string format.
Usually when reading image metadata under WPF, the programmer knows beforehand the symbolic path within the metadata section of an image file. But if you are interested in exploring every metadata item within an image file, you will only be able to get raw query strings back from the procedure and you will have to use some kind of a translation table to convert the raw query strings to symbolic query strings.
You enumerate all of the metadata tags within an image file with the BitmapMetadata
class. The BitmapMetadata
class implements the IEnumerable<String>
interface in such a way that all of the image metadata tags in an instance of the class can be enumerated. Some of the tags enumerated themselves represent BitmapMetadata
instances and so a recursive procedure can enumerate all of the metadata tags in an image, no matter how deeply nested in a hierarchy of BitmapMetadata
instances.
Using the Code
Metadata tag enumeration begins with a BitmapDecoder
for an image file. You create a BitmapDecoder
instance with the BitmapDecoder
static function, Create()
, passing it a file Stream
open for reading. You then use the BitmapDecoder.Frames
property, which is an instance of a ReadOnlyCollection<BitmapFrame>
. A BitmapFrame
has a Metadata
property, which is an instance of ImageMetadata
. ImageMetadata
is an abstract
base class for all metadata operations on imaging related APIs. To access individual metadata tags, you must cast it into an instance of BitmapMetadata
. As mentioned above, besides being derived from ImageMetadata
, BitmapMetadata
also implements the IEnumerable<String>
interface. This is precisely what enables you to enumerate all of the metadata tags recorded in an image file.
Each enumerated String
in a BitmapMetadata
instance is a raw query string as described above that can be used as input to the BitmapMetadata.GetQuery()
instance function. GetQuery()
returns an Object
that is an instance of a metadata query reader. The metadata query reader might be one of two things:
- It may be another instance of
BitmapMetadata
, in which case you can iterate through all the strings in it, performing GetQuery()
on each of them. It is by performing this recursively that you can enumerate all of the query strings in a BitmapFrame.Metadata
object.
- It might also be a displayable tag value of the query string, for example the
Orientation
tag whose raw query string for a JPEG file would be as follows:
/app1/{ushort=0}/{ushort=274}
The Orientation tag value is actually of type, Int16
.
After the above explanation, we can look at some code that actually captures all of the metadata contained in an image file. First we have to get the BitmapMetadata
object from an image file. We can do that with the following code, assuming ImagePath
is the full file system path to an image file, such as a TIFF or JPEG file:
RawMetadataItems = new List<RawMetadataItem>();
using (Stream fileStream = File.Open(ImagePath, FileMode.Open))
{
BitmapDecoder decoder = BitmapDecoder.Create(fileStream,
BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.None);
CaptureMetadata(decoder.Frames[0].Metadata, string.Empty);
}
CaptureMetadata()
is my simple recursive procedure that iterates through all of the metadata in the image file. It uses my RawMetadataItem
class to receive the raw query string from each iteration along with its corresponding value and adds it to a generic List
of type, RawMetadataItem
, as follows:
class RawMetadataItem
{
public String location;
public Object value;
}
List<RawMetadataItem> RawMetadataItems { get; set; }
Here then is the short and sweet recursive CaptureMetadata()
function:
private void CaptureMetadata(ImageMetadata imageMetadata, string query)
{
BitmapMetadata bitmapMetadata = imageMetadata as BitmapMetadata;
if (bitmapMetadata != null)
{
foreach (string relativeQuery in bitmapMetadata)
{
string fullQuery = query + relativeQuery;
object metadataQueryReader = bitmapMetadata.GetQuery(relativeQuery);
RawMetadataItem metadataItem = new RawMetadataItem();
metadataItem.location = fullQuery;
metadataItem.value = metadataQueryReader;
RawMetadataItems.Add(metadataItem);
BitmapMetadata innerBitmapMetadata = metadataQueryReader as BitmapMetadata;
if (innerBitmapMetadata != null)
{
CaptureMetadata(innerBitmapMetadata, fullQuery);
}
}
}
}
In my program, I iterate through RawMetadataItems
to extract the location
query string and the value
to display in an Xceed DataGridControl
. But the displayed data is in a raw format that isn't very informative. Therefore, I have an option in the window that displays the DataGridControl
to translate the location
column from the raw format to a "cooked" format that is more informative to the photographer. I use a simple .NET generic Dictionary
to perform this translation.
The cooked format is more accurately described as "overcooked," because I've taken the liberty of going beyond what the WPF BitmapMetadata.GetQuery()
can understand, and translated the tag IDs to the symbolic field names as specified in the metadata specifications. I did this because my application is addressed to photographers, not to programmers, and photographers are far more interested in the semantics of specific metadata tags, not what you'd pass to the GetQuery()
method.
The Dictionary
was built up through a lot of research looking through metadata specifications, all of which are available through Internet searches. Here is an example of one of those specifications from the MSDN Windows Imaging Component documentation: Native Image Format Metadata Queries.
Here are excerpts from code that I used to build my dictionaries that translate between raw and (over)cooked query strings:
static Dictionary<string, string> TiffLocationDictionary { get; set; }
static Dictionary<string, string> JpegLocationDictionary { get; set; }
static string TiffRawXmpTag { get { return "/ifd/{ushort=700}"; } }
static string TiffCookedXmpTag { get { return "/ifd/xmp"; } }
static string TiffRawPhotoshopTag { get { return "/ifd/{ushort=34377}"; } }
static string TiffCookedPhotoshopTag { get { return "/ifd/Photoshop"; } }
static string JpegRawPhotoshopTag { get { return "/app13/{ushort=0}"; } }
static string JpegCookedPhotoshopTag { get { return "/Photoshop/irb"; } }
Then within my static
constructor, I initialized the dictionaries with code like this:
JpegLocationDictionary = new Dictionary<string, string>();
TiffLocationDictionary = new Dictionary<string, string>();
Dictionary<string, string> j = JpegLocationDictionary;
Dictionary<string, string> t = TiffLocationDictionary;
j.Add("/app0", "/JPEGFileInterchangeFormat");
j.Add("/app0/{ushort=0}", "/JPEGFileInterchangeFormat/Version");
j.Add("/app0/{ushort=1}", "/JPEGFileInterchangeFormat/Units");
j.Add("/app0/{ushort=2}", "/JPEGFileInterchangeFormat/XPixelDensity");
j.Add("/app0/{ushort=3}", "/JPEGFileInterchangeFormat/YPixelDensity");
j.Add("/app0/{ushort=4}", "/JPEGFileInterchangeFormat/Xthumbnail");
j.Add("/app0/{ushort=5}", "/JPEGFileInterchangeFormat/Ythumbnail");
j.Add("/app0/{ushort=6}", "/JPEGFileInterchangeFormat/ThumbnailData");
j.Add("/app1", "/app1");
j.Add("/app1/{ushort=0}", "/app1/ifd");
j.Add("/app1/{ushort=0}/{ushort=254}", "/app1/ifd/NewSubfileType");
j.Add("/app1/{ushort=0}/{ushort=255}", "/app1/ifd/SubfileType");
j.Add("/app1/{ushort=0}/{ushort=256}", "/app1/ifd/OriginalImageWidth");
Etc, etc. There are actually hundreds of entries in this dictionary, and as I continue my research with my own images and receive images from beta testers, I continue to enlarge the dictionary. Most photographers are amazed when they use my application to explore all the metadata tags their camera has placed into their images: there are just so many of them! Even so, the metadata contribution to image file size is usually just a small fraction of the total size of the image file. The metadata may amount to only 5K bytes, whereas the image data itself might be hundreds of KBytes, or even many MBytes.
Points of Interest
The central point to the understanding of how to iterate through all of the metadata in a bitmap image is to realize that the BitmapMetadata
class implements the IEnumerable<String>
interface, as explained above.
History
- 17th March, 2010: Initial post