Friday, December 24, 2010

METADATA using GDI+

1.0 - Introduction

Image files can contain extra information about the file, settings from a camera, geodata, and other info. This metadata is organized as a specific set of items, each with a name, an ID and a value. With the ID you can find the item and read the information or change it. There are more than 200 different items defined, but in practice only a limited number is used. Items can be added or removed.

From the .NET point of view there are two ways of addressing metadata in imges-files, with GDI+ and with the WIC (Windows Imaging Component), installed with Vista, XP SP3 or .NET Framework 3.0.
In this article we look at GDI+ and how metadata can be addressed from code, using Visual Studio 2008 and Framework 2.0 or higher.

2.0 - The metadata environment in Visual Studio

The prerequisites are Visual Studio 2008 and the .NET Framework 2.0 or higer.

There are only a few classes for getting access to metadata in images: the Image and Bitmap classes in System.Drawing and the PropertyItem Class in System.Drawing.Imaging.

Metadata in an image has a straightforward structure, it is a simple list with items. The Image (System.Drawing) has a PropertyItems Property, this is an array with all the pieces of metadata; the PropertyItem objects. Each PropertyItem has four properties, an ID (.Id), a value (.Value), the data-type of the Value (.Type) and the length of the data in Value (.Len).

The strategy for retrieving the metadata is to open the file, get the collection of PropertyItem objects with the PropertyItems property of the image and then search for a specific item with the Id. When the PropertyItem is found you can set or get the properties Id, Value, Type and Len.

The supported file formats are .jpg, .tif, .png and .exif.

These are the classes for working with metadata:


     Table 1 - Image metadata support (GDI+).
Class Description
Image Class
PropertyItems PropertyGets all the property-items stored in the Image
GetPropertyItem MethodGets the specified property-item from the Image
SetPropertyItem MethodStores a property-item in the Image
Bitmap Class
PropertyItems PropertyGets all the property-items stored in the Image
GetPropertyItem MethodGets the specified property-item from the Image
SetPropertyItem MethodStores a property-item in the Image
PropertyItem Class
Id Property Gets or sets the ID of the property
Len Property Gets or sets the length (in bytes) of the Value property
Type Property Defines the type of data contained in the Value property
Value Property Gets or sets the value of the property-item


The Bitmap PropertyItems Property, GetPropertyItem Method and SetPropertyItem Method are inherited from Image. In practice you can work with Image or Bitmap as desired.
The PropertyItems Property returns an array with PropertyItem objects.
There are more than 200 different property-items. Each item has a specific name, and the properties id, Type (the data-type in Value), Value and Len (the length of the data in Value).

Some examples of property-items:


     Table 2 - Image metadata property-items (GDI+).
PropertyType name
[Constant]
Id
[hex]
Type name
[Constant]
Description
PropertyTagImageWidth 0x0100 PropertyTagTypeShort
or PropertyTagTypeLong
Number of pixels per row
PropertyTagImageHeight 0x0101 PropertyTagTypeShort
or PropertyTagTypeLong
Number of pixel rows
PropertyTagDocumentName 0x010D PropertyTagTypeASCII Name of document from which image was scanned
PropertyTagImageDescription 0x010E PropertyTagTypeASCII The title of the image
PropertyTagDateTime 0x0132 PropertyTagTypeASCII Date and time the image was created
PropertyTagArtist 0x013B PropertyTagTypeASCII Name of the person who created the image
PropertyTagImageTitle 0x0320 PropertyTagTypeASCII The title of the image
PropertyTagIndexTransparent 0x5104 PropertyTagTypeByte Index of transparent color in GIF
PropertyTagCopyright 0x8298 PropertyTagTypeASCII Copyright information


Other property-items are for image properties, Exif, geodata (GPS), camera settings, thumbnails, printing, colors, and more. For a complete overview see [1].

For each property-item a data-type is specified (property PropertyItem.Type). These are all the possible types:


     Table 3 - Image Property Tag Type Constants (GDI+).
Type [Constant] Value
[dec]
Description
PropertyTagTypeByte 1An array of bytes
PropertyTagTypeASCII 2A null-terminated ASCII string
PropertyTagTypeShort 3An array of unsigned short (16-bit) integers
PropertyTagTypeLong 4An array of unsigned long (32-bit) integers
PropertyTagTypeRational 5An array of pairs of unsigned long integers
PropertyTagTypeUndefined6An array of bytes of any data type
PropertyTagTypeSLONG 7An array of signed long (32-bit) integers
PropertyTagTypeSRational10An array of pairs of signed long integers

