# Monday, August 16, 2010

How to Edit Meta Data Inside JPG Files with C#

I do (change your EXIF data)We just got back from a great weekend at the beach; my sister-in-law got married on the sands near Haystack Rock in Cannon Beach, OR. Really a great trip from start to finish.

My wife accepted the photographer duties and snapped nearly 2K photos with our Nikon D40 and a Nikon D60 that we borrowed from a friend. She had a zoom lens on one and a short lens on the other to avoid the need to change lenses in mid-moment. That part worked great.

The part that we overlooked was the current date/time on each camera; they were not synchronized. This will make sorting photos by time more difficult. No problem, I say, on the drive back home. I’m a developer and JPG files have meta info that I can manipulate, right?

It turns out that its not all that easy. Its actually a remarkable pain in the ass, hence this blog post (that’s the remark-able part).

I started with an online search and found Hanselman’s post about combining PowerShell with Omar Shahine’s library, ImageLibrary.dll that is evidently no longer accessible. Big phat 404 while trying to get that assembly.

Next, I did some searches specific to StackOverflow.com and saw some posts about how difficult reading/writing this meta data can be. At this point, I was beginning to wonder if my wife was going to hear a broken promise or not. It wasn’t looking good.

I found out that JPG files store meta data using EXIF, or Exchangeable Image File Format, a standard used by the digital still camera industry. It looked like C# 4.0 could crack this open, but I might have to do get my hands dirty with some bit shuffling. No worries, I was up for it. It didn’t look like there was a library out there that was going to fall into my lap, so I cracked open Visual Studio 2010 and saw what kind of trouble I could get into with Intellisense and a gin and tonic.

Let’s Take It Out for a Loop

I created a new project, loaded a Bitmap class with a sample JPG and iterated over the properties:

   1: using System;
   2: using System.Drawing;
   3:  
   4: namespace ReadExifInJPG
   5: {
   6:     class Program
   7:     {
   8:         static void Main()
   9:         {
  10:             var bitmap = new Bitmap("c:\\temp\\somephoto.jpg");
  11:  
  12:             foreach (var item in bitmap.PropertyItems)
  13:             {
  14:                 Console.WriteLine("Id: {0}, Type: {1}, Value: {2}", 
  15:                     item.Id, item.Type, item.Value);
  16:             }
  17:         }
  18:     }
  19: }

I had lots of properties in my sample photo:

output of simple iteration

I was thinking that this was going to be smooth sailing until I saw the values listed as byte arrays. Ugh. Then I read a EXIF specification that said the values can be stored in multiple formats and there are a ton of properties. I had over fifty on a single JPG image. I needed to find the property that told me if the given photo was taken by a Nikon D60 or a D40, and then change the date on the D60 images. Here’s the EXIF specifications: http://www.exif.org/specifications.html, and what a great document to read! Ugh.

I Just Want My Property

I found a list of property tags in numerical order on MSDN, note the id values are listed in hex, so 0x0132 (hex) equals 306 (decimal): http://msdn.microsoft.com/en-us/library/ms534418(VS.85).aspx

When you go about reading or changing a given property, there are three parts to consider:

  1. the id number that identifies the given property
  2. the type number which describes the data format
  3. the actual data, encoded in the least accessible format you could want

Here’s a list of numeric types that identify the corresponding data format:

  1. A Byte
  2. An array of Byte objects encoded as ASCII
  3. A 16-bit integer
  4. A 32-bit integer
  5. An array of two Byte objects that represent a rational number
  6. Not used
  7. Undefined
  8. Not used
  9. SLong
  10. SRational

Fortunately, everything I was interested in was of type 2, a byte array. This is a great MSDN article that gave me a bunch of tips on how to manipulate the EXIF data: http://msdn.microsoft.com/en-us/library/xddt0dz7.aspx

The property id of 306 holds the date of the photo, its a type 2 property, so the value is stored in a byte array. Here’s how to read the string value:

   1: var property = bitmap.GetPropertyItem(306);
   2: System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
   3: string value = encoding.GetString(property.Value);


Caution, Byte Counting Ahead

Note, the date properties that I’m interested in updating have values stored as byte arrays. More specifically, they’re exactly 20 bytes long. At first I wasn’t respecting that boundary and I just let .Net do it’s thing to convert my formatted date string into a byte array. When I tried to read the value back, it was empty because I overflowed the value.

After some noodling, I changed my code to write out exactly 20 bytes of data, starting at precisely 23 bytes into my new byte array, as follows.

   1: private void ChangeTimeStamp(Bitmap bitmap, int minutesToAdd)
   2: {
   3:     DateTime originalDateTime = GetOriginalDateTime(bitmap);
   4:  
   5:     var newDateTime = originalDateTime.AddMinutes(minutesToAdd);
   6:  
   7:     BinaryFormatter bf = new BinaryFormatter();
   8:     MemoryStream ms = new MemoryStream();
   9:     string formattedNewDateTime = newDateTime.ToString("yyyy:MM:dd HH:mm:ss");
  10:     bf.Serialize(ms, formattedNewDateTime);
  11:     ms.Seek(0, 0);
  12:  
  13:     var tempArray = ms.ToArray();
  14:  
  15:     byte[] byteArray = new byte[20];
  16:  
  17:     var x = 0;
  18:     for (int i = 23; i < (23 + 19); i++)
  19:     {
  20:         byteArray[x] = tempArray[i];
  21:         x++;
  22:     }
  23:  
  24:     SetNewDateTime(bitmap, byteArray, 306);
  25:     SetNewDateTime(bitmap, byteArray, 36867);
  26:     SetNewDateTime(bitmap, byteArray, 36868);
  27: }
 

Note, the previous code shows I’m writing the same date to three different property settings (306, 36867, and 36868). They all had the same date value, so I figured the best thing to do was keep them all in sync.