See also [2] for a full description.

With these properties, methods and constants you can make code for reading and writing metadata in images.

3.0 - Things to Do

These are the functions we want to implement:

Open an image file and get access to the metadata.
Read all the metadata from the file to get an overview.
Get a specific piece of metadata (a property-item) and read or change its value.
Add property-items if they do not exist.
Remove property-items from the metadata.
Save an image file after changing the metadata.

We will make a class with some universal functions. The functions will have basic functionality for showing the priciples for working with metadata. You can change the functions or extend the class yourself.

Do not copy code from these pages for use in Visual Studio. There might be some small differences compared with the code in the download and spaces and returns are added. Please use the code from the download.

4.0 - Open an image file and get access to the metadata

Opening a file and retrieving the contents as Bitmap is as simple as this:


  C# - Open an image from file.
// Open bitmap from file.
 public Bitmap File_OpenImage(string sFileName)
 {
     Bitmap oBitmap = new Bitmap(sFileName);
     return oBitmap;
 }


However, the file is kept open until the bitmap is disposed. This prevents us from saving the bitmap again when the meta-data is modified.
This is a tricky situation and it can lead to a lot of gesswork when files refuse to save.
A standard workaround is to make a copy of the original Bitmap and dispose the original bitmap:

Bitmap oBitmapNew = new Bitmap(oBitmapOld);
oBitmapOld.Dispose();

However, in our case, the metadata is lost in this process. So, this is our new workaround when metadata is involved:

1) Open the imagefile as Bitmap.
2) Store all the metadata from the bitmap in a PropertyItems object.
3) Make a new Bitmap with the old bitmap as template. This new Bitmap has no metadata.
4) Copy the stored metadata from the PropertyItems object to the new Bitmap.
5) Dispose the original Bitmap, the file is closed and unlocked.


  C# - Open an image from file, and unlock.
// ---------------------------------------------------------------
 // Date      110708
 // Purpose   Open a bitmap from file.
 // Entry     sFileName - Filename of the image (+ path).
 // Return    The Bitmap from the file.
 // Comments  The file is unlocked.
 // ---------------------------------------------------------------
 public Bitmap File_OpenImage(string sFileName)
 {
     // Open bitmap from file.
     Bitmap oBitmap = new Bitmap(sFileName);
     // Get all metadata.
     PropertyItem[] propItems = oBitmap.PropertyItems;
     // Unlock by making a copy (metadata is lost here).
     Bitmap oBitmap2 = new Bitmap(oBitmap);
     // Copy original metadate to new bitmap.
     for (int i = 0; i < propItems.Length; i++)
     {
         oBitmap2.SetPropertyItem(propItems[i]);
     }
     // Dispose original bitmap.
     oBitmap.Dispose();
     // Return copy.
     return oBitmap2;
 }


This already shows how to get all the metadata from the file; with the Bitmap.PropertyItems property. This is an array with all the pieces of metadata (PropertyItem objects). We will use this in the next section to get an overview of all the items in the metadata.

5.0 - Read all the metadata from the file to get an overview

It is sometimes useful for having an overview of the metadata in an image-file, especially when you are developing a metadata application. For this purpose a function is made for showing the metadata as a single text.


  C# - Get a list of property-items from the metadata
// ---------------------------------------------------------------
 // Date      110708
 // Purpose   Get info from all the property-items in an image.
 // Entry     oBitmap - The bitmap from the image.
 // Return    A single string with the property-items info.
 // Comments  The PropertyItem Value bytes are converted to string. 
 // ---------------------------------------------------------------
 public string Image_GetPropertyItemsInfo(Bitmap oBitmap)
 {
     string s = "";
     string r = "\r\n";
     string sID;

     PropertyItem[] propItems = oBitmap.PropertyItems;

     s += "Number of items: " + propItems.Length.ToString() + r;
     s += r;
     for (int i = 0; i < propItems.Length; i++)
     {
         s += "Item: " + i.ToString() + r;        // Item number in PropertyItem[].
         sID = propItems[i].Id.ToString("x2");                // Hex.
         if (sID.Length == 1) sID = "0x000" + sID;
         if (sID.Length == 2) sID = "0x00" + sID;
         if (sID.Length == 3) sID = "0x0" + sID;
         if (sID.Length == 4) sID = "0x" + sID;
         s += "ID: " + sID + r;                               // ID, format 0xHHHH.
         s += "Type: " + propItems[i].Type.ToString() + r;    // Type (1..10).
         s += "Length: " + propItems[i].Len.ToString() + r;   // Length (bytes).
         s += r;
     }
     return s;
 }


The File_OpenImage() function returns a Bitmap from the file and the Bitmap.PropertyItems property is used to get all the metadata as an array of PropertyItem objects.
In a for-loop the properties Id, Type and Length of all the PropertyItem objects are retrieved and converted to string.

This is the result:

  Text - Property-items in the metadata.
Item: 0
 ID: 0x0301
 Type: 5
 Length: 8

 Item: 1
 ID: 0x010e
 Type: 2
 Length: 4

 Item: 2
 ID: 0x0131
 Type: 2
 Length: 16


The ID of a property-item identifies it (hence the name); there are only pre-defined items in the metadata, each with specific properties (see Table 2 for some examples).
The item with ID=0x0301 is PropertyTagGamma, the gamma value attached to the image.
The item with ID=0x010e is PropertyTagImageDescription, specifies the title of the image.
The item with ID=0x0131 is PropertyTagSoftwareUsed, specifies the name and version of the software or firmware of the device used to generate the image.

Item values are not shown with this function, this needs a specific action depending on the data-type (see Table 3).

If the image has no property-items or if the image format does not support property-items, PropertyItems returns an empty array (that is, an array of length zero).
Property-item ID's are usually expressed in hexadecimal form, e.g. "0x010E", with a prefix "0x" indicating that this number is hex and "010E" the actual value.
Hexadecimal numbers are 16-based, so "010E" converted to decimal is
0*(16^3) + 1*(16^2) + 0*(16^1) + 14*(16^0) = 270.
The meta-data (in PropertyItems) has no reference to a specific file or bitmap, it can be copied from one bitmap to another and it can exist as stand-alone object.

6.0 - Get a specific piece of metadata

When a specific property-item must be changed, or you want to know if a property-item exists in the metadata, you can search it with the ID (PropertyItem.Id).
This can be done in two ways, iterate the array with PropertyItem objects from the Bitmap.PropertyItems property or use the Bitmap.GetPropertyItem Method. In both cases you need the ID (PropertyItem.Id) of the item to be searched.

As an example here the Image_GetImageDescription() function from the download. This function searches for property-item PropertyTagImageDescription with ID=0x010E, the title of the image. In this function the Bitmap.PropertyItems array is iterated until the item with ID=0x010E is found.


  C# - Get the Value of the PropertyItem with ID=0x010E.
// ---------------------------------------------------------------
 // Date      110708
 // Purpose   Get value of the PropertyItem with 
 //           ID = PropertyTagImageDescription (0x010E) of an image.
 // Entry     oBitmap - The bitmap from the image.
 // Return    The vallue from the PropertyItem (as string).
 // Comments  PropertyTagImageDescription. 0x010E = 270.
 // ---------------------------------------------------------------
 public string Image_GetImageDescription(Bitmap oBitmap)
 {
     // int iH = 270;
     // MessageBox.Show(iH.ToString("x2"));

     int iID = 270;          // 0x010E = PropertyTagImageDescription.
     ASCIIEncoding textConverter = new ASCIIEncoding();

     string sValue = "";
     PropertyItem[] propItems = oBitmap.PropertyItems;
     for (int i = 0; i < propItems.Length; i++)
     {
         if (propItems[i].Id == iID)
         {
             sValue = textConverter.GetString(propItems[i].Value);
         }
     }
     return sValue;
 }


In the download is also function Image_SetImageDescription() which sets the value of the PropertyItem. This function uses the same method.


  C# - Set the Value of the PropertyItem with ID=0x010E.