It Works on My Machine

So, I was finally done with the code and I tested it with a bunch of files on my machine. Worked great. I installed VS 2010 C# Express on my netbook so I could create a console application and run it on my wife’s laptop. I had been abusing a ASP.Net Web Forms application with Visual Web Developer Express on my netbook. I made a quick console app, copied it to my wife’s laptop and ka-pow! It no worky. Puzzled, I tried to think what was wrong for about 30 minutes. After a couple of assertions inserted into my code, I realized she had some non-JPG files in the folder and my program wasn’t filtering for only JPG files. Doh! I did manage to get in a quick “hey, it works on my machine” comment to my wife, but she didn’t think it was as funny as I did.

Nothing But The Code, The Whole Code, So Help Me .Net Runtime?

Here’s the final class I built, in “good enough” format. The entire coding time, not including the installation of VS 2010 Express on my netbook, or .Net 4 on my wife’s laptop was about 90 minutes. The console program just takes this class, passes in the source and destination directories, along with the number of minutes to adjust the timestamp on the photo.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.IO;
   6: using System.Drawing;
   7: using System.Runtime.Serialization.Formatters.Binary;
   8:  
   9: namespace ImageMetaTool
  10: {
  11:     public class TimeEditorService
  12:     {
  13:         public void AdjustDateTime(string soureDir, string destDir, int minutesToAdd)
  14:         {
  15:             if (!Directory.Exists(soureDir))
  16:             {
  17:                 Console.WriteLine("Source directory does not exist.");
  18:                 return;
  19:             }
  20:  
  21:             if (!Directory.Exists(destDir))
  22:             {
  23:                 Console.WriteLine("Destination directory does not exist.");
  24:                 return;
  25:             }
  26:  
  27:             int x = 0;
  28:             var list = Directory.GetFiles(soureDir, "*.jpg");
  29:             foreach (var filename in list)
  30:             {
  31:                 Console.WriteLine("Processing image {0} of {1}; {2}", x, list.Length, filename);
  32:  
  33:                 using (var fs = File.OpenRead(filename))
  34:                 {
  35:                     var bitmap = new Bitmap(fs);
  36:                     ProcessFile(bitmap, minutesToAdd);
  37:                     bitmap.Save(Path.Combine(destDir, Path.GetFileName(filename)));
  38:                     x++;
  39:                 }
  40:             }
  41:  
  42:             Console.WriteLine("Processing completed.");
  43:         }
  44:  
  45:         private void ProcessFile(Bitmap bitmap, int minutesToAdd)
  46:         {
  47:             if (!IsD60(bitmap))
  48:                 return;
  49:  
  50:             ChangeTimeStamp(bitmap, minutesToAdd);
  51:         }
  52:  
  53:         private void ChangeTimeStamp(Bitmap bitmap, int minutesToAdd)
  54:         {
  55:             DateTime originalDateTime = GetOriginalDateTime(bitmap);
  56:  
  57:             var newDateTime = originalDateTime.AddMinutes(minutesToAdd);
  58:  
  59:             BinaryFormatter bf = new BinaryFormatter();
  60:             MemoryStream ms = new MemoryStream();
  61:             string formattedNewDateTime = newDateTime.ToString("yyyy:MM:dd HH:mm:ss");
  62:             bf.Serialize(ms, formattedNewDateTime);
  63:             ms.Seek(0, 0);
  64:  
  65:             var tempArray = ms.ToArray();
  66:  
  67:             byte[] byteArray = new byte[20];
  68:  
  69:             var x = 0;
  70:             for (int i = 23; i < (23 + 19); i++)
  71:             {
  72:                 byteArray[x] = tempArray[i];
  73:                 x++;
  74:             }
  75:  
  76:             SetNewDateTime(bitmap, byteArray, 306);
  77:             SetNewDateTime(bitmap, byteArray, 36867);
  78:             SetNewDateTime(bitmap, byteArray, 36868);
  79:         }
  80:  
  81:         private void SetNewDateTime(Bitmap bitmap, byte[] newDateTime, int propertyNumber)
  82:         {
  83:             var property = bitmap.GetPropertyItem(propertyNumber);
  84:             property.Value = newDateTime;
  85:  
  86:             bitmap.SetPropertyItem(property);
  87:         }
  88:  
  89:         private DateTime GetOriginalDateTime(Bitmap bitmap)
  90:         {
  91:             var property = bitmap.GetPropertyItem(306);
  92:  
  93:             System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
  94:             string value = encoding.GetString(property.Value);
  95:  
  96:             string value2 = value.Split(' ')[0].Replace(":", "/") + " " + value.Split(' ')[1];
  97:  
  98:             return DateTime.Parse(value2); //2010:08:14 14:23:14
  99:         }
 100:  
 101:         private bool IsD60(Bitmap bitmap)
 102:         {
 103:             var modelProperty = bitmap.GetPropertyItem(272);
 104:  
 105:             System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
 106:             string modelName = encoding.GetString(modelProperty.Value);
 107:  
 108:             if (modelName.Contains("D60")) // NIKON D60
 109:                 return true;
 110:  
 111:             return false;
 112:         }
 113:     }
 114: }

 

I hope you have a good time futzing with your EXIF data!

#    Comments [0] |
# Thursday, August 05, 2010

Creating Large Files with fsutil.exe

I had to search around again for how to create a large file for testing; time for a blog post.

When you need to work with a large file for testing, fsutil.exe (baked into Windows) has a cool feature for making a large file in a snap. This is useful for when you test your app’s ability to move and edit local files or simply upload a big document.

Here’s the command for creating a 20MB file:

fsutil file createnew c:\temp\largefile.txt 20000000
#    Comments [0] |