// ---------------------------------------------------------------
 // Date      110708
 // Purpose   Set the value of the PropertyItem with 
 //           ID = PropertyTagImageDescription for an image.
 // Entry     oBitmap - The bitmap from the image.
 //           sImageDescription - The value for the PropertyItem.
 // Return    None (Bitmap is returned by ref)
 // Comments  When the PropertyItem with ID = PropertyTagImageDescription
 //           exists, the value of the item is replaced.
 //           When the PropertyItem does not exist, it is created and
 //           added to the image.
 // ---------------------------------------------------------------
 public void Image_SetImageDescription(ref Bitmap oBitmap, string sImageDescription)
 {

     int iID = 270;          // 0x010E = PropertyTagImageDescription.
     bool bItemExists = false;

     PropertyItem[] propItems = oBitmap.PropertyItems;

     // Set the value for an existing PropertyItem with 
     // ID = PropertyTagImageDescription.
     for (int i = 0; i < propItems.Length; i++)
     {
         if (propItems[i].Id == iID)
         {
             byte[] bytValue;
             ASCIIEncoding textConverter = new ASCIIEncoding();
             bytValue = textConverter.GetBytes(sImageDescription);
             propItems[i].Value = bytValue;
             oBitmap.SetPropertyItem(propItems[i]);
             bItemExists = true;
             break;
         }
     }

     // Create a new PropertyItem with ID = PropertyTagImageDescription (0x010E)
     // when such an item does not exist.
     if (bItemExists == false)
     {
         int iType = 2;              // = ASCII string.
         string sValue = sImageDescription;
         PropertyItem oItem = Image_CreateNewPropertyItem(iID, iType, sValue);
         // Add this new item to the image.
         oBitmap.SetPropertyItem(oItem);
     }
 }


When the item is found, the .Value property is set with the desired text (one of the parameters of the function). This value must be a byte-array so, the text is converted to byte-array first with the textConverter.

Setting the value of a property-item is not sufficient, you must use the Bitmap.SetPropertyItem() method for adding the changes to the metadata in the bitmap.

This function also creates the (searched) item if it does not exist in the meta-data. This is explained below.

7.0 - Add property-items if they do not exist

When new property-items must be added to the metadata, the first thing you want to do is to make a new PropertyItem object. This is, however, not possible directly because the PropertyItem class is not inheritable. See also [3]:

A PropertyItem object encapsulates a metadata property to be included in an image file. A PropertyItem object is not intended to be used (as) a stand-alone object. A PropertyItem object is intended to be used by classes that are derived from System.Drawing.Image. A PropertyItem object is used to retrieve and change the metadata of existing image files, not to create the metadata. Therefore, the PropertyItem class does not have a defined Public constructor, and you cannot create an instance of a PropertyItem object.

Microsoft suggests the following (amazing) workaround:

1) Make a dummy-image with metadata and put it somewhere on disk (almost any image created in a paint program has metadata).
2) Open (from code) the file, get the metadata, and retrieve the first PropertyItem.
3) Set the Id, Len, Type and Value properties.
4) Add this new PropertyItem to the metadata of the desired image.

Instead of getting an image from file I made a bitmap in memory and tried to add meta-data by setting different properties, but this did not succeed.

As a compromise I converted an existing image (with metadata) to Base64 code and assigned it to a constant (in code); this is a canned image.

1) Make a small image with metadata in a paint-program and save it.
2) Open this image from code and convert the bytes to Base64 code.
3) Assign the Base64 code to a constant.
4) When you need a PropertyItem object, convert the Base64 code to Bitmap again.

The advantage of this workaround is, that there are no external files. In the download you can find the functions Bitmap_To_Base64() and Base64_To_Bitmap() for doing this. The dummy-image Base_8_8.png for creating the Base64 code is also included.

The image (as Base64 code) looks like this:


  C# - Convert an image, as Base64 code, to Bitmap.
// ---------------------------------------------------------------
 // Date      140607
 // Purpose   Convert Base64 code to Bitmap.
 // Entry     sBase64 - The Base64 code (as single string).
 // Return    The bitmap.
 // Comments  
 // ---------------------------------------------------------------
 private Bitmap Base64_To_Bitmap(string sBase64)
 {
     Byte[] bytData = Convert.FromBase64String(sBase64);
     MemoryStream ms = new MemoryStream(bytData);
     Bitmap oB = new Bitmap(ms);
     ms.Close();
     ms.Dispose();
     return oB;
 }

 // Icon Base_8_8.png (8x8).
 private string Icon_Base_8_8 =
 "iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAAXNSR0IArs4c6QAAAARnQU1BAACx" +
 "jwv8YQUAAAAgY0hSTQAAeiYAAICEAAD6AAAAgOgAAHUwAADqYAAAOpgAABdwnLpRPAAAABZJREFU" +
 "KFNj/A8EDPgASAE+wDAsFAAAxQLjHkhOEUMAAAAASUVORK5CYIIAAAAAAAAAAAAAAAAAAAAAAAAA" +
 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +
 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==";


The image (as Base64 code) and the conversion to Bitmap with Base64_To_Bitmap() are encapsulated in the Image_CreateNewPropertyItem() function which creates a new PropertyItem object from scratch:


  C# - Create a new PropertyItem from scratch.
// ---------------------------------------------------------------
 // Date      110708
 // Purpose   Create a new PropertyItem from scratch.
 // Entry     iId - The PropertyItem ID.
 //           iType - The PropertyItem type.
 //           sValue - The PropertyItem value (as string).
 // Return    A new PropertyItem object.
 // Comments  The first PropertyItem from a dummy-image is used
 //           for making a new item.
 // ---------------------------------------------------------------
 public PropertyItem Image_CreateNewPropertyItem(int iId, int iType, string sValue)
 {
     ASCIIEncoding textConverter = new ASCIIEncoding();
     Bitmap oBitmap = Base64_To_Bitmap(Icon_Base_8_8);
     PropertyItem[] propItems = oBitmap.PropertyItems;
     if (propItems.Length != 0)
     {
         propItems[0].Id = iId;                  // 0x010E = PropertyTagImageDescription.
         propItems[0].Type = (short)iType;       // Null-terminated ASCII string.
         propItems[0].Value = textConverter.GetBytes(sValue);
         propItems[0].Len = sValue.Length;       // Length (+1 ?).
         return propItems[0];
     }
     else
     {
         return null;
     }
 }


Notice that this function is not quite universal: there is always a specific relationship between the properties of the PropertyItem depending on the item ID. In this case a PropertyTagImageDescription is assumed.

In the prevous section you have seen how this function is used.

8.0 - Remove property-items from the metadata

This is not tested.
There are no methods for removing property-items from metadata directly, you can only retrieve and change them.

A possible solution could be:

1) Open the image file and get the image as Bitmap.
2) Get the metadata as Bitmap.PropertyItems (an array with PropertyItem objects).
3) Remove all the metadata from the Bitmap with
Bitmap oBitmapNew = new Bitmap(oBitmapOld).
4) Copy only the desired items from the Bitmap.PropertyItems array to the new bitmap using the Bitmap.SetPropertyItem method.
5) Save the new Bitmap to the original file.

See also function File_OpenImage() described earlier, most of the code is already present.

9.0 - Save an image file after changing the metadata

Saving the Bitmap when the metadata is changed seems to be the easy part, but when the file from which the metadata is retrieved, is not closed you get an error and the file will not save.
See the discussion in H 4.0.


  C# - Save a Bitmap to file.
// ---------------------------------------------------------------
 // Date      110708
 // Purpose   Save a bitmap to file.
 // Entry     oBitmap - The Bitmap to save.
 //           sFileName - Filename of the image (+ path).
 // Return    true if successful.
 // Comments  
 // ---------------------------------------------------------------
 public bool File_SaveImage(Bitmap oBitmap, string sFileName)
 {
     try
     {
         if (File.Exists(sFileName))
         {
             File.SetAttributes(sFileName, FileAttributes.Normal);
         }
         oBitmap.Save(sFileName);
         return true;
     }
     catch
     {
         return false;
     }
 }


10.0 - Hacking metadata

As has been said, this system of metadata is based on (a large number of) specific items, each with a predefined purpose. In the documentation [1] is a description for each item, what the purpose is, how large it is and the type of data which is allowed. As an example, the PropertyTagDateTime item is defined as 'Date and time the image was created', the type (property PropertyItem.Type) is PropertyTagTypeASCII (text), and the size (property PropertyItem.Len) is 20 bytes.
There are, however, less specific items e.g. PropertyTagImageTitle and PropertyTagImageDescription. These items also contain text, but the length of the text is not fixed.

When you look at the specification, you can handle the data in a strict way, but nothing holds you back when 'incorrect' data is stored in one or more items. Most items are for text-data so you can put any text in it. You could, more specifically, decide to use Base64 code (also text), but with Base64 you can store any data which can be expressed as byte-arry: XML, HTML, binary data, scripts, styleheets, other images, encrypted data, just what you like.
Before you add the metadata you can also compress it, so the impact on the size of the image can be reduced.
An alternative is storing data directly as byte-array in items which have Type=1 for the data (property PropertyItem.Type). Item PropertyTagThumbnailData is a suitable candidate for this.

When you look at metadata in this way, an image can be considered as a universal resourcefile or as a container for data-transport.

11.0 - Summary

Metadata in images is organized as a simple list of items, each item with an ID and a value.
There are more than 200 different items possible, for defining the properties of the image, camera settings, geo-information (GPS), Exif, thumbnails, printing, colors, and more. You can read all the information, set the information for each item and add- or remove items.
Approaching and changing metadata in images with GDI+ is straightforward and easy to implement with Visual Studio as is shown in this article.

No comments:

Post a Comment