Index: trunk/src/com/drew/imaging/ImageProcessingException.java
===================================================================
--- trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 6127)
+++ trunk/src/com/drew/imaging/ImageProcessingException.java	(revision 6127)
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+package com.drew.imaging;
+
+import com.drew.lang.CompoundException;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * An exception class thrown upon an unexpected condition that was fatal for the processing of an image.
+ * 
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ImageProcessingException extends CompoundException
+{
+    private static final long serialVersionUID = -9115669182209912676L;
+
+    public ImageProcessingException(@Nullable String message)
+    {
+        super(message);
+    }
+
+    public ImageProcessingException(@Nullable String message, @Nullable Throwable cause)
+    {
+        super(message, cause);
+    }
+
+    public ImageProcessingException(@Nullable Throwable cause)
+    {
+        super(cause);
+    }
+}
Index: trunk/src/com/drew/imaging/PhotographicConversions.java
===================================================================
--- trunk/src/com/drew/imaging/PhotographicConversions.java	(revision 6002)
+++ trunk/src/com/drew/imaging/PhotographicConversions.java	(revision 6127)
@@ -1,15 +1,21 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.imaging;
@@ -17,14 +23,19 @@
 /**
  * Contains helper methods that perform photographic conversions.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class PhotographicConversions
+public final class PhotographicConversions
 {
     public final static double ROOT_TWO = Math.sqrt(2);
 
-    private PhotographicConversions()
-    {}
+    private PhotographicConversions() throws Exception
+    {
+        throw new Exception("Not intended for instantiation.");
+    }
 
     /**
      * Converts an aperture value to its corresponding F-stop number.
+     *
      * @param aperture the aperture value to convert
      * @return the F-stop number of the specified aperture
@@ -32,9 +43,7 @@
     public static double apertureToFStop(double aperture)
     {
-        double fStop = Math.pow(ROOT_TWO, aperture);
-        return fStop;
+        return Math.pow(ROOT_TWO, aperture);
 
-        // Puzzle?!
-        // jhead uses a different calculation as far as i can tell...  this confuses me...
+        // NOTE jhead uses a different calculation as far as i can tell...  this confuses me...
         // fStop = (float)Math.exp(aperture * Math.log(2) * 0.5));
     }
@@ -42,4 +51,5 @@
     /**
      * Converts a shutter speed to an exposure time.
+     *
      * @param shutterSpeed the shutter speed to convert
      * @return the exposure time of the specified shutter speed
@@ -47,5 +57,5 @@
     public static double shutterSpeedToExposureTime(double shutterSpeed)
     {
-        return (float)(1 / Math.exp(shutterSpeed * Math.log(2)));
+        return (float) (1 / Math.exp(shutterSpeed * Math.log(2)));
     }
 }
Index: trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 6002)
+++ trunk/src/com/drew/imaging/jpeg/JpegMetadataReader.java	(revision 6127)
@@ -1,28 +1,31 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 12-Nov-2002 18:51:36 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.imaging.jpeg;
 
-import com.drew.metadata.Directory;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataException;
-import com.drew.metadata.Tag;
-import com.drew.metadata.exif.ExifDirectory;
 import com.drew.metadata.exif.ExifReader;
 import com.drew.metadata.iptc.IptcReader;
 import com.drew.metadata.jpeg.JpegCommentReader;
+import com.drew.metadata.jpeg.JpegDirectory;
 import com.drew.metadata.jpeg.JpegReader;
 
@@ -30,11 +33,13 @@
 import java.io.IOException;
 import java.io.InputStream;
-import java.util.Iterator;
 
 /**
+ * Obtains all available metadata from Jpeg formatted files.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class JpegMetadataReader
 {
+    // TODO investigate supporting javax.imageio
 //    public static Metadata readMetadata(IIOMetadata metadata) throws JpegProcessingException {}
 //    public static Metadata readMetadata(ImageInputStream in) throws JpegProcessingException{}
@@ -42,100 +47,76 @@
 //    public static Metadata readMetadata(ImageReader reader) throws JpegProcessingException{}
 
-    public static Metadata readMetadata(InputStream in) throws JpegProcessingException
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream) throws JpegProcessingException
     {
-        JpegSegmentReader segmentReader = new JpegSegmentReader(in);
-        return extractMetadataFromJpegSegmentReader(segmentReader);
+        return readMetadata(inputStream, true);
     }
 
-    public static Metadata readMetadata(File file) throws JpegProcessingException
+    @NotNull
+    public static Metadata readMetadata(@NotNull InputStream inputStream, final boolean waitForBytes) throws JpegProcessingException
+    {
+        JpegSegmentReader segmentReader = new JpegSegmentReader(inputStream, waitForBytes);
+        return extractMetadataFromJpegSegmentReader(segmentReader.getSegmentData());
+    }
+
+    @NotNull
+    public static Metadata readMetadata(@NotNull File file) throws JpegProcessingException, IOException
     {
         JpegSegmentReader segmentReader = new JpegSegmentReader(file);
-        return extractMetadataFromJpegSegmentReader(segmentReader);
+        return extractMetadataFromJpegSegmentReader(segmentReader.getSegmentData());
     }
 
-    public static Metadata extractMetadataFromJpegSegmentReader(JpegSegmentReader segmentReader)
+    @NotNull
+    public static Metadata extractMetadataFromJpegSegmentReader(@NotNull JpegSegmentData segmentReader)
     {
         final Metadata metadata = new Metadata();
-        try {
-            byte[] exifSegment = segmentReader.readSegment(JpegSegmentReader.SEGMENT_APP1);
-            new ExifReader(exifSegment).extract(metadata);
-        } catch (JpegProcessingException e) {
-            // in the interests of catching as much data as possible, continue
-            // TODO lodge error message within exif directory?
+
+        // Loop through looking for all SOFn segments.  When we find one, we know what type of compression
+        // was used for the JPEG, and we can process the JPEG metadata in the segment too.
+        for (byte i = 0; i < 16; i++) {
+            // There are no SOF4 or SOF12 segments, so don't bother
+            if (i == 4 || i == 12)
+                continue;
+            // Should never have more than one SOFn for a given 'n'.
+            byte[] jpegSegment = segmentReader.getSegment((byte)(JpegSegmentReader.SEGMENT_SOF0 + i));
+            if (jpegSegment == null)
+                continue;
+            JpegDirectory directory = metadata.getOrCreateDirectory(JpegDirectory.class);
+            directory.setInt(JpegDirectory.TAG_JPEG_COMPRESSION_TYPE, i);
+            new JpegReader().extract(new ByteArrayReader(jpegSegment), metadata);
+            break;
         }
 
-        try {
-            byte[] iptcSegment = segmentReader.readSegment(JpegSegmentReader.SEGMENT_APPD);
-            new IptcReader(iptcSegment).extract(metadata);
-        } catch (JpegProcessingException e) {
-            // TODO lodge error message within iptc directory?
+        // There should never be more than one COM segment.
+        byte[] comSegment = segmentReader.getSegment(JpegSegmentReader.SEGMENT_COM);
+        if (comSegment != null)
+            new JpegCommentReader().extract(new ByteArrayReader(comSegment), metadata);
+
+        // Loop through all APP1 segments, checking the leading bytes to identify the format of each.
+        for (byte[] app1Segment : segmentReader.getSegments(JpegSegmentReader.SEGMENT_APP1)) {
+            if (app1Segment.length > 3 && "EXIF".equalsIgnoreCase(new String(app1Segment, 0, 4)))
+                new ExifReader().extract(new ByteArrayReader(app1Segment), metadata);
+
+            //if (app1Segment.length > 27 && "http://ns.adobe.com/xap/1.0/".equalsIgnoreCase(new String(app1Segment, 0, 28)))
+            //    new XmpReader().extract(new ByteArrayReader(app1Segment), metadata);
         }
 
-		try {
-			byte[] jpegSegment = segmentReader.readSegment(JpegSegmentReader.SEGMENT_SOF0);
-			new JpegReader(jpegSegment).extract(metadata);
-		} catch (JpegProcessingException e) {
-			// TODO lodge error message within jpeg directory?
-		}
-
-		try {
-			byte[] jpegCommentSegment = segmentReader.readSegment(JpegSegmentReader.SEGMENT_COM);
-			new JpegCommentReader(jpegCommentSegment).extract(metadata);
-		} catch (JpegProcessingException e) {
-			// TODO lodge error message within jpegcomment directory?
-		}
+        // Loop through all APPD segments, checking the leading bytes to identify the format of each.
+        for (byte[] appdSegment : segmentReader.getSegments(JpegSegmentReader.SEGMENT_APPD)) {
+            if (appdSegment.length > 12 && "Photoshop 3.0".compareTo(new String(appdSegment, 0, 13))==0) {
+                //new PhotoshopReader().extract(new ByteArrayReader(appdSegment), metadata);
+            } else {
+                // TODO might be able to check for a leading 0x1c02 for IPTC data...
+                new IptcReader().extract(new ByteArrayReader(appdSegment), metadata);
+            }
+        }
 
         return metadata;
     }
 
-    private JpegMetadataReader()
+    private JpegMetadataReader() throws Exception
     {
-    }
-
-    public static void main(String[] args) throws MetadataException, IOException
-    {
-        Metadata metadata = null;
-        try {
-            metadata = JpegMetadataReader.readMetadata(new File(args[0]));
-        } catch (Exception e) {
-            e.printStackTrace(System.err);
-            System.exit(1);
-        }
-
-        // iterate over the exif data and print to System.out
-        Iterator directories = metadata.getDirectoryIterator();
-        while (directories.hasNext()) {
-            Directory directory = (Directory)directories.next();
-            Iterator tags = directory.getTagIterator();
-            while (tags.hasNext()) {
-                Tag tag = (Tag)tags.next();
-                try {
-                    System.out.println("[" + directory.getName() + "] " + tag.getTagName() + " = " + tag.getDescription());
-                } catch (MetadataException e) {
-                    System.err.println(e.getMessage());
-                    System.err.println(tag.getDirectoryName() + " " + tag.getTagName() + " (error)");
-                }
-            }
-            if (directory.hasErrors()) {
-                Iterator errors = directory.getErrors();
-                while (errors.hasNext()) {
-                    System.out.println("ERROR: " + errors.next());
-                }
-            }
-        }
-
-        if (args.length>1 && args[1].trim().equals("/thumb"))
-        {
-            ExifDirectory directory = (ExifDirectory)metadata.getDirectory(ExifDirectory.class);
-            if (directory.containsThumbnail())
-            {
-                System.out.println("Writing thumbnail...");
-                directory.writeThumbnail(args[0].trim() + ".thumb.jpg");
-            }
-            else
-            {
-                System.out.println("No thumbnail data exists in this image");
-            }
-        }
+        throw new Exception("Not intended for instantiation");
     }
 }
+
Index: trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 6002)
+++ trunk/src/com/drew/imaging/jpeg/JpegProcessingException.java	(revision 6127)
@@ -1,42 +1,47 @@
 /*
- * JpegProcessingException.java
+ * Copyright 2002-2012 Drew Noakes
  *
- * This class is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
  *
- * Created by dnoakes on 04-Nov-2002 19:31:29 using IntelliJ IDEA.
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.imaging.jpeg;
 
-import com.drew.lang.CompoundException;
+import com.drew.imaging.ImageProcessingException;
+import com.drew.lang.annotations.Nullable;
 
 /**
- * An exception class thrown upon unexpected and fatal conditions while processing
- * a Jpeg file.
- * @author  Drew Noakes http://drewnoakes.com
+ * An exception class thrown upon unexpected and fatal conditions while processing a Jpeg file.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class JpegProcessingException extends CompoundException
+public class JpegProcessingException extends ImageProcessingException
 {
-    public JpegProcessingException(String message)
+    private static final long serialVersionUID = -7870179776125450158L;
+
+    public JpegProcessingException(@Nullable String message)
     {
         super(message);
     }
 
-    public JpegProcessingException(String message, Throwable cause)
+    public JpegProcessingException(@Nullable String message, @Nullable Throwable cause)
     {
         super(message, cause);
     }
 
-    public JpegProcessingException(Throwable cause)
+    public JpegProcessingException(@Nullable Throwable cause)
     {
         super(cause);
Index: trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java	(revision 6002)
+++ trunk/src/com/drew/imaging/jpeg/JpegSegmentData.java	(revision 6127)
@@ -1,17 +1,26 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.imaging.jpeg;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 
 import java.io.*;
@@ -22,25 +31,38 @@
 /**
  * Holds a collection of Jpeg data segments.  This need not necessarily be all segments
- * within the Jpeg.  For example, it may be convenient to port about only the non-image
+ * within the Jpeg.  For example, it may be convenient to store only the non-image
  * segments when analysing (or serializing) metadata.
+ * <p/>
+ * Segments are keyed via their segment marker (a byte).  Where multiple segments use the
+ * same segment marker, they will all be stored and available.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class JpegSegmentData implements Serializable
 {
-    static final long serialVersionUID = 7110175216435025451L;
+    private static final long serialVersionUID = 7110175216435025451L;
     
     /** A map of byte[], keyed by the segment marker */
-    private final HashMap _segmentDataMap;
-
-    public JpegSegmentData()
-    {
-        _segmentDataMap = new HashMap(10);
-    }
-
-    public void addSegment(byte segmentMarker, byte[] segmentBytes)
-    {
-        List segmentList = getOrCreateSegmentList(segmentMarker);
+    @NotNull
+    private final HashMap<Byte, List<byte[]>> _segmentDataMap = new HashMap<Byte, List<byte[]>>(10);
+
+    /**
+     * Adds segment bytes to the collection.
+     * @param segmentMarker
+     * @param segmentBytes
+     */
+    @SuppressWarnings({ "MismatchedQueryAndUpdateOfCollection" })
+    public void addSegment(byte segmentMarker, @NotNull byte[] segmentBytes)
+    {
+        final List<byte[]> segmentList = getOrCreateSegmentList(segmentMarker);
         segmentList.add(segmentBytes);
     }
 
+    /**
+     * Gets the first Jpeg segment data for the specified marker.
+     * @param segmentMarker the byte identifier for the desired segment
+     * @return a byte[] containing segment data or null if no data exists for that segment
+     */
+    @Nullable
     public byte[] getSegment(byte segmentMarker)
     {
@@ -48,73 +70,127 @@
     }
 
+    /**
+     * Gets segment data for a specific occurrence and marker.  Use this method when more than one occurrence
+     * of segment data for a given marker exists.
+     * @param segmentMarker identifies the required segment
+     * @param occurrence the zero-based index of the occurrence
+     * @return the segment data as a byte[], or null if no segment exists for the marker & occurrence
+     */
+    @Nullable
     public byte[] getSegment(byte segmentMarker, int occurrence)
     {
-        final List segmentList = getSegmentList(segmentMarker);
+        final List<byte[]> segmentList = getSegmentList(segmentMarker);
 
         if (segmentList==null || segmentList.size()<=occurrence)
             return null;
         else
-            return (byte[]) segmentList.get(occurrence);
-    }
-
+            return segmentList.get(occurrence);
+    }
+
+    /**
+     * Returns all instances of a given Jpeg segment.  If no instances exist, an empty sequence is returned.
+     *
+     * @param segmentMarker a number which identifies the type of Jpeg segment being queried
+     * @return zero or more byte arrays, each holding the data of a Jpeg segment
+     */
+    @NotNull
+    public Iterable<byte[]> getSegments(byte segmentMarker)
+    {
+        final List<byte[]> segmentList = getSegmentList(segmentMarker);
+        return segmentList==null ? new ArrayList<byte[]>() : segmentList;
+    }
+
+    @Nullable
+    public List<byte[]> getSegmentList(byte segmentMarker)
+    {
+        return _segmentDataMap.get(Byte.valueOf(segmentMarker));
+    }
+
+    @NotNull
+    private List<byte[]> getOrCreateSegmentList(byte segmentMarker)
+    {
+        List<byte[]> segmentList;
+        if (_segmentDataMap.containsKey(segmentMarker)) {
+            segmentList = _segmentDataMap.get(segmentMarker);
+        } else {
+            segmentList = new ArrayList<byte[]>();
+            _segmentDataMap.put(segmentMarker, segmentList);
+        }
+        return segmentList;
+    }
+
+    /**
+     * Returns the count of segment data byte arrays stored for a given segment marker.
+     * @param segmentMarker identifies the required segment
+     * @return the segment count (zero if no segments exist).
+     */
     public int getSegmentCount(byte segmentMarker)
     {
-        final List segmentList = getSegmentList(segmentMarker);
-        if (segmentList==null)
-            return 0;
-        else
-            return segmentList.size();
-    }
-
+        final List<byte[]> segmentList = getSegmentList(segmentMarker);
+        return segmentList == null ? 0 : segmentList.size();
+    }
+
+    /**
+     * Removes a specified instance of a segment's data from the collection.  Use this method when more than one
+     * occurrence of segment data for a given marker exists.
+     * @param segmentMarker identifies the required segment
+     * @param occurrence the zero-based index of the segment occurrence to remove.
+     */
+    @SuppressWarnings({ "MismatchedQueryAndUpdateOfCollection" })
     public void removeSegmentOccurrence(byte segmentMarker, int occurrence)
     {
-        final List segmentList = (List)_segmentDataMap.get(new Byte(segmentMarker));
+        final List<byte[]> segmentList = _segmentDataMap.get(Byte.valueOf(segmentMarker));
         segmentList.remove(occurrence);
     }
 
+    /**
+     * Removes all segments from the collection having the specified marker.
+     * @param segmentMarker identifies the required segment
+     */
     public void removeSegment(byte segmentMarker)
     {
-        _segmentDataMap.remove(new Byte(segmentMarker));
-    }
-
-    private List getSegmentList(byte segmentMarker)
-    {
-        return (List)_segmentDataMap.get(new Byte(segmentMarker));
-    }
-
-    private List getOrCreateSegmentList(byte segmentMarker)
-    {
-        List segmentList;
-        Byte key = new Byte(segmentMarker);
-        if (_segmentDataMap.containsKey(key)) {
-            segmentList = (List)_segmentDataMap.get(key);
-        } else {
-            segmentList = new ArrayList();
-            _segmentDataMap.put(key, segmentList);
-        }
-        return segmentList;
-    }
-
+        _segmentDataMap.remove(Byte.valueOf(segmentMarker));
+    }
+
+    /**
+     * Determines whether data is present for a given segment marker.
+     * @param segmentMarker identifies the required segment
+     * @return true if data exists, otherwise false
+     */
     public boolean containsSegment(byte segmentMarker)
     {
-        return _segmentDataMap.containsKey(new Byte(segmentMarker));
-    }
-
-    public static void ToFile(File file, JpegSegmentData segmentData) throws IOException
-    {
-        ObjectOutputStream outputStream = null;
+        return _segmentDataMap.containsKey(Byte.valueOf(segmentMarker));
+    }
+
+    /**
+     * Serialises the contents of a JpegSegmentData to a file.
+     * @param file to file to write from
+     * @param segmentData the data to write
+     * @throws IOException if problems occur while writing
+     */
+    public static void toFile(@NotNull File file, @NotNull JpegSegmentData segmentData) throws IOException
+    {
+        FileOutputStream fileOutputStream = null;
         try
         {
-            outputStream = new ObjectOutputStream(new FileOutputStream(file));
-            outputStream.writeObject(segmentData);
+            fileOutputStream = new FileOutputStream(file);
+            new ObjectOutputStream(fileOutputStream).writeObject(segmentData);
         }
         finally
         {
-            if (outputStream!=null)
-                outputStream.close();
-        }
-    }
-
-    public static JpegSegmentData FromFile(File file) throws IOException, ClassNotFoundException
+            if (fileOutputStream!=null)
+                fileOutputStream.close();
+        }
+    }
+
+    /**
+     * Deserialises the contents of a JpegSegmentData from a file.
+     * @param file the file to read from
+     * @return the JpegSegmentData as read
+     * @throws IOException if problems occur while reading
+     * @throws ClassNotFoundException if problems occur while deserialising
+     */
+    @NotNull
+    public static JpegSegmentData fromFile(@NotNull File file) throws IOException, ClassNotFoundException
     {
         ObjectInputStream inputStream = null;
Index: trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java
===================================================================
--- trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 6002)
+++ trunk/src/com/drew/imaging/jpeg/JpegSegmentReader.java	(revision 6127)
@@ -1,49 +1,44 @@
 /*
- * JpegSegmentReader.java
- *
- * This class written by Drew Noakes, in accordance with the Jpeg specification.
- *
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 04-Nov-2002 00:54:00 using IntelliJ IDEA
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.imaging.jpeg;
 
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
 import java.io.*;
 
 /**
  * Performs read functions of Jpeg files, returning specific file segments.
- * TODO add a findAvailableSegments() method
- * TODO add more segment identifiers
- * TODO add a getSegmentDescription() method, returning for example 'App1 application data segment, commonly containing Exif data'
  * @author  Drew Noakes http://drewnoakes.com
  */
 public class JpegSegmentReader
 {
-    // Jpeg data can be sourced from either a file, byte[] or InputStream
-
-    /** Jpeg file */
-    private final File _file;
-    /** Jpeg data as byte array */
-    private final byte[] _data;
-    /** Jpeg data as an InputStream */
-    private final InputStream _stream;
-
-    private JpegSegmentData _segmentData;
-
-    /**
-     * Private, because this segment crashes my algorithm, and searching for
-     * it doesn't work (yet).
+    // TODO add a findAvailableSegments() method
+    // TODO add more segment identifiers
+    // TODO add a getSegmentDescription() method, returning for example 'App1 application data segment, commonly containing Exif data'
+
+    @NotNull
+    private final JpegSegmentData _segmentData;
+
+    /**
+     * Private, because this segment crashes my algorithm, and searching for it doesn't work (yet).
      */
     private static final byte SEGMENT_SOS = (byte)0xDA;
@@ -54,7 +49,7 @@
     private static final byte MARKER_EOI = (byte)0xD9;
 
-    /** APP0 Jpeg segment identifier -- Jfif data. */
+    /** APP0 Jpeg segment identifier -- JFIF data (also JFXX apparently). */
     public static final byte SEGMENT_APP0 = (byte)0xE0;
-    /** APP1 Jpeg segment identifier -- where Exif data is kept. */
+    /** APP1 Jpeg segment identifier -- where Exif data is kept.  XMP data is also kept in here, though usually in a second instance. */
     public static final byte SEGMENT_APP1 = (byte)0xE1;
     /** APP2 Jpeg segment identifier. */
@@ -74,15 +69,15 @@
     /** APP9 Jpeg segment identifier. */
     public static final byte SEGMENT_APP9 = (byte)0xE9;
-    /** APPA Jpeg segment identifier -- can hold Unicode comments. */
+    /** APPA (App10) Jpeg segment identifier -- can hold Unicode comments. */
     public static final byte SEGMENT_APPA = (byte)0xEA;
-    /** APPB Jpeg segment identifier. */
+    /** APPB (App11) Jpeg segment identifier. */
     public static final byte SEGMENT_APPB = (byte)0xEB;
-    /** APPC Jpeg segment identifier. */
+    /** APPC (App12) Jpeg segment identifier. */
     public static final byte SEGMENT_APPC = (byte)0xEC;
-    /** APPD Jpeg segment identifier -- IPTC data in here. */
+    /** APPD (App13) Jpeg segment identifier -- IPTC data in here. */
     public static final byte SEGMENT_APPD = (byte)0xED;
-    /** APPE Jpeg segment identifier. */
+    /** APPE (App14) Jpeg segment identifier. */
     public static final byte SEGMENT_APPE = (byte)0xEE;
-    /** APPF Jpeg segment identifier. */
+    /** APPF (App15) Jpeg segment identifier. */
     public static final byte SEGMENT_APPF = (byte)0xEF;
     /** Start Of Image segment identifier. */
@@ -101,11 +96,18 @@
      * @param file the Jpeg file to read segments from
      */
-    public JpegSegmentReader(File file) throws JpegProcessingException
-    {
-        _file = file;
-        _data = null;
-        _stream = null;
-
-        readSegments();
+    @SuppressWarnings({ "ConstantConditions" })
+    public JpegSegmentReader(@NotNull File file) throws JpegProcessingException, IOException
+    {
+        if (file==null)
+            throw new NullPointerException();
+
+        InputStream inputStream = null;
+        try {
+            inputStream = new FileInputStream(file);
+            _segmentData = readSegments(new BufferedInputStream(inputStream), false);
+        } finally {
+            if (inputStream != null)
+                inputStream.close();
+        }
     }
 
@@ -114,29 +116,29 @@
      * @param fileContents the byte array containing Jpeg data
      */
-    public JpegSegmentReader(byte[] fileContents) throws JpegProcessingException
-    {
-        _file = null;
-        _data = fileContents;
-        _stream = null;
-
-        readSegments();
-    }
-
-    public JpegSegmentReader(InputStream in) throws JpegProcessingException
-    {
-        _stream = in;
-        _file = null;
-        _data = null;
-        
-        readSegments();
-    }
-
-    public JpegSegmentReader(JpegSegmentData segmentData)
-    {
-        _file = null;
-        _data = null;
-        _stream = null;
-
-        _segmentData = segmentData;
+    @SuppressWarnings({ "ConstantConditions" })
+    public JpegSegmentReader(@NotNull byte[] fileContents) throws JpegProcessingException
+    {
+        if (fileContents==null)
+            throw new NullPointerException();
+
+        BufferedInputStream stream = new BufferedInputStream(new ByteArrayInputStream(fileContents));
+        _segmentData = readSegments(stream, false);
+    }
+
+    /**
+     * Creates a JpegSegmentReader for an InputStream.
+     * @param inputStream the InputStream containing Jpeg data
+     */
+    @SuppressWarnings({ "ConstantConditions" })
+    public JpegSegmentReader(@NotNull InputStream inputStream, boolean waitForBytes) throws JpegProcessingException
+    {
+        if (inputStream==null)
+            throw new NullPointerException();
+
+        BufferedInputStream bufferedInputStream = inputStream instanceof BufferedInputStream
+                ? (BufferedInputStream)inputStream
+                : new BufferedInputStream(inputStream);
+
+        _segmentData = readSegments(bufferedInputStream, waitForBytes);
     }
 
@@ -146,8 +148,7 @@
      * @param segmentMarker the byte identifier for the desired segment
      * @return the byte array if found, else null
-     * @throws JpegProcessingException for any problems processing the Jpeg data,
-     *         including inner IOExceptions
-     */
-    public byte[] readSegment(byte segmentMarker) throws JpegProcessingException
+     */
+    @Nullable
+    public byte[] readSegment(byte segmentMarker)
     {
         return readSegment(segmentMarker, 0);
@@ -155,10 +156,11 @@
 
     /**
-     * Reads the first instance of a given Jpeg segment, returning the contents as
-     * a byte array.
+     * Reads the Nth instance of a given Jpeg segment, returning the contents as a byte array.
+     * 
      * @param segmentMarker the byte identifier for the desired segment
      * @param occurrence the occurrence of the specified segment within the jpeg file
      * @return the byte array if found, else null
      */
+    @Nullable
     public byte[] readSegment(byte segmentMarker, int occurrence)
     {
@@ -166,4 +168,21 @@
     }
 
+    /**
+     * Returns all instances of a given Jpeg segment.  If no instances exist, an empty sequence is returned.
+     *
+     * @param segmentMarker a number which identifies the type of Jpeg segment being queried
+     * @return zero or more byte arrays, each holding the data of a Jpeg segment
+     */
+    @NotNull
+    public Iterable<byte[]> readSegments(byte segmentMarker)
+    {
+        return _segmentData.getSegments(segmentMarker);
+    }
+
+    /**
+     * Returns the number of segments having the specified JPEG segment marker.
+     * @param segmentMarker the JPEG segment identifying marker.
+     * @return the count of matching segments.
+     */
     public final int getSegmentCount(byte segmentMarker)
     {
@@ -171,4 +190,9 @@
     }
 
+    /**
+     * Returns the JpegSegmentData object used by this reader.
+     * @return the JpegSegmentData object.
+     */
+    @NotNull
     public final JpegSegmentData getSegmentData()
     {
@@ -176,19 +200,27 @@
     }
 
-    private void readSegments() throws JpegProcessingException
-    {
-        _segmentData = new JpegSegmentData();
-
-        BufferedInputStream inStream = getJpegInputStream();
+    @NotNull
+    private JpegSegmentData readSegments(@NotNull final BufferedInputStream jpegInputStream, boolean waitForBytes) throws JpegProcessingException
+    {
+        JpegSegmentData segmentData = new JpegSegmentData();
+
         try {
             int offset = 0;
             // first two bytes should be jpeg magic number
-            if (!isValidJpegHeaderBytes(inStream)) {
+            byte[] headerBytes = new byte[2];
+            if (jpegInputStream.read(headerBytes, 0, 2)!=2)
                 throw new JpegProcessingException("not a jpeg file");
-            }
+            final boolean hasValidHeader = (headerBytes[0] & 0xFF) == 0xFF && (headerBytes[1] & 0xFF) == 0xD8;
+            if (!hasValidHeader)
+                throw new JpegProcessingException("not a jpeg file");
+
             offset += 2;
             do {
+                // need four bytes from stream for segment header before continuing
+                if (!checkForBytesOnStream(jpegInputStream, 4, waitForBytes))
+                    throw new JpegProcessingException("stream ended before segment header could be read");
+
                 // next byte is 0xFF
-                byte segmentIdentifier = (byte)(inStream.read() & 0xFF);
+                byte segmentIdentifier = (byte)(jpegInputStream.read() & 0xFF);
                 if ((segmentIdentifier & 0xFF) != 0xFF) {
                     throw new JpegProcessingException("expected jpeg segment start identifier 0xFF at offset " + offset + ", not 0x" + Integer.toHexString(segmentIdentifier & 0xFF));
@@ -196,19 +228,21 @@
                 offset++;
                 // next byte is <segment-marker>
-                byte thisSegmentMarker = (byte)(inStream.read() & 0xFF);
+                byte thisSegmentMarker = (byte)(jpegInputStream.read() & 0xFF);
                 offset++;
                 // next 2-bytes are <segment-size>: [high-byte] [low-byte]
                 byte[] segmentLengthBytes = new byte[2];
-                inStream.read(segmentLengthBytes, 0, 2);
+                if (jpegInputStream.read(segmentLengthBytes, 0, 2) != 2)
+                    throw new JpegProcessingException("Jpeg data ended unexpectedly.");
                 offset += 2;
                 int segmentLength = ((segmentLengthBytes[0] << 8) & 0xFF00) | (segmentLengthBytes[1] & 0xFF);
                 // segment length includes size bytes, so subtract two
                 segmentLength -= 2;
-                if (segmentLength > inStream.available())
+                if (!checkForBytesOnStream(jpegInputStream, segmentLength, waitForBytes))
                     throw new JpegProcessingException("segment size would extend beyond file stream length");
-                else if (segmentLength < 0)
+                if (segmentLength < 0)
                     throw new JpegProcessingException("segment size would be less than zero");
                 byte[] segmentBytes = new byte[segmentLength];
-                inStream.read(segmentBytes, 0, segmentLength);
+                if (jpegInputStream.read(segmentBytes, 0, segmentLength) != segmentLength)
+                    throw new JpegProcessingException("Jpeg data ended unexpectedly.");
                 offset += segmentLength;
                 if ((thisSegmentMarker & 0xFF) == (SEGMENT_SOS & 0xFF)) {
@@ -216,23 +250,20 @@
                     // have to search for the two bytes: 0xFF 0xD9 (EOI).
                     // It comes last so simply return at this point
-                    return;
+                    return segmentData;
                 } else if ((thisSegmentMarker & 0xFF) == (MARKER_EOI & 0xFF)) {
                     // the 'End-Of-Image' segment -- this should never be found in this fashion
-                    return;
+                    return segmentData;
                 } else {
-                    _segmentData.addSegment(thisSegmentMarker, segmentBytes);
+                    segmentData.addSegment(thisSegmentMarker, segmentBytes);
                 }
-                // didn't find the one we're looking for, loop through to the next segment
             } while (true);
         } catch (IOException ioe) {
-            //throw new JpegProcessingException("IOException processing Jpeg file", ioe);
             throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
         } finally {
             try {
-                if (inStream != null) {
-                    inStream.close();
+                if (jpegInputStream != null) {
+                    jpegInputStream.close();
                 }
             } catch (IOException ioe) {
-                //throw new JpegProcessingException("IOException processing Jpeg file", ioe);
                 throw new JpegProcessingException("IOException processing Jpeg file: " + ioe.getMessage(), ioe);
             }
@@ -240,44 +271,23 @@
     }
 
-    /**
-     * Private helper method to create a BufferedInputStream of Jpeg data from whichever
-     * data source was specified upon construction of this instance.
-     * @return a a BufferedInputStream of Jpeg data
-     * @throws JpegProcessingException for any problems obtaining the stream
-     */
-    private BufferedInputStream getJpegInputStream() throws JpegProcessingException
-    {
-        if (_stream!=null) {
-            if (_stream instanceof BufferedInputStream) {
-                return (BufferedInputStream) _stream;
-            } else {
-                return new BufferedInputStream(_stream);
+    private boolean checkForBytesOnStream(@NotNull BufferedInputStream stream, int bytesNeeded, boolean waitForBytes) throws IOException
+    {
+        // NOTE  waiting is essential for network streams where data can be delayed, but it is not necessary for byte[] or filesystems
+
+        if (!waitForBytes)
+            return bytesNeeded <= stream.available();
+
+        int count = 40; // * 100ms = approx 4 seconds
+        while (count > 0) {
+            if (bytesNeeded <= stream.available())
+               return true;
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException e) {
+                // continue
             }
+            count--;
         }
-        InputStream inputStream;
-        if (_data == null) {
-            try {
-                inputStream = new FileInputStream(_file);
-            } catch (FileNotFoundException e) {
-                throw new JpegProcessingException("Jpeg file does not exist", e);
-            }
-        } else {
-            inputStream = new ByteArrayInputStream(_data);
-        }
-        return new BufferedInputStream(inputStream);
-    }
-
-    /**
-     * Helper method that validates the Jpeg file's magic number.
-     * @param fileStream the InputStream to read bytes from, which must be positioned
-     *        at its start (i.e. no bytes read yet)
-     * @return true if the magic number is Jpeg (0xFFD8)
-     * @throws IOException for any problem in reading the file
-     */
-    private boolean isValidJpegHeaderBytes(InputStream fileStream) throws IOException
-    {
-        byte[] header = new byte[2];
-        fileStream.read(header, 0, 2);
-        return (header[0] & 0xFF) == 0xFF && (header[1] & 0xFF) == 0xD8;
+        return false;
     }
 }
Index: trunk/src/com/drew/lang/BufferBoundsException.java
===================================================================
--- trunk/src/com/drew/lang/BufferBoundsException.java	(revision 6127)
+++ trunk/src/com/drew/lang/BufferBoundsException.java	(revision 6127)
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.IOException;
+
+/**
+ * A checked replacement for IndexOutOfBoundsException.  Used by BufferReader.
+ * 
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public final class BufferBoundsException extends Exception
+{
+    private static final long serialVersionUID = 2911102837808946396L;
+
+    public BufferBoundsException(@NotNull byte[] buffer, int index, int bytesRequested)
+    {
+        super(getMessage(buffer, index, bytesRequested));
+    }
+
+    public BufferBoundsException(final String message)
+    {
+        super(message);
+    }
+
+    public BufferBoundsException(final String message, final IOException innerException)
+    {
+        super(message, innerException);
+    }
+
+    private static String getMessage(@NotNull byte[] buffer, int index, int bytesRequested)
+    {
+        if (index < 0)
+            return String.format("Attempt to read from buffer using a negative index (%s)", index);
+
+        return String.format("Attempt to read %d byte%s from beyond end of buffer (requested index: %d, max index: %d)",
+                bytesRequested, bytesRequested==1?"":"s", index, buffer.length - 1);
+    }
+}
Index: trunk/src/com/drew/lang/BufferReader.java
===================================================================
--- trunk/src/com/drew/lang/BufferReader.java	(revision 6127)
+++ trunk/src/com/drew/lang/BufferReader.java	(revision 6127)
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+public interface BufferReader
+{
+    /**
+     * Returns the length of the buffer.  This value represents the total number of bytes in the underlying source.
+     *
+     * @return The number of bytes in the buffer.
+     */
+    long getLength();
+
+    /**
+     * Sets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness</li>
+     * <li><code>false</code> for Intel (or little) endianness</li>
+     * </ul>
+     *
+     * @param motorolaByteOrder <code>true</code> for motorola/big endian, <code>false</code> for intel/little endian
+     */
+    void setMotorolaByteOrder(boolean motorolaByteOrder);
+
+    /**
+     * Gets the endianness of this reader.
+     * <ul>
+     * <li><code>true</code> for Motorola (or big) endianness</li>
+     * <li><code>false</code> for Intel (or little) endianness</li>
+     * </ul>
+     */
+    boolean isMotorolaByteOrder();
+
+    /**
+     * Returns an unsigned 8-bit int calculated from one byte of data at the specified index.
+     *
+     * @param index position within the data buffer to read byte
+     * @return the 8 bit int value, between 0 and 255
+     * @throws BufferBoundsException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    short getUInt8(int index) throws BufferBoundsException;
+
+    /**
+     * Returns a signed 8-bit int calculated from one byte of data at the specified index.
+     *
+     * @param index position within the data buffer to read byte
+     * @return the 8 bit int value, between 0x00 and 0xFF
+     * @throws BufferBoundsException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    byte getInt8(int index) throws BufferBoundsException;
+
+    /**
+     * Returns an unsigned 16-bit int calculated from two bytes of data at the specified index.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the 16 bit int value, between 0x0000 and 0xFFFF
+     * @throws BufferBoundsException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    int getUInt16(int index) throws BufferBoundsException;
+
+    /**
+     * Returns a signed 16-bit int calculated from two bytes of data at the specified index (MSB, LSB).
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the 16 bit int value, between 0x0000 and 0xFFFF
+     * @throws BufferBoundsException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    short getInt16(int index) throws BufferBoundsException;
+
+    /**
+     * Get a 32-bit unsigned integer from the buffer, returning it as a long.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the unsigned 32-bit int value as a long, between 0x00000000 and 0xFFFFFFFF
+     * @throws BufferBoundsException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    long getUInt32(int index) throws BufferBoundsException;
+
+    /**
+     * Returns a signed 32-bit integer from four bytes of data at the specified index the buffer.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the signed 32 bit int value, between 0x00000000 and 0xFFFFFFFF
+     * @throws BufferBoundsException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    int getInt32(int index) throws BufferBoundsException;
+
+    /**
+     * Get a signed 64-bit integer from the buffer.
+     *
+     * @param index position within the data buffer to read first byte
+     * @return the 64 bit int value, between 0x0000000000000000 and 0xFFFFFFFFFFFFFFFF
+     * @throws BufferBoundsException the buffer does not contain enough bytes to service the request, or index is negative
+     */
+    long getInt64(int index) throws BufferBoundsException;
+
+    float getS15Fixed16(int index) throws BufferBoundsException;
+
+    float getFloat32(int index) throws BufferBoundsException;
+
+    double getDouble64(int index) throws BufferBoundsException;
+
+    @NotNull
+    byte[] getBytes(int index, int count) throws BufferBoundsException;
+
+    @NotNull
+    String getString(int index, int bytesRequested) throws BufferBoundsException;
+
+    @NotNull
+    String getString(int index, int bytesRequested, String charset) throws BufferBoundsException;
+
+    /**
+     * Creates a String from the _data buffer starting at the specified index,
+     * and ending where <code>byte=='\0'</code> or where <code>length==maxLength</code>.
+     *
+     * @param index          The index within the buffer at which to start reading the string.
+     * @param maxLengthBytes The maximum number of bytes to read.  If a zero-byte is not reached within this limit,
+     *                       reading will stop and the string will be truncated to this length.
+     * @return The read string.
+     * @throws BufferBoundsException The buffer does not contain enough bytes to satisfy this request.
+     */
+    @NotNull
+    String getNullTerminatedString(int index, int maxLengthBytes) throws BufferBoundsException;
+}
Index: trunk/src/com/drew/lang/ByteArrayReader.java
===================================================================
--- trunk/src/com/drew/lang/ByteArrayReader.java	(revision 6127)
+++ trunk/src/com/drew/lang/ByteArrayReader.java	(revision 6127)
@@ -0,0 +1,273 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Provides methods to read specific values from a byte array, with a consistent, checked exception structure for
+ * issues.
+ * <p/>
+ * By default, the reader operates with Motorola byte order (big endianness).  This can be changed by calling
+ * {@see setMotorolaByteOrder(boolean)}.
+ * 
+ * @author Drew Noakes http://drewnoakes.com
+ * */
+public class ByteArrayReader implements BufferReader
+{
+    @NotNull
+    private final byte[] _buffer;
+    private boolean _isMotorolaByteOrder = true;
+
+    @SuppressWarnings({ "ConstantConditions" })
+    @com.drew.lang.annotations.SuppressWarnings(value = "EI_EXPOSE_REP2", justification = "Design intent")
+    public ByteArrayReader(@NotNull byte[] buffer)
+    {
+        if (buffer == null)
+            throw new NullPointerException();
+        
+        _buffer = buffer;
+    }
+
+    @Override
+    public long getLength()
+    {
+        return _buffer.length;
+    }
+
+
+    @Override
+    public void setMotorolaByteOrder(boolean motorolaByteOrder)
+    {
+        _isMotorolaByteOrder = motorolaByteOrder;
+    }
+
+    @Override
+    public boolean isMotorolaByteOrder()
+    {
+        return _isMotorolaByteOrder;
+    }
+
+    @Override
+    public short getUInt8(int index) throws BufferBoundsException
+    {
+        checkBounds(index, 1);
+
+        return (short) (_buffer[index] & 255);
+    }
+
+    @Override
+    public byte getInt8(int index) throws BufferBoundsException
+    {
+        checkBounds(index, 1);
+
+        return _buffer[index];
+    }
+
+    @Override
+    public int getUInt16(int index) throws BufferBoundsException
+    {
+        checkBounds(index, 2);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (_buffer[index    ] << 8 & 0xFF00) |
+                   (_buffer[index + 1]      & 0xFF);
+        } else {
+            // Intel ordering - LSB first
+            return (_buffer[index + 1] << 8 & 0xFF00) |
+                   (_buffer[index    ]      & 0xFF);
+        }
+    }
+
+    @Override
+    public short getInt16(int index) throws BufferBoundsException
+    {
+        checkBounds(index, 2);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return (short) (((short)_buffer[index    ] << 8 & (short)0xFF00) |
+                            ((short)_buffer[index + 1]      & (short)0xFF));
+        } else {
+            // Intel ordering - LSB first
+            return (short) (((short)_buffer[index + 1] << 8 & (short)0xFF00) |
+                            ((short)_buffer[index    ]      & (short)0xFF));
+        }
+    }
+
+    @Override
+    public long getUInt32(int index) throws BufferBoundsException
+    {
+        checkBounds(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (((long)_buffer[index    ]) << 24 & 0xFF000000L) |
+                    (((long)_buffer[index + 1]) << 16 & 0xFF0000L) |
+                    (((long)_buffer[index + 2]) << 8  & 0xFF00L) |
+                    (((long)_buffer[index + 3])       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (((long)_buffer[index + 3]) << 24 & 0xFF000000L) |
+                    (((long)_buffer[index + 2]) << 16 & 0xFF0000L) |
+                    (((long)_buffer[index + 1]) << 8  & 0xFF00L) |
+                    (((long)_buffer[index    ])       & 0xFFL);
+        }
+    }
+
+    @Override
+    public int getInt32(int index) throws BufferBoundsException
+    {
+        checkBounds(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first (big endian)
+            return (_buffer[index    ] << 24 & 0xFF000000) |
+                   (_buffer[index + 1] << 16 & 0xFF0000) |
+                   (_buffer[index + 2] << 8  & 0xFF00) |
+                   (_buffer[index + 3]       & 0xFF);
+        } else {
+            // Intel ordering - LSB first (little endian)
+            return (_buffer[index + 3] << 24 & 0xFF000000) |
+                   (_buffer[index + 2] << 16 & 0xFF0000) |
+                   (_buffer[index + 1] << 8  & 0xFF00) |
+                   (_buffer[index    ]       & 0xFF);
+        }
+    }
+
+    @Override
+    public long getInt64(int index) throws BufferBoundsException
+    {
+        checkBounds(index, 8);
+
+        if (_isMotorolaByteOrder) {
+            // Motorola - MSB first
+            return ((long)_buffer[index    ] << 56 & 0xFF00000000000000L) |
+                   ((long)_buffer[index + 1] << 48 & 0xFF000000000000L) |
+                   ((long)_buffer[index + 2] << 40 & 0xFF0000000000L) |
+                   ((long)_buffer[index + 3] << 32 & 0xFF00000000L) |
+                   ((long)_buffer[index + 4] << 24 & 0xFF000000L) |
+                   ((long)_buffer[index + 5] << 16 & 0xFF0000L) |
+                   ((long)_buffer[index + 6] << 8  & 0xFF00L) |
+                   ((long)_buffer[index + 7]       & 0xFFL);
+        } else {
+            // Intel ordering - LSB first
+            return ((long)_buffer[index + 7] << 56 & 0xFF00000000000000L) |
+                   ((long)_buffer[index + 6] << 48 & 0xFF000000000000L) |
+                   ((long)_buffer[index + 5] << 40 & 0xFF0000000000L) |
+                   ((long)_buffer[index + 4] << 32 & 0xFF00000000L) |
+                   ((long)_buffer[index + 3] << 24 & 0xFF000000L) |
+                   ((long)_buffer[index + 2] << 16 & 0xFF0000L) |
+                   ((long)_buffer[index + 1] << 8  & 0xFF00L) |
+                   ((long)_buffer[index    ]       & 0xFFL);
+        }
+    }
+
+    @Override
+    public float getS15Fixed16(int index) throws BufferBoundsException
+    {
+        checkBounds(index, 4);
+
+        if (_isMotorolaByteOrder) {
+            float res = (_buffer[index    ] & 255) << 8 |
+                        (_buffer[index + 1] & 255);
+            int d =     (_buffer[index + 2] & 255) << 8 |
+                        (_buffer[index + 3] & 255);
+            return (float)(res + d/65536.0);
+        } else {
+            // this particular branch is untested
+            float res = (_buffer[index + 3] & 255) << 8 |
+                        (_buffer[index + 2] & 255);
+            int d =     (_buffer[index + 1] & 255) << 8 |
+                        (_buffer[index    ] & 255);
+            return (float)(res + d/65536.0);
+        }
+    }
+
+    @Override
+    public float getFloat32(int index) throws BufferBoundsException
+    {
+        return Float.intBitsToFloat(getInt32(index));
+    }
+
+    @Override
+    public double getDouble64(int index) throws BufferBoundsException
+    {
+        return Double.longBitsToDouble(getInt64(index));
+    }
+    
+    @Override
+    @NotNull
+    public byte[] getBytes(int index, int count) throws BufferBoundsException
+    {
+        checkBounds(index, count);
+
+        byte[] bytes = new byte[count];
+        System.arraycopy(_buffer, index, bytes, 0, count);
+        return bytes;
+    }
+
+    @Override
+    @NotNull
+    public String getString(int index, int bytesRequested) throws BufferBoundsException
+    {
+        return new String(getBytes(index, bytesRequested));
+    }
+
+    @Override
+    @NotNull
+    public String getString(int index, int bytesRequested, String charset) throws BufferBoundsException
+    {
+        byte[] bytes = getBytes(index, bytesRequested);
+        try {
+            return new String(bytes, charset);
+        } catch (UnsupportedEncodingException e) {
+            return new String(bytes);
+        }
+    }
+
+    @Override
+    @NotNull
+    public String getNullTerminatedString(int index, int maxLengthBytes) throws BufferBoundsException
+    {
+        // NOTE currently only really suited to single-byte character strings
+
+        checkBounds(index, maxLengthBytes);
+
+        // Check for null terminators
+        int length = 0;
+        while ((index + length) < _buffer.length && _buffer[index + length] != '\0' && length < maxLengthBytes)
+            length++;
+
+        byte[] bytes = getBytes(index, length);
+        return new String(bytes);
+    }
+
+    private void checkBounds(final int index, final int bytesRequested) throws BufferBoundsException
+    {
+        if (bytesRequested < 0 || index < 0 || (long)index + (long)bytesRequested - 1L >= (long)_buffer.length)
+            throw new BufferBoundsException(_buffer, index, bytesRequested);
+    }
+}
Index: trunk/src/com/drew/lang/CompoundException.java
===================================================================
--- trunk/src/com/drew/lang/CompoundException.java	(revision 6002)
+++ trunk/src/com/drew/lang/CompoundException.java	(revision 6127)
@@ -1,17 +1,26 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 
 import java.io.PrintStream;
@@ -22,58 +31,65 @@
  * unavailable in previous versions.  This class allows support
  * of these previous JDK versions.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class CompoundException extends Exception
 {
-    private final Throwable _innnerException;
+    private static final long serialVersionUID = -9207883813472069925L;
 
-    public CompoundException(String msg)
+    @Nullable
+    private final Throwable _innerException;
+
+    public CompoundException(@Nullable String msg)
     {
         this(msg, null);
     }
 
-    public CompoundException(Throwable exception)
+    public CompoundException(@Nullable Throwable exception)
     {
         this(null, exception);
     }
 
-    public CompoundException(String msg, Throwable innerException)
+    public CompoundException(@Nullable String msg, @Nullable Throwable innerException)
     {
         super(msg);
-        _innnerException = innerException;
+        _innerException = innerException;
     }
 
+    @Nullable
     public Throwable getInnerException()
     {
-        return _innnerException;
+        return _innerException;
     }
 
+    @NotNull
     public String toString()
     {
-        StringBuffer sbuffer = new StringBuffer();
-        sbuffer.append(super.toString());
-        if (_innnerException != null) {
-            sbuffer.append("\n");
-            sbuffer.append("--- inner exception ---");
-            sbuffer.append("\n");
-            sbuffer.append(_innnerException.toString());
+        StringBuilder string = new StringBuilder();
+        string.append(super.toString());
+        if (_innerException != null) {
+            string.append("\n");
+            string.append("--- inner exception ---");
+            string.append("\n");
+            string.append(_innerException.toString());
         }
-        return sbuffer.toString();
+        return string.toString();
     }
 
-    public void printStackTrace(PrintStream s)
+    public void printStackTrace(@NotNull PrintStream s)
     {
         super.printStackTrace(s);
-        if (_innnerException != null) {
+        if (_innerException != null) {
             s.println("--- inner exception ---");
-            _innnerException.printStackTrace(s);
+            _innerException.printStackTrace(s);
         }
     }
 
-    public void printStackTrace(PrintWriter s)
+    public void printStackTrace(@NotNull PrintWriter s)
     {
         super.printStackTrace(s);
-        if (_innnerException != null) {
+        if (_innerException != null) {
             s.println("--- inner exception ---");
-            _innnerException.printStackTrace(s);
+            _innerException.printStackTrace(s);
         }
     }
@@ -82,7 +98,7 @@
     {
         super.printStackTrace();
-        if (_innnerException != null) {
+        if (_innerException != null) {
             System.err.println("--- inner exception ---");
-            _innnerException.printStackTrace();
+            _innerException.printStackTrace();
         }
     }
Index: trunk/src/com/drew/lang/GeoLocation.java
===================================================================
--- trunk/src/com/drew/lang/GeoLocation.java	(revision 6127)
+++ trunk/src/com/drew/lang/GeoLocation.java	(revision 6127)
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * Represents a latitude and longitude pair, giving a position on earth in spherical coordinates.
+ * Values of latitude and longitude are given in degrees.
+ * This type is immutable.
+ */
+public final class GeoLocation
+{
+    private final double _latitude;
+    private final double _longitude;
+
+    /**
+     * Instantiates a new instance of {@link GeoLocation}.
+     *
+     * @param latitude the latitude, in degrees
+     * @param longitude the longitude, in degrees
+     */
+    public GeoLocation(double latitude, double longitude)
+    {
+        _latitude = latitude;
+        _longitude = longitude;
+    }
+
+    /**
+     * @return the latitudinal angle of this location, in degrees.
+     */
+    public double getLatitude()
+    {
+        return _latitude;
+    }
+
+    /**
+     * @return the longitudinal angle of this location, in degrees.
+     */
+    public double getLongitude()
+    {
+        return _longitude;
+    }
+
+    /**
+     * @return true, if both latitude and longitude are equal to zero
+     */
+    public boolean isZero()
+    {
+        return _latitude == 0 && _longitude == 0;
+    }
+
+    /**
+     * Converts a decimal degree angle into its corresponding DMS (degrees-minutes-seconds) representation as a string,
+     * of format: {@code -1° 23' 4.56"}
+     */
+    @NotNull
+    public static String decimalToDegreesMinutesSecondsString(double decimal)
+    {
+        double[] dms = decimalToDegreesMinutesSeconds(decimal);
+        return dms[0] + "° " + dms[1] + "' " + dms[2] + '"';
+    }
+
+    /**
+     * Converts a decimal degree angle into its corresponding DMS (degrees-minutes-seconds) component values, as
+     * a double array.
+     */
+    @NotNull
+    public static double[] decimalToDegreesMinutesSeconds(double decimal)
+    {
+        int d = (int)decimal;
+        double m = Math.abs((decimal % 1) * 60);
+        double s = (m % 1) * 60;
+        return new double[] { d, (int)m, s};
+    }
+
+    /**
+     * Converts DMS (degrees-minutes-seconds) rational values, as given in {@link com.drew.metadata.exif.GpsDirectory},
+     * into a single value in degrees, as a double.
+     */
+    @Nullable
+    public static Double degreesMinutesSecondsToDecimal(@NotNull final Rational degs, @NotNull final Rational mins, @NotNull final Rational secs, final boolean isNegative)
+    {
+        double decimal = Math.abs(degs.doubleValue())
+                + mins.doubleValue() / 60.0d
+                + secs.doubleValue() / 3600.0d;
+
+        if (Double.isNaN(decimal))
+            return null;
+
+        if (isNegative)
+            decimal *= -1;
+
+        return decimal;
+    }
+
+    @Override
+    public boolean equals(final Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        GeoLocation that = (GeoLocation) o;
+        if (Double.compare(that._latitude, _latitude) != 0) return false;
+        if (Double.compare(that._longitude, _longitude) != 0) return false;
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result;
+        long temp;
+        temp = _latitude != +0.0d ? Double.doubleToLongBits(_latitude) : 0L;
+        result = (int) (temp ^ (temp >>> 32));
+        temp = _longitude != +0.0d ? Double.doubleToLongBits(_longitude) : 0L;
+        result = 31 * result + (int) (temp ^ (temp >>> 32));
+        return result;
+    }
+
+    /**
+     * @return a string representation of this location, of format: {@code 1.23, 4.56}
+     */
+    @Override
+    @NotNull
+    public String toString()
+    {
+        return _latitude + ", " + _longitude;
+    }
+
+    /**
+     * @return a string representation of this location, of format: {@code -1° 23' 4.56", 54° 32' 1.92"}
+     */
+    @NotNull
+    public String toDMSString()
+    {
+        return decimalToDegreesMinutesSecondsString(_latitude) + ", " + decimalToDegreesMinutesSecondsString(_longitude);
+    }
+}
Index: trunk/src/com/drew/lang/NullOutputStream.java
===================================================================
--- trunk/src/com/drew/lang/NullOutputStream.java	(revision 6002)
+++ trunk/src/com/drew/lang/NullOutputStream.java	(revision 6127)
@@ -1,17 +1,21 @@
-/**
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+/*
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on Dec 15, 2002 3:30:59 PM using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.lang;
@@ -20,4 +24,9 @@
 import java.io.OutputStream;
 
+/**
+ * An implementation of OutputSteam that ignores write requests by doing nothing.  This class may be useful in tests.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
 public class NullOutputStream extends OutputStream
 {
Index: trunk/src/com/drew/lang/Rational.java
===================================================================
--- trunk/src/com/drew/lang/Rational.java	(revision 6002)
+++ trunk/src/com/drew/lang/Rational.java	(revision 6127)
@@ -1,35 +1,27 @@
 /*
- * Rational.java
- *
- * This class is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  Similarly, I release this Java version under the
- * same license, though I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew.noakes@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created on 6 May 2002, 18:06
- * Updated 26 Aug 2002 by Drew
- * - Added toSimpleString() method, which returns a simplified and hopefully more
- *   readable version of the Rational.  i.e. 2/10 -> 1/5, and 10/2 -> 5
- * Modified 29 Oct 2002 (v1.2)
- * - Improved toSimpleString() to factor more complex rational numbers into
- *   a simpler form
- *     i.e.
- *       10/15 -> 2/3
- * - toSimpleString() now accepts a boolean flag, 'allowDecimals' which will
- *   display the rational number in decimal form if it fits within 5 digits
- *     i.e.
- *       3/4 -> 0.75 when allowDecimal == true
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 
 package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 
 import java.io.Serializable;
@@ -38,20 +30,16 @@
  * Immutable class for holding a rational number without loss of precision.  Provides
  * a familiar representation via toString() in form <code>numerator/denominator</code>.
- * <p>
- * @author  Drew Noakes http://drewnoakes.com
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class Rational extends java.lang.Number implements Serializable
 {
-    /**
-     * Holds the numerator.
-     */
-    private final int numerator;
-
-    /**
-     * Holds the denominator.
-     */
-    private final int denominator;
-
-    private int maxSimplificationCalculations = 1000;
+    private static final long serialVersionUID = 510688928138848770L;
+
+    /** Holds the numerator. */
+    private final long _numerator;
+
+    /** Holds the denominator. */
+    private final long _denominator;
 
     /**
@@ -60,8 +48,8 @@
      * with them!
      */
-    public Rational(int numerator, int denominator)
-    {
-        this.numerator = numerator;
-        this.denominator = denominator;
+    public Rational(long numerator, long denominator)
+    {
+        _numerator = numerator;
+        _denominator = denominator;
     }
 
@@ -70,10 +58,10 @@
      * This may involve rounding.
      *
-     * @return  the numeric value represented by this object after conversion
-     *          to type <code>double</code>.
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>double</code>.
      */
     public double doubleValue()
     {
-        return (double)numerator / (double)denominator;
+        return (double) _numerator / (double) _denominator;
     }
 
@@ -82,10 +70,10 @@
      * This may involve rounding.
      *
-     * @return  the numeric value represented by this object after conversion
-     *          to type <code>float</code>.
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>float</code>.
      */
     public float floatValue()
     {
-        return (float)numerator / (float)denominator;
+        return (float) _numerator / (float) _denominator;
     }
 
@@ -95,10 +83,10 @@
      * casts the result of <code>doubleValue()</code> to <code>byte</code>.
      *
-     * @return  the numeric value represented by this object after conversion
-     *          to type <code>byte</code>.
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>byte</code>.
      */
     public final byte byteValue()
     {
-        return (byte)doubleValue();
+        return (byte) doubleValue();
     }
 
@@ -108,10 +96,10 @@
      * casts the result of <code>doubleValue()</code> to <code>int</code>.
      *
-     * @return  the numeric value represented by this object after conversion
-     *          to type <code>int</code>.
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>int</code>.
      */
     public final int intValue()
     {
-        return (int)doubleValue();
+        return (int) doubleValue();
     }
 
@@ -121,10 +109,10 @@
      * casts the result of <code>doubleValue()</code> to <code>long</code>.
      *
-     * @return  the numeric value represented by this object after conversion
-     *          to type <code>long</code>.
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>long</code>.
      */
     public final long longValue()
     {
-        return (long)doubleValue();
+        return (long) doubleValue();
     }
 
@@ -134,74 +122,66 @@
      * casts the result of <code>doubleValue()</code> to <code>short</code>.
      *
-     * @return  the numeric value represented by this object after conversion
-     *          to type <code>short</code>.
+     * @return the numeric value represented by this object after conversion
+     *         to type <code>short</code>.
      */
     public final short shortValue()
     {
-        return (short)doubleValue();
-    }
-
-
-    /**
-     * Returns the denominator.
-     */
-    public final int getDenominator()
-    {
-        return this.denominator;
-    }
-
-    /**
-     * Returns the numerator.
-     */
-    public final int getNumerator()
-    {
-        return this.numerator;
-    }
-
-    /**
-     * Returns the reciprocal value of this obejct as a new Rational.
+        return (short) doubleValue();
+    }
+
+
+    /** Returns the denominator. */
+    public final long getDenominator()
+    {
+        return this._denominator;
+    }
+
+    /** Returns the numerator. */
+    public final long getNumerator()
+    {
+        return this._numerator;
+    }
+
+    /**
+     * Returns the reciprocal value of this object as a new Rational.
+     *
      * @return the reciprocal in a new object
      */
+    @NotNull
     public Rational getReciprocal()
     {
-        return new Rational(this.denominator, this.numerator);
-    }
-
-    /**
-     * Checks if this rational number is an Integer, either positive or negative.
-     */
+        return new Rational(this._denominator, this._numerator);
+    }
+
+    /** Checks if this rational number is an Integer, either positive or negative. */
     public boolean isInteger()
     {
-        if (denominator == 1 ||
-                (denominator != 0 && (numerator % denominator == 0)) ||
-                (denominator == 0 && numerator == 0)
-        ) {
-            return true;
-        } else {
-            return false;
-        }
+        return _denominator == 1 ||
+                (_denominator != 0 && (_numerator % _denominator == 0)) ||
+                (_denominator == 0 && _numerator == 0);
     }
 
     /**
      * Returns a string representation of the object of form <code>numerator/denominator</code>.
-     * @return  a string representation of the object.
-     */
+     *
+     * @return a string representation of the object.
+     */
+    @NotNull
     public String toString()
     {
-        return numerator + "/" + denominator;
-    }
-
-    /**
-     * Returns the simplest represenation of this Rational's value possible.
-     */
+        return _numerator + "/" + _denominator;
+    }
+
+    /** Returns the simplest representation of this Rational's value possible. */
+    @NotNull
     public String toSimpleString(boolean allowDecimal)
     {
-        if (denominator == 0 && numerator != 0) {
+        if (_denominator == 0 && _numerator != 0) {
             return toString();
         } else if (isInteger()) {
             return Integer.toString(intValue());
-        } else if (numerator != 1 && denominator % numerator == 0) {
+        } else if (_numerator != 1 && _denominator % _numerator == 0) {
             // common factor between denominator and numerator
-            int newDenominator = denominator / numerator;
+            long newDenominator = _denominator / _numerator;
             return new Rational(1, newDenominator).toSimpleString(allowDecimal);
         } else {
@@ -220,9 +200,11 @@
      * Decides whether a brute-force simplification calculation should be avoided
      * by comparing the maximum number of possible calculations with some threshold.
+     *
      * @return true if the simplification should be performed, otherwise false
      */
     private boolean tooComplexForSimplification()
     {
-        double maxPossibleCalculations = (((double)(Math.min(denominator, numerator) - 1) / 5d) + 2);
+        double maxPossibleCalculations = (((double) (Math.min(_denominator, _numerator) - 1) / 5d) + 2);
+        final int maxSimplificationCalculations = 1000;
         return maxPossibleCalculations > maxSimplificationCalculations;
     }
@@ -231,15 +213,22 @@
      * Compares two <code>Rational</code> instances, returning true if they are mathematically
      * equivalent.
+     *
      * @param obj the Rational to compare this instance to.
      * @return true if instances are mathematically equivalent, otherwise false.  Will also
      *         return false if <code>obj</code> is not an instance of <code>Rational</code>.
      */
-    public boolean equals(Object obj)
-    {
-        if (!(obj instanceof Rational)) {
+    @Override
+    public boolean equals(@Nullable Object obj)
+    {
+        if (obj==null || !(obj instanceof Rational))
             return false;
-        }
-        Rational that = (Rational)obj;
+        Rational that = (Rational) obj;
         return this.doubleValue() == that.doubleValue();
+    }
+
+    @Override
+    public int hashCode()
+    {
+        return (23 * (int)_denominator) + (int)_numerator;
     }
 
@@ -252,5 +241,5 @@
      * To reduce a rational, need to see if both numerator and denominator are divisible
      * by a common factor.  Using the prime number series in ascending order guarantees
-     * the minimun number of checks required.</p>
+     * the minimum number of checks required.</p>
      * <p>
      * However, generating the prime number series seems to be a hefty task.  Perhaps
@@ -265,12 +254,14 @@
      *   -- * ------------------------------------ + 2
      *   10                    2
-     *
+     * <p/>
      *   Math.min(denominator, numerator) - 1
      * = ------------------------------------ + 2
      *                  5
      * </pre></code>
-     * @return a simplified instance, or if the Rational could not be simpliffied,
+     *
+     * @return a simplified instance, or if the Rational could not be simplified,
      *         returns itself (unchanged)
      */
+    @NotNull
     public Rational getSimplifiedInstance()
     {
@@ -278,11 +269,11 @@
             return this;
         }
-        for (int factor = 2; factor <= Math.min(denominator, numerator); factor++) {
+        for (int factor = 2; factor <= Math.min(_denominator, _numerator); factor++) {
             if ((factor % 2 == 0 && factor > 2) || (factor % 5 == 0 && factor > 5)) {
                 continue;
             }
-            if (denominator % factor == 0 && numerator % factor == 0) {
+            if (_denominator % factor == 0 && _numerator % factor == 0) {
                 // found a common factor
-                return new Rational(numerator / factor, denominator / factor);
+                return new Rational(_numerator / factor, _denominator / factor);
             }
         }
Index: trunk/src/com/drew/lang/StringUtil.java
===================================================================
--- trunk/src/com/drew/lang/StringUtil.java	(revision 6127)
+++ trunk/src/com/drew/lang/StringUtil.java	(revision 6127)
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.lang;
+
+import com.drew.lang.annotations.NotNull;
+
+import java.util.Iterator;
+
+/** @author Drew Noakes http://drewnoakes.com */
+public class StringUtil
+{
+    public static String join(@NotNull Iterable<? extends CharSequence> strings, @NotNull String delimiter)
+    {
+        int capacity = 0;
+        int delimLength = delimiter.length();
+
+        Iterator<? extends CharSequence> iter = strings.iterator();
+        if (iter.hasNext())
+            capacity += iter.next().length() + delimLength;
+
+        StringBuilder buffer = new StringBuilder(capacity);
+        iter = strings.iterator();
+        if (iter.hasNext()) {
+            buffer.append(iter.next());
+            while (iter.hasNext()) {
+                buffer.append(delimiter);
+                buffer.append(iter.next());
+            }
+        }
+        return buffer.toString();
+    }
+
+    public static <T extends CharSequence> String join(@NotNull T[] strings, @NotNull String delimiter)
+    {
+        int capacity = 0;
+        int delimLength = delimiter.length();
+        for (T value : strings)
+            capacity += value.length() + delimLength;
+
+        StringBuilder buffer = new StringBuilder(capacity);
+        boolean first = true;
+        for (T value : strings) {
+            if (!first) {
+                buffer.append(delimiter);
+            } else {
+                first = false;
+            }
+            buffer.append(value);
+        }
+        return buffer.toString();
+    }
+}
Index: trunk/src/com/drew/lang/annotations/NotNull.java
===================================================================
--- trunk/src/com/drew/lang/annotations/NotNull.java	(revision 6127)
+++ trunk/src/com/drew/lang/annotations/NotNull.java	(revision 6127)
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.lang.annotations;
+
+/**
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public @interface NotNull
+{
+}
Index: trunk/src/com/drew/lang/annotations/Nullable.java
===================================================================
--- trunk/src/com/drew/lang/annotations/Nullable.java	(revision 6127)
+++ trunk/src/com/drew/lang/annotations/Nullable.java	(revision 6127)
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.lang.annotations;
+
+/**
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public @interface Nullable
+{
+}
Index: trunk/src/com/drew/lang/annotations/SuppressWarnings.java
===================================================================
--- trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 6127)
+++ trunk/src/com/drew/lang/annotations/SuppressWarnings.java	(revision 6127)
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-2011 Andreas Ziermann
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.lang.annotations;
+
+/**
+ * Used to suppress specific code analysis warnings produced by the Findbugs tool.
+ *
+ * @author Andreas Ziermann
+ */
+public @interface SuppressWarnings
+{
+    /**
+     * The name of the warning to be suppressed.
+     * @return The name of the warning to be suppressed.
+     */
+    @NotNull String value();
+
+    /**
+     * An explanation of why it is valid to suppress the warning in a particular situation/context.
+     * @return An explanation of why it is valid to suppress the warning in a particular situation/context.
+     */
+    @NotNull String justification();
+}
Index: trunk/src/com/drew/metadata/Age.java
===================================================================
--- trunk/src/com/drew/metadata/Age.java	(revision 6127)
+++ trunk/src/com/drew/metadata/Age.java	(revision 6127)
@@ -0,0 +1,172 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * Represents an age in years, months, days, hours, minutes and seconds.
+ * <p/>
+ * Used by certain Panasonic cameras which have face recognition features.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class Age
+{
+    private int _years;
+    private int _months;
+    private int _days;
+    private int _hours;
+    private int _minutes;
+    private int _seconds;
+
+    /**
+     * Parses an age object from the string format used by Panasonic cameras:
+     * <code>0031:07:15 00:00:00</code>
+     * @param s The String in format <code>0031:07:15 00:00:00</code>.
+     * @return The parsed Age object, or null if the value could not be parsed
+     */
+    @Nullable
+    public static Age fromPanasonicString(@NotNull String s)
+    {
+        if (s == null)
+            throw new NullPointerException();
+
+        if (s.length() != 19 || s.startsWith("9999:99:99"))
+            return null;
+
+        try {
+            int years = Integer.parseInt(s.substring(0, 4));
+            int months = Integer.parseInt(s.substring(5, 7));
+            int days = Integer.parseInt(s.substring(8, 10));
+            int hours = Integer.parseInt(s.substring(11, 13));
+            int minutes = Integer.parseInt(s.substring(14, 16));
+            int seconds = Integer.parseInt(s.substring(17, 19));
+
+            return new Age(years, months, days, hours, minutes, seconds);
+        }
+        catch (NumberFormatException ignored)
+        {
+            return null;
+        }
+    }
+
+    public Age(int years, int months, int days, int hours, int minutes, int seconds)
+    {
+        _years = years;
+        _months = months;
+        _days = days;
+        _hours = hours;
+        _minutes = minutes;
+        _seconds = seconds;
+    }
+
+    public int getYears()
+    {
+        return _years;
+    }
+
+    public int getMonths()
+    {
+        return _months;
+    }
+
+    public int getDays()
+    {
+        return _days;
+    }
+
+    public int getHours()
+    {
+        return _hours;
+    }
+
+    public int getMinutes()
+    {
+        return _minutes;
+    }
+
+    public int getSeconds()
+    {
+        return _seconds;
+    }
+
+    @Override
+    public String toString()
+    {
+        return String.format("%04d:%02d:%02d %02d:%02d:%02d", _years, _months, _days, _hours, _minutes, _seconds);
+    }
+
+    public String toFriendlyString()
+    {
+        StringBuilder result = new StringBuilder();
+        appendAgePart(result, _years, "year");
+        appendAgePart(result, _months, "month");
+        appendAgePart(result, _days, "day");
+        appendAgePart(result, _hours, "hour");
+        appendAgePart(result, _minutes, "minute");
+        appendAgePart(result, _seconds, "second");
+        return result.toString();
+    }
+
+    private static void appendAgePart(StringBuilder result, final int num, final String singularName)
+    {
+        if (num == 0)
+            return;
+        if (result.length()!=0)
+            result.append(' ');
+        result.append(num).append(' ').append(singularName);
+        if (num != 1)
+            result.append('s');
+    }
+
+    @Override
+    public boolean equals(Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Age age = (Age)o;
+
+        if (_days != age._days) return false;
+        if (_hours != age._hours) return false;
+        if (_minutes != age._minutes) return false;
+        if (_months != age._months) return false;
+        if (_seconds != age._seconds) return false;
+        if (_years != age._years) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = _years;
+        result = 31 * result + _months;
+        result = 31 * result + _days;
+        result = 31 * result + _hours;
+        result = 31 * result + _minutes;
+        result = 31 * result + _seconds;
+        return result;
+    }
+}
Index: trunk/src/com/drew/metadata/DefaultTagDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/DefaultTagDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/DefaultTagDescriptor.java	(revision 6127)
@@ -1,30 +1,46 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 22-Nov-2002 16:45:19 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata;
 
+import com.drew.lang.annotations.NotNull;
+
 /**
+ * A default implementation of the abstract TagDescriptor.  As this class is not coded with awareness of any metadata
+ * tags, it simply reports tag names using the format 'Unknown tag 0x00' (with the corresponding tag number in hex)
+ * and gives descriptions using the default string representation of the value.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class DefaultTagDescriptor extends TagDescriptor
+public class DefaultTagDescriptor extends TagDescriptor<Directory>
 {
-    public DefaultTagDescriptor(Directory directory)
+    public DefaultTagDescriptor(@NotNull Directory directory)
     {
         super(directory);
     }
 
+    /**
+     * Gets a best-effort tag name using the format 'Unknown tag 0x00' (with the corresponding tag type in hex).
+     * @param tagType the tag type identifier.
+     * @return a string representation of the tag name.
+     */
+    @NotNull
     public String getTagName(int tagType)
     {
@@ -33,8 +49,3 @@
         return "Unknown tag 0x" + hex;
     }
-
-    public String getDescription(int tagType)
-    {
-        return _directory.getString(tagType);
-    }
 }
Index: trunk/src/com/drew/metadata/Directory.java
===================================================================
--- trunk/src/com/drew/metadata/Directory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/Directory.java	(revision 6127)
@@ -1,45 +1,49 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 25-Nov-2002 20:30:39 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata;
 
 import com.drew.lang.Rational;
-
-import java.io.Serializable;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.lang.annotations.SuppressWarnings;
+
+import java.io.UnsupportedEncodingException;
 import java.lang.reflect.Array;
 import java.text.DateFormat;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.Iterator;
-import java.util.List;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.*;
 
 /**
- * Base class for all Metadata directory types with supporting methods for setting and
- * getting tag values.
+ * Abstract base class for all directory implementations, having methods for getting and setting tag values of various
+ * data types.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public abstract class Directory implements Serializable
+public abstract class Directory
 {
-    /**
-     * Map of values hashed by type identifiers.
-     */
-    protected final HashMap _tagMap;
-
-    /**
-     * The descriptor used to interperet tag values.
-     */
-    protected TagDescriptor _descriptor;
+    // TODO get Array methods need to return cloned data, to maintain this directory's integrity
+
+    /** Map of values hashed by type identifiers. */
+    @NotNull
+    protected final Map<Integer, Object> _tagMap = new HashMap<Integer, Object>();
 
     /**
@@ -48,7 +52,12 @@
      * defined tags.
      */
-    protected final List _definedTagList;
-
-    private List _errorList;
+    @NotNull
+    protected final Collection<Tag> _definedTagList = new ArrayList<Tag>();
+
+    @NotNull
+    private final Collection<String> _errorList = new ArrayList<String>(4);
+
+    /** The descriptor used to interpret tag values. */
+    protected TagDescriptor _descriptor;
 
 // ABSTRACT METHODS
@@ -56,24 +65,20 @@
     /**
      * Provides the name of the directory, for display purposes.  E.g. <code>Exif</code>
+     *
      * @return the name of the directory
      */
+    @NotNull
     public abstract String getName();
 
     /**
      * Provides the map of tag names, hashed by tag type identifier.
+     *
      * @return the map of tag names
      */
-    protected abstract HashMap getTagNameMap();
-
-// CONSTRUCTORS
-
-    /**
-     * Creates a new Directory.
-     */
-    public Directory()
-    {
-        _tagMap = new HashMap();
-        _definedTagList = new ArrayList();
-    }
+    @NotNull
+    protected abstract HashMap<Integer, String> getTagNameMap();
+
+    protected Directory()
+    {}
 
 // VARIOUS METHODS
@@ -81,23 +86,28 @@
     /**
      * Indicates whether the specified tag type has been set.
+     *
      * @param tagType the tag type to check for
      * @return true if a value exists for the specified tag type, false if not
      */
+    @java.lang.SuppressWarnings({ "UnnecessaryBoxing" })
     public boolean containsTag(int tagType)
     {
-        return _tagMap.containsKey(new Integer(tagType));
+        return _tagMap.containsKey(Integer.valueOf(tagType));
     }
 
     /**
      * Returns an Iterator of Tag instances that have been set in this Directory.
+     *
      * @return an Iterator of Tag instances
      */
-    public Iterator getTagIterator()
-    {
-        return _definedTagList.iterator();
+    @NotNull
+    public Collection<Tag> getTags()
+    {
+        return _definedTagList;
     }
 
     /**
      * Returns the number of tags set in this Directory.
+     *
      * @return the number of tags set in this Directory
      */
@@ -108,33 +118,48 @@
 
     /**
-     * Sets the descriptor used to interperet tag values.
-     * @param descriptor the descriptor used to interperet tag values
-     */
-    public void setDescriptor(TagDescriptor descriptor)
-    {
-        if (descriptor==null) {
+     * Sets the descriptor used to interpret tag values.
+     *
+     * @param descriptor the descriptor used to interpret tag values
+     */
+    @java.lang.SuppressWarnings({ "ConstantConditions" })
+    public void setDescriptor(@NotNull TagDescriptor descriptor)
+    {
+        if (descriptor == null)
             throw new NullPointerException("cannot set a null descriptor");
-        }
         _descriptor = descriptor;
     }
 
-    public void addError(String message)
-    {
-        if (_errorList==null) {
-            _errorList = new ArrayList();
-        }
+    /**
+     * Registers an error message with this directory.
+     *
+     * @param message an error message.
+     */
+    public void addError(@NotNull String message)
+    {
         _errorList.add(message);
     }
 
+    /**
+     * Gets a value indicating whether this directory has any error messages.
+     *
+     * @return true if the directory contains errors, otherwise false
+     */
     public boolean hasErrors()
     {
-        return (_errorList!=null && _errorList.size()>0);
-    }
-
-    public Iterator getErrors()
-    {
-        return _errorList.iterator();
-    }
-
+        return _errorList.size() > 0;
+    }
+
+    /**
+     * Used to iterate over any error messages contained in this directory.
+     *
+     * @return an iterable collection of error message strings.
+     */
+    @NotNull
+    public Iterable<String> getErrors()
+    {
+        return _errorList;
+    }
+
+    /** Returns the count of error messages in this directory. */
     public int getErrorCount()
     {
@@ -145,119 +170,90 @@
 
     /**
-     * Sets an int value for the specified tag.
+     * Sets an <code>int</code> value for the specified tag.
+     *
      * @param tagType the tag's value as an int
-     * @param value the value for the specified tag as an int
+     * @param value   the value for the specified tag as an int
      */
     public void setInt(int tagType, int value)
     {
-        setObject(tagType, new Integer(value));
-    }
-
-    /**
-     * Sets a double value for the specified tag.
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets an <code>int[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param ints    the int array to store
+     */
+    public void setIntArray(int tagType, @NotNull int[] ints)
+    {
+        setObjectArray(tagType, ints);
+    }
+
+    /**
+     * Sets a <code>float</code> value for the specified tag.
+     *
      * @param tagType the tag's value as an int
-     * @param value the value for the specified tag as a double
+     * @param value   the value for the specified tag as a float
+     */
+    public void setFloat(int tagType, float value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>float[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param floats  the float array to store
+     */
+    public void setFloatArray(int tagType, @NotNull float[] floats)
+    {
+        setObjectArray(tagType, floats);
+    }
+
+    /**
+     * Sets a <code>double</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a double
      */
     public void setDouble(int tagType, double value)
     {
-        setObject(tagType, new Double(value));
-    }
-
-    /**
-     * Sets a float value for the specified tag.
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>double[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param doubles the double array to store
+     */
+    public void setDoubleArray(int tagType, @NotNull double[] doubles)
+    {
+        setObjectArray(tagType, doubles);
+    }
+
+    /**
+     * Sets a <code>String</code> value for the specified tag.
+     *
      * @param tagType the tag's value as an int
-     * @param value the value for the specified tag as a float
-     */
-    public void setFloat(int tagType, float value)
-    {
-        setObject(tagType, new Float(value));
-    }
-
-    /**
-     * Sets an int value for the specified tag.
-     * @param tagType the tag's value as an int
-     * @param value the value for the specified tag as a String
-     */
-    public void setString(int tagType, String value)
-    {
+     * @param value   the value for the specified tag as a String
+     */
+    @java.lang.SuppressWarnings({ "ConstantConditions" })
+    public void setString(int tagType, @NotNull String value)
+    {
+        if (value == null)
+            throw new NullPointerException("cannot set a null String");
         setObject(tagType, value);
     }
 
     /**
-     * Sets an int value for the specified tag.
-     * @param tagType the tag's value as an int
-     * @param value the value for the specified tag as a boolean
-     */
-    public void setBoolean(int tagType, boolean value)
-    {
-        setObject(tagType, new Boolean(value));
-    }
-
-    /**
-     * Sets a long value for the specified tag.
-     * @param tagType the tag's value as an int
-     * @param value the value for the specified tag as a long
-     */
-    public void setLong(int tagType, long value)
-    {
-        setObject(tagType, new Long(value));
-    }
-
-    /**
-     * Sets a java.util.Date value for the specified tag.
-     * @param tagType the tag's value as an int
-     * @param value the value for the specified tag as a java.util.Date
-     */
-    public void setDate(int tagType, java.util.Date value)
-    {
-        setObject(tagType, value);
-    }
-
-    /**
-     * Sets a Rational value for the specified tag.
-     * @param tagType the tag's value as an int
-     * @param rational rational number
-     */
-    public void setRational(int tagType, Rational rational)
-    {
-        setObject(tagType, rational);
-    }
-
-    /**
-     * Sets a Rational array for the specified tag.
-     * @param tagType the tag identifier
-     * @param rationals the Rational array to store
-     */
-    public void setRationalArray(int tagType, Rational[] rationals)
-    {
-        setObjectArray(tagType, rationals);
-    }
-
-    /**
-     * Sets an int array for the specified tag.
-     * @param tagType the tag identifier
-     * @param ints the int array to store
-     */
-    public void setIntArray(int tagType, int[] ints)
-    {
-        setObjectArray(tagType, ints);
-    }
-
-    /**
-     * Sets a byte array for the specified tag.
-     * @param tagType the tag identifier
-     * @param bytes the byte array to store
-     */
-    public void setByteArray(int tagType, byte[] bytes)
-    {
-        setObjectArray(tagType, bytes);
-    }
-
-    /**
-     * Sets a String array for the specified tag.
+     * Sets a <code>String[]</code> (array) for the specified tag.
+     *
      * @param tagType the tag identifier
      * @param strings the String array to store
      */
-    public void setStringArray(int tagType, String[] strings)
+    public void setStringArray(int tagType, @NotNull String[] strings)
     {
         setObjectArray(tagType, strings);
@@ -265,30 +261,100 @@
 
     /**
-     * Private helper method, containing common functionality for all 'add'
-     * methods.
+     * Sets a <code>boolean</code> value for the specified tag.
+     *
      * @param tagType the tag's value as an int
-     * @param value the value for the specified tag
+     * @param value   the value for the specified tag as a boolean
+     */
+    public void setBoolean(int tagType, boolean value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>long</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a long
+     */
+    public void setLong(int tagType, long value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>java.util.Date</code> value for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag as a java.util.Date
+     */
+    public void setDate(int tagType, @NotNull java.util.Date value)
+    {
+        setObject(tagType, value);
+    }
+
+    /**
+     * Sets a <code>Rational</code> value for the specified tag.
+     *
+     * @param tagType  the tag's value as an int
+     * @param rational rational number
+     */
+    public void setRational(int tagType, @NotNull Rational rational)
+    {
+        setObject(tagType, rational);
+    }
+
+    /**
+     * Sets a <code>Rational[]</code> (array) for the specified tag.
+     *
+     * @param tagType   the tag identifier
+     * @param rationals the Rational array to store
+     */
+    public void setRationalArray(int tagType, @NotNull Rational[] rationals)
+    {
+        setObjectArray(tagType, rationals);
+    }
+
+    /**
+     * Sets a <code>byte[]</code> (array) for the specified tag.
+     *
+     * @param tagType the tag identifier
+     * @param bytes   the byte array to store
+     */
+    public void setByteArray(int tagType, @NotNull byte[] bytes)
+    {
+        setObjectArray(tagType, bytes);
+    }
+
+    /**
+     * Sets a <code>Object</code> for the specified tag.
+     *
+     * @param tagType the tag's value as an int
+     * @param value   the value for the specified tag
      * @throws NullPointerException if value is <code>null</code>
      */
-    public void setObject(int tagType, Object value)
-    {
-        if (value==null) {
+    @java.lang.SuppressWarnings( { "ConstantConditions", "UnnecessaryBoxing" })
+    public void setObject(int tagType, @NotNull Object value)
+    {
+        if (value == null)
             throw new NullPointerException("cannot set a null object");
-        }
-
-        Integer key = new Integer(tagType);
-        if (!_tagMap.containsKey(key)) {
+
+        if (!_tagMap.containsKey(Integer.valueOf(tagType))) {
             _definedTagList.add(new Tag(tagType, this));
         }
-        _tagMap.put(key, value);
-    }
-
-    /**
-     * Private helper method, containing common functionality for all 'add...Array'
-     * methods.
+//        else {
+//            final Object oldValue = _tagMap.get(tagType);
+//            if (!oldValue.equals(value))
+//                addError(String.format("Overwritten tag 0x%s (%s).  Old=%s, New=%s", Integer.toHexString(tagType), getTagName(tagType), oldValue, value));
+//        }
+        _tagMap.put(tagType, value);
+    }
+
+    /**
+     * Sets an array <code>Object</code> for the specified tag.
+     *
      * @param tagType the tag's value as an int
-     * @param array the array of values for the specified tag
-     */
-    public void setObjectArray(int tagType, Object array)
+     * @param array   the array of values for the specified tag
+     */
+    public void setObjectArray(int tagType, @NotNull Object array)
     {
         // for now, we don't do anything special -- this method might be a candidate for removal once the dust settles
@@ -299,12 +365,54 @@
 
     /**
-     * Returns the specified tag's value as an int, if possible.
+     * Returns the specified tag's value as an int, if possible.  Every attempt to represent the tag's value as an int
+     * is taken.  Here is a list of the action taken depending upon the tag's original type:
+     * <ul>
+     * <li> int - Return unchanged.
+     * <li> Number - Return an int value (real numbers are truncated).
+     * <li> Rational - Truncate any fractional part and returns remaining int.
+     * <li> String - Attempt to parse string as an int.  If this fails, convert the char[] to an int (using shifts and OR).
+     * <li> Rational[] - Return int value of first item in array.
+     * <li> byte[] - Return int value of first item in array.
+     * <li> int[] - Return int value of first item in array.
+     * </ul>
+     *
+     * @throws MetadataException if no value exists for tagType or if it cannot be converted to an int.
      */
     public int getInt(int tagType) throws MetadataException
     {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof String) {
+        Integer integer = getInteger(tagType);
+        if (integer!=null)
+            return integer;
+
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to int.  It is of type '" + o.getClass() + "'.");
+    }
+
+    /**
+     * Returns the specified tag's value as an Integer, if possible.  Every attempt to represent the tag's value as an
+     * Integer is taken.  Here is a list of the action taken depending upon the tag's original type:
+     * <ul>
+     * <li> int - Return unchanged
+     * <li> Number - Return an int value (real numbers are truncated)
+     * <li> Rational - Truncate any fractional part and returns remaining int
+     * <li> String - Attempt to parse string as an int.  If this fails, convert the char[] to an int (using shifts and OR)
+     * <li> Rational[] - Return int value of first item in array if length &gt; 0
+     * <li> byte[] - Return int value of first item in array if length &gt; 0
+     * <li> int[] - Return int value of first item in array if length &gt; 0
+     * </ul>
+     *
+     * If the value is not found or cannot be converted to int, <code>null</code> is returned.
+     */
+    @Nullable
+    public Integer getInteger(int tagType)
+    {
+        Object o = getObject(tagType);
+
+        if (o == null)
+            return null;
+
+        if (o instanceof String) {
             try {
                 return Integer.parseInt((String)o);
@@ -314,7 +422,7 @@
                 byte[] bytes = s.getBytes();
                 long val = 0;
-                for (int i = 0; i < bytes.length; i++) {
+                for (byte aByte : bytes) {
                     val = val << 8;
-                    val += bytes[i];
+                    val += (aByte & 0xff);
                 }
                 return (int)val;
@@ -324,121 +432,120 @@
         } else if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
-            if (rationals.length==1)
+            if (rationals.length == 1)
                 return rationals[0].intValue();
         } else if (o instanceof byte[]) {
             byte[] bytes = (byte[])o;
-            if (bytes.length==1)
-                return bytes[0];
+            if (bytes.length == 1)
+                return (int)bytes[0];
         } else if (o instanceof int[]) {
             int[] ints = (int[])o;
-            if (ints.length==1)
+            if (ints.length == 1)
                 return ints[0];
         }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to int.  It is of type '" + o.getClass() + "'.");
-    }
-
-    // TODO get Array methods need to return cloned data, to maintain this directory's integrity
+        return null;
+    }
 
     /**
      * Gets the specified tag's value as a String array, if possible.  Only supported
      * where the tag is set as String[], String, int[], byte[] or Rational[].
+     *
      * @param tagType the tag identifier
-     * @return the tag's value as an array of Strings
-     * @throws MetadataException if the tag has not been set or cannot be represented
-     *         as a String[]
-     */
-    public String[] getStringArray(int tagType) throws MetadataException
-    {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof String[]) {
+     * @return the tag's value as an array of Strings. If the value is unset or cannot be converted, <code>null</code> is returned.
+     */
+    @Nullable
+    public String[] getStringArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof String[])
             return (String[])o;
-        } else if (o instanceof String) {
-            String[] strings = {(String)o};
-            return strings;
-        } else if (o instanceof int[]) {
+        if (o instanceof String)
+            return new String[] { (String)o };
+        if (o instanceof int[]) {
             int[] ints = (int[])o;
             String[] strings = new String[ints.length];
-            for (int i = 0; i<strings.length; i++) {
+            for (int i = 0; i < strings.length; i++)
                 strings[i] = Integer.toString(ints[i]);
-            }
             return strings;
         } else if (o instanceof byte[]) {
             byte[] bytes = (byte[])o;
             String[] strings = new String[bytes.length];
-            for (int i = 0; i<strings.length; i++) {
+            for (int i = 0; i < strings.length; i++)
                 strings[i] = Byte.toString(bytes[i]);
-            }
             return strings;
         } else if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
             String[] strings = new String[rationals.length];
-            for (int i = 0; i<strings.length; i++) {
+            for (int i = 0; i < strings.length; i++)
                 strings[i] = rationals[i].toSimpleString(false);
-            }
             return strings;
         }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to an String array.  It is of type '" + o.getClass() + "'.");
+        return null;
     }
 
     /**
      * Gets the specified tag's value as an int array, if possible.  Only supported
-     * where the tag is set as String, int[], byte[] or Rational[].
+     * where the tag is set as String, Integer, int[], byte[] or Rational[].
+     *
      * @param tagType the tag identifier
      * @return the tag's value as an int array
-     * @throws MetadataException if the tag has not been set, or cannot be converted to
-     *         an int array
-     */
-    public int[] getIntArray(int tagType) throws MetadataException
-    {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof Rational[]) {
+     */
+    @Nullable
+    public int[] getIntArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
             int[] ints = new int[rationals.length];
-            for (int i = 0; i<ints.length; i++) {
+            for (int i = 0; i < ints.length; i++) {
                 ints[i] = rationals[i].intValue();
             }
             return ints;
-        } else if (o instanceof int[]) {
+        }
+        if (o instanceof int[])
             return (int[])o;
-        } else if (o instanceof byte[]) {
+        if (o instanceof byte[]) {
             byte[] bytes = (byte[])o;
             int[] ints = new int[bytes.length];
-            for (int i = 0; i<bytes.length; i++) {
+            for (int i = 0; i < bytes.length; i++) {
                 byte b = bytes[i];
                 ints[i] = b;
             }
             return ints;
-        } else if (o instanceof String) {
-            String str = (String)o;
+        }
+        if (o instanceof CharSequence) {
+            CharSequence str = (CharSequence)o;
             int[] ints = new int[str.length()];
-            for (int i = 0; i<str.length(); i++) {
+            for (int i = 0; i < str.length(); i++) {
                 ints[i] = str.charAt(i);
             }
             return ints;
         }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to an int array.  It is of type '" + o.getClass() + "'.");
+        if (o instanceof Integer)
+            return new int[] { (Integer)o };
+        
+        return null;
     }
 
     /**
      * Gets the specified tag's value as an byte array, if possible.  Only supported
-     * where the tag is set as String, int[], byte[] or Rational[].
+     * where the tag is set as String, Integer, int[], byte[] or Rational[].
+     *
      * @param tagType the tag identifier
      * @return the tag's value as a byte array
-     * @throws MetadataException if the tag has not been set, or cannot be converted to
-     *         a byte array
-     */
-    public byte[] getByteArray(int tagType) throws MetadataException
-    {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
+     */
+    @Nullable
+    public byte[] getByteArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null) {
+            return null;
         } else if (o instanceof Rational[]) {
             Rational[] rationals = (Rational[])o;
             byte[] bytes = new byte[rationals.length];
-            for (int i = 0; i<bytes.length; i++) {
+            for (int i = 0; i < bytes.length; i++) {
                 bytes[i] = rationals[i].byteValue();
             }
@@ -449,156 +556,237 @@
             int[] ints = (int[])o;
             byte[] bytes = new byte[ints.length];
-            for (int i = 0; i<ints.length; i++) {
+            for (int i = 0; i < ints.length; i++) {
                 bytes[i] = (byte)ints[i];
             }
             return bytes;
-        } else if (o instanceof String) {
-            String str = (String)o;
+        } else if (o instanceof CharSequence) {
+            CharSequence str = (CharSequence)o;
             byte[] bytes = new byte[str.length()];
-            for (int i = 0; i<str.length(); i++) {
+            for (int i = 0; i < str.length(); i++) {
                 bytes[i] = (byte)str.charAt(i);
             }
             return bytes;
         }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to a byte array.  It is of type '" + o.getClass() + "'.");
-    }
-
-    /**
-     * Returns the specified tag's value as a double, if possible.
-     */
+        if (o instanceof Integer)
+            return new byte[] { ((Integer)o).byteValue() };
+
+        return null;
+    }
+
+    /** Returns the specified tag's value as a double, if possible. */
     public double getDouble(int tagType) throws MetadataException
     {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof String) {
+        Double value = getDoubleObject(tagType);
+        if (value!=null)
+            return value;
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to a double.  It is of type '" + o.getClass() + "'.");
+    }
+    /** Returns the specified tag's value as a Double.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Double getDoubleObject(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof String) {
             try {
                 return Double.parseDouble((String)o);
             } catch (NumberFormatException nfe) {
-                throw new MetadataException("unable to parse string " + o + " as a double", nfe);
-            }
-        } else if (o instanceof Number) {
+                return null;
+            }
+        }
+        if (o instanceof Number)
             return ((Number)o).doubleValue();
-        }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to a double.  It is of type '" + o.getClass() + "'.");
-    }
-
-    /**
-     * Returns the specified tag's value as a float, if possible.
-     */
+
+        return null;
+    }
+
+    /** Returns the specified tag's value as a float, if possible. */
     public float getFloat(int tagType) throws MetadataException
     {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof String) {
+        Float value = getFloatObject(tagType);
+        if (value!=null)
+            return value;
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to a float.  It is of type '" + o.getClass() + "'.");
+    }
+
+    /** Returns the specified tag's value as a float.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Float getFloatObject(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof String) {
             try {
                 return Float.parseFloat((String)o);
             } catch (NumberFormatException nfe) {
-                throw new MetadataException("unable to parse string " + o + " as a float", nfe);
-            }
-        } else if (o instanceof Number) {
+                return null;
+            }
+        }
+        if (o instanceof Number)
             return ((Number)o).floatValue();
-        }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to a float.  It is of type '" + o.getClass() + "'.");
-    }
-
-    /**
-     * Returns the specified tag's value as a long, if possible.
-     */
+        return null;
+    }
+
+    /** Returns the specified tag's value as a long, if possible. */
     public long getLong(int tagType) throws MetadataException
     {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof String) {
+        Long value = getLongObject(tagType);
+        if (value!=null)
+            return value;
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to a long.  It is of type '" + o.getClass() + "'.");
+    }
+
+    /** Returns the specified tag's value as a long.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Long getLongObject(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof String) {
             try {
                 return Long.parseLong((String)o);
             } catch (NumberFormatException nfe) {
-                throw new MetadataException("unable to parse string " + o + " as a long", nfe);
-            }
-        } else if (o instanceof Number) {
+                return null;
+            }
+        }
+        if (o instanceof Number)
             return ((Number)o).longValue();
-        }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to a long.  It is of type '" + o.getClass() + "'.");
-    }
-
-    /**
-     * Returns the specified tag's value as a boolean, if possible.
-     */
+        return null;
+    }
+
+    /** Returns the specified tag's value as a boolean, if possible. */
     public boolean getBoolean(int tagType) throws MetadataException
     {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof Boolean) {
-            return ((Boolean)o).booleanValue();
-        } else if (o instanceof String) {
+        Boolean value = getBooleanObject(tagType);
+        if (value!=null)
+            return value;
+        Object o = getObject(tagType);
+        if (o == null)
+            throw new MetadataException("Tag '" + getTagName(tagType) + "' has not been set -- check using containsTag() first");
+        throw new MetadataException("Tag '" + tagType + "' cannot be converted to a boolean.  It is of type '" + o.getClass() + "'.");
+    }
+
+    /** Returns the specified tag's value as a boolean.  If the tag is not set or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    @SuppressWarnings(value = "NP_BOOLEAN_RETURN_NULL", justification = "keep API interface consistent")
+    public Boolean getBooleanObject(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+        if (o instanceof Boolean)
+            return (Boolean)o;
+        if (o instanceof String) {
             try {
                 return Boolean.getBoolean((String)o);
             } catch (NumberFormatException nfe) {
-                throw new MetadataException("unable to parse string " + o + " as a boolean", nfe);
-            }
-        } else if (o instanceof Number) {
-            return (((Number)o).doubleValue()!=0);
-        }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to a boolean.  It is of type '" + o.getClass() + "'.");
-    }
-
-    /**
-     * Returns the specified tag's value as a java.util.Date, if possible.
-     */
-    public java.util.Date getDate(int tagType) throws MetadataException
-    {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof java.util.Date) {
+                return null;
+            }
+        }
+        if (o instanceof Number)
+            return (((Number)o).doubleValue() != 0);
+        return null;
+    }
+
+    /**
+     * Returns the specified tag's value as a java.util.Date.  If the value is unset or cannot be converted, <code>null</code> is returned.
+     * <p/>
+     * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in
+     * the current {@link TimeZone}.  If the {@link TimeZone} is known, call the overload that accepts one as an argument.
+     */
+    @Nullable
+    public java.util.Date getDate(int tagType)
+    {
+        return getDate(tagType, null);
+    }
+    
+    /**
+     * Returns the specified tag's value as a java.util.Date.  If the value is unset or cannot be converted, <code>null</code> is returned.
+     * <p/>
+     * If the underlying value is a {@link String}, then attempts will be made to parse the string as though it is in
+     * the {@link TimeZone} represented by the {@code timeZone} parameter (if it is non-null).  Note that this parameter
+     * is only considered if the underlying value is a string and parsing occurs, otherwise it has no effect.
+     */
+    @Nullable
+    public java.util.Date getDate(int tagType, @Nullable TimeZone timeZone)
+    {
+        Object o = getObject(tagType);
+
+        if (o == null)
+            return null;
+
+        if (o instanceof java.util.Date)
             return (java.util.Date)o;
-        } else if (o instanceof String) {
-            // add new dateformat strings to make this method even smarter
-            // so far, this seems to cover all known date strings
-            // (for example, AM and PM strings are not supported...)
+
+        if (o instanceof String) {
+            // This seems to cover all known Exif date strings
+            // Note that "    :  :     :  :  " is a valid date string according to the Exif spec (which means 'unknown date'): http://www.awaresystems.be/imaging/tiff/tifftags/privateifd/exif/datetimeoriginal.html
             String datePatterns[] = {
-                "yyyy:MM:dd HH:mm:ss",
-                "yyyy:MM:dd HH:mm",
-                "yyyy-MM-dd HH:mm:ss",
-                "yyyy-MM-dd HH:mm"};
+                    "yyyy:MM:dd HH:mm:ss",
+                    "yyyy:MM:dd HH:mm",
+                    "yyyy-MM-dd HH:mm:ss",
+                    "yyyy-MM-dd HH:mm",
+                    "yyyy.MM.dd HH:mm:ss",
+                    "yyyy.MM.dd HH:mm" };
             String dateString = (String)o;
-            for (int i = 0; i<datePatterns.length; i++) {
+            for (String datePattern : datePatterns) {
                 try {
-                    DateFormat parser = new java.text.SimpleDateFormat(datePatterns[i]);
+                    DateFormat parser = new SimpleDateFormat(datePattern);
+                    if (timeZone != null)
+                        parser.setTimeZone(timeZone);
                     return parser.parse(dateString);
-                } catch (java.text.ParseException ex) {
+                } catch (ParseException ex) {
                     // simply try the next pattern
                 }
             }
         }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to a java.util.Date.  It is of type '" + o.getClass() + "'.");
-    }
-
-    /**
-     * Returns the specified tag's value as a Rational, if possible.
-     */
-    public Rational getRational(int tagType) throws MetadataException
-    {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof Rational) {
+        return null;
+    }
+
+    /** Returns the specified tag's value as a Rational.  If the value is unset or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Rational getRational(int tagType)
+    {
+        Object o = getObject(tagType);
+
+        if (o == null)
+            return null;
+
+        if (o instanceof Rational)
             return (Rational)o;
-        }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to a Rational.  It is of type '" + o.getClass() + "'.");
-    }
-
-    public Rational[] getRationalArray(int tagType) throws MetadataException
-    {
-        Object o = getObject(tagType);
-        if (o==null) {
-            throw new MetadataException("Tag " + getTagName(tagType) + " has not been set -- check using containsTag() first");
-        } else if (o instanceof Rational[]) {
+        if (o instanceof Integer)
+            return new Rational((Integer)o, 1);
+        if (o instanceof Long)
+            return new Rational((Long)o, 1);
+
+        // NOTE not doing conversions for real number types
+
+        return null;
+    }
+
+    /** Returns the specified tag's value as an array of Rational.  If the value is unset or cannot be converted, <code>null</code> is returned. */
+    @Nullable
+    public Rational[] getRationalArray(int tagType)
+    {
+        Object o = getObject(tagType);
+        if (o == null)
+            return null;
+
+        if (o instanceof Rational[])
             return (Rational[])o;
-        }
-        throw new MetadataException("Tag '" + tagType + "' cannot be cast to a Rational array.  It is of type '" + o.getClass() + "'.");
+
+        return null;
     }
 
@@ -606,11 +794,13 @@
      * Returns the specified tag's value as a String.  This value is the 'raw' value.  A more presentable decoding
      * of this value may be obtained from the corresponding Descriptor.
-     * @return the String reprensentation of the tag's value, or
+     *
+     * @return the String representation of the tag's value, or
      *         <code>null</code> if the tag hasn't been defined.
      */
+    @Nullable
     public String getString(int tagType)
     {
         Object o = getObject(tagType);
-        if (o==null)
+        if (o == null)
             return null;
 
@@ -618,34 +808,67 @@
             return ((Rational)o).toSimpleString(true);
 
-        if (o.getClass().isArray())
-        {
+        if (o.getClass().isArray()) {
             // handle arrays of objects and primitives
             int arrayLength = Array.getLength(o);
-            // determine if this is an array of objects i.e. [Lcom.drew.blah
-            boolean isObjectArray = o.getClass().toString().startsWith("class [L");
-            StringBuffer sbuffer = new StringBuffer();
-            for (int i = 0; i<arrayLength; i++)
-            {
-                if (i!=0)
-                    sbuffer.append(' ');
+            final Class<?> componentType = o.getClass().getComponentType();
+            boolean isObjectArray = Object.class.isAssignableFrom(componentType);
+            boolean isFloatArray = componentType.getName().equals("float");
+            boolean isDoubleArray = componentType.getName().equals("double");
+            boolean isIntArray = componentType.getName().equals("int");
+            boolean isLongArray = componentType.getName().equals("long");
+            boolean isByteArray = componentType.getName().equals("byte");
+            StringBuilder string = new StringBuilder();
+            for (int i = 0; i < arrayLength; i++) {
+                if (i != 0)
+                    string.append(' ');
                 if (isObjectArray)
-                    sbuffer.append(Array.get(o, i).toString());
+                    string.append(Array.get(o, i).toString());
+                else if (isIntArray)
+                    string.append(Array.getInt(o, i));
+                else if (isLongArray)
+                    string.append(Array.getLong(o, i));
+                else if (isFloatArray)
+                    string.append(Array.getFloat(o, i));
+                else if (isDoubleArray)
+                    string.append(Array.getDouble(o, i));
+                else if (isByteArray)
+                    string.append(Array.getByte(o, i));
                 else
-                    sbuffer.append(Array.getInt(o, i));
-            }
-            return sbuffer.toString();
-        }
-
+                    addError("Unexpected array component type: " + componentType.getName());
+            }
+            return string.toString();
+        }
+
+        // Note that several cameras leave trailing spaces (Olympus, Nikon) but this library is intended to show
+        // the actual data within the file.  It is not inconceivable that whitespace may be significant here, so we
+        // do not trim.  Also, if support is added for writing data back to files, this may cause issues.
+        // We leave trimming to the presentation layer.
         return o.toString();
     }
 
+    @Nullable
+    public String getString(int tagType, String charset)
+    {
+        byte[] bytes = getByteArray(tagType);
+        if (bytes==null)
+            return null;
+        try {
+            return new String(bytes, charset);
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+    }
+
     /**
      * Returns the object hashed for the particular tag type specified, if available.
+     *
      * @param tagType the tag type identifier
-     * @return the tag's value as an Object if available, else null
-     */
+     * @return the tag's value as an Object if available, else <code>null</code>
+     */
+    @java.lang.SuppressWarnings({ "UnnecessaryBoxing" })
+    @Nullable
     public Object getObject(int tagType)
     {
-        return _tagMap.get(new Integer(tagType));
+        return _tagMap.get(Integer.valueOf(tagType));
     }
 
@@ -654,19 +877,20 @@
     /**
      * Returns the name of a specified tag as a String.
+     *
      * @param tagType the tag type identifier
      * @return the tag's name as a String
      */
+    @NotNull
     public String getTagName(int tagType)
     {
-        Integer key = new Integer(tagType);
-        HashMap nameMap = getTagNameMap();
-        if (!nameMap.containsKey(key)) {
+        HashMap<Integer, String> nameMap = getTagNameMap();
+        if (!nameMap.containsKey(tagType)) {
             String hex = Integer.toHexString(tagType);
-            while (hex.length()<4) {
+            while (hex.length() < 4) {
                 hex = "0" + hex;
             }
             return "Unknown tag (0x" + hex + ")";
         }
-        return (String)nameMap.get(key);
+        return nameMap.get(tagType);
     }
 
@@ -674,15 +898,12 @@
      * Provides a description of a tag's value using the descriptor set by
      * <code>setDescriptor(Descriptor)</code>.
+     *
      * @param tagType the tag type identifier
      * @return the tag value's description as a String
-     * @throws MetadataException if a descriptor hasn't been set, or if an error
-     * occurs during calculation of the description within the Descriptor
-     */
-    public String getDescription(int tagType) throws MetadataException
-    {
-        if (_descriptor==null) {
-            throw new MetadataException("a descriptor must be set using setDescriptor(...) before descriptions can be provided");
-        }
-
+     */
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        assert(_descriptor != null);
         return _descriptor.getDescription(tagType);
     }
Index: trunk/src/com/drew/metadata/Face.java
===================================================================
--- trunk/src/com/drew/metadata/Face.java	(revision 6127)
+++ trunk/src/com/drew/metadata/Face.java	(revision 6127)
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+package com.drew.metadata;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+/**
+ * Class to hold information about a detected or recognized face in a photo.
+ * <p/>
+ * When a face is <em>detected</em>, the camera believes that a face is present at a given location in
+ * the image, but is not sure whose face it is.  When a face is <em>recognised</em>, then the face is
+ * both detected and identified as belonging to a known person.
+ *
+ * @author Philipp Sandhaus, Drew Noakes
+ */
+public class Face
+{
+    private final int _x;
+    private final int _y;
+    private final int _width;
+    private final int _height;
+    @Nullable
+    private final String _name;
+    @Nullable
+    private final Age _age;
+
+    public Face(int x, int y, int width, int height, @Nullable String name, @Nullable Age age)
+    {
+        _x = x;
+        _y = y;
+        _width = width;
+        _height = height;
+        _name = name;
+        _age = age;
+    }
+
+    public int getX()
+    {
+        return _x;
+    }
+
+    public int getY()
+    {
+        return _y;
+    }
+
+    public int getWidth()
+    {
+        return _width;
+    }
+
+    public int getHeight()
+    {
+        return _height;
+    }
+
+    @Nullable
+    public String getName()
+    {
+        return _name;
+    }
+
+    @Nullable
+    public Age getAge()
+    {
+        return _age;
+    }
+
+    @Override
+    public boolean equals(@Nullable Object o)
+    {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        Face face = (Face)o;
+
+        if (_height != face._height) return false;
+        if (_width != face._width) return false;
+        if (_x != face._x) return false;
+        if (_y != face._y) return false;
+        if (_age != null ? !_age.equals(face._age) : face._age != null) return false;
+        if (_name != null ? !_name.equals(face._name) : face._name != null) return false;
+
+        return true;
+    }
+
+    @Override
+    public int hashCode()
+    {
+        int result = _x;
+        result = 31 * result + _y;
+        result = 31 * result + _width;
+        result = 31 * result + _height;
+        result = 31 * result + (_name != null ? _name.hashCode() : 0);
+        result = 31 * result + (_age != null ? _age.hashCode() : 0);
+        return result;
+    }
+
+    @NotNull
+    public String toString()
+    {
+        StringBuilder result = new StringBuilder();
+        result.append("x: ").append(_x);
+        result.append(" y: ").append(_y);
+        result.append(" width: ").append(_width);
+        result.append(" height: ").append(_height);
+        if (_name != null)
+            result.append(" name: ").append(_name);
+        if (_age != null)
+            result.append(" age: ").append(_age.toFriendlyString());
+        return result.toString();
+    }
+}
Index: trunk/src/com/drew/metadata/Metadata.java
===================================================================
--- trunk/src/com/drew/metadata/Metadata.java	(revision 6002)
+++ trunk/src/com/drew/metadata/Metadata.java	(revision 6127)
@@ -1,85 +1,72 @@
 /*
- * Metadata.java
+ * Copyright 2002-2012 Drew Noakes
  *
- * This class is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  Similarly, I release this Java version under the
- * same license, though I do ask that you leave this header in tact.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew.noakes@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
  *
- * Created on 28 April 2002, 17:40
- * Modified 04 Aug 2002
- * - Adjusted javadoc
- * - Added
- * Modified 29 Oct 2002 (v1.2)
- * - Stored IFD directories in separate tag-spaces
- * - iterator() now returns an Iterator over a list of TagValue objects
- * - More get*Description() methods to detail GPS tags, among others
- * - Put spaces between words of tag name for presentation reasons (they had no
- *   significance in compound form)
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata;
 
-import java.io.Serializable;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.HashMap;
-import java.util.Iterator;
+import java.util.Map;
 
 /**
- * Result from an exif extraction operation, containing all tags, their
- * values and support for retrieving them.
- * @author  Drew Noakes http://drewnoakes.com
+ * A top-level object to hold the various types of metadata (Exif/IPTC/etc) related to one entity (such as a file
+ * or stream).
+ * <p/>
+ * Metadata objects may contain zero or more directories.  Each directory may contain zero or more tags with
+ * corresponding values.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public final class Metadata implements Serializable
+public final class Metadata
 {
-    /**
-     *
-     */
-    private final HashMap directoryMap;
-
+    @NotNull
+    private final Map<Class<? extends Directory>,Directory> _directoryByClass = new HashMap<Class<? extends Directory>, Directory>();
+    
     /**
      * List of Directory objects set against this object.  Keeping a list handy makes
      * creation of an Iterator and counting tags simple.
      */
-    private final ArrayList directoryList;
+    @NotNull
+    private final Collection<Directory> _directoryList = new ArrayList<Directory>();
 
     /**
-     * Creates a new instance of Metadata.  Package private.
+     * Returns an objects for iterating over Directory objects in the order in which they were added.
+     *
+     * @return an iterable collection of directories
      */
-    public Metadata()
+    @NotNull
+    public Iterable<Directory> getDirectories()
     {
-        directoryMap = new HashMap();
-        directoryList = new ArrayList();
-    }
-
-
-// OTHER METHODS
-
-    /**
-     * Creates an Iterator over the tag types set against this image, preserving the order
-     * in which they were set.  Should the same tag have been set more than once, it's first
-     * position is maintained, even though the final value is used.
-     * @return an Iterator of tag types set for this image
-     */
-    public Iterator getDirectoryIterator()
-    {
-        return directoryList.iterator();
+        return _directoryList;
     }
 
     /**
      * Returns a count of unique directories in this metadata collection.
+     *
      * @return the number of unique directory types set for this metadata collection
      */
     public int getDirectoryCount()
     {
-        return directoryList.size();
+        return _directoryList.size();
     }
 
@@ -88,17 +75,20 @@
      * such a directory, it is returned.  Otherwise a new instance of this directory will be created and stored within
      * this Metadata object.
+     *
      * @param type the type of the Directory implementation required.
      * @return a directory of the specified type.
      */
-    public Directory getDirectory(Class type)
+    @NotNull
+    @SuppressWarnings("unchecked")
+    public <T extends Directory> T getOrCreateDirectory(@NotNull Class<T> type)
     {
-        if (!Directory.class.isAssignableFrom(type)) {
-            throw new RuntimeException("Class type passed to getDirectory must be an implementation of com.drew.metadata.Directory");
-        }
+        // We suppress the warning here as the code asserts a map signature of Class<T>,T.
+        // So after get(Class<T>) it is for sure the result is from type T.
+
         // check if we've already issued this type of directory
-        if (directoryMap.containsKey(type)) {
-            return (Directory)directoryMap.get(type);
-        }
-        Object directory;
+        if (_directoryByClass.containsKey(type))
+            return (T)_directoryByClass.get(type);
+
+        T directory;
         try {
             directory = type.newInstance();
@@ -106,19 +96,54 @@
             throw new RuntimeException("Cannot instantiate provided Directory type: " + type.toString());
         }
-        // store the directory in case it's requested later
-        directoryMap.put(type, directory);
-        directoryList.add(directory);
-        return (Directory)directory;
+        // store the directory
+        _directoryByClass.put(type, directory);
+        _directoryList.add(directory);
+
+        return directory;
+    }
+
+    /**
+     * If this <code>Metadata</code> object contains a <code>Directory</code> of the specified type, it is returned.
+     * Otherwise <code>null</code> is returned.
+     *
+     * @param type the Directory type
+     * @param <T> the Directory type
+     * @return a Directory of type T if it exists in this Metadata object, otherwise <code>null</code>.
+     */
+    @Nullable
+    @SuppressWarnings("unchecked")
+    public <T extends Directory> T getDirectory(@NotNull Class<T> type)
+    {
+        // We suppress the warning here as the code asserts a map signature of Class<T>,T.
+        // So after get(Class<T>) it is for sure the result is from type T.
+
+        return (T)_directoryByClass.get(type);
     }
 
     /**
      * Indicates whether a given directory type has been created in this metadata
-     * repository.  Directories are created by calling getDirectory(Class).
+     * repository.  Directories are created by calling <code>getOrCreateDirectory(Class)</code>.
+     *
      * @param type the Directory type
      * @return true if the metadata directory has been created
      */
-    public boolean containsDirectory(Class type)
+    public boolean containsDirectory(Class<? extends Directory> type)
     {
-        return directoryMap.containsKey(type);
+        return _directoryByClass.containsKey(type);
+    }
+
+    /**
+     * Indicates whether any errors were reported during the reading of metadata values.
+     * This value will be true if Directory.hasErrors() is true for one of the contained Directory objects.
+     *
+     * @return whether one of the contained directories has an error
+     */
+    public boolean hasErrors()
+    {
+        for (Directory directory : _directoryList) {
+            if (directory.hasErrors())
+                return true;
+        }
+        return false;
     }
 }
Index: trunk/src/com/drew/metadata/MetadataException.java
===================================================================
--- trunk/src/com/drew/metadata/MetadataException.java	(revision 6002)
+++ trunk/src/com/drew/metadata/MetadataException.java	(revision 6127)
@@ -1,38 +1,47 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 13-Nov-2002 18:10:23 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata;
 
 import com.drew.lang.CompoundException;
+import com.drew.lang.annotations.Nullable;
 
 /**
+ * Base class for all metadata specific exceptions.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class MetadataException extends CompoundException
 {
-    public MetadataException(String msg)
+    private static final long serialVersionUID = 8612756143363919682L;
+
+    public MetadataException(@Nullable String msg)
     {
         super(msg);
     }
 
-    public MetadataException(Throwable exception)
+    public MetadataException(@Nullable Throwable exception)
     {
         super(exception);
     }
 
-    public MetadataException(String msg, Throwable innerException)
+    public MetadataException(@Nullable String msg, @Nullable Throwable innerException)
     {
         super(msg, innerException);
Index: trunk/src/com/drew/metadata/MetadataReader.java
===================================================================
--- trunk/src/com/drew/metadata/MetadataReader.java	(revision 6002)
+++ trunk/src/com/drew/metadata/MetadataReader.java	(revision 6127)
@@ -1,27 +1,44 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 26-Nov-2002 11:21:43 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata;
 
+import com.drew.lang.BufferReader;
+import com.drew.lang.annotations.NotNull;
+
 /**
+ * Interface through which all classes responsible for decoding a particular type of metadata may be called.
+ * Note that the data source is not specified on this interface.  Instead it is suggested that implementations
+ * take their data within a constructor.  Constructors might be overloaded to allow for different sources, such as
+ * files, streams and byte arrays.  As such, instances of implementations of this interface would be single-use and
+ * not thread-safe.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public interface MetadataReader
 {
-    public Metadata extract();
-
-    public Metadata extract(Metadata metadata);
+    /**
+     * Extract metadata from the source and merge it into an existing Metadata object.
+     *
+     * @param reader   The reader from which the metadata should be extracted.
+     * @param metadata The Metadata object into which extracted values should be merged.
+     */
+    public void extract(@NotNull final BufferReader reader, @NotNull final Metadata metadata);
 }
Index: trunk/src/com/drew/metadata/SampleUsage.java
===================================================================
--- trunk/src/com/drew/metadata/SampleUsage.java	(revision 6002)
+++ 	(revision )
@@ -1,120 +1,0 @@
-/*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 05-Nov-2002 18:57:14 using IntelliJ IDEA.
- */
-package com.drew.metadata;
-
-import com.drew.imaging.jpeg.JpegMetadataReader;
-import com.drew.imaging.jpeg.JpegProcessingException;
-import com.drew.imaging.jpeg.JpegSegmentReader;
-import com.drew.metadata.exif.ExifReader;
-import com.drew.metadata.iptc.IptcReader;
-
-import java.awt.image.BufferedImage;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.util.Iterator;
-
-/**
- *
- */
-public class SampleUsage
-{
-    /**
-     * Constructor which executes multiple sample usages, each of which return the same output.  This class showcases
-     * multiple usages of this metadata class library.
-     * @param fileName path to a jpeg file upon which to operate
-     */
-    public SampleUsage(String fileName)
-    {
-        File jpegFile = new File(fileName);
-
-        // There are multiple ways to get a Metadata object
-
-        // Approach 1
-        // This approach reads all types of known Jpeg metadata (at present,
-        // Exif and Iptc) in a single call.  In most cases, this is the most
-        // appropriate usage.
-        try {
-            Metadata metadata = JpegMetadataReader.readMetadata(jpegFile);
-            printImageTags(1, metadata);
-        } catch (JpegProcessingException e) {
-            System.err.println("error 1a");
-        }
-
-        // Approach 2
-        // This approach shows using individual MetadataReader implementations
-        // to read a file.  This is less efficient than approach 1, as the file
-        // is opened and read twice.
-        try {
-            Metadata metadata = new Metadata();
-            new ExifReader(jpegFile).extract(metadata);
-            new IptcReader(jpegFile).extract(metadata);
-            printImageTags(2, metadata);
-        } catch (JpegProcessingException jpe) {
-            System.err.println("error 2a");
-        }
-
-        // Approach 3
-        // As fast as approach 1 (this is what goes on inside the JpegMetadataReader's
-        // readMetadata() method), this code is handy if you want to look into other
-        // Jpeg segments too.
-        try {
-            JpegSegmentReader segmentReader = new JpegSegmentReader(jpegFile);
-            byte[] exifSegment = segmentReader.readSegment(JpegSegmentReader.SEGMENT_APP1);
-            byte[] iptcSegment = segmentReader.readSegment(JpegSegmentReader.SEGMENT_APPD);
-            Metadata metadata = new Metadata();
-            new ExifReader(exifSegment).extract(metadata);
-            new IptcReader(iptcSegment).extract(metadata);
-            printImageTags(3, metadata);
-        } catch (JpegProcessingException jpe) {
-            System.err.println("error 3a");
-        }
-    }
-
-    private void printImageTags(int approachCount, Metadata metadata)
-    {
-        System.out.println();
-        System.out.println("*** APPROACH " + approachCount + " ***");
-        System.out.println();
-        // iterate over the exif data and print to System.out
-        Iterator directories = metadata.getDirectoryIterator();
-        while (directories.hasNext()) {
-            Directory directory = (Directory)directories.next();
-            Iterator tags = directory.getTagIterator();
-            while (tags.hasNext()) {
-                Tag tag = (Tag)tags.next();
-                System.out.println(tag);
-            }
-            if (directory.hasErrors()) {
-                Iterator errors = directory.getErrors();
-                while (errors.hasNext()) {
-                    System.out.println("ERROR: " + errors.next());
-                }
-            }
-        }
-    }
-
-    /**
-     * Executes the sample usage program.
-     * @param args command line parameters
-     */
-    public static void main(String[] args)
-    {
-        new SampleUsage("src/com/drew/metadata/test/withIptcExifGps.jpg");
-    }
-}
Index: trunk/src/com/drew/metadata/Tag.java
===================================================================
--- trunk/src/com/drew/metadata/Tag.java	(revision 6002)
+++ trunk/src/com/drew/metadata/Tag.java	(revision 6127)
@@ -1,31 +1,40 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 26-Nov-2002 18:29:12 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata;
 
-import java.io.Serializable;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 
 /**
+ * Models a particular tag within a directory and provides methods for obtaining its value.  Note that a Tag instance is
+ * specific to a particular metadata extraction and cannot be reused.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class Tag implements Serializable
+public class Tag
 {
     private final int _tagType;
+    @NotNull
     private final Directory _directory;
 
-    public Tag(int tagType, Directory directory)
+    public Tag(int tagType, @NotNull Directory directory)
     {
         _tagType = tagType;
@@ -35,4 +44,5 @@
     /**
      * Gets the tag type as an int
+     *
      * @return the tag type as an int
      */
@@ -45,6 +55,8 @@
      * Gets the tag type in hex notation as a String with padded leading
      * zeroes if necessary (i.e. <code>0x100E</code>).
+     *
      * @return the tag type as a string in hexadecimal notation
      */
+    @NotNull
     public String getTagTypeHex()
     {
@@ -57,7 +69,9 @@
      * Get a description of the tag's value, considering enumerated values
      * and units.
+     *
      * @return a description of the tag's value
      */
-    public String getDescription() throws MetadataException
+    @Nullable
+    public String getDescription()
     {
         return _directory.getDescription(_tagType);
@@ -67,6 +81,8 @@
      * Get the name of the tag, such as <code>Aperture</code>, or
      * <code>InteropVersion</code>.
+     *
      * @return the tag's name
      */
+    @NotNull
     public String getTagName()
     {
@@ -77,6 +93,8 @@
      * Get the name of the directory in which the tag exists, such as
      * <code>Exif</code>, <code>GPS</code> or <code>Interoperability</code>.
+     *
      * @return name of the directory in which this tag exists
      */
+    @NotNull
     public String getDirectoryName()
     {
@@ -85,16 +103,14 @@
 
     /**
-     * A basic representation of the tag's type and value in format:
-     * <code>FNumber - F2.8</code>.
+     * A basic representation of the tag's type and value.  EG: <code>[FNumber] F2.8</code>.
+     *
      * @return the tag's type and value
      */
+    @NotNull
     public String toString()
     {
-        String description;
-        try {
-            description = getDescription();
-        } catch (MetadataException e) {
+        String description = getDescription();
+        if (description==null)
             description = _directory.getString(getTagType()) + " (unable to formulate description)";
-        }
         return "[" + _directory.getName() + "] " + getTagName() + " - " + description;
     }
Index: trunk/src/com/drew/metadata/TagDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/TagDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/TagDescriptor.java	(revision 6127)
@@ -1,30 +1,42 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata;
 
-import java.io.Serializable;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+
+import java.lang.reflect.Array;
 
 /**
  * Abstract base class for all tag descriptor classes.  Implementations are responsible for
- * providing the human-readable string represenation of tag values stored in a directory.
+ * providing the human-readable string representation of tag values stored in a directory.
  * The directory is provided to the tag descriptor via its constructor.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public abstract class TagDescriptor implements Serializable
+public abstract class TagDescriptor<T extends Directory>
 {
-    protected final Directory _directory;
+    @NotNull
+    protected final T _directory;
 
-    public TagDescriptor(Directory directory)
+    public TagDescriptor(@NotNull T directory)
     {
         _directory = directory;
@@ -34,13 +46,62 @@
      * Returns a descriptive value of the the specified tag for this image.
      * Where possible, known values will be substituted here in place of the raw
-     * tokens actually kept in the Exif segment.  If no substitution is
-     * available, the value provided by getString(int) will be returned.
-     * <p>
-     * This and getString(int) are the only 'get' methods that won't throw an
-     * exception.
+     * tokens actually kept in the metadata segment.  If no substitution is
+     * available, the value provided by <code>getString(tagType)</code> will be returned.
+     * 
      * @param tagType the tag to find a description for
      * @return a description of the image's value for the specified tag, or
      *         <code>null</code> if the tag hasn't been defined.
      */
-    public abstract String getDescription(int tagType) throws MetadataException;
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        Object object = _directory.getObject(tagType);
+
+        if (object==null)
+            return null;
+
+        // special presentation for long arrays
+        if (object.getClass().isArray()) {
+            final int length = Array.getLength(object);
+            if (length > 16) {
+                final String componentTypeName = object.getClass().getComponentType().getName();
+                return String.format("[%d %s%s]", length, componentTypeName, length==1 ? "" : "s");
+            }
+        }
+
+        // no special handling required, so use default conversion to a string
+        return _directory.getString(tagType);
+    }
+
+    /**
+     * Takes a series of 4 bytes from the specified offset, and converts these to a
+     * well-known version number, where possible.
+     * <p/>
+     * Two different formats are processed:
+     * <ul>
+     *     <li>[30 32 31 30] -&gt; 2.10</li>
+     *     <li>[0 1 0 0] -&gt; 1.00</li>
+     * </ul>
+     * @param components the four version values
+     * @param majorDigits the number of components to be 
+     * @return the version as a string of form "2.10" or null if the argument cannot be converted
+     */
+    @Nullable
+    public static String convertBytesToVersionString(@Nullable int[] components, final int majorDigits)
+    {
+        if (components==null)
+            return null;
+        StringBuilder version = new StringBuilder();
+        for (int i = 0; i < 4 && i < components.length; i++) {
+            if (i == majorDigits)
+                version.append('.');
+            char c = (char)components[i];
+            if (c < '0')
+                c += '0';
+            if (i == 0 && c=='0')
+                continue;
+            version.append(c);
+        }
+        return version.toString();
+    }
 }
Index: trunk/src/com/drew/metadata/exif/CanonMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/CanonMakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/CanonMakernoteDescriptor.java	(revision 6127)
@@ -1,124 +1,149 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 27-Nov-2002 10:12:05 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
+ * Provides human-readable string representations of tag values stored in a <code>CanonMakernoteDirectory</code>.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class CanonMakernoteDescriptor extends TagDescriptor
+public class CanonMakernoteDescriptor extends TagDescriptor<CanonMakernoteDirectory>
 {
-    public CanonMakernoteDescriptor(Directory directory)
+    public CanonMakernoteDescriptor(@NotNull CanonMakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_ACTIVITY:
+            case CanonMakernoteDirectory.TAG_CANON_SERIAL_NUMBER:
+                return getSerialNumberDescription();
+            case CanonMakernoteDirectory.CameraSettings.TAG_FLASH_ACTIVITY:
                 return getFlashActivityDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_TYPE:
+            case CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_TYPE:
                 return getFocusTypeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_DIGITAL_ZOOM:
+            case CanonMakernoteDirectory.CameraSettings.TAG_DIGITAL_ZOOM:
                 return getDigitalZoomDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_QUALITY:
+            case CanonMakernoteDirectory.CameraSettings.TAG_QUALITY:
                 return getQualityDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_MACRO_MODE:
+            case CanonMakernoteDirectory.CameraSettings.TAG_MACRO_MODE:
                 return getMacroModeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_SELF_TIMER_DELAY:
+            case CanonMakernoteDirectory.CameraSettings.TAG_SELF_TIMER_DELAY:
                 return getSelfTimerDelayDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_MODE:
+            case CanonMakernoteDirectory.CameraSettings.TAG_FLASH_MODE:
                 return getFlashModeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_CONTINUOUS_DRIVE_MODE:
+            case CanonMakernoteDirectory.CameraSettings.TAG_CONTINUOUS_DRIVE_MODE:
                 return getContinuousDriveModeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_MODE_1:
+            case CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_MODE_1:
                 return getFocusMode1Description();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_IMAGE_SIZE:
+            case CanonMakernoteDirectory.CameraSettings.TAG_IMAGE_SIZE:
                 return getImageSizeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_EASY_SHOOTING_MODE:
+            case CanonMakernoteDirectory.CameraSettings.TAG_EASY_SHOOTING_MODE:
                 return getEasyShootingModeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_CONTRAST:
+            case CanonMakernoteDirectory.CameraSettings.TAG_CONTRAST:
                 return getContrastDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_SATURATION:
+            case CanonMakernoteDirectory.CameraSettings.TAG_SATURATION:
                 return getSaturationDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_SHARPNESS:
+            case CanonMakernoteDirectory.CameraSettings.TAG_SHARPNESS:
                 return getSharpnessDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_ISO:
+            case CanonMakernoteDirectory.CameraSettings.TAG_ISO:
                 return getIsoDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_METERING_MODE:
+            case CanonMakernoteDirectory.CameraSettings.TAG_METERING_MODE:
                 return getMeteringModeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_AF_POINT_SELECTED:
+            case CanonMakernoteDirectory.CameraSettings.TAG_AF_POINT_SELECTED:
                 return getAfPointSelectedDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_EXPOSURE_MODE:
+            case CanonMakernoteDirectory.CameraSettings.TAG_EXPOSURE_MODE:
                 return getExposureModeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_LONG_FOCAL_LENGTH:
+            case CanonMakernoteDirectory.CameraSettings.TAG_LONG_FOCAL_LENGTH:
                 return getLongFocalLengthDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_SHORT_FOCAL_LENGTH:
+            case CanonMakernoteDirectory.CameraSettings.TAG_SHORT_FOCAL_LENGTH:
                 return getShortFocalLengthDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_FOCAL_UNITS_PER_MM:
+            case CanonMakernoteDirectory.CameraSettings.TAG_FOCAL_UNITS_PER_MM:
                 return getFocalUnitsPerMillimetreDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_DETAILS:
+            case CanonMakernoteDirectory.CameraSettings.TAG_FLASH_DETAILS:
                 return getFlashDetailsDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_MODE_2:
+            case CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_MODE_2:
                 return getFocusMode2Description();
-            case CanonMakernoteDirectory.TAG_CANON_STATE2_WHITE_BALANCE:
+            case CanonMakernoteDirectory.FocalLength.TAG_WHITE_BALANCE:
                 return getWhiteBalanceDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE2_AF_POINT_USED:
+            case CanonMakernoteDirectory.FocalLength.TAG_AF_POINT_USED:
                 return getAfPointUsedDescription();
-            case CanonMakernoteDirectory.TAG_CANON_STATE2_FLASH_BIAS:
+            case CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS:
                 return getFlashBiasDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION:
-                return getLongExposureNoiseReductionDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS:
-                return getShutterAutoExposureLockButtonDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP:
-                return getMirrorLockupDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL:
-                return getTvAndAvExposureLevelDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT:
-                return getAutoFocusAssistLightDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE:
-                return getShutterSpeedInAvModeDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_BRACKETTING:
-                return getAutoExposureBrackettingSequenceAndAutoCancellationDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC:
-                return getShutterCurtainSyncDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_STOP:
-                return getLensAutoFocusStopButtonDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION:
-                return getFillFlashReductionDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN:
-                return getMenuButtonReturnPositionDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION:
-                return getSetButtonFunctionWhenShootingDescription();
-            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING:
-                return getSensorCleaningDescription();
-            default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getLongExposureNoiseReductionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION);
+
+            // It turns out that these values are dependent upon the camera model and therefore the below code was
+            // incorrect for some Canon models.  This needs to be revisited.
+
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION:
+//                return getLongExposureNoiseReductionDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS:
+//                return getShutterAutoExposureLockButtonDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP:
+//                return getMirrorLockupDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL:
+//                return getTvAndAvExposureLevelDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT:
+//                return getAutoFocusAssistLightDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE:
+//                return getShutterSpeedInAvModeDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_BRACKETTING:
+//                return getAutoExposureBrackettingSequenceAndAutoCancellationDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC:
+//                return getShutterCurtainSyncDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_STOP:
+//                return getLensAutoFocusStopButtonDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION:
+//                return getFillFlashReductionDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN:
+//                return getMenuButtonReturnPositionDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION:
+//                return getSetButtonFunctionWhenShootingDescription();
+//            case CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING:
+//                return getSensorCleaningDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getSerialNumberDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_SERIAL_NUMBER);
+        if (value==null)
+            return null;
+        return String.format("%04X%05d", (value >> 8) & 0xFF, value & 0xFF);
+    }
+
+/*
+    @Nullable
+    public String getLongExposureNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "Off";
@@ -127,8 +152,11 @@
         }
     }
-    public String getShutterAutoExposureLockButtonDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS);
+
+    @Nullable
+    public String getShutterAutoExposureLockButtonDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "AF/AE lock";
@@ -139,8 +167,11 @@
         }
     }
-    public String getMirrorLockupDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP);
+
+    @Nullable
+    public String getMirrorLockupDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "Disabled";
@@ -149,8 +180,11 @@
         }
     }
-    public String getTvAndAvExposureLevelDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL);
+
+    @Nullable
+    public String getTvAndAvExposureLevelDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "1/2 stop";
@@ -159,8 +193,11 @@
         }
     }
-    public String getAutoFocusAssistLightDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT);
+
+    @Nullable
+    public String getAutoFocusAssistLightDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "On (Auto)";
@@ -169,8 +206,11 @@
         }
     }
-    public String getShutterSpeedInAvModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE);
+
+    @Nullable
+    public String getShutterSpeedInAvModeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "Automatic";
@@ -179,8 +219,11 @@
         }
     }
-    public String getAutoExposureBrackettingSequenceAndAutoCancellationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_BRACKETTING)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_BRACKETTING);
+
+    @Nullable
+    public String getAutoExposureBrackettingSequenceAndAutoCancellationDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_BRACKETTING);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "0,-,+ / Enabled";
@@ -191,8 +234,11 @@
         }
     }
-    public String getShutterCurtainSyncDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC);
+
+    @Nullable
+    public String getShutterCurtainSyncDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "1st Curtain Sync";
@@ -201,8 +247,11 @@
         }
     }
-    public String getLensAutoFocusStopButtonDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_STOP)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_STOP);
+
+    @Nullable
+    public String getLensAutoFocusStopButtonDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_AF_STOP);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "AF stop";
@@ -212,8 +261,11 @@
         }
     }
-    public String getFillFlashReductionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION);
+
+    @Nullable
+    public String getFillFlashReductionDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "Enabled";
@@ -222,8 +274,11 @@
         }
     }
-    public String getMenuButtonReturnPositionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN);
+
+    @Nullable
+    public String getMenuButtonReturnPositionDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "Top";
@@ -233,8 +288,11 @@
         }
     }
-    public String getSetButtonFunctionWhenShootingDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION);
+
+    @Nullable
+    public String getSetButtonFunctionWhenShootingDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "Not Assigned";
@@ -245,8 +303,11 @@
         }
     }
-    public String getSensorCleaningDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING);
+
+    @Nullable
+    public String getSensorCleaningDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:     return "Disabled";
@@ -255,10 +316,13 @@
         }
     }
-
-    public String getFlashBiasDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE2_FLASH_BIAS)) return null;
-
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE2_FLASH_BIAS);
+*/
+
+    @Nullable
+    public String getFlashBiasDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.FocalLength.TAG_FLASH_BIAS);
+
+        if (value==null)
+            return null;
 
         boolean isNegative = false;
@@ -278,8 +342,10 @@
     }
 
-    public String getAfPointUsedDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE2_AF_POINT_USED)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE2_AF_POINT_USED);
+    @Nullable
+    public String getAfPointUsedDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.FocalLength.TAG_AF_POINT_USED);
+        if (value==null)
+            return null;
         if ((value & 0x7) == 0) {
             return "Right";
@@ -293,8 +359,10 @@
     }
 
-    public String getWhiteBalanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE2_WHITE_BALANCE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE2_WHITE_BALANCE);
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.FocalLength.TAG_WHITE_BALANCE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -307,5 +375,5 @@
                 return "Tungsten";
             case 4:
-                return "Flourescent";
+                return "Florescent";
             case 5:
                 return "Flash";
@@ -317,8 +385,10 @@
     }
 
-    public String getFocusMode2Description() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_MODE_2)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_MODE_2);
+    @Nullable
+    public String getFocusMode2Description()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_MODE_2);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -331,18 +401,20 @@
     }
 
-    public String getFlashDetailsDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_DETAILS)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_DETAILS);
-        if (((value << 14) & 1) > 0) {
+    @Nullable
+    public String getFlashDetailsDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FLASH_DETAILS);
+        if (value==null)
+            return null;
+        if (((value >> 14) & 1) > 0) {
             return "External E-TTL";
         }
-        if (((value << 13) & 1) > 0) {
+        if (((value >> 13) & 1) > 0) {
             return "Internal flash";
         }
-        if (((value << 11) & 1) > 0) {
+        if (((value >> 11) & 1) > 0) {
             return "FP sync used";
         }
-        if (((value << 4) & 1) > 0) {
+        if (((value >> 4) & 1) > 0) {
             return "FP sync enabled";
         }
@@ -350,8 +422,10 @@
     }
 
-    public String getFocalUnitsPerMillimetreDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_FOCAL_UNITS_PER_MM)) return "";
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_FOCAL_UNITS_PER_MM);
+    @Nullable
+    public String getFocalUnitsPerMillimetreDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FOCAL_UNITS_PER_MM);
+        if (value==null)
+            return null;
         if (value != 0) {
             return Integer.toString(value);
@@ -361,24 +435,30 @@
     }
 
-    public String getShortFocalLengthDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_SHORT_FOCAL_LENGTH)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_SHORT_FOCAL_LENGTH);
+    @Nullable
+    public String getShortFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_SHORT_FOCAL_LENGTH);
+        if (value==null)
+            return null;
         String units = getFocalUnitsPerMillimetreDescription();
         return Integer.toString(value) + " " + units;
     }
 
-    public String getLongFocalLengthDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_LONG_FOCAL_LENGTH)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_LONG_FOCAL_LENGTH);
+    @Nullable
+    public String getLongFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_LONG_FOCAL_LENGTH);
+        if (value==null)
+            return null;
         String units = getFocalUnitsPerMillimetreDescription();
         return Integer.toString(value) + " " + units;
     }
 
-    public String getExposureModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_EXPOSURE_MODE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_EXPOSURE_MODE);
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_EXPOSURE_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -399,8 +479,10 @@
     }
 
-    public String getAfPointSelectedDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_AF_POINT_SELECTED)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_AF_POINT_SELECTED);
+    @Nullable
+    public String getAfPointSelectedDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_AF_POINT_SELECTED);
+        if (value==null)
+            return null;
         switch (value) {
             case 0x3000:
@@ -419,8 +501,10 @@
     }
 
-    public String getMeteringModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_METERING_MODE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_METERING_MODE);
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_METERING_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 3:
@@ -435,8 +519,16 @@
     }
 
-    public String getIsoDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_ISO)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_ISO);
+    @Nullable
+    public String getIsoDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_ISO);
+        if (value==null)
+            return null;
+
+        // Canon PowerShot S3 is special
+        int canonMask = 0x4000;
+        if ((value & canonMask) > 0)
+            return "" + (value & ~canonMask);
+
         switch (value) {
             case 0:
@@ -457,8 +549,10 @@
     }
 
-    public String getSharpnessDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_SHARPNESS)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_SHARPNESS);
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_SHARPNESS);
+        if (value==null)
+            return null;
         switch (value) {
             case 0xFFFF:
@@ -473,8 +567,10 @@
     }
 
-    public String getSaturationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_SATURATION)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_SATURATION);
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_SATURATION);
+        if (value==null)
+            return null;
         switch (value) {
             case 0xFFFF:
@@ -489,8 +585,10 @@
     }
 
-    public String getContrastDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_CONTRAST)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_CONTRAST);
+    @Nullable
+    public String getContrastDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_CONTRAST);
+        if (value==null)
+            return null;
         switch (value) {
             case 0xFFFF:
@@ -505,8 +603,10 @@
     }
 
-    public String getEasyShootingModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_EASY_SHOOTING_MODE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_EASY_SHOOTING_MODE);
+    @Nullable
+    public String getEasyShootingModeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_EASY_SHOOTING_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -539,8 +639,10 @@
     }
 
-    public String getImageSizeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_IMAGE_SIZE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_IMAGE_SIZE);
+    @Nullable
+    public String getImageSizeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_IMAGE_SIZE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -555,8 +657,10 @@
     }
 
-    public String getFocusMode1Description() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_MODE_1)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_MODE_1);
+    @Nullable
+    public String getFocusMode1Description()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_MODE_1);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -580,26 +684,27 @@
     }
 
-    public String getContinuousDriveModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_CONTINUOUS_DRIVE_MODE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_CONTINUOUS_DRIVE_MODE);
-        switch (value) {
-            case 0:
-                if (_directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_SELF_TIMER_DELAY) == 0) {
-                    return "Single shot";
-                } else {
-                    return "Single shot with self-timer";
-                }
+    @Nullable
+    public String getContinuousDriveModeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_CONTINUOUS_DRIVE_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                final Integer delay = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_SELF_TIMER_DELAY);
+                if (delay!=null)
+                    return delay == 0 ? "Single shot" : "Single shot with self-timer";
             case 1:
                 return "Continuous";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getFlashModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_MODE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_MODE);
+        }
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FLASH_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -619,14 +724,16 @@
             case 16:
                 // note: this value not set on Canon D30
-                return "Extenal flash";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getSelfTimerDelayDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_SELF_TIMER_DELAY)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_SELF_TIMER_DELAY);
+                return "External flash";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSelfTimerDelayDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_SELF_TIMER_DELAY);
+        if (value==null)
+            return null;
         if (value == 0) {
             return "Self timer not used";
@@ -637,8 +744,10 @@
     }
 
-    public String getMacroModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_MACRO_MODE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_MACRO_MODE);
+    @Nullable
+    public String getMacroModeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_MACRO_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 1:
@@ -651,8 +760,10 @@
     }
 
-    public String getQualityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_QUALITY)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_QUALITY);
+    @Nullable
+    public String getQualityDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_QUALITY);
+        if (value==null)
+            return null;
         switch (value) {
             case 2:
@@ -667,8 +778,10 @@
     }
 
-    public String getDigitalZoomDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_DIGITAL_ZOOM)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_DIGITAL_ZOOM);
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_DIGITAL_ZOOM);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -683,8 +796,10 @@
     }
 
-    public String getFocusTypeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_TYPE)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_FOCUS_TYPE);
+    @Nullable
+    public String getFocusTypeDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FOCUS_TYPE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -701,8 +816,10 @@
     }
 
-    public String getFlashActivityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_ACTIVITY)) return null;
-        int value = _directory.getInt(CanonMakernoteDirectory.TAG_CANON_STATE1_FLASH_ACTIVITY);
+    @Nullable
+    public String getFlashActivityDescription()
+    {
+        Integer value = _directory.getInteger(CanonMakernoteDirectory.CameraSettings.TAG_FLASH_ACTIVITY);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
Index: trunk/src/com/drew/metadata/exif/CanonMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/CanonMakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/CanonMakernoteDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -27,373 +32,612 @@
  *
  * Many tag definitions explained here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/canon_mn.html
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class CanonMakernoteDirectory extends Directory
 {
-    // CANON cameras have some funny bespoke fields that need further processing...
-    public static final int TAG_CANON_CAMERA_STATE_1 = 0x0001;
-    public static final int TAG_CANON_CAMERA_STATE_2 = 0x0004;
-
-    public static final int TAG_CANON_IMAGE_TYPE = 0x0006;
-    public static final int TAG_CANON_FIRMWARE_VERSION = 0x0007;
-    public static final int TAG_CANON_IMAGE_NUMBER = 0x0008;
-    public static final int TAG_CANON_OWNER_NAME = 0x0009;
-    /**
-     * To display serial number as on camera use: printf( "%04X%05d", highbyte, lowbyte )
-     * TODO handle this in CanonMakernoteDescriptor
-     */
-    public static final int TAG_CANON_SERIAL_NUMBER = 0x000C;
-    public static final int TAG_CANON_UNKNOWN_1 = 0x000D;
-    public static final int TAG_CANON_CUSTOM_FUNCTIONS = 0x000F;
-
-    // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
-    /**
-     * 1 = Macro
-     * 2 = Normal
-     */
-    public static final int TAG_CANON_STATE1_MACRO_MODE = 0xC101;
-    public static final int TAG_CANON_STATE1_SELF_TIMER_DELAY = 0xC102;
-    /**
-     * 2 = Normal
-     * 3 = Fine
-     * 5 = Superfine
-     */
-    public static final int TAG_CANON_STATE1_QUALITY = 0xC103;
-    /**
-     * 0 = Flash Not Fired
-     * 1 = Auto
-     * 2 = On
-     * 3 = Red Eye Reduction
-     * 4 = Slow Synchro
-     * 5 = Auto + Red Eye Reduction
-     * 6 = On + Red Eye Reduction
-     * 16 = External Flash
-     */
-    public static final int TAG_CANON_STATE1_FLASH_MODE = 0xC104;
-    /**
-     * 0 = Single Frame or Timer Mode
-     * 1 = Continuous
-     */
-    public static final int TAG_CANON_STATE1_CONTINUOUS_DRIVE_MODE = 0xC105;
-    public static final int TAG_CANON_STATE1_UNKNOWN_2 = 0xC106;
-    /**
-     * 0 = One-Shot
-     * 1 = AI Servo
-     * 2 = AI Focus
-     * 3 = Manual Focus
-     * 4 = Single
-     * 5 = Continuous
-     * 6 = Manual Focus
-     */
-    public static final int TAG_CANON_STATE1_FOCUS_MODE_1 = 0xC107;
-    public static final int TAG_CANON_STATE1_UNKNOWN_3 = 0xC108;
-    public static final int TAG_CANON_STATE1_UNKNOWN_4 = 0xC109;
-    /**
-     * 0 = Large
-     * 1 = Medium
-     * 2 = Small
-     */
-    public static final int TAG_CANON_STATE1_IMAGE_SIZE = 0xC10A;
-    /**
-     * 0 = Full Auto
-     * 1 = Manual
-     * 2 = Landscape
-     * 3 = Fast Shutter
-     * 4 = Slow Shutter
-     * 5 = Night
-     * 6 = Black & White
-     * 7 = Sepia
-     * 8 = Portrait
-     * 9 = Sports
-     * 10 = Macro / Close-Up
-     * 11 = Pan Focus
-     */
-    public static final int TAG_CANON_STATE1_EASY_SHOOTING_MODE = 0xC10B;
-    /**
-     * 0 = No Digital Zoom
-     * 1 = 2x
-     * 2 = 4x
-     */
-    public static final int TAG_CANON_STATE1_DIGITAL_ZOOM = 0xC10C;
-    /**
-     * 0 = Normal
-     * 1 = High
-     * 65535 = Low
-     */
-    public static final int TAG_CANON_STATE1_CONTRAST = 0xC10D;
-    /**
-     * 0 = Normal
-     * 1 = High
-     * 65535 = Low
-     */
-    public static final int TAG_CANON_STATE1_SATURATION = 0xC10E;
-    /**
-     * 0 = Normal
-     * 1 = High
-     * 65535 = Low
-     */
-    public static final int TAG_CANON_STATE1_SHARPNESS = 0xC10F;
-    /**
-     * 0 = Check ISOSpeedRatings EXIF tag for ISO Speed
-     * 15 = Auto ISO
-     * 16 = ISO 50
-     * 17 = ISO 100
-     * 18 = ISO 200
-     * 19 = ISO 400
-     */
-    public static final int TAG_CANON_STATE1_ISO = 0xC110;
-    /**
-     * 3 = Evaluative
-     * 4 = Partial
-     * 5 = Centre Weighted
-     */
-    public static final int TAG_CANON_STATE1_METERING_MODE = 0xC111;
-    /**
-     * 0 = Manual
-     * 1 = Auto
-     * 3 = Close-up (Macro)
-     * 8 = Locked (Pan Mode)
-     */
-    public static final int TAG_CANON_STATE1_FOCUS_TYPE = 0xC112;
-    /**
-     * 12288 = None (Manual Focus)
-     * 12289 = Auto Selected
-     * 12290 = Right
-     * 12291 = Centre
-     * 12292 = Left
-     */
-    public static final int TAG_CANON_STATE1_AF_POINT_SELECTED = 0xC113;
-    /**
-     * 0 = Easy Shooting (See Easy Shooting Mode)
-     * 1 = Program
-     * 2 = Tv-Priority
-     * 3 = Av-Priority
-     * 4 = Manual
-     * 5 = A-DEP
-     */
-    public static final int TAG_CANON_STATE1_EXPOSURE_MODE = 0xC114;
-    public static final int TAG_CANON_STATE1_UNKNOWN_7 = 0xC115;
-    public static final int TAG_CANON_STATE1_UNKNOWN_8 = 0xC116;
-    public static final int TAG_CANON_STATE1_LONG_FOCAL_LENGTH = 0xC117;
-    public static final int TAG_CANON_STATE1_SHORT_FOCAL_LENGTH = 0xC118;
-    public static final int TAG_CANON_STATE1_FOCAL_UNITS_PER_MM = 0xC119;
-    public static final int TAG_CANON_STATE1_UNKNOWN_9 = 0xC11A;
-    public static final int TAG_CANON_STATE1_UNKNOWN_10 = 0xC11B;
-    /**
-     * 0 = Flash Did Not Fire
-     * 1 = Flash Fired
-     */
-    public static final int TAG_CANON_STATE1_FLASH_ACTIVITY = 0xC11C;
-    public static final int TAG_CANON_STATE1_FLASH_DETAILS = 0xC11D;
-    public static final int TAG_CANON_STATE1_UNKNOWN_12 = 0xC11E;
-    public static final int TAG_CANON_STATE1_UNKNOWN_13 = 0xC11F;
-    /**
-     * 0 = Focus Mode: Single
-     * 1 = Focus Mode: Continuous
-     */
-    public static final int TAG_CANON_STATE1_FOCUS_MODE_2 = 0xC120;
-
-    /**
-     * 0 = Auto
-     * 1 = Sunny
-     * 2 = Cloudy
-     * 3 = Tungsten
-     * 4 = Flourescent
-     * 5 = Flash
-     * 6 = Custom
-     */
-    public static final int TAG_CANON_STATE2_WHITE_BALANCE = 0xC207;
-    public static final int TAG_CANON_STATE2_SEQUENCE_NUMBER = 0xC209;
-    public static final int TAG_CANON_STATE2_AF_POINT_USED = 0xC20E;
-    /**
-     * The value of this tag may be translated into a flash bias value, in EV.
-     *
-     * 0xffc0 = -2 EV
-     * 0xffcc = -1.67 EV
-     * 0xffd0 = -1.5 EV
-     * 0xffd4 = -1.33 EV
-     * 0xffe0 = -1 EV
-     * 0xffec = -0.67 EV
-     * 0xfff0 = -0.5 EV
-     * 0xfff4 = -0.33 EV
-     * 0x0000 = 0 EV
-     * 0x000c = 0.33 EV
-     * 0x0010 = 0.5 EV
-     * 0x0014 = 0.67 EV
-     * 0x0020 = 1 EV
-     * 0x002c = 1.33 EV
-     * 0x0030 = 1.5 EV
-     * 0x0034 = 1.67 EV
-     * 0x0040 = 2 EV 
-     */
-    public static final int TAG_CANON_STATE2_FLASH_BIAS = 0xC20F;
-    public static final int TAG_CANON_STATE2_AUTO_EXPOSURE_BRACKETING = 0xC210;
-    public static final int TAG_CANON_STATE2_AEB_BRACKET_VALUE = 0xC211;
-    public static final int TAG_CANON_STATE2_SUBJECT_DISTANCE = 0xC213;
-
-    /**
-     * Long Exposure Noise Reduction
-     * 0 = Off
-     * 1 = On
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION = 0xC301;
-
-    /**
-     * Shutter/Auto Exposure-lock buttons
-     * 0 = AF/AE lock
-     * 1 = AE lock/AF
-     * 2 = AF/AF lock
-     * 3 = AE+release/AE+AF
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS = 0xC302;
-
-    /**
-     * Mirror lockup
-     * 0 = Disable
-     * 1 = Enable
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP = 0xC303;
-
-    /**
-     * Tv/Av and exposure level
-     * 0 = 1/2 stop
-     * 1 = 1/3 stop
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL = 0xC304;
-
-    /**
-     * AF-assist light
-     * 0 = On (Auto)
-     * 1 = Off
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT = 0xC305;
-
-    /**
-     * Shutter speed in Av mode
-     * 0 = Automatic
-     * 1 = 1/200 (fixed)
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE = 0xC306;
-
-    /**
-     * Auto-Exposure Bracketting sequence/auto cancellation
-     * 0 = 0,-,+ / Enabled
-     * 1 = 0,-,+ / Disabled
-     * 2 = -,0,+ / Enabled
-     * 3 = -,0,+ / Disabled
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_BRACKETTING = 0xC307;
-
-    /**
-     * Shutter Curtain Sync
-     * 0 = 1st Curtain Sync
-     * 1 = 2nd Curtain Sync
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC = 0xC308;
-
-    /**
-     * Lens Auto-Focus stop button Function Switch
-     * 0 = AF stop
-     * 1 = Operate AF
-     * 2 = Lock AE and start timer
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_AF_STOP = 0xC309;
-
-    /**
-     * Auto reduction of fill flash
-     * 0 = Enable
-     * 1 = Disable
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION = 0xC30A;
-
-    /**
-     * Menu button return position
-     * 0 = Top
-     * 1 = Previous (volatile)
-     * 2 = Previous
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN = 0xC30B;
-
-    /**
-     * SET button function when shooting
-     * 0 = Not Assigned
-     * 1 = Change Quality
-     * 2 = Change ISO Speed
-     * 3 = Select Parameters
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION = 0xC30C;
-
-    /**
-     * Sensor cleaning
-     * 0 = Disable
-     * 1 = Enable
-     */
-    public static final int TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING = 0xC30D;
+    // These TAG_*_ARRAY Exif tags map to arrays of int16 values which are split out into separate 'fake' tags.
+    // When an attempt is made to set one of these on the directory, it is split and the corresponding offset added to the tagType.
+    // So the resulting tag is the offset + the index into the array.
+
+    private static final int TAG_CAMERA_SETTINGS_ARRAY  = 0x0001;
+    private static final int TAG_FOCAL_LENGTH_ARRAY = 0x0002;
+    private static final int TAG_SHOT_INFO_ARRAY = 0x0004;
+    private static final int TAG_PANORAMA_ARRAY = 0x0005;
+
+    public static final int TAG_CANON_IMAGE_TYPE            = 0x0006;
+    public static final int TAG_CANON_FIRMWARE_VERSION      = 0x0007;
+    public static final int TAG_CANON_IMAGE_NUMBER          = 0x0008;
+    public static final int TAG_CANON_OWNER_NAME            = 0x0009;
+    public static final int TAG_CANON_SERIAL_NUMBER         = 0x000C;
+    public static final int TAG_CAMERA_INFO_ARRAY           = 0x000D; // depends upon model, so leave for now
+    public static final int TAG_CANON_FILE_LENGTH           = 0x000E;
+    public static final int TAG_CANON_CUSTOM_FUNCTIONS_ARRAY = 0x000F; // depends upon model, so leave for now
+    public static final int TAG_MODEL_ID                    = 0x0010;
+    public static final int TAG_MOVIE_INFO_ARRAY            = 0x0011; // not currently decoded as not sure we see it in still images
+    private static final int TAG_AF_INFO_ARRAY              = 0x0012; // not currently decoded
+    public static final int TAG_THUMBNAIL_IMAGE_VALID_AREA  = 0x0013;
+    public static final int TAG_SERIAL_NUMBER_FORMAT        = 0x0015;
+    public static final int TAG_SUPER_MACRO                 = 0x001A;
+    public static final int TAG_DATE_STAMP_MODE             = 0x001C;
+    public static final int TAG_MY_COLORS                   = 0x001D;
+    public static final int TAG_FIRMWARE_REVISION           = 0x001E;
+    public static final int TAG_CATEGORIES                  = 0x0023;
+    public static final int TAG_FACE_DETECT_ARRAY_1         = 0x0024;
+    public static final int TAG_FACE_DETECT_ARRAY_2         = 0x0025;
+    public static final int TAG_AF_INFO_ARRAY_2             = 0x0026;
+    public static final int TAG_IMAGE_UNIQUE_ID             = 0x0028;
+    public static final int TAG_RAW_DATA_OFFSET             = 0x0081;
+    public static final int TAG_ORIGINAL_DECISION_DATA_OFFSET = 0x0083;
+    public static final int TAG_CUSTOM_FUNCTIONS_1D_ARRAY   = 0x0090; // not currently decoded
+    public static final int TAG_PERSONAL_FUNCTIONS_ARRAY    = 0x0091; // not currently decoded
+    public static final int TAG_PERSONAL_FUNCTION_VALUES_ARRAY = 0x0092; // not currently decoded
+    public static final int TAG_FILE_INFO_ARRAY             = 0x0093; // not currently decoded
+    public static final int TAG_AF_POINTS_IN_FOCUS_1D       = 0x0094;
+    public static final int TAG_LENS_MODEL                  = 0x0095;
+    public static final int TAG_SERIAL_INFO_ARRAY           = 0x0096; // not currently decoded
+    public static final int TAG_DUST_REMOVAL_DATA           = 0x0097;
+    public static final int TAG_CROP_INFO                   = 0x0098; // not currently decoded
+    public static final int TAG_CUSTOM_FUNCTIONS_ARRAY_2    = 0x0099; // not currently decoded
+    public static final int TAG_ASPECT_INFO_ARRAY           = 0x009A; // not currently decoded
+    public static final int TAG_PROCESSING_INFO_ARRAY       = 0x00A0; // not currently decoded
+    public static final int TAG_TONE_CURVE_TABLE            = 0x00A1;
+    public static final int TAG_SHARPNESS_TABLE             = 0x00A2;
+    public static final int TAG_SHARPNESS_FREQ_TABLE        = 0x00A3;
+    public static final int TAG_WHITE_BALANCE_TABLE         = 0x00A4;
+    public static final int TAG_COLOR_BALANCE_ARRAY         = 0x00A9; // not currently decoded
+    public static final int TAG_MEASURED_COLOR_ARRAY        = 0x00AA; // not currently decoded
+    public static final int TAG_COLOR_TEMPERATURE           = 0x00AE;
+    public static final int TAG_CANON_FLAGS_ARRAY           = 0x00B0; // not currently decoded
+    public static final int TAG_MODIFIED_INFO_ARRAY         = 0x00B1; // not currently decoded
+    public static final int TAG_TONE_CURVE_MATCHING         = 0x00B2;
+    public static final int TAG_WHITE_BALANCE_MATCHING      = 0x00B3;
+    public static final int TAG_COLOR_SPACE                 = 0x00B4;
+    public static final int TAG_PREVIEW_IMAGE_INFO_ARRAY    = 0x00B6; // not currently decoded
+    public static final int TAG_VRD_OFFSET                  = 0x00D0;
+    public static final int TAG_SENSOR_INFO_ARRAY           = 0x00E0; // not currently decoded
+    public static final int TAG_COLOR_DATA_ARRAY_2 = 0x4001; // depends upon camera model, not currently decoded
+    public static final int TAG_COLOR_INFO_ARRAY_2          = 0x4003; // not currently decoded
+    public static final int TAG_CUSTOM_PICTURE_STYLE_FILE_NAME = 0x4010;
+    public static final int TAG_COLOR_INFO_ARRAY            = 0x4013; // not currently decoded
+    public static final int TAG_VIGNETTING_CORRECTION_ARRAY_1 = 0x4015; // not currently decoded
+    public static final int TAG_VIGNETTING_CORRECTION_ARRAY_2 = 0x4016; // not currently decoded
+    public static final int TAG_LIGHTING_OPTIMIZER_ARRAY = 0x4018; // not currently decoded
+    public static final int TAG_LENS_INFO_ARRAY             = 0x4019; // not currently decoded
+    public static final int TAG_AMBIANCE_INFO_ARRAY         = 0x4020; // not currently decoded
+    public static final int TAG_FILTER_INFO_ARRAY           = 0x4024; // not currently decoded
+
+    public final static class CameraSettings
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+        private static final int OFFSET = 0xC100;
+
+        /**
+         * 1 = Macro
+         * 2 = Normal
+         */
+        public static final int TAG_MACRO_MODE = OFFSET + 0x01;
+        public static final int TAG_SELF_TIMER_DELAY = OFFSET + 0x02;
+        /**
+         * 2 = Normal
+         * 3 = Fine
+         * 5 = Superfine
+         */
+        public static final int TAG_QUALITY = OFFSET + 0x03;
+        /**
+         * 0 = Flash Not Fired
+         * 1 = Auto
+         * 2 = On
+         * 3 = Red Eye Reduction
+         * 4 = Slow Synchro
+         * 5 = Auto + Red Eye Reduction
+         * 6 = On + Red Eye Reduction
+         * 16 = External Flash
+         */
+        public static final int TAG_FLASH_MODE = OFFSET + 0x04;
+        /**
+         * 0 = Single Frame or Timer Mode
+         * 1 = Continuous
+         */
+        public static final int TAG_CONTINUOUS_DRIVE_MODE = OFFSET + 0x05;
+        public static final int TAG_UNKNOWN_2 = OFFSET + 0x06;
+        /**
+         * 0 = One-Shot
+         * 1 = AI Servo
+         * 2 = AI Focus
+         * 3 = Manual Focus
+         * 4 = Single
+         * 5 = Continuous
+         * 6 = Manual Focus
+         */
+        public static final int TAG_FOCUS_MODE_1 = OFFSET + 0x07;
+        public static final int TAG_UNKNOWN_3 = OFFSET + 0x08;
+        public static final int TAG_UNKNOWN_4 = OFFSET + 0x09;
+        /**
+         * 0 = Large
+         * 1 = Medium
+         * 2 = Small
+         */
+        public static final int TAG_IMAGE_SIZE = OFFSET + 0x0A;
+        /**
+         * 0 = Full Auto
+         * 1 = Manual
+         * 2 = Landscape
+         * 3 = Fast Shutter
+         * 4 = Slow Shutter
+         * 5 = Night
+         * 6 = Black & White
+         * 7 = Sepia
+         * 8 = Portrait
+         * 9 = Sports
+         * 10 = Macro / Close-Up
+         * 11 = Pan Focus
+         */
+        public static final int TAG_EASY_SHOOTING_MODE = OFFSET + 0x0B;
+        /**
+         * 0 = No Digital Zoom
+         * 1 = 2x
+         * 2 = 4x
+         */
+        public static final int TAG_DIGITAL_ZOOM = OFFSET + 0x0C;
+        /**
+         * 0 = Normal
+         * 1 = High
+         * 65535 = Low
+         */
+        public static final int TAG_CONTRAST = OFFSET + 0x0D;
+        /**
+         * 0 = Normal
+         * 1 = High
+         * 65535 = Low
+         */
+        public static final int TAG_SATURATION = OFFSET + 0x0E;
+        /**
+         * 0 = Normal
+         * 1 = High
+         * 65535 = Low
+         */
+        public static final int TAG_SHARPNESS = OFFSET + 0x0F;
+        /**
+         * 0 = Check ISOSpeedRatings EXIF tag for ISO Speed
+         * 15 = Auto ISO
+         * 16 = ISO 50
+         * 17 = ISO 100
+         * 18 = ISO 200
+         * 19 = ISO 400
+         */
+        public static final int TAG_ISO = OFFSET + 0x10;
+        /**
+         * 3 = Evaluative
+         * 4 = Partial
+         * 5 = Centre Weighted
+         */
+        public static final int TAG_METERING_MODE = OFFSET + 0x11;
+        /**
+         * 0 = Manual
+         * 1 = Auto
+         * 3 = Close-up (Macro)
+         * 8 = Locked (Pan Mode)
+         */
+        public static final int TAG_FOCUS_TYPE = OFFSET + 0x12;
+        /**
+         * 12288 = None (Manual Focus)
+         * 12289 = Auto Selected
+         * 12290 = Right
+         * 12291 = Centre
+         * 12292 = Left
+         */
+        public static final int TAG_AF_POINT_SELECTED = OFFSET + 0x13;
+        /**
+         * 0 = Easy Shooting (See Easy Shooting Mode)
+         * 1 = Program
+         * 2 = Tv-Priority
+         * 3 = Av-Priority
+         * 4 = Manual
+         * 5 = A-DEP
+         */
+        public static final int TAG_EXPOSURE_MODE = OFFSET + 0x14;
+        public static final int TAG_UNKNOWN_7 = OFFSET + 0x15;
+        public static final int TAG_UNKNOWN_8 = OFFSET + 0x16;
+        public static final int TAG_LONG_FOCAL_LENGTH = OFFSET + 0x17;
+        public static final int TAG_SHORT_FOCAL_LENGTH = OFFSET + 0x18;
+        public static final int TAG_FOCAL_UNITS_PER_MM = OFFSET + 0x19;
+        public static final int TAG_UNKNOWN_9 = OFFSET + 0x1A;
+        public static final int TAG_UNKNOWN_10 = OFFSET + 0x1B;
+        /**
+         * 0 = Flash Did Not Fire
+         * 1 = Flash Fired
+         */
+        public static final int TAG_FLASH_ACTIVITY = OFFSET + 0x1C;
+        public static final int TAG_FLASH_DETAILS = OFFSET + 0x1D;
+        public static final int TAG_UNKNOWN_12 = OFFSET + 0x1E;
+        public static final int TAG_UNKNOWN_13 = OFFSET + 0x1F;
+        /**
+         * 0 = Focus Mode: Single
+         * 1 = Focus Mode: Continuous
+         */
+        public static final int TAG_FOCUS_MODE_2 = OFFSET + 0x20;
+    }
+    
+    public final static class FocalLength
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+
+        private static final int OFFSET = 0xC200;
+        
+        /**
+         * 0 = Auto
+         * 1 = Sunny
+         * 2 = Cloudy
+         * 3 = Tungsten
+         * 4 = Florescent
+         * 5 = Flash
+         * 6 = Custom
+         */
+        public static final int TAG_WHITE_BALANCE = OFFSET + 0x07;
+        public static final int TAG_SEQUENCE_NUMBER = OFFSET + 0x09;
+        public static final int TAG_AF_POINT_USED = OFFSET + 0x0E;
+        /**
+         * The value of this tag may be translated into a flash bias value, in EV.
+         *
+         * 0xffc0 = -2 EV
+         * 0xffcc = -1.67 EV
+         * 0xffd0 = -1.5 EV
+         * 0xffd4 = -1.33 EV
+         * 0xffe0 = -1 EV
+         * 0xffec = -0.67 EV
+         * 0xfff0 = -0.5 EV
+         * 0xfff4 = -0.33 EV
+         * 0x0000 = 0 EV
+         * 0x000c = 0.33 EV
+         * 0x0010 = 0.5 EV
+         * 0x0014 = 0.67 EV
+         * 0x0020 = 1 EV
+         * 0x002c = 1.33 EV
+         * 0x0030 = 1.5 EV
+         * 0x0034 = 1.67 EV
+         * 0x0040 = 2 EV
+         */
+        public static final int TAG_FLASH_BIAS = OFFSET + 0x0F;
+        public static final int TAG_AUTO_EXPOSURE_BRACKETING = OFFSET + 0x10;
+        public static final int TAG_AEB_BRACKET_VALUE = OFFSET + 0x11;
+        public static final int TAG_SUBJECT_DISTANCE = OFFSET + 0x13;
+    }
+    
+    public final static class ShotInfo
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+
+        private static final int OFFSET = 0xC400;
+
+        public static final int TAG_AUTO_ISO = OFFSET + 1;
+        public static final int TAG_BASE_ISO = OFFSET + 2;
+        public static final int TAG_MEASURED_EV = OFFSET + 3;
+        public static final int TAG_TARGET_APERTURE = OFFSET + 4;
+        public static final int TAG_TARGET_EXPOSURE_TIME = OFFSET + 5;
+        public static final int TAG_EXPOSURE_COMPENSATION = OFFSET + 6;
+        public static final int TAG_WHITE_BALANCE = OFFSET + 7;
+        public static final int TAG_SLOW_SHUTTER = OFFSET + 8;
+        public static final int TAG_SEQUENCE_NUMBER = OFFSET + 9;
+        public static final int TAG_OPTICAL_ZOOM_CODE = OFFSET + 10;
+        public static final int TAG_CAMERA_TEMPERATURE = OFFSET + 12;
+        public static final int TAG_FLASH_GUIDE_NUMBER = OFFSET + 13;
+        public static final int TAG_AF_POINTS_IN_FOCUS = OFFSET + 14;
+        public static final int TAG_FLASH_EXPOSURE_BRACKETING = OFFSET + 15;
+        public static final int TAG_AUTO_EXPOSURE_BRACKETING = OFFSET + 16;
+        public static final int TAG_AEB_BRACKET_VALUE = OFFSET + 17;
+        public static final int TAG_CONTROL_MODE = OFFSET + 18;
+        public static final int TAG_FOCUS_DISTANCE_UPPER = OFFSET + 19;
+        public static final int TAG_FOCUS_DISTANCE_LOWER = OFFSET + 20;
+        public static final int TAG_F_NUMBER = OFFSET + 21;
+        public static final int TAG_EXPOSURE_TIME = OFFSET + 22;
+        public static final int TAG_MEASURED_EV_2 = OFFSET + 23;
+        public static final int TAG_BULB_DURATION = OFFSET + 24;
+        public static final int TAG_CAMERA_TYPE = OFFSET + 26;
+        public static final int TAG_AUTO_ROTATE = OFFSET + 27;
+        public static final int TAG_ND_FILTER = OFFSET + 28;
+        public static final int TAG_SELF_TIMER_2 = OFFSET + 29;
+        public static final int TAG_FLASH_OUTPUT = OFFSET + 33;
+    }
+
+    public final static class Panorama
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+
+        private static final int OFFSET = 0xC500;
+
+        public static final int TAG_PANORAMA_FRAME_NUMBER = OFFSET + 2;
+        public static final int TAG_PANORAMA_DIRECTION = OFFSET + 5;
+    }
+
+    public final static class AFInfo
+    {
+        // These 'sub'-tag values have been created for consistency -- they don't exist within the exif segment
+
+        private static final int OFFSET = 0xD200;
+
+        public static final int TAG_NUM_AF_POINTS = OFFSET;
+        public static final int TAG_VALID_AF_POINTS = OFFSET + 1;
+        public static final int TAG_IMAGE_WIDTH = OFFSET + 2;
+        public static final int TAG_IMAGE_HEIGHT = OFFSET + 3;
+        public static final int TAG_AF_IMAGE_WIDTH = OFFSET + 4;
+        public static final int TAG_AF_IMAGE_HEIGHT = OFFSET + 5;
+        public static final int TAG_AF_AREA_WIDTH = OFFSET + 6;
+        public static final int TAG_AF_AREA_HEIGHT = OFFSET + 7;
+        public static final int TAG_AF_AREA_X_POSITIONS = OFFSET + 8;
+        public static final int TAG_AF_AREA_Y_POSITIONS = OFFSET + 9;
+        public static final int TAG_AF_POINTS_IN_FOCUS = OFFSET + 10;
+        public static final int TAG_PRIMARY_AF_POINT_1 = OFFSET + 11;
+        public static final int TAG_PRIMARY_AF_POINT_2 = OFFSET + 12; // not sure why there are two of these
+    }
+
+//    /**
+//     * Long Exposure Noise Reduction
+//     * 0 = Off
+//     * 1 = On
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION = 0xC301;
+//
+//    /**
+//     * Shutter/Auto Exposure-lock buttons
+//     * 0 = AF/AE lock
+//     * 1 = AE lock/AF
+//     * 2 = AF/AF lock
+//     * 3 = AE+release/AE+AF
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS = 0xC302;
+//
+//    /**
+//     * Mirror lockup
+//     * 0 = Disable
+//     * 1 = Enable
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP = 0xC303;
+//
+//    /**
+//     * Tv/Av and exposure level
+//     * 0 = 1/2 stop
+//     * 1 = 1/3 stop
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL = 0xC304;
+//
+//    /**
+//     * AF-assist light
+//     * 0 = On (Auto)
+//     * 1 = Off
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT = 0xC305;
+//
+//    /**
+//     * Shutter speed in Av mode
+//     * 0 = Automatic
+//     * 1 = 1/200 (fixed)
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE = 0xC306;
+//
+//    /**
+//     * Auto-Exposure Bracketting sequence/auto cancellation
+//     * 0 = 0,-,+ / Enabled
+//     * 1 = 0,-,+ / Disabled
+//     * 2 = -,0,+ / Enabled
+//     * 3 = -,0,+ / Disabled
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_BRACKETTING = 0xC307;
+//
+//    /**
+//     * Shutter Curtain Sync
+//     * 0 = 1st Curtain Sync
+//     * 1 = 2nd Curtain Sync
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC = 0xC308;
+//
+//    /**
+//     * Lens Auto-Focus stop button Function Switch
+//     * 0 = AF stop
+//     * 1 = Operate AF
+//     * 2 = Lock AE and start timer
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_AF_STOP = 0xC309;
+//
+//    /**
+//     * Auto reduction of fill flash
+//     * 0 = Enable
+//     * 1 = Disable
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION = 0xC30A;
+//
+//    /**
+//     * Menu button return position
+//     * 0 = Top
+//     * 1 = Previous (volatile)
+//     * 2 = Previous
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN = 0xC30B;
+//
+//    /**
+//     * SET button function when shooting
+//     * 0 = Not Assigned
+//     * 1 = Change Quality
+//     * 2 = Change ISO Speed
+//     * 3 = Select Parameters
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION = 0xC30C;
+//
+//    /**
+//     * Sensor cleaning
+//     * 0 = Disable
+//     * 1 = Enable
+//     */
+//    public static final int TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING = 0xC30D;
 
     // 9  A  B  C  D  E  F  10 11 12 13
     // 9  10 11 12 13 14 15 16 17 18 19
-    protected static final HashMap _tagNameMap = new HashMap();
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        _tagNameMap.put(new Integer(TAG_CANON_FIRMWARE_VERSION), "Firmware Version");
-        _tagNameMap.put(new Integer(TAG_CANON_IMAGE_NUMBER), "Image Number");
-        _tagNameMap.put(new Integer(TAG_CANON_IMAGE_TYPE), "Image Type");
-        _tagNameMap.put(new Integer(TAG_CANON_OWNER_NAME), "Owner Name");
-        _tagNameMap.put(new Integer(TAG_CANON_UNKNOWN_1), "Makernote Unknown 1");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTIONS), "Custom Functions");
-        _tagNameMap.put(new Integer(TAG_CANON_SERIAL_NUMBER), "Camera Serial Number");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_AF_POINT_SELECTED), "AF Point Selected");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_CONTINUOUS_DRIVE_MODE), "Continuous Drive Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_CONTRAST), "Contrast");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_EASY_SHOOTING_MODE), "Easy Shooting Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_EXPOSURE_MODE), "Exposure Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_FLASH_DETAILS), "Flash Details");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_FLASH_MODE), "Flash Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_FOCAL_UNITS_PER_MM), "Focal Units per mm");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_FOCUS_MODE_1), "Focus Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_FOCUS_MODE_2), "Focus Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_IMAGE_SIZE), "Image Size");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_ISO), "Iso");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_LONG_FOCAL_LENGTH), "Long Focal Length");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_MACRO_MODE), "Macro Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_METERING_MODE), "Metering Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_SATURATION), "Saturation");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_SELF_TIMER_DELAY), "Self Timer Delay");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_SHARPNESS), "Sharpness");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_SHORT_FOCAL_LENGTH), "Short Focal Length");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_QUALITY), "Quality");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_2), "Unknown Camera State 2");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_3), "Unknown Camera State 3");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_4), "Unknown Camera State 4");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_DIGITAL_ZOOM), "Digital Zoom");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_FOCUS_TYPE), "Focus Type");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_7), "Unknown Camera State 7");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_8), "Unknown Camera State 8");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_9), "Unknown Camera State 9");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_10), "Unknown Camera State 10");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_FLASH_ACTIVITY), "Flash Activity");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_12), "Unknown Camera State 12");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE1_UNKNOWN_13), "Unknown Camera State 13");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE2_WHITE_BALANCE), "White Balance");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE2_SEQUENCE_NUMBER), "Sequence Number");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE2_AF_POINT_USED), "AF Point Used");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE2_FLASH_BIAS), "Flash Bias");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE2_AUTO_EXPOSURE_BRACKETING), "Auto Exposure Bracketing");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE2_AEB_BRACKET_VALUE), "AEB Bracket Value");
-        _tagNameMap.put(new Integer(TAG_CANON_STATE2_SUBJECT_DISTANCE), "Subject Distance");
-
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION), "Long Exposure Noise Reduction");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS), "Shutter/Auto Exposure-lock Buttons");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP), "Mirror Lockup");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL), "Tv/Av And Exposure Level");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT), "AF-Assist Light");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE), "Shutter Speed in Av Mode");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_BRACKETTING), "Auto-Exposure Bracketting Sequence/Auto Cancellation");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC), "Shutter Curtain Sync");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_AF_STOP), "Lens Auto-Focus Stop Button Function Switch");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION), "Auto Reduction of Fill Flash");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN), "Menu Button Return Position");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION), "SET Button Function When Shooting");
-        _tagNameMap.put(new Integer(TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING), "Sensor Cleaning");
+        _tagNameMap.put(TAG_CANON_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_CANON_IMAGE_NUMBER, "Image Number");
+        _tagNameMap.put(TAG_CANON_IMAGE_TYPE, "Image Type");
+        _tagNameMap.put(TAG_CANON_OWNER_NAME, "Owner Name");
+        _tagNameMap.put(TAG_CANON_SERIAL_NUMBER, "Camera Serial Number");
+        _tagNameMap.put(TAG_CAMERA_INFO_ARRAY, "Camera Info Array");
+        _tagNameMap.put(TAG_CANON_FILE_LENGTH, "File Length");
+        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTIONS_ARRAY, "Custom Functions");
+        _tagNameMap.put(TAG_MODEL_ID, "Canon Model ID");
+        _tagNameMap.put(TAG_MOVIE_INFO_ARRAY, "Movie Info Array");
+
+        _tagNameMap.put(CameraSettings.TAG_AF_POINT_SELECTED, "AF Point Selected");
+        _tagNameMap.put(CameraSettings.TAG_CONTINUOUS_DRIVE_MODE, "Continuous Drive Mode");
+        _tagNameMap.put(CameraSettings.TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(CameraSettings.TAG_EASY_SHOOTING_MODE, "Easy Shooting Mode");
+        _tagNameMap.put(CameraSettings.TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_DETAILS, "Flash Details");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(CameraSettings.TAG_FOCAL_UNITS_PER_MM, "Focal Units per mm");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_MODE_1, "Focus Mode");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_MODE_2, "Focus Mode");
+        _tagNameMap.put(CameraSettings.TAG_IMAGE_SIZE, "Image Size");
+        _tagNameMap.put(CameraSettings.TAG_ISO, "Iso");
+        _tagNameMap.put(CameraSettings.TAG_LONG_FOCAL_LENGTH, "Long Focal Length");
+        _tagNameMap.put(CameraSettings.TAG_MACRO_MODE, "Macro Mode");
+        _tagNameMap.put(CameraSettings.TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(CameraSettings.TAG_SATURATION, "Saturation");
+        _tagNameMap.put(CameraSettings.TAG_SELF_TIMER_DELAY, "Self Timer Delay");
+        _tagNameMap.put(CameraSettings.TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(CameraSettings.TAG_SHORT_FOCAL_LENGTH, "Short Focal Length");
+        _tagNameMap.put(CameraSettings.TAG_QUALITY, "Quality");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_2, "Unknown Camera Setting 2");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_3, "Unknown Camera Setting 3");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_4, "Unknown Camera Setting 4");
+        _tagNameMap.put(CameraSettings.TAG_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(CameraSettings.TAG_FOCUS_TYPE, "Focus Type");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_7, "Unknown Camera Setting 7");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_8, "Unknown Camera Setting 8");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_9, "Unknown Camera Setting 9");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_10, "Unknown Camera Setting 10");
+        _tagNameMap.put(CameraSettings.TAG_FLASH_ACTIVITY, "Flash Activity");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_12, "Unknown Camera Setting 12");
+        _tagNameMap.put(CameraSettings.TAG_UNKNOWN_13, "Unknown Camera Setting 13");
+
+        _tagNameMap.put(FocalLength.TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(FocalLength.TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(FocalLength.TAG_AF_POINT_USED, "AF Point Used");
+        _tagNameMap.put(FocalLength.TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(FocalLength.TAG_AUTO_EXPOSURE_BRACKETING, "Auto Exposure Bracketing");
+        _tagNameMap.put(FocalLength.TAG_AEB_BRACKET_VALUE, "AEB Bracket Value");
+        _tagNameMap.put(FocalLength.TAG_SUBJECT_DISTANCE, "Subject Distance");
+
+        _tagNameMap.put(ShotInfo.TAG_AUTO_ISO, "Auto ISO");
+        _tagNameMap.put(ShotInfo.TAG_BASE_ISO, "Base ISO");
+        _tagNameMap.put(ShotInfo.TAG_MEASURED_EV, "Measured EV");
+        _tagNameMap.put(ShotInfo.TAG_TARGET_APERTURE, "Target Aperture");
+        _tagNameMap.put(ShotInfo.TAG_TARGET_EXPOSURE_TIME, "Target Exposure Time");
+        _tagNameMap.put(ShotInfo.TAG_EXPOSURE_COMPENSATION, "Exposure Compensation");
+        _tagNameMap.put(ShotInfo.TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(ShotInfo.TAG_SLOW_SHUTTER, "Slow Shutter");
+        _tagNameMap.put(ShotInfo.TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(ShotInfo.TAG_OPTICAL_ZOOM_CODE, "Optical Zoom Code");
+        _tagNameMap.put(ShotInfo.TAG_CAMERA_TEMPERATURE, "Camera Temperature");
+        _tagNameMap.put(ShotInfo.TAG_FLASH_GUIDE_NUMBER, "Flash Guide Number");
+        _tagNameMap.put(ShotInfo.TAG_AF_POINTS_IN_FOCUS, "AF Points in Focus");
+        _tagNameMap.put(ShotInfo.TAG_FLASH_EXPOSURE_BRACKETING, "Flash Exposure Compensation");
+        _tagNameMap.put(ShotInfo.TAG_AUTO_EXPOSURE_BRACKETING, "Auto Exposure Bracketing");
+        _tagNameMap.put(ShotInfo.TAG_AEB_BRACKET_VALUE, "AEB Bracket Value");
+        _tagNameMap.put(ShotInfo.TAG_CONTROL_MODE, "Control Mode");
+        _tagNameMap.put(ShotInfo.TAG_FOCUS_DISTANCE_UPPER, "Focus Distance Upper");
+        _tagNameMap.put(ShotInfo.TAG_FOCUS_DISTANCE_LOWER, "Focus Distance Lower");
+        _tagNameMap.put(ShotInfo.TAG_F_NUMBER, "F Number");
+        _tagNameMap.put(ShotInfo.TAG_EXPOSURE_TIME, "Exposure Time");
+        _tagNameMap.put(ShotInfo.TAG_MEASURED_EV_2, "Measured EV 2");
+        _tagNameMap.put(ShotInfo.TAG_BULB_DURATION, "Bulb Duration");
+        _tagNameMap.put(ShotInfo.TAG_CAMERA_TYPE, "Camera Type");
+        _tagNameMap.put(ShotInfo.TAG_AUTO_ROTATE, "Auto Rotate");
+        _tagNameMap.put(ShotInfo.TAG_ND_FILTER, "ND Filter");
+        _tagNameMap.put(ShotInfo.TAG_SELF_TIMER_2, "Self Timer 2");
+        _tagNameMap.put(ShotInfo.TAG_FLASH_OUTPUT, "Flash Output");
+
+        _tagNameMap.put(Panorama.TAG_PANORAMA_FRAME_NUMBER, "Panorama Frame Number");
+        _tagNameMap.put(Panorama.TAG_PANORAMA_DIRECTION, "Panorama Direction");
+
+        _tagNameMap.put(AFInfo.TAG_NUM_AF_POINTS, "AF Point Count");
+        _tagNameMap.put(AFInfo.TAG_VALID_AF_POINTS, "Valid AF Point Count");
+        _tagNameMap.put(AFInfo.TAG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(AFInfo.TAG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(AFInfo.TAG_AF_IMAGE_WIDTH, "AF Image Width");
+        _tagNameMap.put(AFInfo.TAG_AF_IMAGE_HEIGHT, "AF Image Height");
+        _tagNameMap.put(AFInfo.TAG_AF_AREA_WIDTH, "AF Area Width");
+        _tagNameMap.put(AFInfo.TAG_AF_AREA_HEIGHT, "AF Area Height");
+        _tagNameMap.put(AFInfo.TAG_AF_AREA_X_POSITIONS, "AF Area X Positions");
+        _tagNameMap.put(AFInfo.TAG_AF_AREA_Y_POSITIONS, "AF Area Y Positions");
+        _tagNameMap.put(AFInfo.TAG_AF_POINTS_IN_FOCUS, "AF Points in Focus Count");
+        _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_1, "Primary AF Point 1");
+        _tagNameMap.put(AFInfo.TAG_PRIMARY_AF_POINT_2, "Primary AF Point 2");
+        
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_LONG_EXPOSURE_NOISE_REDUCTION, "Long Exposure Noise Reduction");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_AUTO_EXPOSURE_LOCK_BUTTONS, "Shutter/Auto Exposure-lock Buttons");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_MIRROR_LOCKUP, "Mirror Lockup");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_TV_AV_AND_EXPOSURE_LEVEL, "Tv/Av And Exposure Level");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_AF_ASSIST_LIGHT, "AF-Assist Light");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_SPEED_IN_AV_MODE, "Shutter Speed in Av Mode");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_BRACKETTING, "Auto-Exposure Bracketting Sequence/Auto Cancellation");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SHUTTER_CURTAIN_SYNC, "Shutter Curtain Sync");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_AF_STOP, "Lens Auto-Focus Stop Button Function Switch");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_FILL_FLASH_REDUCTION, "Auto Reduction of Fill Flash");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_MENU_BUTTON_RETURN, "Menu Button Return Position");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SET_BUTTON_FUNCTION, "SET Button Function When Shooting");
+//        _tagNameMap.put(TAG_CANON_CUSTOM_FUNCTION_SENSOR_CLEANING, "Sensor Cleaning");
+
+        _tagNameMap.put(TAG_THUMBNAIL_IMAGE_VALID_AREA, "Thumbnail Image Valid Area");
+        _tagNameMap.put(TAG_SERIAL_NUMBER_FORMAT, "Serial Number Format");
+        _tagNameMap.put(TAG_SUPER_MACRO, "Super Macro");
+        _tagNameMap.put(TAG_DATE_STAMP_MODE, "Date Stamp Mode");
+        _tagNameMap.put(TAG_MY_COLORS, "My Colors");
+        _tagNameMap.put(TAG_FIRMWARE_REVISION, "Firmware Revision");
+        _tagNameMap.put(TAG_CATEGORIES, "Categories");
+        _tagNameMap.put(TAG_FACE_DETECT_ARRAY_1, "Face Detect Array 1");
+        _tagNameMap.put(TAG_FACE_DETECT_ARRAY_2, "Face Detect Array 2");
+        _tagNameMap.put(TAG_AF_INFO_ARRAY_2, "AF Info Array 2");
+        _tagNameMap.put(TAG_IMAGE_UNIQUE_ID, "Image Unique ID");
+        _tagNameMap.put(TAG_RAW_DATA_OFFSET, "Raw Data Offset");
+        _tagNameMap.put(TAG_ORIGINAL_DECISION_DATA_OFFSET, "Original Decision Data Offset");
+        _tagNameMap.put(TAG_CUSTOM_FUNCTIONS_1D_ARRAY, "Custom Functions (1D) Array");
+        _tagNameMap.put(TAG_PERSONAL_FUNCTIONS_ARRAY, "Personal Functions Array");
+        _tagNameMap.put(TAG_PERSONAL_FUNCTION_VALUES_ARRAY, "Personal Function Values Array");
+        _tagNameMap.put(TAG_FILE_INFO_ARRAY, "File Info Array");
+        _tagNameMap.put(TAG_AF_POINTS_IN_FOCUS_1D, "AF Points in Focus (1D)");
+        _tagNameMap.put(TAG_LENS_MODEL, "Lens Model");
+        _tagNameMap.put(TAG_SERIAL_INFO_ARRAY, "Serial Info Array");
+        _tagNameMap.put(TAG_DUST_REMOVAL_DATA, "Dust Removal Data");
+        _tagNameMap.put(TAG_CROP_INFO, "Crop Info");
+        _tagNameMap.put(TAG_CUSTOM_FUNCTIONS_ARRAY_2, "Custom Functions Array 2");
+        _tagNameMap.put(TAG_ASPECT_INFO_ARRAY, "Aspect Information Array");
+        _tagNameMap.put(TAG_PROCESSING_INFO_ARRAY, "Processing Information Array");
+        _tagNameMap.put(TAG_TONE_CURVE_TABLE, "Tone Curve Table");
+        _tagNameMap.put(TAG_SHARPNESS_TABLE, "Sharpness Table");
+        _tagNameMap.put(TAG_SHARPNESS_FREQ_TABLE, "Sharpness Frequency Table");
+        _tagNameMap.put(TAG_WHITE_BALANCE_TABLE, "White Balance Table");
+        _tagNameMap.put(TAG_COLOR_BALANCE_ARRAY, "Color Balance Array");
+        _tagNameMap.put(TAG_MEASURED_COLOR_ARRAY, "Measured Color Array");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_CANON_FLAGS_ARRAY, "Canon Flags Array");
+        _tagNameMap.put(TAG_MODIFIED_INFO_ARRAY, "Modified Information Array");
+        _tagNameMap.put(TAG_TONE_CURVE_MATCHING, "Tone Curve Matching");
+        _tagNameMap.put(TAG_WHITE_BALANCE_MATCHING, "White Balance Matching");
+        _tagNameMap.put(TAG_COLOR_SPACE, "Color Space");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE_INFO_ARRAY, "Preview Image Info Array");
+        _tagNameMap.put(TAG_VRD_OFFSET, "VRD Offset");
+        _tagNameMap.put(TAG_SENSOR_INFO_ARRAY, "Sensor Information Array");
+        _tagNameMap.put(TAG_COLOR_DATA_ARRAY_2, "Color Data Array 1");
+        _tagNameMap.put(TAG_COLOR_INFO_ARRAY_2, "Color Data Array 2");
+        _tagNameMap.put(TAG_CUSTOM_PICTURE_STYLE_FILE_NAME, "Custom Picture Style File Name");
+        _tagNameMap.put(TAG_COLOR_INFO_ARRAY, "Color Info Array");
+        _tagNameMap.put(TAG_VIGNETTING_CORRECTION_ARRAY_1, "Vignetting Correction Array 1");
+        _tagNameMap.put(TAG_VIGNETTING_CORRECTION_ARRAY_2, "Vignetting Correction Array 2");
+        _tagNameMap.put(TAG_LIGHTING_OPTIMIZER_ARRAY, "Lighting Optimizer Array");
+        _tagNameMap.put(TAG_LENS_INFO_ARRAY, "Lens Info Array");
+        _tagNameMap.put(TAG_AMBIANCE_INFO_ARRAY, "Ambiance Info Array");
+        _tagNameMap.put(TAG_FILTER_INFO_ARRAY, "Filter Info Array");
     }
 
@@ -403,4 +647,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -408,40 +653,51 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
         return _tagNameMap;
     }
 
-    /**
-     * We need special handling for selected tags.
-     * @param tagType
-     * @param ints
-     */
-    public void setIntArray(int tagType, int[] ints)
-    {
-        if (tagType == TAG_CANON_CAMERA_STATE_1) {
-            // this single tag has multiple values within
-            int subTagTypeBase = 0xC100;
-            // we intentionally skip the first array member
-            for (int i = 1; i < ints.length; i++) {
-                setInt(subTagTypeBase + i, ints[i]);
-            }
-        } else if (tagType == TAG_CANON_CAMERA_STATE_2) {
-            // this single tag has multiple values within
-            int subTagTypeBase = 0xC200;
-            // we intentionally skip the first array member
-            for (int i = 1; i < ints.length; i++) {
-                setInt(subTagTypeBase + i, ints[i]);
-            }
-        } if (tagType == TAG_CANON_CUSTOM_FUNCTIONS) {
-            // this single tag has multiple values within
-            int subTagTypeBase = 0xC300;
-            // we intentionally skip the first array member
-            for (int i = 1; i < ints.length; i++) {
-                setInt(subTagTypeBase + i + 1, ints[i] & 0x0F);
-            }
-        } else {
-            // no special handling...
-            super.setIntArray(tagType, ints);
+    @Override
+    public void setIntArray(int tagType, @NotNull int[] ints)
+    {
+        // TODO is there some way to drop out 'null' or 'zero' values that are present in the array to reduce the noise?
+        
+        // Certain Canon tags contain arrays of values that we split into 'fake' tags as each
+        // index in the array has its own meaning and decoding.
+        // Pick those tags out here and throw away the original array.
+        // Otherwise just add as usual.
+        switch (tagType) {
+            case TAG_CAMERA_SETTINGS_ARRAY:
+                for (int i = 0; i < ints.length; i++)
+                    setInt(CameraSettings.OFFSET + i, ints[i]);
+                break;
+            case TAG_FOCAL_LENGTH_ARRAY:
+                for (int i = 0; i < ints.length; i++)
+                    setInt(FocalLength.OFFSET + i, ints[i]);
+                break;
+            case TAG_SHOT_INFO_ARRAY:
+                for (int i = 0; i < ints.length; i++)
+                    setInt(ShotInfo.OFFSET + i, ints[i]);
+                break;
+            case TAG_PANORAMA_ARRAY:
+                for (int i = 0; i < ints.length; i++)
+                    setInt(Panorama.OFFSET + i, ints[i]);
+                break;
+            // TODO the interpretation of the custom functions tag depends upon the camera model
+//            case TAG_CANON_CUSTOM_FUNCTIONS_ARRAY:
+//                int subTagTypeBase = 0xC300;
+//                // we intentionally skip the first array member
+//                for (int i = 1; i < ints.length; i++)
+//                    setInt(subTagTypeBase + i + 1, ints[i] & 0x0F);
+//                break;
+            case TAG_AF_INFO_ARRAY:
+                for (int i = 0; i < ints.length; i++)
+                    setInt(AFInfo.OFFSET + i, ints[i]);
+                break;
+            default:
+                // no special handling...
+                super.setIntArray(tagType, ints);
+                break;
         }
     }
Index: trunk/src/com/drew/metadata/exif/CasioType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/CasioType1MakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/CasioType1MakernoteDescriptor.java	(revision 6127)
@@ -1,35 +1,42 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 27-Nov-2002 10:12:05 using IntelliJ IDEA.
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
- *
+ * Provides human-readable string representations of tag values stored in a <code>CasioType1MakernoteDirectory</code>.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class CasioType1MakernoteDescriptor extends TagDescriptor
+public class CasioType1MakernoteDescriptor extends TagDescriptor<CasioType1MakernoteDirectory>
 {
-    public CasioType1MakernoteDescriptor(Directory directory)
+    public CasioType1MakernoteDescriptor(@NotNull CasioType1MakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
@@ -59,12 +66,16 @@
                 return getCcdSensitivityDescription();
             default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getCcdSensitivityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_CCD_SENSITIVITY)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_CCD_SENSITIVITY);
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getCcdSensitivityDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_CCD_SENSITIVITY);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             // these four for QV3000
@@ -87,8 +98,12 @@
     }
 
-    public String getSaturationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_SATURATION)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_SATURATION);
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_SATURATION);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 0:
@@ -103,8 +118,12 @@
     }
 
-    public String getContrastDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_CONTRAST)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_CONTRAST);
+    @Nullable
+    public String getContrastDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_CONTRAST);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 0:
@@ -119,8 +138,12 @@
     }
 
-    public String getSharpnessDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_SHARPNESS)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_SHARPNESS);
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_SHARPNESS);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 0:
@@ -135,8 +158,12 @@
     }
 
-    public String getDigitalZoomDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_DIGITAL_ZOOM)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_DIGITAL_ZOOM);
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_DIGITAL_ZOOM);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 0x10000:
@@ -153,8 +180,12 @@
     }
 
-    public String getWhiteBalanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_WHITE_BALANCE)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_WHITE_BALANCE);
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_WHITE_BALANCE);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 1:
@@ -165,5 +196,5 @@
                 return "Daylight";
             case 4:
-                return "Flourescent";
+                return "Florescent";
             case 5:
                 return "Shade";
@@ -175,15 +206,23 @@
     }
 
-    public String getObjectDistanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_OBJECT_DISTANCE)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_OBJECT_DISTANCE);
+    @Nullable
+    public String getObjectDistanceDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_OBJECT_DISTANCE);
+
+        if (value == null)
+            return null;
+
         return value + " mm";
     }
 
-    public String getFlashIntensityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_FLASH_INTENSITY)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_FLASH_INTENSITY);
+    @Nullable
+    public String getFlashIntensityDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_FLASH_INTENSITY);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 11:
@@ -198,8 +237,12 @@
     }
 
-    public String getFlashModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_FLASH_MODE)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_FLASH_MODE);
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_FLASH_MODE);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 1:
@@ -218,8 +261,12 @@
     }
 
-    public String getFocusingModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_FOCUSING_MODE)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_FOCUSING_MODE);
+    @Nullable
+    public String getFocusingModeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_FOCUSING_MODE);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 2:
@@ -236,8 +283,12 @@
     }
 
-    public String getQualityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_QUALITY)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_QUALITY);
+    @Nullable
+    public String getQualityDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_QUALITY);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 1:
@@ -252,8 +303,12 @@
     }
 
-    public String getRecordingModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType1MakernoteDirectory.TAG_CASIO_RECORDING_MODE)) return null;
-        int value = _directory.getInt(CasioType1MakernoteDirectory.TAG_CASIO_RECORDING_MODE);
+    @Nullable
+    public String getRecordingModeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType1MakernoteDirectory.TAG_CASIO_RECORDING_MODE);
+
+        if (value == null)
+            return null;
+
         switch (value) {
             case 1:
Index: trunk/src/com/drew/metadata/exif/CasioType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/CasioType1MakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/CasioType1MakernoteDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,6 +27,10 @@
 
 /**
+ * Describes tags specific to Casio (type 1) cameras.
+ *
  * A standard TIFF IFD directory but always uses Motorola (Big-Endian) Byte Alignment.
  * Makernote data begins immediately (no header).
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class CasioType1MakernoteDirectory extends Directory
@@ -48,28 +57,29 @@
     public static final int TAG_CASIO_CCD_SENSITIVITY = 0x0014;
 
-    protected static final HashMap<Integer, String> tagNameMap = new HashMap<Integer, String>();
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        tagNameMap.put(new Integer(TAG_CASIO_CCD_SENSITIVITY), "CCD Sensitivity");
-        tagNameMap.put(new Integer(TAG_CASIO_CONTRAST), "Contrast");
-        tagNameMap.put(new Integer(TAG_CASIO_DIGITAL_ZOOM), "Digital Zoom");
-        tagNameMap.put(new Integer(TAG_CASIO_FLASH_INTENSITY), "Flash Intensity");
-        tagNameMap.put(new Integer(TAG_CASIO_FLASH_MODE), "Flash Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_FOCUSING_MODE), "Focussing Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_OBJECT_DISTANCE), "Object Distance");
-        tagNameMap.put(new Integer(TAG_CASIO_QUALITY), "Quality");
-        tagNameMap.put(new Integer(TAG_CASIO_RECORDING_MODE), "Recording Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_SATURATION), "Saturation");
-        tagNameMap.put(new Integer(TAG_CASIO_SHARPNESS), "Sharpness");
-        tagNameMap.put(new Integer(TAG_CASIO_UNKNOWN_1), "Makernote Unknown 1");
-        tagNameMap.put(new Integer(TAG_CASIO_UNKNOWN_2), "Makernote Unknown 2");
-        tagNameMap.put(new Integer(TAG_CASIO_UNKNOWN_3), "Makernote Unknown 3");
-        tagNameMap.put(new Integer(TAG_CASIO_UNKNOWN_4), "Makernote Unknown 4");
-        tagNameMap.put(new Integer(TAG_CASIO_UNKNOWN_5), "Makernote Unknown 5");
-        tagNameMap.put(new Integer(TAG_CASIO_UNKNOWN_6), "Makernote Unknown 6");
-        tagNameMap.put(new Integer(TAG_CASIO_UNKNOWN_7), "Makernote Unknown 7");
-        tagNameMap.put(new Integer(TAG_CASIO_UNKNOWN_8), "Makernote Unknown 8");
-        tagNameMap.put(new Integer(TAG_CASIO_WHITE_BALANCE), "White Balance");
+        _tagNameMap.put(TAG_CASIO_CCD_SENSITIVITY, "CCD Sensitivity");
+        _tagNameMap.put(TAG_CASIO_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_CASIO_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_CASIO_FLASH_INTENSITY, "Flash Intensity");
+        _tagNameMap.put(TAG_CASIO_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_CASIO_FOCUSING_MODE, "Focusing Mode");
+        _tagNameMap.put(TAG_CASIO_OBJECT_DISTANCE, "Object Distance");
+        _tagNameMap.put(TAG_CASIO_QUALITY, "Quality");
+        _tagNameMap.put(TAG_CASIO_RECORDING_MODE, "Recording Mode");
+        _tagNameMap.put(TAG_CASIO_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_CASIO_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_CASIO_UNKNOWN_1, "Makernote Unknown 1");
+        _tagNameMap.put(TAG_CASIO_UNKNOWN_2, "Makernote Unknown 2");
+        _tagNameMap.put(TAG_CASIO_UNKNOWN_3, "Makernote Unknown 3");
+        _tagNameMap.put(TAG_CASIO_UNKNOWN_4, "Makernote Unknown 4");
+        _tagNameMap.put(TAG_CASIO_UNKNOWN_5, "Makernote Unknown 5");
+        _tagNameMap.put(TAG_CASIO_UNKNOWN_6, "Makernote Unknown 6");
+        _tagNameMap.put(TAG_CASIO_UNKNOWN_7, "Makernote Unknown 7");
+        _tagNameMap.put(TAG_CASIO_UNKNOWN_8, "Makernote Unknown 8");
+        _tagNameMap.put(TAG_CASIO_WHITE_BALANCE, "White Balance");
     }
 
@@ -79,4 +89,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -84,7 +95,8 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
-        return tagNameMap;
+        return _tagNameMap;
     }
 }
Index: trunk/src/com/drew/metadata/exif/CasioType2MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/CasioType2MakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/CasioType2MakernoteDescriptor.java	(revision 6127)
@@ -1,35 +1,42 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 27-Nov-2002 10:12:05 using IntelliJ IDEA.
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
- *
+ * Provides human-readable string representations of tag values stored in a <code>CasioType2MakernoteDirectory</code>.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class CasioType2MakernoteDescriptor extends TagDescriptor
+public class CasioType2MakernoteDescriptor extends TagDescriptor<CasioType2MakernoteDirectory>
 {
-    public CasioType2MakernoteDescriptor(Directory directory)
+    public CasioType2MakernoteDescriptor(@NotNull CasioType2MakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
@@ -91,48 +98,56 @@
                 return getFilterDescription();
             default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getFilterDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FILTER)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FILTER);
-        switch (value) {
-            case 0:
-                return "Off";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getEnhancementDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ENHANCEMENT)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ENHANCEMENT);
-        switch (value) {
-            case 0:
-                return "Off";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getColourModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_COLOUR_MODE)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_COLOUR_MODE);
-        switch (value) {
-            case 0:
-                return "Off";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getCcdIsoSensitivityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CCD_ISO_SENSITIVITY)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CCD_ISO_SENSITIVITY);
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getFilterDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FILTER);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Off";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getEnhancementDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ENHANCEMENT);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Off";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColourModeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_COLOUR_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Off";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getCcdIsoSensitivityDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CCD_ISO_SENSITIVITY);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -145,24 +160,28 @@
     }
 
-    public String getBestShotModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_BESTSHOT_MODE)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_BESTSHOT_MODE);
-        switch (value) {
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
+    @Nullable
+    public String getBestShotModeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_BESTSHOT_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
     public String getTimeZoneDescription()
     {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_TIME_ZONE)) return null;
         return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_TIME_ZONE);
     }
 
-    public String getFocusMode2Description() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_2)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_2);
+    @Nullable
+    public String getFocusMode2Description()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_2);
+        if (value==null)
+            return null;
         switch (value) {
             case 1:
@@ -175,8 +194,10 @@
     }
 
-    public String getQualityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY);
+    @Nullable
+    public String getQualityDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY);
+        if (value==null)
+            return null;
         switch (value) {
             case 3:
@@ -187,20 +208,24 @@
     }
 
-    public String getSelfTimerDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SELF_TIMER)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SELF_TIMER);
-        switch (value) {
-            case 1:
-                return "Off";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getRecordModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_RECORD_MODE)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_RECORD_MODE);
+    @Nullable
+    public String getSelfTimerDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SELF_TIMER);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Off";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getRecordModeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_RECORD_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 2:
@@ -211,27 +236,33 @@
     }
 
-    public String getFlashDistanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FLASH_DISTANCE)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FLASH_DISTANCE);
-        switch (value) {
-            case 0:
-                return "Off";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getObjectDistanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_OBJECT_DISTANCE)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_OBJECT_DISTANCE);
+    @Nullable
+    public String getFlashDistanceDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FLASH_DISTANCE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Off";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getObjectDistanceDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_OBJECT_DISTANCE);
+        if (value==null)
+            return null;
         return Integer.toString(value) + " mm";
     }
 
-    public String getWhiteBalance2Description() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_2)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_2);
+    @Nullable
+    public String getWhiteBalance2Description()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_2);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -248,28 +279,32 @@
     }
 
+    @Nullable
     public String getWhiteBalanceBiasDescription()
     {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_BIAS)) return null;
         return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_BIAS);
     }
 
-    public String getCasioPreviewThumbnailDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CASIO_PREVIEW_THUMBNAIL)) return null;
+    @Nullable
+    public String getCasioPreviewThumbnailDescription()
+    {
         final byte[] bytes = _directory.getByteArray(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CASIO_PREVIEW_THUMBNAIL);
+        if (bytes==null)
+            return null;
         return "<" + bytes.length + " bytes of image data>";
     }
 
+    @Nullable
     public String getPrintImageMatchingInfoDescription()
     {
         // TODO research PIM specification http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_PRINT_IMAGE_MATCHING_INFO)) return null;
         return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_PRINT_IMAGE_MATCHING_INFO);
     }
 
-    public String getSharpnessDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SHARPNESS)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SHARPNESS);
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SHARPNESS);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -284,8 +319,10 @@
     }
 
-    public String getContrastDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CONTRAST)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CONTRAST);
+    @Nullable
+    public String getContrastDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_CONTRAST);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -300,8 +337,10 @@
     }
 
-    public String getSaturationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SATURATION)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SATURATION);
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_SATURATION);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -316,15 +355,19 @@
     }
 
-    public String getFocalLengthDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCAL_LENGTH)) return null;
-        double value = _directory.getDouble(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCAL_LENGTH);
+    @Nullable
+    public String getFocalLengthDescription()
+    {
+        Double value = _directory.getDoubleObject(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCAL_LENGTH);
+        if (value==null)
+            return null;
         return Double.toString(value / 10d) + " mm";
     }
 
-    public String getWhiteBalance1Description() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_1)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_1);
+    @Nullable
+    public String getWhiteBalance1Description()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_WHITE_BALANCE_1);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -337,5 +380,5 @@
                 return "Tungsten";
             case 4:
-                return "Flourescent";
+                return "Florescent";
             case 5:
                 return "Manual";
@@ -345,8 +388,10 @@
     }
 
-    public String getIsoSensitivityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ISO_SENSITIVITY)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ISO_SENSITIVITY);
+    @Nullable
+    public String getIsoSensitivityDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_ISO_SENSITIVITY);
+        if (value==null)
+            return null;
         switch (value) {
             case 3:
@@ -363,8 +408,10 @@
     }
 
-    public String getFocusMode1Description() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_1)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_1);
+    @Nullable
+    public String getFocusMode1Description()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_FOCUS_MODE_1);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -377,8 +424,10 @@
     }
 
-    public String getImageSizeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_IMAGE_SIZE)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_IMAGE_SIZE);
+    @Nullable
+    public String getImageSizeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_IMAGE_SIZE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:  return "640 x 480 pixels";
@@ -393,8 +442,10 @@
     }
 
-    public String getQualityModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY_MODE)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY_MODE);
+    @Nullable
+    public String getQualityModeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_QUALITY_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 1:
@@ -407,22 +458,24 @@
     }
 
+    @Nullable
     public String getThumbnailOffsetDescription()
     {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_OFFSET)) return null;
         return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_OFFSET);
     }
 
-    public String getThumbnailSizeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_SIZE)) return null;
-        int value = _directory.getInt(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_SIZE);
+    @Nullable
+    public String getThumbnailSizeDescription()
+    {
+        Integer value = _directory.getInteger(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_SIZE);
+        if (value==null)
+            return null;
         return Integer.toString(value) + " bytes";
     }
 
-    public String getThumbnailDimensionsDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS)) return null;
+    @Nullable
+    public String getThumbnailDimensionsDescription()
+    {
         int[] dimensions = _directory.getIntArray(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS);
-        if (dimensions.length!=2)
+        if (dimensions==null || dimensions.length!=2)
             return _directory.getString(CasioType2MakernoteDirectory.TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS);
         return dimensions[0] + " x " + dimensions[1] + " pixels";
Index: trunk/src/com/drew/metadata/exif/CasioType2MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/CasioType2MakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/CasioType2MakernoteDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,6 +27,10 @@
 
 /**
+ * Describes tags specific to Casio (type 2) cameras.
+ *
  * A standard TIFF IFD directory but always uses Motorola (Big-Endian) Byte Alignment.
  * Makernote data begins after a 6-byte header: "QVC\x00\x00\x00"
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class CasioType2MakernoteDirectory extends Directory
@@ -167,37 +176,38 @@
     public static final int TAG_CASIO_TYPE2_FILTER = 0x3017;
 
-    protected static final HashMap tagNameMap = new HashMap();
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        // TODO add names
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS), "Thumbnail Dimensions");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_THUMBNAIL_SIZE), "Thumbnail Size");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_THUMBNAIL_OFFSET), "Thumbnail Offset");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_QUALITY_MODE), "Quality Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_IMAGE_SIZE), "Image Size");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_FOCUS_MODE_1), "Focus Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_ISO_SENSITIVITY), "ISO Sensitivity");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_WHITE_BALANCE_1), "White Balance");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_FOCAL_LENGTH), "Focal Length");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_SATURATION), "Saturation");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_CONTRAST), "Contrast");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_SHARPNESS), "Sharpness");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_PRINT_IMAGE_MATCHING_INFO), "Print Image Matching (PIM) Info");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_CASIO_PREVIEW_THUMBNAIL), "Casio Preview Thumbnail");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_WHITE_BALANCE_BIAS), "White Balance Bias");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_WHITE_BALANCE_2), "White Balance");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_OBJECT_DISTANCE), "Object Distance");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_FLASH_DISTANCE), "Flash Distance");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_RECORD_MODE), "Record Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_SELF_TIMER), "Self Timer");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_QUALITY), "Quality");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_FOCUS_MODE_2), "Focus Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_TIME_ZONE), "Time Zone");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_BESTSHOT_MODE), "BestShot Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_CCD_ISO_SENSITIVITY), "CCD ISO Sensitivity");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_COLOUR_MODE), "Colour Mode");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_ENHANCEMENT), "Enhancement");
-        tagNameMap.put(new Integer(TAG_CASIO_TYPE2_FILTER), "Filter");
+        // TODO add missing names
+        _tagNameMap.put(TAG_CASIO_TYPE2_THUMBNAIL_DIMENSIONS, "Thumbnail Dimensions");
+        _tagNameMap.put(TAG_CASIO_TYPE2_THUMBNAIL_SIZE, "Thumbnail Size");
+        _tagNameMap.put(TAG_CASIO_TYPE2_THUMBNAIL_OFFSET, "Thumbnail Offset");
+        _tagNameMap.put(TAG_CASIO_TYPE2_QUALITY_MODE, "Quality Mode");
+        _tagNameMap.put(TAG_CASIO_TYPE2_IMAGE_SIZE, "Image Size");
+        _tagNameMap.put(TAG_CASIO_TYPE2_FOCUS_MODE_1, "Focus Mode");
+        _tagNameMap.put(TAG_CASIO_TYPE2_ISO_SENSITIVITY, "ISO Sensitivity");
+        _tagNameMap.put(TAG_CASIO_TYPE2_WHITE_BALANCE_1, "White Balance");
+        _tagNameMap.put(TAG_CASIO_TYPE2_FOCAL_LENGTH, "Focal Length");
+        _tagNameMap.put(TAG_CASIO_TYPE2_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_CASIO_TYPE2_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_CASIO_TYPE2_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_CASIO_TYPE2_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_CASIO_TYPE2_CASIO_PREVIEW_THUMBNAIL, "Casio Preview Thumbnail");
+        _tagNameMap.put(TAG_CASIO_TYPE2_WHITE_BALANCE_BIAS, "White Balance Bias");
+        _tagNameMap.put(TAG_CASIO_TYPE2_WHITE_BALANCE_2, "White Balance");
+        _tagNameMap.put(TAG_CASIO_TYPE2_OBJECT_DISTANCE, "Object Distance");
+        _tagNameMap.put(TAG_CASIO_TYPE2_FLASH_DISTANCE, "Flash Distance");
+        _tagNameMap.put(TAG_CASIO_TYPE2_RECORD_MODE, "Record Mode");
+        _tagNameMap.put(TAG_CASIO_TYPE2_SELF_TIMER, "Self Timer");
+        _tagNameMap.put(TAG_CASIO_TYPE2_QUALITY, "Quality");
+        _tagNameMap.put(TAG_CASIO_TYPE2_FOCUS_MODE_2, "Focus Mode");
+        _tagNameMap.put(TAG_CASIO_TYPE2_TIME_ZONE, "Time Zone");
+        _tagNameMap.put(TAG_CASIO_TYPE2_BESTSHOT_MODE, "BestShot Mode");
+        _tagNameMap.put(TAG_CASIO_TYPE2_CCD_ISO_SENSITIVITY, "CCD ISO Sensitivity");
+        _tagNameMap.put(TAG_CASIO_TYPE2_COLOUR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_CASIO_TYPE2_ENHANCEMENT, "Enhancement");
+        _tagNameMap.put(TAG_CASIO_TYPE2_FILTER, "Filter");
     }
 
@@ -207,4 +217,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -212,7 +223,8 @@
     }
 
-    protected HashMap getTagNameMap()
-    {
-        return tagNameMap;
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
     }
 }
Index: trunk/src/com/drew/metadata/exif/DataFormat.java
===================================================================
--- trunk/src/com/drew/metadata/exif/DataFormat.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/DataFormat.java	(revision 6127)
@@ -1,41 +1,51 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.MetadataException;
 
 /**
  * An enumeration of data formats used in the TIFF IFDs.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class DataFormat
 {
-    public static final DataFormat BYTE = new DataFormat("BYTE", 1);
-    public static final DataFormat STRING = new DataFormat("STRING", 2);
-    public static final DataFormat USHORT = new DataFormat("USHORT", 3);
-    public static final DataFormat ULONG = new DataFormat("ULONG", 4);
-    public static final DataFormat URATIONAL = new DataFormat("URATIONAL", 5);
-    public static final DataFormat SBYTE = new DataFormat("SBYTE", 6);
-    public static final DataFormat UNDEFINED = new DataFormat("UNDEFINED", 7);
-    public static final DataFormat SSHORT = new DataFormat("SSHORT", 8);
-    public static final DataFormat SLONG = new DataFormat("SLONG", 9);
-    public static final DataFormat SRATIONAL = new DataFormat("SRATIONAL", 10);
-    public static final DataFormat SINGLE = new DataFormat("SINGLE", 11);
-    public static final DataFormat DOUBLE = new DataFormat("DOUBLE", 12);
+    @NotNull public static final DataFormat BYTE = new DataFormat("BYTE", 1);
+    @NotNull public static final DataFormat STRING = new DataFormat("STRING", 2);
+    @NotNull public static final DataFormat USHORT = new DataFormat("USHORT", 3);
+    @NotNull public static final DataFormat ULONG = new DataFormat("ULONG", 4);
+    @NotNull public static final DataFormat URATIONAL = new DataFormat("URATIONAL", 5);
+    @NotNull public static final DataFormat SBYTE = new DataFormat("SBYTE", 6);
+    @NotNull public static final DataFormat UNDEFINED = new DataFormat("UNDEFINED", 7);
+    @NotNull public static final DataFormat SSHORT = new DataFormat("SSHORT", 8);
+    @NotNull public static final DataFormat SLONG = new DataFormat("SLONG", 9);
+    @NotNull public static final DataFormat SRATIONAL = new DataFormat("SRATIONAL", 10);
+    @NotNull public static final DataFormat SINGLE = new DataFormat("SINGLE", 11);
+    @NotNull public static final DataFormat DOUBLE = new DataFormat("DOUBLE", 12);
 
-    private final String myName;
-    private final int value;
+    @NotNull private final String _name;
+    private final int _value;
 
+    @NotNull
     public static DataFormat fromValue(int value) throws MetadataException
     {
@@ -59,18 +69,19 @@
     }
 
-    private DataFormat(String name, int value)
+    private DataFormat(@NotNull String name, int value)
     {
-        myName = name;
-        this.value = value;
+        _name = name;
+        _value = value;
     }
 
     public int getValue()
     {
-        return value;
+        return _value;
     }
 
+    @NotNull
     public String toString()
     {
-        return myName;
+        return _name;
     }
 }
Index: trunk/src/com/drew/metadata/exif/ExifDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifDescriptor.java	(revision 6002)
+++ 	(revision )
@@ -1,1121 +1,0 @@
-/*
- * ExifDescriptor.java
- *
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 12-Nov-2002 22:27:15 using IntelliJ IDEA.
- */
-package com.drew.metadata.exif;
-
-import com.drew.imaging.PhotographicConversions;
-import com.drew.lang.Rational;
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
-import com.drew.metadata.TagDescriptor;
-
-import java.io.UnsupportedEncodingException;
-import java.text.DecimalFormat;
-
-/**
- * Contains all logic for the presentation of raw Exif data, as stored in ExifDirectory.  Use
- * this class to provide human-readable descriptions of tag values.
- */
-public class ExifDescriptor extends TagDescriptor
-{
-    /**
-     * Dictates whether rational values will be represented in decimal format in instances
-     * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3).
-     */
-    private boolean _allowDecimalRepresentationOfRationals = true;
-
-    private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#");
-
-    public ExifDescriptor(Directory directory)
-    {
-        super(directory);
-    }
-
-    // Note for the potential addition of brightness presentation in eV:
-    // Brightness of taken subject. To calculate Exposure(Ev) from BrigtnessValue(Bv),
-    // you must add SensitivityValue(Sv).
-    // Ev=BV+Sv   Sv=log2(ISOSpeedRating/3.125)
-    // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32.
-
-    /**
-     * Returns a descriptive value of the the specified tag for this image.
-     * Where possible, known values will be substituted here in place of the raw
-     * tokens actually kept in the Exif segment.  If no substitution is
-     * available, the value provided by getString(int) will be returned.
-     * @param tagType the tag to find a description for
-     * @return a description of the image's value for the specified tag, or
-     *         <code>null</code> if the tag hasn't been defined.
-     */
-    public String getDescription(int tagType) throws MetadataException
-    {
-        switch (tagType) {
-            case ExifDirectory.TAG_ORIENTATION:
-                return getOrientationDescription();
-            case ExifDirectory.TAG_NEW_SUBFILE_TYPE:
-                return getNewSubfileTypeDescription();
-            case ExifDirectory.TAG_SUBFILE_TYPE:
-                return getSubfileTypeDescription();
-            case ExifDirectory.TAG_THRESHOLDING:
-                return getThresholdingDescription();
-            case ExifDirectory.TAG_FILL_ORDER:
-                return getFillOrderDescription();
-            case ExifDirectory.TAG_RESOLUTION_UNIT:
-                return getResolutionDescription();
-            case ExifDirectory.TAG_YCBCR_POSITIONING:
-                return getYCbCrPositioningDescription();
-            case ExifDirectory.TAG_EXPOSURE_TIME:
-                return getExposureTimeDescription();
-            case ExifDirectory.TAG_SHUTTER_SPEED:
-                return getShutterSpeedDescription();
-            case ExifDirectory.TAG_FNUMBER:
-                return getFNumberDescription();
-            case ExifDirectory.TAG_X_RESOLUTION:
-                return getXResolutionDescription();
-            case ExifDirectory.TAG_Y_RESOLUTION:
-                return getYResolutionDescription();
-            case ExifDirectory.TAG_THUMBNAIL_OFFSET:
-                return getThumbnailOffsetDescription();
-            case ExifDirectory.TAG_THUMBNAIL_LENGTH:
-                return getThumbnailLengthDescription();
-            case ExifDirectory.TAG_COMPRESSION_LEVEL:
-                return getCompressionLevelDescription();
-            case ExifDirectory.TAG_SUBJECT_DISTANCE:
-                return getSubjectDistanceDescription();
-            case ExifDirectory.TAG_METERING_MODE:
-                return getMeteringModeDescription();
-            case ExifDirectory.TAG_WHITE_BALANCE:
-                return getWhiteBalanceDescription();
-            case ExifDirectory.TAG_FLASH:
-                return getFlashDescription();
-            case ExifDirectory.TAG_FOCAL_LENGTH:
-                return getFocalLengthDescription();
-            case ExifDirectory.TAG_COLOR_SPACE:
-                return getColorSpaceDescription();
-            case ExifDirectory.TAG_EXIF_IMAGE_WIDTH:
-                return getExifImageWidthDescription();
-            case ExifDirectory.TAG_EXIF_IMAGE_HEIGHT:
-                return getExifImageHeightDescription();
-            case ExifDirectory.TAG_FOCAL_PLANE_UNIT:
-                return getFocalPlaneResolutionUnitDescription();
-            case ExifDirectory.TAG_FOCAL_PLANE_X_RES:
-                return getFocalPlaneXResolutionDescription();
-            case ExifDirectory.TAG_FOCAL_PLANE_Y_RES:
-                return getFocalPlaneYResolutionDescription();
-            case ExifDirectory.TAG_THUMBNAIL_IMAGE_WIDTH:
-                return getThumbnailImageWidthDescription();
-            case ExifDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT:
-                return getThumbnailImageHeightDescription();
-            case ExifDirectory.TAG_BITS_PER_SAMPLE:
-                return getBitsPerSampleDescription();
-            case ExifDirectory.TAG_COMPRESSION:
-                return getCompressionDescription();
-            case ExifDirectory.TAG_PHOTOMETRIC_INTERPRETATION:
-                return getPhotometricInterpretationDescription();
-            case ExifDirectory.TAG_ROWS_PER_STRIP:
-                return getRowsPerStripDescription();
-            case ExifDirectory.TAG_STRIP_BYTE_COUNTS:
-                return getStripByteCountsDescription();
-            case ExifDirectory.TAG_SAMPLES_PER_PIXEL:
-                return getSamplesPerPixelDescription();
-            case ExifDirectory.TAG_PLANAR_CONFIGURATION:
-                return getPlanarConfigurationDescription();
-            case ExifDirectory.TAG_YCBCR_SUBSAMPLING:
-                return getYCbCrSubsamplingDescription();
-            case ExifDirectory.TAG_EXPOSURE_PROGRAM:
-                return getExposureProgramDescription();
-            case ExifDirectory.TAG_APERTURE:
-                return getApertureValueDescription();
-            case ExifDirectory.TAG_MAX_APERTURE:
-                return getMaxApertureValueDescription();
-            case ExifDirectory.TAG_SENSING_METHOD:
-                return getSensingMethodDescription();
-            case ExifDirectory.TAG_EXPOSURE_BIAS:
-                return getExposureBiasDescription();
-            case ExifDirectory.TAG_FILE_SOURCE:
-                return getFileSourceDescription();
-            case ExifDirectory.TAG_SCENE_TYPE:
-                return getSceneTypeDescription();
-            case ExifDirectory.TAG_COMPONENTS_CONFIGURATION:
-                return getComponentConfigurationDescription();
-            case ExifDirectory.TAG_EXIF_VERSION:
-                return getExifVersionDescription();
-            case ExifDirectory.TAG_FLASHPIX_VERSION:
-                return getFlashPixVersionDescription();
-            case ExifDirectory.TAG_REFERENCE_BLACK_WHITE:
-                return getReferenceBlackWhiteDescription();
-            case ExifDirectory.TAG_ISO_EQUIVALENT:
-                return getIsoEquivalentDescription();
-            case ExifDirectory.TAG_THUMBNAIL_DATA:
-                return getThumbnailDescription();
-            case ExifDirectory.TAG_USER_COMMENT:
-                return getUserCommentDescription();
-            case ExifDirectory.TAG_CUSTOM_RENDERED:
-                return getCustomRenderedDescription();
-            case ExifDirectory.TAG_EXPOSURE_MODE:
-                return getExposureModeDescription();
-            case ExifDirectory.TAG_WHITE_BALANCE_MODE:
-                return getWhiteBalanceModeDescription();
-            case ExifDirectory.TAG_DIGITAL_ZOOM_RATIO:
-                return getDigitalZoomRatioDescription();
-            case ExifDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
-                return get35mmFilmEquivFocalLengthDescription();
-            case ExifDirectory.TAG_SCENE_CAPTURE_TYPE:
-                return getSceneCaptureTypeDescription();
-            case ExifDirectory.TAG_GAIN_CONTROL:
-                return getGainControlDescription();
-            case ExifDirectory.TAG_CONTRAST:
-                return getContrastDescription();
-            case ExifDirectory.TAG_SATURATION:
-                return getSaturationDescription();
-            case ExifDirectory.TAG_SHARPNESS:
-                return getSharpnessDescription();
-            case ExifDirectory.TAG_SUBJECT_DISTANCE_RANGE:
-                return getSubjectDistanceRangeDescription();
-
-            case ExifDirectory.TAG_WIN_AUTHOR:
-               return getWindowsAuthorDescription();
-            case ExifDirectory.TAG_WIN_COMMENT:
-               return getWindowsCommentDescription();
-            case ExifDirectory.TAG_WIN_KEYWORDS:
-               return getWindowsKeywordsDescription();
-            case ExifDirectory.TAG_WIN_SUBJECT:
-               return getWindowsSubjectDescription();
-            case ExifDirectory.TAG_WIN_TITLE:
-               return getWindowsTitleDescription();
-            default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getNewSubfileTypeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_NEW_SUBFILE_TYPE)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_NEW_SUBFILE_TYPE)) {
-            case 1: return "Full-resolution image";
-            case 2: return "Reduced-resolution image";
-            case 3: return "Single page of multi-page reduced-resolution image";
-            case 4: return "Transparency mask";
-            case 5: return "Transparency mask of reduced-resolution image";
-            case 6: return "Transparency mask of multi-page image";
-            case 7: return "Transparency mask of reduced-resolution multi-page image";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_NEW_SUBFILE_TYPE) + ")";
-        }
-    }
-
-    public String getSubfileTypeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SUBFILE_TYPE)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_SUBFILE_TYPE)) {
-            case 1: return "Full-resolution image";
-            case 2: return "Reduced-resolution image";
-            case 3: return "Single page of multi-page image";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SUBFILE_TYPE) + ")";
-        }
-    }
-
-    public String getThresholdingDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_THRESHOLDING)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_THRESHOLDING)) {
-            case 1: return "No dithering or halftoning";
-            case 2: return "Ordered dither or halftone";
-            case 3: return "Randomized dither";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_THRESHOLDING) + ")";
-        }
-    }
-
-    public String getFillOrderDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FILL_ORDER)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_FILL_ORDER)) {
-            case 1: return "Normal";
-            case 2: return "Reversed";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_FILL_ORDER) + ")";
-        }
-    }
-
-    public String getSubjectDistanceRangeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SUBJECT_DISTANCE_RANGE)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_SUBJECT_DISTANCE_RANGE)) {
-            case 0:
-                return "Unknown";
-            case 1:
-                return "Macro";
-            case 2:
-                return "Close view";
-            case 3:
-                return "Distant view";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SUBJECT_DISTANCE_RANGE) + ")";
-        }
-    }
-
-    public String getSharpnessDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SHARPNESS)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_SHARPNESS)) {
-            case 0:
-                return "None";
-            case 1:
-                return "Low";
-            case 2:
-                return "Hard";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SHARPNESS) + ")";
-        }
-    }
-
-    public String getSaturationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SATURATION)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_SATURATION)) {
-            case 0:
-                return "None";
-            case 1:
-                return "Low saturation";
-            case 2:
-                return "High saturation";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SATURATION) + ")";
-        }
-    }
-
-    public String getContrastDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_CONTRAST)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_CONTRAST)) {
-            case 0:
-                return "None";
-            case 1:
-                return "Soft";
-            case 2:
-                return "Hard";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_CONTRAST) + ")";
-        }
-    }
-
-    public String getGainControlDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_GAIN_CONTROL)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_GAIN_CONTROL)) {
-            case 0:
-                return "None";
-            case 1:
-                return "Low gain up";
-            case 2:
-                return "Low gain down";
-            case 3:
-                return "High gain up";
-            case 4:
-                return "High gain down";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_GAIN_CONTROL) + ")";
-        }
-    }
-
-    public String getSceneCaptureTypeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SCENE_CAPTURE_TYPE)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_SCENE_CAPTURE_TYPE)) {
-            case 0:
-                return "Standard";
-            case 1:
-                return "Landscape";
-            case 2:
-                return "Portrait";
-            case 3:
-                return "Night scene";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_SCENE_CAPTURE_TYPE) + ")";
-        }
-    }
-
-    public String get35mmFilmEquivFocalLengthDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH)) return null;
-        int equivalentFocalLength = _directory.getInt(ExifDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
-
-        if (equivalentFocalLength==0)
-            return "Unknown";
-        else
-            return SimpleDecimalFormatter.format(equivalentFocalLength) + "mm";
-    }
-
-    public String getDigitalZoomRatioDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_DIGITAL_ZOOM_RATIO)) return null;
-        Rational rational = _directory.getRational(ExifDirectory.TAG_DIGITAL_ZOOM_RATIO);
-        if (rational.getNumerator()==0)
-            return "Digital zoom not used.";
-
-        return SimpleDecimalFormatter.format(rational.doubleValue());
-    }
-
-    public String getWhiteBalanceModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_WHITE_BALANCE_MODE)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_WHITE_BALANCE_MODE)) {
-            case 0:
-                return "Auto white balance";
-            case 1:
-                return "Manual white balance";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_WHITE_BALANCE_MODE) + ")";
-        }
-    }
-
-    public String getExposureModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_EXPOSURE_MODE)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_EXPOSURE_MODE)) {
-            case 0:
-                return "Auto exposure";
-            case 1:
-                return "Manual exposure";
-            case 2:
-                return "Auto bracket";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_EXPOSURE_MODE) + ")";
-        }
-    }
-
-    public String getCustomRenderedDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_CUSTOM_RENDERED)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_CUSTOM_RENDERED)) {
-            case 0:
-                return "Normal process";
-            case 1:
-                return "Custom process";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_CUSTOM_RENDERED) + ")";
-        }
-    }
-
-    public String getUserCommentDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_USER_COMMENT)) return null;
-
-        byte[] commentBytes = _directory.getByteArray(ExifDirectory.TAG_USER_COMMENT);
-
-        if (commentBytes.length==0)
-            return "";
-
-        final String[] encodingNames = new String[] { "ASCII", "UNICODE", "JIS" };
-
-        if (commentBytes.length>=10)
-        {
-            String encodingRegion = new String(commentBytes, 0, 10);
-
-            // try each encoding name
-            for (int i = 0; i<encodingNames.length; i++) {
-                String encodingName = encodingNames[i];
-                if (encodingRegion.startsWith(encodingName))
-                {
-                    // remove the null characters (and any spaces) commonly present after the encoding name
-                    for (int j = encodingName.length(); j<10; j++) {
-                        byte b = commentBytes[j];
-                        if (b!='\0' && b!=' ') {
-                           if (encodingName.equals("UNICODE")) {
-                              try {
-                                 return new String(commentBytes, j, commentBytes.length - j, "UTF-16LE").trim();
-                              }
-                              catch (UnsupportedEncodingException ex) {
-                                 return null;
-                              }
-                           }
-                           return new String(commentBytes, j, commentBytes.length - j).trim();
-                        }
-                    }
-                    return new String(commentBytes, 10, commentBytes.length - 10).trim();
-                }
-            }
-        }
-
-        // special handling fell through, return a plain string representation
-        return new String(commentBytes).trim();
-    }
-
-    public String getThumbnailDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_DATA)) return null;
-        int[] thumbnailBytes = _directory.getIntArray(ExifDirectory.TAG_THUMBNAIL_DATA);
-        return "[" + thumbnailBytes.length + " bytes of thumbnail data]";
-    }
-
-    public String getIsoEquivalentDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_ISO_EQUIVALENT)) return null;
-        int isoEquiv = _directory.getInt(ExifDirectory.TAG_ISO_EQUIVALENT);
-        if (isoEquiv < 50) {
-            isoEquiv *= 200;
-        }
-        return Integer.toString(isoEquiv);
-    }
-
-    public String getReferenceBlackWhiteDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_REFERENCE_BLACK_WHITE)) return null;
-        int[] ints = _directory.getIntArray(ExifDirectory.TAG_REFERENCE_BLACK_WHITE);
-        int blackR = ints[0];
-        int whiteR = ints[1];
-        int blackG = ints[2];
-        int whiteG = ints[3];
-        int blackB = ints[4];
-        int whiteB = ints[5];
-        String pos = "[" + blackR + "," + blackG + "," + blackB + "] " +
-                "[" + whiteR + "," + whiteG + "," + whiteB + "]";
-        return pos;
-    }
-
-    public String getExifVersionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_EXIF_VERSION)) return null;
-        int[] ints = _directory.getIntArray(ExifDirectory.TAG_EXIF_VERSION);
-        return ExifDescriptor.convertBytesToVersionString(ints);
-    }
-
-    public String getFlashPixVersionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FLASHPIX_VERSION)) return null;
-        int[] ints = _directory.getIntArray(ExifDirectory.TAG_FLASHPIX_VERSION);
-        return ExifDescriptor.convertBytesToVersionString(ints);
-    }
-
-    public String getSceneTypeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SCENE_TYPE)) return null;
-        int sceneType = _directory.getInt(ExifDirectory.TAG_SCENE_TYPE);
-        if (sceneType == 1) {
-            return "Directly photographed image";
-        } else {
-            return "Unknown (" + sceneType + ")";
-        }
-    }
-
-    public String getFileSourceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FILE_SOURCE)) return null;
-        int fileSource = _directory.getInt(ExifDirectory.TAG_FILE_SOURCE);
-        if (fileSource == 3) {
-            return "Digital Still Camera (DSC)";
-        } else {
-            return "Unknown (" + fileSource + ")";
-        }
-    }
-
-    public String getExposureBiasDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_EXPOSURE_BIAS)) return null;
-        Rational exposureBias = _directory.getRational(ExifDirectory.TAG_EXPOSURE_BIAS);
-        return exposureBias.toSimpleString(true) + " EV";
-    }
-
-    public String getMaxApertureValueDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_MAX_APERTURE)) return null;
-        double aperture = _directory.getDouble(ExifDirectory.TAG_MAX_APERTURE);
-        double fStop = PhotographicConversions.apertureToFStop(aperture);
-        return "F" + SimpleDecimalFormatter.format(fStop);
-    }
-
-    public String getApertureValueDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_APERTURE)) return null;
-        double aperture = _directory.getDouble(ExifDirectory.TAG_APERTURE);
-        double fStop = PhotographicConversions.apertureToFStop(aperture);
-        return "F" + SimpleDecimalFormatter.format(fStop);
-    }
-
-    public String getExposureProgramDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_EXPOSURE_PROGRAM)) return null;
-        // '1' means manual control, '2' program normal, '3' aperture priority,
-        // '4' shutter priority, '5' program creative (slow program),
-        // '6' program action(high-speed program), '7' portrait mode, '8' landscape mode.
-        switch (_directory.getInt(ExifDirectory.TAG_EXPOSURE_PROGRAM)) {
-            case 1: return "Manual control";
-            case 2: return "Program normal";
-            case 3: return "Aperture priority";
-            case 4: return "Shutter priority";
-            case 5: return "Program creative (slow program)";
-            case 6: return "Program action (high-speed program)";
-            case 7: return "Portrait mode";
-            case 8: return "Landscape mode";
-            default:
-                return "Unknown program (" + _directory.getInt(ExifDirectory.TAG_EXPOSURE_PROGRAM) + ")";
-        }
-    }
-
-    public String getYCbCrSubsamplingDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_YCBCR_SUBSAMPLING)) return null;
-        int[] positions = _directory.getIntArray(ExifDirectory.TAG_YCBCR_SUBSAMPLING);
-        if (positions[0] == 2 && positions[1] == 1) {
-            return "YCbCr4:2:2";
-        } else if (positions[0] == 2 && positions[1] == 2) {
-            return "YCbCr4:2:0";
-        } else {
-            return "(Unknown)";
-        }
-    }
-
-    public String getPlanarConfigurationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_PLANAR_CONFIGURATION)) return null;
-        // When image format is no compression YCbCr, this value shows byte aligns of YCbCr
-        // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling
-        // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr
-        // plane format.
-
-        switch (_directory.getInt(ExifDirectory.TAG_PLANAR_CONFIGURATION)) {
-            case 1: return "Chunky (contiguous for each subsampling pixel)";
-            case 2: return "Separate (Y-plane/Cb-plane/Cr-plane format)";
-            default:
-                return "Unknown configuration";
-        }
-    }
-
-    public String getSamplesPerPixelDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SAMPLES_PER_PIXEL)) return null;
-        return _directory.getString(ExifDirectory.TAG_SAMPLES_PER_PIXEL) + " samples/pixel";
-    }
-
-    public String getRowsPerStripDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_ROWS_PER_STRIP)) return null;
-        return _directory.getString(ExifDirectory.TAG_ROWS_PER_STRIP) + " rows/strip";
-    }
-
-    public String getStripByteCountsDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_STRIP_BYTE_COUNTS)) return null;
-        return _directory.getString(ExifDirectory.TAG_STRIP_BYTE_COUNTS) + " bytes";
-    }
-
-    public String getPhotometricInterpretationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_PHOTOMETRIC_INTERPRETATION)) return null;
-        // Shows the color space of the image data components
-        switch (_directory.getInt(ExifDirectory.TAG_PHOTOMETRIC_INTERPRETATION)) {
-            case 0: return "WhiteIsZero";
-            case 1: return "BlackIsZero";
-            case 2: return "RGB";
-            case 3: return "RGB Palette";
-            case 4: return "Transparency Mask";
-            case 5: return "CMYK";
-            case 6: return "YCbCr";
-            case 8: return "CIELab";
-            case 9: return "ICCLab";
-            case 10: return "ITULab";
-            case 32803: return "Color Filter Array";
-            case 32844: return "Pixar LogL";
-            case 32845: return "Pixar LogLuv";
-            case 32892: return "Linear Raw";
-            default:
-                return "Unknown colour space";
-        }
-    }
-
-    public String getCompressionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_COMPRESSION)) return null;
-        switch (_directory.getInt(ExifDirectory.TAG_COMPRESSION)) {
-            case 1: return "Uncompressed";
-            case 2: return "CCITT 1D";
-            case 3: return "T4/Group 3 Fax";
-            case 4: return "T6/Group 4 Fax";
-            case 5: return "LZW";
-            case 6: return "JPEG (old-style)";
-            case 7: return "JPEG";
-            case 8: return "Adobe Deflate";
-            case 9: return "JBIG B&W";
-            case 10: return "JBIG Color";
-            case 32766: return "Next";
-            case 32771: return "CCIRLEW";
-            case 32773: return "PackBits";
-            case 32809: return "Thunderscan";
-            case 32895: return "IT8CTPAD";
-            case 32896: return "IT8LW";
-            case 32897: return "IT8MP";
-            case 32898: return "IT8BL";
-            case 32908: return "PixarFilm";
-            case 32909: return "PixarLog";
-            case 32946: return "Deflate";
-            case 32947: return "DCS";
-            case 32661: return "JBIG";
-            case 32676: return "SGILog";
-            case 32677: return "SGILog24";
-            case 32712: return "JPEG 2000";
-            case 32713: return "Nikon NEF Compressed";
-            default:
-                return "Unknown compression";
-        }
-    }
-
-    public String getBitsPerSampleDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_BITS_PER_SAMPLE)) return null;
-        return _directory.getString(ExifDirectory.TAG_BITS_PER_SAMPLE) + " bits/component/pixel";
-    }
-
-    public String getThumbnailImageWidthDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_IMAGE_WIDTH)) return null;
-        return _directory.getString(ExifDirectory.TAG_THUMBNAIL_IMAGE_WIDTH) + " pixels";
-    }
-
-    public String getThumbnailImageHeightDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT)) return null;
-        return _directory.getString(ExifDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT) + " pixels";
-    }
-
-    public String getFocalPlaneXResolutionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FOCAL_PLANE_X_RES)) return null;
-        Rational rational = _directory.getRational(ExifDirectory.TAG_FOCAL_PLANE_X_RES);
-        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) + " " +
-                getFocalPlaneResolutionUnitDescription().toLowerCase();
-    }
-
-    public String getFocalPlaneYResolutionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FOCAL_PLANE_Y_RES)) return null;
-        Rational rational = _directory.getRational(ExifDirectory.TAG_FOCAL_PLANE_Y_RES);
-        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals) + " " +
-                getFocalPlaneResolutionUnitDescription().toLowerCase();
-    }
-
-    public String getFocalPlaneResolutionUnitDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FOCAL_PLANE_UNIT)) return null;
-        // Unit of FocalPlaneXResoluton/FocalPlaneYResolution. '1' means no-unit,
-        // '2' inch, '3' centimeter.
-        switch (_directory.getInt(ExifDirectory.TAG_FOCAL_PLANE_UNIT)) {
-            case 1:
-                return "(No unit)";
-            case 2:
-                return "Inches";
-            case 3:
-                return "cm";
-            default:
-                return "";
-        }
-    }
-
-    public String getExifImageWidthDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_EXIF_IMAGE_WIDTH)) return null;
-        return _directory.getInt(ExifDirectory.TAG_EXIF_IMAGE_WIDTH) + " pixels";
-    }
-
-    public String getExifImageHeightDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT)) return null;
-        return _directory.getInt(ExifDirectory.TAG_EXIF_IMAGE_HEIGHT) + " pixels";
-    }
-
-    public String getColorSpaceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_COLOR_SPACE)) return null;
-        int colorSpace = _directory.getInt(ExifDirectory.TAG_COLOR_SPACE);
-        if (colorSpace == 1) {
-            return "sRGB";
-        } else if (colorSpace == 65535) {
-            return "Undefined";
-        } else {
-            return "Unknown";
-        }
-    }
-
-    public String getFocalLengthDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FOCAL_LENGTH)) return null;
-        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
-        Rational focalLength = _directory.getRational(ExifDirectory.TAG_FOCAL_LENGTH);
-        return formatter.format(focalLength.doubleValue()) + " mm";
-    }
-
-    public String getFlashDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FLASH)) return null;
-
-        /*
-         * This is a bitmask.
-         * 0 = flash fired
-         * 1 = return detected
-         * 2 = return able to be detected
-         * 3 = unknown
-         * 4 = auto used
-         * 5 = unknown
-         * 6 = red eye reduction used
-         */
-
-        int val = _directory.getInt(ExifDirectory.TAG_FLASH);
-
-        StringBuffer sb = new StringBuffer();
-
-        if ((val & 0x1)!=0)
-            sb.append("Flash fired");
-        else
-            sb.append("Flash did not fire");
-
-        // check if we're able to detect a return, before we mention it
-        if ((val & 0x4)!=0)
-        {
-            if ((val & 0x2)!=0)
-                sb.append(", return detected");
-            else
-                sb.append(", return not detected");
-        }
-
-        if ((val & 0x10)!=0)
-            sb.append(", auto");
-
-        if ((val & 0x40)!=0)
-            sb.append(", red-eye reduction");
-
-        return sb.toString();
-    }
-
-    public String getWhiteBalanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_WHITE_BALANCE)) return null;
-        // '0' means unknown, '1' daylight, '2' fluorescent, '3' tungsten, '10' flash,
-        // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55,
-        // '21' D65, '22' D75, '255' other.
-        switch (_directory.getInt(ExifDirectory.TAG_WHITE_BALANCE)) {
-            case 0:
-                return "Unknown";
-            case 1:
-                return "Daylight";
-            case 2:
-                return "Flourescent";
-            case 3:
-                return "Tungsten";
-            case 10:
-                return "Flash";
-            case 17:
-                return "Standard light";
-            case 18:
-                return "Standard light (B)";
-            case 19:
-                return "Standard light (C)";
-            case 20:
-                return "D55";
-            case 21:
-                return "D65";
-            case 22:
-                return "D75";
-            case 255:
-                return "(Other)";
-            default:
-                return "Unknown (" + _directory.getInt(ExifDirectory.TAG_WHITE_BALANCE) + ")";
-        }
-    }
-
-    public String getMeteringModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_METERING_MODE)) return null;
-        // '0' means unknown, '1' average, '2' center weighted average, '3' spot
-        // '4' multi-spot, '5' multi-segment, '6' partial, '255' other
-        int meteringMode = _directory.getInt(ExifDirectory.TAG_METERING_MODE);
-        switch (meteringMode) {
-            case 0:
-                return "Unknown";
-            case 1:
-                return "Average";
-            case 2:
-                return "Center weighted average";
-            case 3:
-                return "Spot";
-            case 4:
-                return "Multi-spot";
-            case 5:
-                return "Multi-segment";
-            case 6:
-                return "Partial";
-            case 255:
-                return "(Other)";
-            default:
-                return "";
-        }
-    }
-
-    public String getSubjectDistanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SUBJECT_DISTANCE)) return null;
-        Rational distance = _directory.getRational(ExifDirectory.TAG_SUBJECT_DISTANCE);
-        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
-        return formatter.format(distance.doubleValue()) + " metres";
-    }
-
-    public String getCompressionLevelDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_COMPRESSION_LEVEL)) return null;
-        Rational compressionRatio = _directory.getRational(ExifDirectory.TAG_COMPRESSION_LEVEL);
-        String ratio = compressionRatio.toSimpleString(_allowDecimalRepresentationOfRationals);
-        if (compressionRatio.isInteger() && compressionRatio.intValue() == 1) {
-            return ratio + " bit/pixel";
-        } else {
-            return ratio + " bits/pixel";
-        }
-    }
-
-    public String getThumbnailLengthDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_LENGTH)) return null;
-        return _directory.getString(ExifDirectory.TAG_THUMBNAIL_LENGTH) + " bytes";
-    }
-
-    public String getThumbnailOffsetDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_THUMBNAIL_OFFSET)) return null;
-        return _directory.getString(ExifDirectory.TAG_THUMBNAIL_OFFSET) + " bytes";
-    }
-
-    public String getYResolutionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_Y_RESOLUTION)) return null;
-        Rational resolution = _directory.getRational(ExifDirectory.TAG_Y_RESOLUTION);
-        return resolution.toSimpleString(_allowDecimalRepresentationOfRationals) +
-                " dots per " +
-                getResolutionDescription().toLowerCase();
-    }
-
-    public String getXResolutionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_X_RESOLUTION)) return null;
-        Rational resolution = _directory.getRational(ExifDirectory.TAG_X_RESOLUTION);
-        return resolution.toSimpleString(_allowDecimalRepresentationOfRationals) +
-                " dots per " +
-                getResolutionDescription().toLowerCase();
-    }
-
-    public String getExposureTimeDescription()
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_EXPOSURE_TIME)) return null;
-        return _directory.getString(ExifDirectory.TAG_EXPOSURE_TIME) + " sec";
-    }
-
-    public String getShutterSpeedDescription() throws MetadataException
-    {
-        // I believe this method to now be stable, but am leaving some alternative snippets of
-        // code in here, to assist anyone who's looking into this (given that I don't have a public CVS).
-
-        if (!_directory.containsTag(ExifDirectory.TAG_SHUTTER_SPEED)) return null;
-//        float apexValue = _directory.getFloat(ExifDirectory.TAG_SHUTTER_SPEED);
-//        int apexPower = (int)Math.pow(2.0, apexValue);
-//        return "1/" + apexPower + " sec";
-        // TODO test this method
-        // thanks to Mark Edwards for spotting and patching a bug in the calculation of this
-        // description (spotted bug using a Canon EOS 300D)
-        // thanks also to Gli Blr for spotting this bug
-        float apexValue = _directory.getFloat(ExifDirectory.TAG_SHUTTER_SPEED);
-        if (apexValue<=1) {
-            float apexPower = (float)(1/(Math.exp(apexValue*Math.log(2))));
-            long apexPower10 = Math.round((double)apexPower * 10.0);
-            float fApexPower = (float) apexPower10 / 10.0f;
-            return fApexPower + " sec";
-        } else {
-            int apexPower = (int)((Math.exp(apexValue*Math.log(2))));
-            return "1/" + apexPower + " sec";
-        }
-
-/*
-        // This alternative implementation offered by Bill Richards
-        // TODO determine which is the correct / more-correct implementation
-        double apexValue = _directory.getDouble(ExifDirectory.TAG_SHUTTER_SPEED);
-        double apexPower = Math.pow(2.0, apexValue);
-
-        StringBuffer sb = new StringBuffer();
-        if (apexPower > 1)
-            apexPower = Math.floor(apexPower);
-
-        if (apexPower < 1) {
-            sb.append((int)Math.round(1/apexPower));
-        } else {
-            sb.append("1/");
-            sb.append((int)apexPower);
-        }
-        sb.append(" sec");
-        return sb.toString();
-*/
-
-    }
-
-    public String getFNumberDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_FNUMBER)) return null;
-        Rational fNumber = _directory.getRational(ExifDirectory.TAG_FNUMBER);
-        return "F" + SimpleDecimalFormatter.format(fNumber.doubleValue());
-    }
-
-    public String getYCbCrPositioningDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_YCBCR_POSITIONING)) return null;
-        int yCbCrPosition = _directory.getInt(ExifDirectory.TAG_YCBCR_POSITIONING);
-        switch (yCbCrPosition) {
-            case 1: return "Center of pixel array";
-            case 2: return "Datum point";
-            default:
-                return String.valueOf(yCbCrPosition);
-        }
-    }
-
-    public String getOrientationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_ORIENTATION)) return null;
-        int orientation = _directory.getInt(ExifDirectory.TAG_ORIENTATION);
-        switch (orientation) {
-            case 1: return "Top, left side (Horizontal / normal)";
-            case 2: return "Top, right side (Mirror horizontal)";
-            case 3: return "Bottom, right side (Rotate 180)";
-            case 4: return "Bottom, left side (Mirror vertical)";
-            case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)";
-            case 6: return "Right side, top (Rotate 90 CW)";
-            case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)";
-            case 8: return "Left side, bottom (Rotate 270 CW)";
-            default:
-                return String.valueOf(orientation);
-        }
-    }
-
-    public String getResolutionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_RESOLUTION_UNIT)) return "";
-        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
-        int resolutionUnit = _directory.getInt(ExifDirectory.TAG_RESOLUTION_UNIT);
-        switch (resolutionUnit) {
-            case 1: return "(No unit)";
-            case 2: return "Inch";
-            case 3: return "cm";
-            default:
-                return "";
-        }
-    }
-
-    public String getSensingMethodDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(ExifDirectory.TAG_SENSING_METHOD)) return null;
-        // '1' Not defined, '2' One-chip color area sensor, '3' Two-chip color area sensor
-        // '4' Three-chip color area sensor, '5' Color sequential area sensor
-        // '7' Trilinear sensor '8' Color sequential linear sensor,  'Other' reserved
-        int sensingMethod = _directory.getInt(ExifDirectory.TAG_SENSING_METHOD);
-        switch (sensingMethod) {
-            case 1:
-                return "(Not defined)";
-            case 2:
-                return "One-chip color area sensor";
-            case 3:
-                return "Two-chip color area sensor";
-            case 4:
-                return "Three-chip color area sensor";
-            case 5:
-                return "Color sequential area sensor";
-            case 7:
-                return "Trilinear sensor";
-            case 8:
-                return "Color sequential linear sensor";
-            default:
-                return "";
-        }
-    }
-
-    public String getComponentConfigurationDescription() throws MetadataException
-    {
-        int[] components = _directory.getIntArray(ExifDirectory.TAG_COMPONENTS_CONFIGURATION);
-        String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"};
-        StringBuffer componentConfig = new StringBuffer();
-        for (int i = 0; i < Math.min(4, components.length); i++) {
-            int j = components[i];
-            if (j > 0 && j < componentStrings.length) {
-                componentConfig.append(componentStrings[j]);
-            }
-        }
-        return componentConfig.toString();
-    }
-
-    /**
-     * Takes a series of 4 bytes from the specified offset, and converts these to a
-     * well-known version number, where possible.  For example, (hex) 30 32 31 30 == 2.10).
-     * @param components the four version values
-     * @return the version as a string of form 2.10
-     */
-    public static String convertBytesToVersionString(int[] components)
-    {
-        StringBuffer version = new StringBuffer();
-        for (int i = 0; i < 4 && i < components.length; i++) {
-            if (i == 2) version.append('.');
-            String digit = String.valueOf((char)components[i]);
-            if (i == 0 && "0".equals(digit)) continue;
-            version.append(digit);
-        }
-        return version.toString();
-    }
-
-    /**
-     * The Windows specific tags uses plain Unicode
-     */
-    private String getUnicodeDescription(int tag) throws MetadataException
-    {
-         if (!_directory.containsTag(tag)) return null;
-         byte[] commentBytes = _directory.getByteArray(tag);
-         try {
-             // decode the unicode string
-             // trim it, as i'm seeing a junk character on the end
-            return new String(commentBytes, "UTF-16LE").trim();
-         }
-         catch (UnsupportedEncodingException ex) {
-            return null;
-         }
-    }
-
-    public String getWindowsAuthorDescription() throws MetadataException
-    {
-       return getUnicodeDescription(ExifDirectory.TAG_WIN_AUTHOR);
-    }
-
-    public String getWindowsCommentDescription() throws MetadataException
-    {
-       return getUnicodeDescription(ExifDirectory.TAG_WIN_COMMENT);
-    }
-
-    public String getWindowsKeywordsDescription() throws MetadataException
-    {
-       return getUnicodeDescription(ExifDirectory.TAG_WIN_KEYWORDS);
-    }
-
-    public String getWindowsTitleDescription() throws MetadataException
-    {
-       return getUnicodeDescription(ExifDirectory.TAG_WIN_TITLE);
-    }
-
-    public String getWindowsSubjectDescription() throws MetadataException
-    {
-       return getUnicodeDescription(ExifDirectory.TAG_WIN_SUBJECT);
-    }
-}
Index: trunk/src/com/drew/metadata/exif/ExifDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifDirectory.java	(revision 6002)
+++ 	(revision )
@@ -1,954 +1,0 @@
-/*
- * ExifDirectory.java
- *
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 25-Nov-2002 20:41:00 using IntelliJ IDEA.
- */
-package com.drew.metadata.exif;
-
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
-
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.HashMap;
-
-/**
- *
- */
-public class ExifDirectory extends Directory
-{
-    // TODO do these tags belong in the exif directory?
-    public static final int TAG_SUB_IFDS = 0x014A;
-    public static final int TAG_GPS_INFO = 0x8825;
-
-    /**
-     * The actual aperture value of lens when the image was taken. Unit is APEX.
-     * To convert this value to ordinary F-number (F-stop), calculate this value's
-     * power of root 2 (=1.4142). For example, if the ApertureValue is '5',
-     * F-number is 1.4142^5 = F5.6.
-     */
-    public static final int TAG_APERTURE = 0x9202;
-    /**
-     * When image format is no compression, this value shows the number of bits
-     * per component for each pixel. Usually this value is '8,8,8'.
-     */
-    public static final int TAG_BITS_PER_SAMPLE = 0x0102;
-    /**
-     * Shows compression method for Thumbnail.
-     * 1 = Uncompressed
-     * 2 = CCITT 1D
-     * 3 = T4/Group 3 Fax
-     * 4 = T6/Group 4 Fax
-     * 5 = LZW
-     * 6 = JPEG (old-style)
-     * 7 = JPEG
-     * 8 = Adobe Deflate
-     * 9 = JBIG B&W
-     * 10 = JBIG Color
-     * 32766 = Next
-     * 32771 = CCIRLEW
-     * 32773 = PackBits
-     * 32809 = Thunderscan
-     * 32895 = IT8CTPAD
-     * 32896 = IT8LW
-     * 32897 = IT8MP
-     * 32898 = IT8BL
-     * 32908 = PixarFilm
-     * 32909 = PixarLog
-     * 32946 = Deflate
-     * 32947 = DCS
-     * 34661 = JBIG
-     * 34676 = SGILog
-     * 34677 = SGILog24
-     * 34712 = JPEG 2000
-     * 34713 = Nikon NEF Compressed
-     */
-    public static final int TAG_COMPRESSION = 0x0103;
-    public static final int COMPRESSION_NONE = 1;
-    public static final int COMPRESSION_JPEG = 6;
-
-    /**
-     * Shows the color space of the image data components.
-     * 0 = WhiteIsZero
-     * 1 = BlackIsZero
-     * 2 = RGB
-     * 3 = RGB Palette
-     * 4 = Transparency Mask
-     * 5 = CMYK
-     * 6 = YCbCr
-     * 8 = CIELab
-     * 9 = ICCLab
-     * 10 = ITULab
-     * 32803 = Color Filter Array
-     * 32844 = Pixar LogL
-     * 32845 = Pixar LogLuv
-     * 34892 = Linear Raw
-     */
-    public static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106;
-    /**
-     * 1 = No dithering or halftoning
-     * 2 = Ordered dither or halftone
-     * 3 = Randomized dither
-     */
-    public static final int TAG_THRESHOLDING = 0x0107;
-    public static final int PHOTOMETRIC_INTERPRETATION_MONOCHROME = 1;
-    public static final int PHOTOMETRIC_INTERPRETATION_RGB = 2;
-    public static final int PHOTOMETRIC_INTERPRETATION_YCBCR = 6;
-
-    /** The position in the file of raster data. */
-    public static final int TAG_STRIP_OFFSETS = 0x0111;
-    /** Each pixel is composed of this many samples. */
-    public static final int TAG_SAMPLES_PER_PIXEL = 0x0115;
-    /** The raster is codified by a single block of data holding this many rows. */
-    public static final int TAG_ROWS_PER_STRIP = 0x116;
-    /** The size of the raster data in bytes. */
-    public static final int TAG_STRIP_BYTE_COUNTS = 0x0117;
-    public static final int TAG_MIN_SAMPLE_VALUE = 0x0118;
-    public static final int TAG_MAX_SAMPLE_VALUE = 0x0119;
-    /**
-     * When image format is no compression YCbCr, this value shows byte aligns of
-     * YCbCr data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for
-     * each subsampling pixel. If value is '2', Y/Cb/Cr value is separated and
-     * stored to Y plane/Cb plane/Cr plane format.
-     */
-    public static final int TAG_PLANAR_CONFIGURATION = 0x011C;
-    public static final int TAG_YCBCR_SUBSAMPLING = 0x0212;
-    public static final int TAG_IMAGE_DESCRIPTION = 0x010E;
-    public static final int TAG_SOFTWARE = 0x0131;
-    public static final int TAG_DATETIME = 0x0132;
-    public static final int TAG_WHITE_POINT = 0x013E;
-    public static final int TAG_PRIMARY_CHROMATICITIES = 0x013F;
-    public static final int TAG_YCBCR_COEFFICIENTS = 0x0211;
-    public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214;
-    public static final int TAG_COPYRIGHT = 0x8298;
-
-    /**
-     * The new subfile type tag.
-     * 0 = Full-resolution Image
-     * 1 = Reduced-resolution image
-     * 2 = Single page of multi-page image
-     * 3 = Single page of multi-page reduced-resolution image
-     * 4 = Transparency mask
-     * 5 = Transparency mask of reduced-resolution image
-     * 6 = Transparency mask of multi-page image
-     * 7 = Transparency mask of reduced-resolution multi-page image
-     */
-    public static final int TAG_NEW_SUBFILE_TYPE = 0x00FE;
-    /**
-     * The old subfile type tag.
-     * 1 = Full-resolution image (Main image)
-     * 2 = Reduced-resolution image (Thumbnail)
-     * 3 = Single page of multi-page image
-     */
-    public static final int TAG_SUBFILE_TYPE = 0x00FF;
-    public static final int TAG_TRANSFER_FUNCTION = 0x012D;
-    public static final int TAG_ARTIST = 0x013B;
-    public static final int TAG_PREDICTOR = 0x013D;
-    public static final int TAG_TILE_WIDTH = 0x0142;
-    public static final int TAG_TILE_LENGTH = 0x0143;
-    public static final int TAG_TILE_OFFSETS = 0x0144;
-    public static final int TAG_TILE_BYTE_COUNTS = 0x0145;
-    public static final int TAG_JPEG_TABLES = 0x015B;
-    public static final int TAG_CFA_REPEAT_PATTERN_DIM = 0x828D;
-    /** There are two definitions for CFA pattern, I don't know the difference... */
-    public static final int TAG_CFA_PATTERN_2 = 0x828E;
-    public static final int TAG_BATTERY_LEVEL = 0x828F;
-    public static final int TAG_IPTC_NAA = 0x83BB;
-    public static final int TAG_INTER_COLOR_PROFILE = 0x8773;
-    public static final int TAG_SPECTRAL_SENSITIVITY = 0x8824;
-    public static final int TAG_OECF = 0x8828;
-    public static final int TAG_INTERLACE = 0x8829;
-    public static final int TAG_TIME_ZONE_OFFSET = 0x882A;
-    public static final int TAG_SELF_TIMER_MODE = 0x882B;
-    public static final int TAG_FLASH_ENERGY = 0x920B;
-    public static final int TAG_SPATIAL_FREQ_RESPONSE = 0x920C;
-    public static final int TAG_NOISE = 0x920D;
-    public static final int TAG_IMAGE_NUMBER = 0x9211;
-    public static final int TAG_SECURITY_CLASSIFICATION = 0x9212;
-    public static final int TAG_IMAGE_HISTORY = 0x9213;
-    public static final int TAG_SUBJECT_LOCATION = 0x9214;
-    /** There are two definitions for exposure index, I don't know the difference... */
-    public static final int TAG_EXPOSURE_INDEX_2 = 0x9215;
-    public static final int TAG_TIFF_EP_STANDARD_ID = 0x9216;
-    public static final int TAG_FLASH_ENERGY_2 = 0xA20B;
-    public static final int TAG_SPATIAL_FREQ_RESPONSE_2 = 0xA20C;
-    public static final int TAG_SUBJECT_LOCATION_2 = 0xA214;
-    public static final int TAG_MAKE = 0x010F;
-    public static final int TAG_MODEL = 0x0110;
-    public static final int TAG_ORIENTATION = 0x0112;
-    public static final int TAG_X_RESOLUTION = 0x011A;
-    public static final int TAG_Y_RESOLUTION = 0x011B;
-    public static final int TAG_PAGE_NAME = 0x011D;
-    public static final int TAG_RESOLUTION_UNIT = 0x0128;
-    public static final int TAG_THUMBNAIL_OFFSET = 0x0201;
-    public static final int TAG_THUMBNAIL_LENGTH = 0x0202;
-    public static final int TAG_YCBCR_POSITIONING = 0x0213;
-    /**
-     * Exposure time (reciprocal of shutter speed). Unit is second.
-     */
-    public static final int TAG_EXPOSURE_TIME = 0x829A;
-    /**
-     * The actual F-number(F-stop) of lens when the image was taken.
-     */
-    public static final int TAG_FNUMBER = 0x829D;
-    /**
-     * Exposure program that the camera used when image was taken. '1' means
-     * manual control, '2' program normal, '3' aperture priority, '4' shutter
-     * priority, '5' program creative (slow program), '6' program action
-     * (high-speed program), '7' portrait mode, '8' landscape mode.
-     */
-    public static final int TAG_EXPOSURE_PROGRAM = 0x8822;
-    public static final int TAG_ISO_EQUIVALENT = 0x8827;
-    public static final int TAG_EXIF_VERSION = 0x9000;
-    public static final int TAG_DATETIME_ORIGINAL = 0x9003;
-    public static final int TAG_DATETIME_DIGITIZED = 0x9004;
-    public static final int TAG_COMPONENTS_CONFIGURATION = 0x9101;
-    /**
-     * Average (rough estimate) compression level in JPEG bits per pixel.
-     * */
-    public static final int TAG_COMPRESSION_LEVEL = 0x9102;
-    /**
-     * Shutter speed by APEX value. To convert this value to ordinary 'Shutter Speed';
-     * calculate this value's power of 2, then reciprocal. For example, if the
-     * ShutterSpeedValue is '4', shutter speed is 1/(24)=1/16 second.
-     */
-    public static final int TAG_SHUTTER_SPEED = 0x9201;
-    public static final int TAG_BRIGHTNESS_VALUE = 0x9203;
-    public static final int TAG_EXPOSURE_BIAS = 0x9204;
-    /**
-     * Maximum aperture value of lens. You can convert to F-number by calculating
-     * power of root 2 (same process of ApertureValue:0x9202).
-     * The actual aperture value of lens when the image was taken. To convert this
-     * value to ordinary f-number(f-stop), calculate the value's power of root 2
-     * (=1.4142). For example, if the ApertureValue is '5', f-number is 1.41425^5 = F5.6.
-     */
-    public static final int TAG_MAX_APERTURE = 0x9205;
-    /**
-     * Indicates the distance the autofocus camera is focused to.  Tends to be less accurate as distance increases.
-     */
-    public static final int TAG_SUBJECT_DISTANCE = 0x9206;
-    /**
-     * Exposure metering method. '0' means unknown, '1' average, '2' center
-     * weighted average, '3' spot, '4' multi-spot, '5' multi-segment, '6' partial,
-     * '255' other.
-     */
-    public static final int TAG_METERING_MODE = 0x9207;
-
-    public static final int TAG_LIGHT_SOURCE = 0x9208;
-    /**
-     * White balance (aka light source). '0' means unknown, '1' daylight,
-     * '2' fluorescent, '3' tungsten, '10' flash, '17' standard light A,
-     * '18' standard light B, '19' standard light C, '20' D55, '21' D65,
-     * '22' D75, '255' other.
-     */
-    public static final int TAG_WHITE_BALANCE = 0x9208;
-    /**
-     * 0x0  = 0000000 = No Flash
-     * 0x1  = 0000001 = Fired
-     * 0x5  = 0000101 = Fired, Return not detected
-     * 0x7  = 0000111 = Fired, Return detected
-     * 0x9  = 0001001 = On
-     * 0xd  = 0001101 = On, Return not detected
-     * 0xf  = 0001111 = On, Return detected
-     * 0x10 = 0010000 = Off
-     * 0x18 = 0011000 = Auto, Did not fire
-     * 0x19 = 0011001 = Auto, Fired
-     * 0x1d = 0011101 = Auto, Fired, Return not detected
-     * 0x1f = 0011111 = Auto, Fired, Return detected
-     * 0x20 = 0100000 = No flash function
-     * 0x41 = 1000001 = Fired, Red-eye reduction
-     * 0x45 = 1000101 = Fired, Red-eye reduction, Return not detected
-     * 0x47 = 1000111 = Fired, Red-eye reduction, Return detected
-     * 0x49 = 1001001 = On, Red-eye reduction
-     * 0x4d = 1001101 = On, Red-eye reduction, Return not detected
-     * 0x4f = 1001111 = On, Red-eye reduction, Return detected
-     * 0x59 = 1011001 = Auto, Fired, Red-eye reduction
-     * 0x5d = 1011101 = Auto, Fired, Red-eye reduction, Return not detected
-     * 0x5f = 1011111 = Auto, Fired, Red-eye reduction, Return detected
-     *        6543210 (positions)
-     *
-     * This is a bitmask.
-     * 0 = flash fired
-     * 1 = return detected
-     * 2 = return able to be detected
-     * 3 = unknown
-     * 4 = auto used
-     * 5 = unknown
-     * 6 = red eye reduction used
-     */
-    public static final int TAG_FLASH = 0x9209;
-    /**
-     * Focal length of lens used to take image.  Unit is millimeter.
-     * Nice digital cameras actually save the focal length as a function of how far they are zoomed in.
-     */
-    public static final int TAG_FOCAL_LENGTH = 0x920A;
-    public static final int TAG_USER_COMMENT = 0x9286;
-    public static final int TAG_SUBSECOND_TIME = 0x9290;
-    public static final int TAG_SUBSECOND_TIME_ORIGINAL = 0x9291;
-    public static final int TAG_SUBSECOND_TIME_DIGITIZED = 0x9292;
-    public static final int TAG_FLASHPIX_VERSION = 0xA000;
-    /**
-     * Defines Color Space. DCF image must use sRGB color space so value is
-     * always '1'. If the picture uses the other color space, value is
-     * '65535':Uncalibrated.
-     */
-    public static final int TAG_COLOR_SPACE = 0xA001;
-    public static final int TAG_EXIF_IMAGE_WIDTH = 0xA002;
-    public static final int TAG_EXIF_IMAGE_HEIGHT = 0xA003;
-    public static final int TAG_RELATED_SOUND_FILE = 0xA004;
-    public static final int TAG_FOCAL_PLANE_X_RES = 0xA20E;
-    public static final int TAG_FOCAL_PLANE_Y_RES = 0xA20F;
-    /**
-     * Unit of FocalPlaneXResoluton/FocalPlaneYResolution. '1' means no-unit,
-     * '2' inch, '3' centimeter.
-     *
-     * Note: Some of Fujifilm's digicam(e.g.FX2700,FX2900,Finepix4700Z/40i etc)
-     * uses value '3' so it must be 'centimeter', but it seems that they use a
-     * '8.3mm?'(1/3in.?) to their ResolutionUnit. Fuji's BUG? Finepix4900Z has
-     * been changed to use value '2' but it doesn't match to actual value also.
-     */
-    public static final int TAG_FOCAL_PLANE_UNIT = 0xA210;
-    public static final int TAG_EXPOSURE_INDEX = 0xA215;
-    public static final int TAG_SENSING_METHOD = 0xA217;
-    public static final int TAG_FILE_SOURCE = 0xA300;
-    public static final int TAG_SCENE_TYPE = 0xA301;
-    public static final int TAG_CFA_PATTERN = 0xA302;
-
-    // these tags new with Exif 2.2 (?) [A401 - A4
-    /**
-     * This tag indicates the use of special processing on image data, such as rendering
-     * geared to output. When special processing is performed, the reader is expected to
-     * disable or minimize any further processing.
-     * Tag = 41985 (A401.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = 0
-     *   0 = Normal process
-     *   1 = Custom process
-     *   Other = reserved
-     */
-    public static final int TAG_CUSTOM_RENDERED = 0xA401;
-
-    /**
-     * This tag indicates the exposure mode set when the image was shot. In auto-bracketing
-     * mode, the camera shoots a series of frames of the same scene at different exposure settings.
-     * Tag = 41986 (A402.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = none
-     *   0 = Auto exposure
-     *   1 = Manual exposure
-     *   2 = Auto bracket
-     *   Other = reserved
-     */
-    public static final int TAG_EXPOSURE_MODE = 0xA402;
-
-    /**
-     * This tag indicates the white balance mode set when the image was shot.
-     * Tag = 41987 (A403.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = none
-     *   0 = Auto white balance
-     *   1 = Manual white balance
-     *   Other = reserved
-     */
-    public static final int TAG_WHITE_BALANCE_MODE = 0xA403;
-
-    /**
-     * This tag indicates the digital zoom ratio when the image was shot. If the
-     * numerator of the recorded value is 0, this indicates that digital zoom was
-     * not used.
-     * Tag = 41988 (A404.H)
-     * Type = RATIONAL
-     * Count = 1
-     * Default = none
-     */
-    public static final int TAG_DIGITAL_ZOOM_RATIO = 0xA404;
-
-    /**
-     * This tag indicates the equivalent focal length assuming a 35mm film camera,
-     * in mm. A value of 0 means the focal length is unknown. Note that this tag
-     * differs from the FocalLength tag.
-     * Tag = 41989 (A405.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = none
-     */
-    public static final int TAG_35MM_FILM_EQUIV_FOCAL_LENGTH = 0xA405;
-
-    /**
-     * This tag indicates the type of scene that was shot. It can also be used to
-     * record the mode in which the image was shot. Note that this differs from
-     * the scene type (SceneType) tag.
-     * Tag = 41990 (A406.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = 0
-     *   0 = Standard
-     *   1 = Landscape
-     *   2 = Portrait
-     *   3 = Night scene
-     *   Other = reserved
-     */
-    public static final int TAG_SCENE_CAPTURE_TYPE = 0xA406;
-
-    /**
-     * This tag indicates the degree of overall image gain adjustment.
-     * Tag = 41991 (A407.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = none
-     *   0 = None
-     *   1 = Low gain up
-     *   2 = High gain up
-     *   3 = Low gain down
-     *   4 = High gain down
-     *   Other = reserved
-     */
-    public static final int TAG_GAIN_CONTROL = 0xA407;
-
-    /**
-     * This tag indicates the direction of contrast processing applied by the camera
-     * when the image was shot.
-     * Tag = 41992 (A408.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = 0
-     *   0 = Normal
-     *   1 = Soft
-     *   2 = Hard
-     *   Other = reserved
-     */
-    public static final int TAG_CONTRAST = 0xA408;
-
-    /**
-     * This tag indicates the direction of saturation processing applied by the camera
-     * when the image was shot.
-     * Tag = 41993 (A409.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = 0
-     *   0 = Normal
-     *   1 = Low saturation
-     *   2 = High saturation
-     *   Other = reserved
-     */
-    public static final int TAG_SATURATION = 0xA409;
-
-    /**
-     * This tag indicates the direction of sharpness processing applied by the camera
-     * when the image was shot.
-     * Tag = 41994 (A40A.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = 0
-     *   0 = Normal
-     *   1 = Soft
-     *   2 = Hard
-     *   Other = reserved
-     */
-    public static final int TAG_SHARPNESS = 0xA40A;
-
-    // TODO support this tag (I haven't seen a camera's actual implementation of this yet)
-
-    /**
-     * This tag indicates information on the picture-taking conditions of a particular
-     * camera model. The tag is used only to indicate the picture-taking conditions in
-     * the reader.
-     * Tag = 41995 (A40B.H)
-     * Type = UNDEFINED
-     * Count = Any
-     * Default = none
-     *
-     * The information is recorded in the format shown below. The data is recorded
-     * in Unicode using SHORT type for the number of display rows and columns and
-     * UNDEFINED type for the camera settings. The Unicode (UCS-2) string including
-     * Signature is NULL terminated. The specifics of the Unicode string are as given
-     * in ISO/IEC 10464-1.
-     *
-     *      Length  Type        Meaning
-     *      ------+-----------+------------------
-     *      2       SHORT       Display columns
-     *      2       SHORT       Display rows
-     *      Any     UNDEFINED   Camera setting-1
-     *      Any     UNDEFINED   Camera setting-2
-     *      :       :           :
-     *      Any     UNDEFINED   Camera setting-n
-     */
-    public static final int TAG_DEVICE_SETTING_DESCRIPTION = 0xA40B;
-
-    /**
-     * This tag indicates the distance to the subject.
-     * Tag = 41996 (A40C.H)
-     * Type = SHORT
-     * Count = 1
-     * Default = none
-     *   0 = unknown
-     *   1 = Macro
-     *   2 = Close view
-     *   3 = Distant view
-     *   Other = reserved
-     */
-    public static final int TAG_SUBJECT_DISTANCE_RANGE = 0xA40C;
-
-    /**
-     * The image title, as used by Windows XP.
-     */
-    public static final int TAG_WIN_TITLE = 0x9C9B;
-
-    /**
-     * The image comment, as used by Windows XP.
-     */
-    public static final int TAG_WIN_COMMENT = 0x9C9C;
-
-    /**
-     * The image author, as used by Windows XP (called Artist in the Windows shell).
-     */
-    public static final int TAG_WIN_AUTHOR = 0x9C9D;
-
-    /**
-     * The image keywords, as used by Windows XP.
-     */
-    public static final int TAG_WIN_KEYWORDS = 0x9C9E;
-
-    /**
-     * The image subject, as used by Windows XP.
-     */
-    public static final int TAG_WIN_SUBJECT = 0x9C9F;
-
-    /**
-     * This tag indicates an identifier assigned uniquely to each image. It is
-     * recorded as an ASCII string equivalent to hexadecimal notation and 128-bit
-     * fixed length.
-     * Tag = 42016 (A420.H)
-     * Type = ASCII
-     * Count = 33
-     * Default = none
-     */
-    public static final int TAG_IMAGE_UNIQUE_ID = 0xA420;
-
-    public static final int TAG_THUMBNAIL_IMAGE_WIDTH = 0x0100;
-    public static final int TAG_THUMBNAIL_IMAGE_HEIGHT = 0x0101;
-    public static final int TAG_THUMBNAIL_DATA = 0xF001;
-
-    /**
-     * 1 = Normal
-     * 2 = Reversed
-     */
-    public static final int TAG_FILL_ORDER = 0x010A;
-    public static final int TAG_DOCUMENT_NAME = 0x010D;
-
-    protected static final HashMap tagNameMap = new HashMap();
-
-    static
-    {
-        tagNameMap.put(new Integer(TAG_FILL_ORDER), "Fill Order");
-        tagNameMap.put(new Integer(TAG_DOCUMENT_NAME), "Document Name");
-        tagNameMap.put(new Integer(0x1000), "Related Image File Format");
-        tagNameMap.put(new Integer(0x1001), "Related Image Width");
-        tagNameMap.put(new Integer(0x1002), "Related Image Length");
-        tagNameMap.put(new Integer(0x0156), "Transfer Range");
-        tagNameMap.put(new Integer(0x0200), "JPEG Proc");
-        tagNameMap.put(new Integer(0x8769), "Exif Offset");
-        tagNameMap.put(new Integer(TAG_COMPRESSION_LEVEL), "Compressed Bits Per Pixel");
-        tagNameMap.put(new Integer(0x927C), "Maker Note");
-        tagNameMap.put(new Integer(0xA005), "Interoperability Offset");
-
-        tagNameMap.put(new Integer(TAG_NEW_SUBFILE_TYPE), "New Subfile Type");
-        tagNameMap.put(new Integer(TAG_SUBFILE_TYPE), "Subfile Type");
-        tagNameMap.put(new Integer(TAG_THUMBNAIL_IMAGE_WIDTH), "Thumbnail Image Width");
-        tagNameMap.put(new Integer(TAG_THUMBNAIL_IMAGE_HEIGHT), "Thumbnail Image Height");
-        tagNameMap.put(new Integer(TAG_BITS_PER_SAMPLE), "Bits Per Sample");
-        tagNameMap.put(new Integer(TAG_COMPRESSION), "Compression");
-        tagNameMap.put(new Integer(TAG_PHOTOMETRIC_INTERPRETATION), "Photometric Interpretation");
-        tagNameMap.put(new Integer(TAG_THRESHOLDING), "Thresholding");
-        tagNameMap.put(new Integer(TAG_IMAGE_DESCRIPTION), "Image Description");
-        tagNameMap.put(new Integer(TAG_MAKE), "Make");
-        tagNameMap.put(new Integer(TAG_MODEL), "Model");
-        tagNameMap.put(new Integer(TAG_STRIP_OFFSETS), "Strip Offsets");
-        tagNameMap.put(new Integer(TAG_ORIENTATION), "Orientation");
-        tagNameMap.put(new Integer(TAG_SAMPLES_PER_PIXEL), "Samples Per Pixel");
-        tagNameMap.put(new Integer(TAG_ROWS_PER_STRIP), "Rows Per Strip");
-        tagNameMap.put(new Integer(TAG_STRIP_BYTE_COUNTS), "Strip Byte Counts");
-        tagNameMap.put(new Integer(TAG_X_RESOLUTION), "X Resolution");
-        tagNameMap.put(new Integer(TAG_Y_RESOLUTION), "Y Resolution");
-        tagNameMap.put(new Integer(TAG_PAGE_NAME), "Page Name");
-        tagNameMap.put(new Integer(TAG_PLANAR_CONFIGURATION), "Planar Configuration");
-        tagNameMap.put(new Integer(TAG_RESOLUTION_UNIT), "Resolution Unit");
-        tagNameMap.put(new Integer(TAG_TRANSFER_FUNCTION), "Transfer Function");
-        tagNameMap.put(new Integer(TAG_SOFTWARE), "Software");
-        tagNameMap.put(new Integer(TAG_DATETIME), "Date/Time");
-        tagNameMap.put(new Integer(TAG_ARTIST), "Artist");
-        tagNameMap.put(new Integer(TAG_PREDICTOR), "Predictor");
-        tagNameMap.put(new Integer(TAG_WHITE_POINT), "White Point");
-        tagNameMap.put(new Integer(TAG_PRIMARY_CHROMATICITIES), "Primary Chromaticities");
-        tagNameMap.put(new Integer(TAG_TILE_WIDTH), "Tile Width");
-        tagNameMap.put(new Integer(TAG_TILE_LENGTH), "Tile Length");
-        tagNameMap.put(new Integer(TAG_TILE_OFFSETS), "Tile Offsets");
-        tagNameMap.put(new Integer(TAG_TILE_BYTE_COUNTS), "Tile Byte Counts");
-        tagNameMap.put(new Integer(TAG_SUB_IFDS), "Sub IFDs");
-        tagNameMap.put(new Integer(TAG_JPEG_TABLES), "JPEG Tables");
-        tagNameMap.put(new Integer(TAG_THUMBNAIL_OFFSET), "Thumbnail Offset");
-        tagNameMap.put(new Integer(TAG_THUMBNAIL_LENGTH), "Thumbnail Length");
-        tagNameMap.put(new Integer(TAG_THUMBNAIL_DATA), "Thumbnail Data");
-        tagNameMap.put(new Integer(TAG_YCBCR_COEFFICIENTS), "YCbCr Coefficients");
-        tagNameMap.put(new Integer(TAG_YCBCR_SUBSAMPLING), "YCbCr Sub-Sampling");
-        tagNameMap.put(new Integer(TAG_YCBCR_POSITIONING), "YCbCr Positioning");
-        tagNameMap.put(new Integer(TAG_REFERENCE_BLACK_WHITE), "Reference Black/White");
-        tagNameMap.put(new Integer(TAG_CFA_REPEAT_PATTERN_DIM), "CFA Repeat Pattern Dim");
-        tagNameMap.put(new Integer(TAG_CFA_PATTERN_2), "CFA Pattern");
-        tagNameMap.put(new Integer(TAG_BATTERY_LEVEL), "Battery Level");
-        tagNameMap.put(new Integer(TAG_COPYRIGHT), "Copyright");
-        tagNameMap.put(new Integer(TAG_EXPOSURE_TIME), "Exposure Time");
-        tagNameMap.put(new Integer(TAG_FNUMBER), "F-Number");
-        tagNameMap.put(new Integer(TAG_IPTC_NAA), "IPTC/NAA");
-        tagNameMap.put(new Integer(TAG_INTER_COLOR_PROFILE), "Inter Color Profile");
-        tagNameMap.put(new Integer(TAG_EXPOSURE_PROGRAM), "Exposure Program");
-        tagNameMap.put(new Integer(TAG_SPECTRAL_SENSITIVITY), "Spectral Sensitivity");
-        tagNameMap.put(new Integer(TAG_GPS_INFO), "GPS Info");
-        tagNameMap.put(new Integer(TAG_ISO_EQUIVALENT), "ISO Speed Ratings");
-        tagNameMap.put(new Integer(TAG_OECF), "OECF");
-        tagNameMap.put(new Integer(TAG_INTERLACE), "Interlace");
-        tagNameMap.put(new Integer(TAG_TIME_ZONE_OFFSET), "Time Zone Offset");
-        tagNameMap.put(new Integer(TAG_SELF_TIMER_MODE), "Self Timer Mode");
-        tagNameMap.put(new Integer(TAG_EXIF_VERSION), "Exif Version");
-        tagNameMap.put(new Integer(TAG_DATETIME_ORIGINAL), "Date/Time Original");
-        tagNameMap.put(new Integer(TAG_DATETIME_DIGITIZED), "Date/Time Digitized");
-        tagNameMap.put(new Integer(TAG_COMPONENTS_CONFIGURATION), "Components Configuration");
-        tagNameMap.put(new Integer(TAG_SHUTTER_SPEED), "Shutter Speed Value");
-        tagNameMap.put(new Integer(TAG_APERTURE), "Aperture Value");
-        tagNameMap.put(new Integer(TAG_BRIGHTNESS_VALUE), "Brightness Value");
-        tagNameMap.put(new Integer(TAG_EXPOSURE_BIAS), "Exposure Bias Value");
-        tagNameMap.put(new Integer(TAG_MAX_APERTURE), "Max Aperture Value");
-        tagNameMap.put(new Integer(TAG_SUBJECT_DISTANCE), "Subject Distance");
-        tagNameMap.put(new Integer(TAG_METERING_MODE), "Metering Mode");
-        tagNameMap.put(new Integer(TAG_WHITE_BALANCE), "Light Source");
-        tagNameMap.put(new Integer(TAG_FLASH), "Flash");
-        tagNameMap.put(new Integer(TAG_FOCAL_LENGTH), "Focal Length");
-        tagNameMap.put(new Integer(TAG_FLASH_ENERGY), "Flash Energy");
-        tagNameMap.put(new Integer(TAG_SPATIAL_FREQ_RESPONSE), "Spatial Frequency Response");
-        tagNameMap.put(new Integer(TAG_NOISE), "Noise");
-        tagNameMap.put(new Integer(TAG_IMAGE_NUMBER), "Image Number");
-        tagNameMap.put(new Integer(TAG_SECURITY_CLASSIFICATION), "Security Classification");
-        tagNameMap.put(new Integer(TAG_IMAGE_HISTORY), "Image History");
-        tagNameMap.put(new Integer(TAG_SUBJECT_LOCATION), "Subject Location");
-        tagNameMap.put(new Integer(TAG_EXPOSURE_INDEX), "Exposure Index");
-        tagNameMap.put(new Integer(TAG_TIFF_EP_STANDARD_ID), "TIFF/EP Standard ID");
-        tagNameMap.put(new Integer(TAG_USER_COMMENT), "User Comment");
-        tagNameMap.put(new Integer(TAG_SUBSECOND_TIME), "Sub-Sec Time");
-        tagNameMap.put(new Integer(TAG_SUBSECOND_TIME_ORIGINAL), "Sub-Sec Time Original");
-        tagNameMap.put(new Integer(TAG_SUBSECOND_TIME_DIGITIZED), "Sub-Sec Time Digitized");
-        tagNameMap.put(new Integer(TAG_FLASHPIX_VERSION), "FlashPix Version");
-        tagNameMap.put(new Integer(TAG_COLOR_SPACE), "Color Space");
-        tagNameMap.put(new Integer(TAG_EXIF_IMAGE_WIDTH), "Exif Image Width");
-        tagNameMap.put(new Integer(TAG_EXIF_IMAGE_HEIGHT), "Exif Image Height");
-        tagNameMap.put(new Integer(TAG_RELATED_SOUND_FILE), "Related Sound File");
-        // 0x920B in TIFF/EP
-        tagNameMap.put(new Integer(TAG_FLASH_ENERGY_2), "Flash Energy");
-        // 0x920C in TIFF/EP
-        tagNameMap.put(new Integer(TAG_SPATIAL_FREQ_RESPONSE_2), "Spatial Frequency Response");
-        // 0x920E in TIFF/EP
-        tagNameMap.put(new Integer(TAG_FOCAL_PLANE_X_RES), "Focal Plane X Resolution");
-        // 0x920F in TIFF/EP
-        tagNameMap.put(new Integer(TAG_FOCAL_PLANE_Y_RES), "Focal Plane Y Resolution");
-        // 0x9210 in TIFF/EP
-        tagNameMap.put(new Integer(TAG_FOCAL_PLANE_UNIT), "Focal Plane Resolution Unit");
-        // 0x9214 in TIFF/EP
-        tagNameMap.put(new Integer(TAG_SUBJECT_LOCATION_2), "Subject Location");
-        // 0x9215 in TIFF/EP
-        tagNameMap.put(new Integer(TAG_EXPOSURE_INDEX_2), "Exposure Index");
-        // 0x9217 in TIFF/EP
-        tagNameMap.put(new Integer(TAG_SENSING_METHOD), "Sensing Method");
-        tagNameMap.put(new Integer(TAG_FILE_SOURCE), "File Source");
-        tagNameMap.put(new Integer(TAG_SCENE_TYPE), "Scene Type");
-        tagNameMap.put(new Integer(TAG_CFA_PATTERN), "CFA Pattern");
-
-        tagNameMap.put(new Integer(TAG_CUSTOM_RENDERED), "Custom Rendered");
-        tagNameMap.put(new Integer(TAG_EXPOSURE_MODE), "Exposure Mode");
-        tagNameMap.put(new Integer(TAG_WHITE_BALANCE_MODE), "White Balance");
-        tagNameMap.put(new Integer(TAG_DIGITAL_ZOOM_RATIO), "Digital Zoom Ratio");
-        tagNameMap.put(new Integer(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH), "Focal Length 35");
-        tagNameMap.put(new Integer(TAG_SCENE_CAPTURE_TYPE), "Scene Capture Type");
-        tagNameMap.put(new Integer(TAG_GAIN_CONTROL), "Gain Control");
-        tagNameMap.put(new Integer(TAG_CONTRAST), "Contrast");
-        tagNameMap.put(new Integer(TAG_SATURATION), "Saturation");
-        tagNameMap.put(new Integer(TAG_SHARPNESS), "Sharpness");
-        tagNameMap.put(new Integer(TAG_DEVICE_SETTING_DESCRIPTION), "Device Setting Description");
-        tagNameMap.put(new Integer(TAG_SUBJECT_DISTANCE_RANGE), "Subject Distance Range");
-
-        tagNameMap.put(new Integer(TAG_WIN_AUTHOR), "Windows XP Author");
-        tagNameMap.put(new Integer(TAG_WIN_COMMENT), "Windows XP Comment");
-        tagNameMap.put(new Integer(TAG_WIN_KEYWORDS), "Windows XP Keywords");
-        tagNameMap.put(new Integer(TAG_WIN_SUBJECT), "Windows XP Subject");
-        tagNameMap.put(new Integer(TAG_WIN_TITLE), "Windows XP Title");
-
-        tagNameMap.put(new Integer(TAG_MIN_SAMPLE_VALUE), "Minimum sample value");
-        tagNameMap.put(new Integer(TAG_MAX_SAMPLE_VALUE), "Maximum sample value");
-    }
-
-    public ExifDirectory()
-    {
-        this.setDescriptor(new ExifDescriptor(this));
-    }
-
-    public String getName()
-    {
-        return "Exif";
-    }
-
-    protected HashMap getTagNameMap()
-    {
-        return tagNameMap;
-    }
-
-    public byte[] getThumbnailData() throws MetadataException
-    {
-        if (!containsThumbnail())
-            return null;
-        
-        return this.getByteArray(ExifDirectory.TAG_THUMBNAIL_DATA);
-    }
-
-    public void writeThumbnail(String filename) throws MetadataException, IOException
-    {
-        byte[] data = getThumbnailData();
-
-        if (data==null)
-            throw new MetadataException("No thumbnail data exists.");
-
-        FileOutputStream stream = null;
-        try {
-            stream = new FileOutputStream(filename);
-            stream.write(data);
-        } finally {
-            if (stream!=null)
-                stream.close();
-        }
-    }
-
-/*
-    // This thumbnail extraction code is not complete, and is included to assist anyone who feels like looking into
-    // it.  Please share any progress with the original author, and hence the community.  Thanks.
-
-    /**
-     *
-     * @return
-     * @throws MetadataException
-     * /
-    public Image getThumbnailImage() throws MetadataException
-    {
-        if (!containsThumbnail())
-            return null;
-
-        int compression = 0;
-        try {
-        	compression = this.getInt(ExifDirectory.TAG_COMPRESSION);
-        } catch (Throwable e) {
-        	this.addError("Unable to determine thumbnail type " + e.getMessage());
-        }
-
-        final byte[] thumbnailBytes = getThumbnailData();
-
-        if (compression == ExifDirectory.COMPRESSION_JPEG)
-        {
-            // JPEG Thumbnail
-            // operate directly on thumbnailBytes
-//            try {
-//                int offset = this.getInt(ExifDirectory.TAG_THUMBNAIL_OFFSET);
-//                int length = this.getInt(ExifDirectory.TAG_THUMBNAIL_LENGTH);
-//                byte[] result = new byte[length];
-//                for (int i = 0; i<result.length; i++) {
-//                    result[i] = _data[tiffHeaderOffset + offset + i];
-//                }
-//                this.setByteArray(ExifDirectory.TAG_THUMBNAIL_DATA, result);
-//            } catch (Throwable e) {
-//                this.addError("Unable to extract thumbnail: " + e.getMessage());
-//            }
-            // TODO decode the JPEG bytes as an image
-            return null; //new Image();
-        }
-        else if (compression == ExifDirectory.COMPRESSION_NONE)
-        {
-            // uncompressed thumbnail (raw RGB data)
-        	if (!this.containsTag(ExifDirectory.TAG_PHOTOMETRIC_INTERPRETATION))
-	            return null;
-
-        	try
-            {
-        		// If the image is RGB format, then convert it to a bitmap
-                final int photometricInterpretation = this.getInt(ExifDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
-                if (photometricInterpretation == ExifDirectory.PHOTOMETRIC_INTERPRETATION_RGB)
-                {
-                    // RGB
-                    Image image = createImageFromRawRgb(thumbnailBytes);
-                    return image;
-        		}
-                else if (photometricInterpretation == ExifDirectory.PHOTOMETRIC_INTERPRETATION_YCBCR)
-                {
-                    // YCbCr
-                    Image image = createImageFromRawYCbCr(thumbnailBytes);
-                    return image;
-        		}
-                else if (photometricInterpretation == ExifDirectory.PHOTOMETRIC_INTERPRETATION_MONOCHROME)
-                {
-                    // Monochrome
-                    // TODO
-                    return null;
-                }
-	        } catch (Throwable e) {
-	            this.addError("Unable to extract thumbnail: " + e.getMessage());
-	        }
-        }
-        return null;
-    }
-
-    /**
-     * Handle the YCbCr thumbnail encoding used by Ricoh RDC4200/4300, Fuji DS-7/300 and DX-5/7/9 cameras.
-     *
-     * At DX-5/7/9, YCbCrSubsampling(0x0212) has values of '2,1', PlanarConfiguration(0x011c) has a value '1'. So the
-     * data align of this image is below.
-     *
-     * Y(0,0),Y(1,0),Cb(0,0),Cr(0,0), Y(2,0),Y(3,0),Cb(2,0),Cr(3.0), Y(4,0),Y(5,0),Cb(4,0),Cr(4,0). . . .
-     *
-     * The numerics in parenthesis are pixel coordinates. DX series' YCbCrCoefficients(0x0211) has values '0.299/0.587/0.114',
-     * ReferenceBlackWhite(0x0214) has values '0,255,128,255,128,255'. Therefore to convert from Y/Cb/Cr to RGB is;
-     *
-     * B(0,0)=(Cb-128)*(2-0.114*2)+Y(0,0)
-     * R(0,0)=(Cr-128)*(2-0.299*2)+Y(0,0)
-     * G(0,0)=(Y(0,0)-0.114*B(0,0)-0.299*R(0,0))/0.587
-     *
-     * Horizontal subsampling is a value '2', so you can calculate B(1,0)/R(1,0)/G(1,0) by using the Y(1,0) and Cr(0,0)/Cb(0,0).
-     * Repeat this conversion by value of ImageWidth(0x0100) and ImageLength(0x0101).
-     *
-     * @param thumbnailBytes
-     * @return
-     * @throws com.drew.metadata.MetadataException
-     * /
-    private Image createImageFromRawYCbCr(byte[] thumbnailBytes) throws MetadataException
-    {
-        /*
-            Y  =  0.257R + 0.504G + 0.098B + 16
-            Cb = -0.148R - 0.291G + 0.439B + 128
-            Cr =  0.439R - 0.368G - 0.071B + 128
-
-            G = 1.164(Y-16) - 0.391(Cb-128) - 0.813(Cr-128)
-            R = 1.164(Y-16) + 1.596(Cr-128)
-            B = 1.164(Y-16) + 2.018(Cb-128)
-
-            R, G and B range from 0 to 255.
-            Y ranges from 16 to 235.
-            Cb and Cr range from 16 to 240.
-
-            http://www.faqs.org/faqs/graphics/colorspace-faq/
-        * /
-
-        int length = thumbnailBytes.length; // this.getInt(ExifDirectory.TAG_STRIP_BYTE_COUNTS);
-        final int imageWidth = this.getInt(ExifDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
-        final int imageHeight = this.getInt(ExifDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
-//        final int headerLength = 54;
-//        byte[] result = new byte[length + headerLength];
-//        // Add a windows BMP header described:
-//        // http://www.onicos.com/staff/iz/formats/bmp.html
-//        result[0] = 'B';
-//        result[1] = 'M'; // File Type identifier
-//        result[3] = (byte)(result.length / 256);
-//        result[2] = (byte)result.length;
-//        result[10] = (byte)headerLength;
-//        result[14] = 40; // MS Windows BMP header
-//        result[18] = (byte)imageWidth;
-//        result[22] = (byte)imageHeight;
-//        result[26] = 1;  // 1 Plane
-//        result[28] = 24; // Colour depth
-//        result[34] = (byte)length;
-//        result[35] = (byte)(length / 256);
-
-        final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
-
-        // order is YCbCr and image is upside down, bitmaps are BGR
-////        for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3)
-//        {
-//            final int y =  thumbnailBytes[dataOffset - 2] & 0xFF;
-//            final int cb = thumbnailBytes[dataOffset - 1] & 0xFF;
-//            final int cr = thumbnailBytes[dataOffset] & 0xFF;
-//            if (y<16 || y>235 || cb<16 || cb>240 || cr<16 || cr>240)
-//                "".toString();
-//
-//            int g = (int)(1.164*(y-16) - 0.391*(cb-128) - 0.813*(cr-128));
-//            int r = (int)(1.164*(y-16) + 1.596*(cr-128));
-//            int b = (int)(1.164*(y-16) + 2.018*(cb-128));
-//
-////            result[i] = (byte)b;
-////            result[i + 1] = (byte)g;
-////            result[i + 2] = (byte)r;
-//
-//            // TODO compose the image here
-//            image.setRGB(1, 2, 3);
-//        }
-
-        return image;
-    }
-
-    /**
-     * Creates a thumbnail image in (Windows) BMP format from raw RGB data.
-     * @param thumbnailBytes
-     * @return
-     * @throws com.drew.metadata.MetadataException
-     * /
-    private Image createImageFromRawRgb(byte[] thumbnailBytes) throws MetadataException
-    {
-        final int length = thumbnailBytes.length; // this.getInt(ExifDirectory.TAG_STRIP_BYTE_COUNTS);
-        final int imageWidth = this.getInt(ExifDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
-        final int imageHeight = this.getInt(ExifDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
-//        final int headerlength = 54;
-//        final byte[] result = new byte[length + headerlength];
-//        // Add a windows BMP header described:
-//        // http://www.onicos.com/staff/iz/formats/bmp.html
-//        result[0] = 'B';
-//        result[1] = 'M'; // File Type identifier
-//        result[3] = (byte)(result.length / 256);
-//        result[2] = (byte)result.length;
-//        result[10] = (byte)headerlength;
-//        result[14] = 40; // MS Windows BMP header
-//        result[18] = (byte)imageWidth;
-//        result[22] = (byte)imageHeight;
-//        result[26] = 1;  // 1 Plane
-//        result[28] = 24; // Colour depth
-//        result[34] = (byte)length;
-//        result[35] = (byte)(length / 256);
-
-        final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
-
-        // order is RGB and image is upside down, bitmaps are BGR
-//        for (int i = headerlength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3)
-//        {
-//            byte b = thumbnailBytes[dataOffset - 2];
-//            byte g = thumbnailBytes[dataOffset - 1];
-//            byte r = thumbnailBytes[dataOffset];
-//
-//            // TODO compose the image here
-//            image.setRGB(1, 2, 3);
-//        }
-
-        return image;
-    }
-*/
-
-    public boolean containsThumbnail()
-    {
-        return containsTag(ExifDirectory.TAG_THUMBNAIL_DATA);
-    }
-}
Index: trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/ExifIFD0Descriptor.java	(revision 6127)
@@ -0,0 +1,232 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Provides human-readable string representations of tag values stored in a <code>ExifIFD0Directory</code>.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ExifIFD0Descriptor extends TagDescriptor<ExifIFD0Directory>
+{
+    /**
+     * Dictates whether rational values will be represented in decimal format in instances
+     * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3).
+     */
+    private final boolean _allowDecimalRepresentationOfRationals = true;
+
+    public ExifIFD0Descriptor(@NotNull ExifIFD0Directory directory)
+    {
+        super(directory);
+    }
+
+    // Note for the potential addition of brightness presentation in eV:
+    // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv),
+    // you must add SensitivityValue(Sv).
+    // Ev=BV+Sv   Sv=log2(ISOSpeedRating/3.125)
+    // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32.
+
+    /**
+     * Returns a descriptive value of the the specified tag for this image.
+     * Where possible, known values will be substituted here in place of the raw
+     * tokens actually kept in the Exif segment.  If no substitution is
+     * available, the value provided by getString(int) will be returned.
+     * @param tagType the tag to find a description for
+     * @return a description of the image's value for the specified tag, or
+     *         <code>null</code> if the tag hasn't been defined.
+     */
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case ExifIFD0Directory.TAG_RESOLUTION_UNIT:
+                return getResolutionDescription();
+            case ExifIFD0Directory.TAG_YCBCR_POSITIONING:
+                return getYCbCrPositioningDescription();
+            case ExifIFD0Directory.TAG_X_RESOLUTION:
+                return getXResolutionDescription();
+            case ExifIFD0Directory.TAG_Y_RESOLUTION:
+                return getYResolutionDescription();
+            case ExifIFD0Directory.TAG_REFERENCE_BLACK_WHITE:
+                return getReferenceBlackWhiteDescription();
+            case ExifIFD0Directory.TAG_ORIENTATION:
+                return getOrientationDescription();
+
+            case ExifIFD0Directory.TAG_WIN_AUTHOR:
+               return getWindowsAuthorDescription();
+            case ExifIFD0Directory.TAG_WIN_COMMENT:
+               return getWindowsCommentDescription();
+            case ExifIFD0Directory.TAG_WIN_KEYWORDS:
+               return getWindowsKeywordsDescription();
+            case ExifIFD0Directory.TAG_WIN_SUBJECT:
+               return getWindowsSubjectDescription();
+            case ExifIFD0Directory.TAG_WIN_TITLE:
+               return getWindowsTitleDescription();
+
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getReferenceBlackWhiteDescription()
+    {
+        int[] ints = _directory.getIntArray(ExifIFD0Directory.TAG_REFERENCE_BLACK_WHITE);
+        if (ints==null)
+            return null;
+        int blackR = ints[0];
+        int whiteR = ints[1];
+        int blackG = ints[2];
+        int whiteG = ints[3];
+        int blackB = ints[4];
+        int whiteB = ints[5];
+        return "[" + blackR + "," + blackG + "," + blackB + "] " +
+               "[" + whiteR + "," + whiteG + "," + whiteB + "]";
+    }
+
+    @Nullable
+    public String getYResolutionDescription()
+    {
+        Rational value = _directory.getRational(ExifIFD0Directory.TAG_Y_RESOLUTION);
+        if (value==null)
+            return null;
+        final String unit = getResolutionDescription();
+        return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
+                " dots per " +
+                (unit==null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getXResolutionDescription()
+    {
+        Rational value = _directory.getRational(ExifIFD0Directory.TAG_X_RESOLUTION);
+        if (value==null)
+            return null;
+        final String unit = getResolutionDescription();
+        return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
+                " dots per " +
+                (unit==null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getYCbCrPositioningDescription()
+    {
+        Integer value = _directory.getInteger(ExifIFD0Directory.TAG_YCBCR_POSITIONING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Center of pixel array";
+            case 2: return "Datum point";
+            default:
+                return String.valueOf(value);
+        }
+    }
+
+    @Nullable
+    public String getOrientationDescription()
+    {
+        Integer value = _directory.getInteger(ExifIFD0Directory.TAG_ORIENTATION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Top, left side (Horizontal / normal)";
+            case 2: return "Top, right side (Mirror horizontal)";
+            case 3: return "Bottom, right side (Rotate 180)";
+            case 4: return "Bottom, left side (Mirror vertical)";
+            case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)";
+            case 6: return "Right side, top (Rotate 90 CW)";
+            case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)";
+            case 8: return "Left side, bottom (Rotate 270 CW)";
+            default:
+                return String.valueOf(value);
+        }
+    }
+
+    @Nullable
+    public String getResolutionDescription()
+    {
+        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
+        Integer value = _directory.getInteger(ExifIFD0Directory.TAG_RESOLUTION_UNIT);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "(No unit)";
+            case 2: return "Inch";
+            case 3: return "cm";
+            default:
+                return "";
+        }
+    }
+
+    /** The Windows specific tags uses plain Unicode. */
+    @Nullable
+    private String getUnicodeDescription(int tag)
+    {
+         byte[] commentBytes = _directory.getByteArray(tag);
+        if (commentBytes==null)
+            return null;
+         try {
+             // Decode the unicode string and trim the unicode zero "\0" from the end.
+            return new String(commentBytes, "UTF-16LE").trim();
+         }
+         catch (UnsupportedEncodingException ex) {
+            return null;
+         }
+    }
+
+    @Nullable
+    public String getWindowsAuthorDescription()
+    {
+       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_AUTHOR);
+    }
+
+    @Nullable
+    public String getWindowsCommentDescription()
+    {
+       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_COMMENT);
+    }
+
+    @Nullable
+    public String getWindowsKeywordsDescription()
+    {
+       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_KEYWORDS);
+    }
+
+    @Nullable
+    public String getWindowsTitleDescription()
+    {
+       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_TITLE);
+    }
+
+    @Nullable
+    public String getWindowsSubjectDescription()
+    {
+       return getUnicodeDescription(ExifIFD0Directory.TAG_WIN_SUBJECT);
+    }
+}
Index: trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/ExifIFD0Directory.java	(revision 6127)
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes Exif tags from the IFD0 directory.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ExifIFD0Directory extends Directory
+{
+    public static final int TAG_IMAGE_DESCRIPTION = 0x010E;
+    public static final int TAG_MAKE = 0x010F;
+    public static final int TAG_MODEL = 0x0110;
+    public static final int TAG_ORIENTATION = 0x0112;
+    public static final int TAG_X_RESOLUTION = 0x011A;
+    public static final int TAG_Y_RESOLUTION = 0x011B;
+    public static final int TAG_RESOLUTION_UNIT = 0x0128;
+    public static final int TAG_SOFTWARE = 0x0131;
+    public static final int TAG_DATETIME = 0x0132;
+    public static final int TAG_ARTIST = 0x013B;
+    public static final int TAG_WHITE_POINT = 0x013E;
+    public static final int TAG_PRIMARY_CHROMATICITIES = 0x013F;
+    public static final int TAG_YCBCR_COEFFICIENTS = 0x0211;
+    public static final int TAG_YCBCR_POSITIONING = 0x0213;
+    public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214;
+    public static final int TAG_COPYRIGHT = 0x8298;
+
+    /** The image title, as used by Windows XP. */
+    public static final int TAG_WIN_TITLE = 0x9C9B;
+    /** The image comment, as used by Windows XP. */
+    public static final int TAG_WIN_COMMENT = 0x9C9C;
+    /** The image author, as used by Windows XP (called Artist in the Windows shell). */
+    public static final int TAG_WIN_AUTHOR = 0x9C9D;
+    /** The image keywords, as used by Windows XP. */
+    public static final int TAG_WIN_KEYWORDS = 0x9C9E;
+    /** The image subject, as used by Windows XP. */
+    public static final int TAG_WIN_SUBJECT = 0x9C9F;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_IMAGE_DESCRIPTION, "Image Description");
+        _tagNameMap.put(TAG_MAKE, "Make");
+        _tagNameMap.put(TAG_MODEL, "Model");
+        _tagNameMap.put(TAG_ORIENTATION, "Orientation");
+        _tagNameMap.put(TAG_X_RESOLUTION, "X Resolution");
+        _tagNameMap.put(TAG_Y_RESOLUTION, "Y Resolution");
+        _tagNameMap.put(TAG_RESOLUTION_UNIT, "Resolution Unit");
+        _tagNameMap.put(TAG_SOFTWARE, "Software");
+        _tagNameMap.put(TAG_DATETIME, "Date/Time");
+        _tagNameMap.put(TAG_ARTIST, "Artist");
+        _tagNameMap.put(TAG_WHITE_POINT, "White Point");
+        _tagNameMap.put(TAG_PRIMARY_CHROMATICITIES, "Primary Chromaticities");
+        _tagNameMap.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients");
+        _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
+        _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
+        _tagNameMap.put(TAG_COPYRIGHT, "Copyright");
+
+        _tagNameMap.put(TAG_WIN_AUTHOR, "Windows XP Author");
+        _tagNameMap.put(TAG_WIN_COMMENT, "Windows XP Comment");
+        _tagNameMap.put(TAG_WIN_KEYWORDS, "Windows XP Keywords");
+        _tagNameMap.put(TAG_WIN_SUBJECT, "Windows XP Subject");
+        _tagNameMap.put(TAG_WIN_TITLE, "Windows XP Title");
+    }
+
+    public ExifIFD0Directory()
+    {
+        this.setDescriptor(new ExifIFD0Descriptor(this));
+    }
+
+    @NotNull
+    public String getName()
+    {
+        return "Exif IFD0";
+    }
+
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/ExifInteropDescriptor.java	(revision 6127)
@@ -1,35 +1,42 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 12-Nov-2002 22:27:34 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
+ * Provides human-readable string representations of tag values stored in a <code>ExifInteropDirectory</code>.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class ExifInteropDescriptor extends TagDescriptor
+public class ExifInteropDescriptor extends TagDescriptor<ExifInteropDirectory>
 {
-    public ExifInteropDescriptor(Directory directory)
+    public ExifInteropDescriptor(@NotNull ExifInteropDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
@@ -39,24 +46,26 @@
                 return getInteropVersionDescription();
             default:
-                return _directory.getString(tagType);
+                return super.getDescription(tagType);
         }
     }
 
-    public String getInteropVersionDescription() throws MetadataException
+    @Nullable
+    public String getInteropVersionDescription()
     {
-        if (!_directory.containsTag(ExifInteropDirectory.TAG_INTEROP_VERSION)) return null;
         int[] ints = _directory.getIntArray(ExifInteropDirectory.TAG_INTEROP_VERSION);
-        return ExifDescriptor.convertBytesToVersionString(ints);
+        return convertBytesToVersionString(ints, 2);
     }
 
+    @Nullable
     public String getInteropIndexDescription()
     {
-        if (!_directory.containsTag(ExifInteropDirectory.TAG_INTEROP_INDEX)) return null;
-        String interopIndex = _directory.getString(ExifInteropDirectory.TAG_INTEROP_INDEX).trim();
-        if ("R98".equalsIgnoreCase(interopIndex)) {
-            return "Recommended Exif Interoperability Rules (ExifR98)";
-        } else {
-            return "Unknown (" + interopIndex + ")";
-        }
+        String value = _directory.getString(ExifInteropDirectory.TAG_INTEROP_INDEX);
+
+        if (value==null)
+            return null;
+
+        return "R98".equalsIgnoreCase(value.trim())
+                ? "Recommended Exif Interoperability Rules (ExifR98)"
+                : "Unknown (" + value + ")";
     }
 }
Index: trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/ExifInteropDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 26-Nov-2002 10:58:13 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,5 +27,7 @@
 
 /**
+ * Describes Exif interoperability tags.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class ExifInteropDirectory extends Directory
@@ -32,14 +39,14 @@
     public static final int TAG_RELATED_IMAGE_LENGTH = 0x1002;
 
-    protected static final HashMap tagNameMap;
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        tagNameMap = new HashMap();
-        tagNameMap.put(new Integer(TAG_INTEROP_INDEX), "Interoperability Index");
-        tagNameMap.put(new Integer(TAG_INTEROP_VERSION), "Interoperability Version");
-        tagNameMap.put(new Integer(TAG_RELATED_IMAGE_FILE_FORMAT), "Related Image File Format");
-        tagNameMap.put(new Integer(TAG_RELATED_IMAGE_WIDTH), "Related Image Width");
-        tagNameMap.put(new Integer(TAG_RELATED_IMAGE_LENGTH), "Related Image Length");
+        _tagNameMap.put(TAG_INTEROP_INDEX, "Interoperability Index");
+        _tagNameMap.put(TAG_INTEROP_VERSION, "Interoperability Version");
+        _tagNameMap.put(TAG_RELATED_IMAGE_FILE_FORMAT, "Related Image File Format");
+        _tagNameMap.put(TAG_RELATED_IMAGE_WIDTH, "Related Image Width");
+        _tagNameMap.put(TAG_RELATED_IMAGE_LENGTH, "Related Image Length");
     }
 
@@ -49,4 +56,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -54,7 +62,8 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
-        return tagNameMap;
+        return _tagNameMap;
     }
 }
Index: trunk/src/com/drew/metadata/exif/ExifProcessingException.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifProcessingException.java	(revision 6002)
+++ 	(revision )
@@ -1,50 +1,0 @@
-/*
- * ExifProcessingException.java
- *
- * This class is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created on 29 April 2002, 00:33
- */
-package com.drew.metadata.exif;
-
-import com.drew.metadata.MetadataException;
-
-/**
- * The exception type raised during reading of Exif data in the instance of
- * unexpected data conditions.
- * @author  Drew Noakes http://drewnoakes.com
- */
-public class ExifProcessingException extends MetadataException
-{
-    /**
-     * Constructs an instance of <code>ExifProcessingException</code> with the
-     * specified detail message.
-     * @param message the detail message
-     */
-    public ExifProcessingException(String message)
-    {
-        super(message);
-    }
-
-    /**
-     * Constructs an instance of <code>ExifProcessingException</code> with the
-     * specified detail message and inner exception.
-     * @param message the detail message
-     * @param cause an inner exception
-     */
-    public ExifProcessingException(String message, Throwable cause)
-    {
-        super(message, cause);
-    }
-}
Index: trunk/src/com/drew/metadata/exif/ExifReader.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/ExifReader.java	(revision 6127)
@@ -1,204 +1,165 @@
 /*
- * EXIFExtractor.java
+ * Copyright 2002-2012 Drew Noakes
  *
- * This class based upon code from Jhead, a C program for extracting and
- * manipulating the Exif data within files written by Matthias Wandel.
- *   http://www.sentex.net/~mwandel/jhead/
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * Jhead is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  Similarly, I release this Java version under the
- * same license, though I do ask that you leave this header in tact.
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.  Unlike
- * Jhead, this code (as it stands) only supports reading of Exif data - no
- * manipulation, and no thumbnail stuff.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew.noakes@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ * More information about this project is available at:
  *
- * Created on 28 April 2002, 23:54
- * Modified 04 Aug 2002
- * - Renamed constants to be inline with changes to ExifTagValues interface
- * - Substituted usage of JDK 1.4 features (java.nio package)
- * Modified 29 Oct 2002 (v1.2)
- * - Proper traversing of Exif file structure and complete refactor & tidy of
- *   the codebase (a few unnoticed bugs removed)
- * - Reads makernote data for 6 families of camera (5 makes)
- * - Tags now stored in directories... use the IFD_* constants to refer to the
- *   image file directory you require (Exif, Interop, GPS and Makernote*) --
- *   this avoids collisions where two tags share the same code
- * - Takes componentCount of unknown tags into account
- * - Now understands GPS tags (thanks to Colin Briton for his help with this)
- * - Some other bug fixes, pointed out by users around the world.  Thanks!
- * Modified 27 Nov 2002 (v2.0)
- * - Renamed to ExifReader
- * - Moved to new package com.drew.metadata.exif
- * Modified since, however changes have not been logged.  See release notes for
- * library-wide modifications.
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.imaging.jpeg.JpegProcessingException;
-import com.drew.imaging.jpeg.JpegSegmentData;
-import com.drew.imaging.jpeg.JpegSegmentReader;
+import com.drew.lang.BufferBoundsException;
+import com.drew.lang.BufferReader;
 import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
 import com.drew.metadata.MetadataReader;
 
-import java.io.File;
-import java.io.InputStream;
-import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Set;
 
 /**
- * Extracts Exif data from a JPEG header segment, providing information about the
- * camera/scanner/capture device (if available).  Information is encapsulated in
- * an <code>Metadata</code> object.
- * @author  Drew Noakes http://drewnoakes.com
+ * Decodes Exif binary data, populating a {@link Metadata} object with tag values in {@link ExifSubIFDDirectory},
+ * {@link ExifThumbnailDirectory}, {@link ExifInteropDirectory}, {@link GpsDirectory} and one of the many camera makernote directories.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class ExifReader implements MetadataReader
 {
-    /**
-     * The JPEG segment as an array of bytes.
-     */
-    private final byte[] _data;
-
-    /**
-     * Represents the native byte ordering used in the JPEG segment.  If true,
-     * then we're using Motorolla ordering (Big endian), else we're using Intel
-     * ordering (Little endian).
-     */
-    private boolean _isMotorollaByteOrder;
-
-    /**
-     * Bean instance to store information about the image and camera/scanner/capture
-     * device.
-     */
-    private Metadata _metadata;
-
-    /**
-     * The number of bytes used per format descriptor.
-     */
-    private static final int[] BYTES_PER_FORMAT = {0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8};
-
-    /**
-     * The number of formats known.
-     */
+    // TODO extract a reusable TiffReader from this class with hooks for special tag handling and subdir following
+    
+    /** The number of bytes used per format descriptor. */
+    @NotNull
+    private static final int[] BYTES_PER_FORMAT = { 0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8 };
+
+    /** The number of formats known. */
     private static final int MAX_FORMAT_CODE = 12;
 
     // Format types
-    // Note: Cannot use the DataFormat enumeration in the case statement that uses these tags.
-    //       Is there a better way?
+    // TODO use an enum for these?
+    /** An 8-bit unsigned integer. */
     private static final int FMT_BYTE = 1;
+    /** A fixed-length character string. */
     private static final int FMT_STRING = 2;
+    /** An unsigned 16-bit integer. */
     private static final int FMT_USHORT = 3;
+    /** An unsigned 32-bit integer. */
     private static final int FMT_ULONG = 4;
     private static final int FMT_URATIONAL = 5;
+    /** An 8-bit signed integer. */
     private static final int FMT_SBYTE = 6;
     private static final int FMT_UNDEFINED = 7;
+    /** A signed 16-bit integer. */
     private static final int FMT_SSHORT = 8;
+    /** A signed 32-bit integer. */
     private static final int FMT_SLONG = 9;
     private static final int FMT_SRATIONAL = 10;
+    /** A 32-bit floating point number. */
     private static final int FMT_SINGLE = 11;
+    /** A 64-bit floating point number. */
     private static final int FMT_DOUBLE = 12;
 
-    public static final int TAG_EXIF_OFFSET = 0x8769;
+    /** This tag is a pointer to the Exif SubIFD. */
+    public static final int TAG_EXIF_SUB_IFD_OFFSET = 0x8769;
+    /** This tag is a pointer to the Exif Interop IFD. */
     public static final int TAG_INTEROP_OFFSET = 0xA005;
+    /** This tag is a pointer to the Exif GPS IFD. */
     public static final int TAG_GPS_INFO_OFFSET = 0x8825;
-    public static final int TAG_MAKER_NOTE = 0x927C;
+    /** This tag is a pointer to the Exif Makernote IFD. */
+    public static final int TAG_MAKER_NOTE_OFFSET = 0x927C;
 
     public static final int TIFF_HEADER_START_OFFSET = 6;
-
-    /**
-     * Creates an ExifReader for a JpegSegmentData object.
-     * @param segmentData
-     */
-    public ExifReader(JpegSegmentData segmentData)
-    {
-        this(segmentData.getSegment(JpegSegmentReader.SEGMENT_APP1));
-    }
-
-    /**
-     * Creates an ExifReader for a Jpeg file.
-     * @param file
-     * @throws JpegProcessingException
-     */
-    public ExifReader(File file) throws JpegProcessingException
-    {
-        this(new JpegSegmentReader(file).readSegment(JpegSegmentReader.SEGMENT_APP1));
-    }
-
-    /**
-     * Creates an ExifReader for a Jpeg stream.
-     * @param is JPEG stream. Stream will be closed.
-     */
-    public ExifReader(InputStream is) throws JpegProcessingException
-    {
-        this(new JpegSegmentReader(is).readSegment(JpegSegmentReader.SEGMENT_APP1));
-    }
-
-    /**
-     * Creates an ExifReader for the given JPEG header segment.
-     */
-    public ExifReader(byte[] data)
-    {
-        _data = data;
-    }
-
-    /**
-     * Performs the Exif data extraction, returning a new instance of <code>Metadata</code>.
-     */
-    public Metadata extract()
-    {
-        return extract(new Metadata());
-    }
 
     /**
      * Performs the Exif data extraction, adding found values to the specified
      * instance of <code>Metadata</code>.
+     *
+     * @param reader   The buffer reader from which Exif data should be read.
+     * @param metadata The Metadata object into which extracted values should be merged.
      */
-    public Metadata extract(Metadata metadata)
-    {
-        _metadata = metadata;
-        if (_data==null)
-            return _metadata;
-
-        // once we know there's some data, create the directory and start working on it
-        ExifDirectory directory = (ExifDirectory)_metadata.getDirectory(ExifDirectory.class);
+    public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata)
+    {
+        final ExifSubIFDDirectory directory = metadata.getOrCreateDirectory(ExifSubIFDDirectory.class);
 
         // check for the header length
-        if (_data.length<=14) {
+        if (reader.getLength() <= 14) {
             directory.addError("Exif data segment must contain at least 14 bytes");
-            return _metadata;
+            return;
         }
 
         // check for the header preamble
-        if (!"Exif\0\0".equals(new String(_data, 0, 6))) {
-            directory.addError("Exif data segment doesn't begin with 'Exif'");
-            return _metadata;
-        }
-
+        try {
+            if (!reader.getString(0, 6).equals("Exif\0\0")) {
+                directory.addError("Exif data segment doesn't begin with 'Exif'");
+                return;
+            }
+
+            extractIFD(metadata, metadata.getOrCreateDirectory(ExifIFD0Directory.class), TIFF_HEADER_START_OFFSET, reader);
+        } catch (BufferBoundsException e) {
+            directory.addError("Exif data segment ended prematurely");
+        }
+    }
+
+    /**
+     * Performs the Exif data extraction on a TIFF/RAW, adding found values to the specified
+     * instance of <code>Metadata</code>.
+     *
+     * @param reader   The BufferReader from which TIFF data should be read.
+     * @param metadata The Metadata object into which extracted values should be merged.
+     */
+    public void extractTiff(@NotNull BufferReader reader, @NotNull Metadata metadata)
+    {
+        final ExifIFD0Directory directory = metadata.getOrCreateDirectory(ExifIFD0Directory.class);
+
+        try {
+            extractIFD(metadata, directory, 0, reader);
+        } catch (BufferBoundsException e) {
+            directory.addError("Exif data segment ended prematurely");
+        }
+    }
+
+    private void extractIFD(@NotNull Metadata metadata, @NotNull final ExifIFD0Directory directory, int tiffHeaderOffset, @NotNull BufferReader reader) throws BufferBoundsException
+    {
         // this should be either "MM" or "II"
-        String byteOrderIdentifier = new String(_data, 6, 2);
-        if (!setByteOrder(byteOrderIdentifier)) {
+        String byteOrderIdentifier = reader.getString(tiffHeaderOffset, 2);
+
+        if ("MM".equals(byteOrderIdentifier)) {
+            reader.setMotorolaByteOrder(true);
+        } else if ("II".equals(byteOrderIdentifier)) {
+            reader.setMotorolaByteOrder(false);
+        } else {
             directory.addError("Unclear distinction between Motorola/Intel byte ordering: " + byteOrderIdentifier);
-            return _metadata;
+            return;
         }
 
         // Check the next two values for correctness.
-        if (get16Bits(8)!=0x2a) {
-            directory.addError("Invalid Exif start - should have 0x2A at offset 8 in Exif header");
-            return _metadata;
-        }
-
-        int firstDirectoryOffset = get32Bits(10) + TIFF_HEADER_START_OFFSET;
-
-        // David Ekholm sent an digital camera image that has this problem
-        if (firstDirectoryOffset>=_data.length - 1) {
+        final int tiffMarker = reader.getUInt16(2 + tiffHeaderOffset);
+
+        final int standardTiffMarker = 0x002A;
+        final int olympusRawTiffMarker = 0x4F52; // for ORF files
+        final int panasonicRawTiffMarker = 0x0055; // for RW2 files
+
+        if (tiffMarker != standardTiffMarker && tiffMarker != olympusRawTiffMarker && tiffMarker != panasonicRawTiffMarker) {
+            directory.addError("Unexpected TIFF marker after byte order identifier: 0x" + Integer.toHexString(tiffMarker));
+            return;
+        }
+
+        int firstDirectoryOffset = reader.getInt32(4 + tiffHeaderOffset) + tiffHeaderOffset;
+
+        // David Ekholm sent a digital camera image that has this problem
+        if (firstDirectoryOffset >= reader.getLength() - 1) {
             directory.addError("First exif directory offset is beyond end of Exif data segment");
             // First directory normally starts 14 bytes in -- try it here and catch another error in the worst case
@@ -206,105 +167,101 @@
         }
 
-        HashMap processedDirectoryOffsets = new HashMap();
-
-        // 0th IFD (we merge with Exif IFD)
-        processDirectory(directory, processedDirectoryOffsets, firstDirectoryOffset, TIFF_HEADER_START_OFFSET);
+        Set<Integer> processedDirectoryOffsets = new HashSet<Integer>();
+
+        processDirectory(directory, processedDirectoryOffsets, firstDirectoryOffset, tiffHeaderOffset, metadata, reader);
 
         // after the extraction process, if we have the correct tags, we may be able to store thumbnail information
-        storeThumbnailBytes(directory, TIFF_HEADER_START_OFFSET);
-
-        return _metadata;
-    }
-
-    private void storeThumbnailBytes(ExifDirectory exifDirectory, int tiffHeaderOffset)
-    {
-        if (!exifDirectory.containsTag(ExifDirectory.TAG_COMPRESSION))
-        	return;
-
-        if (!exifDirectory.containsTag(ExifDirectory.TAG_THUMBNAIL_LENGTH) ||
-            !exifDirectory.containsTag(ExifDirectory.TAG_THUMBNAIL_OFFSET))
-            return;
-
-        try {
-            int offset = exifDirectory.getInt(ExifDirectory.TAG_THUMBNAIL_OFFSET);
-            int length = exifDirectory.getInt(ExifDirectory.TAG_THUMBNAIL_LENGTH);
-            byte[] result = new byte[length];
-            for (int i = 0; i<result.length; i++) {
-                result[i] = _data[tiffHeaderOffset + offset + i];
-            }
-            exifDirectory.setByteArray(ExifDirectory.TAG_THUMBNAIL_DATA, result);
-        } catch (Throwable e) {
-            exifDirectory.addError("Unable to extract thumbnail: " + e.getMessage());
-        }
-    }
-
-    private boolean setByteOrder(String byteOrderIdentifier)
-    {
-        if ("MM".equals(byteOrderIdentifier)) {
-            _isMotorollaByteOrder = true;
-        } else if ("II".equals(byteOrderIdentifier)) {
-            _isMotorollaByteOrder = false;
-        } else {
-            return false;
-        }
-        return true;
+        ExifThumbnailDirectory thumbnailDirectory = metadata.getDirectory(ExifThumbnailDirectory.class);
+        if (thumbnailDirectory!=null && thumbnailDirectory.containsTag(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION)) {
+            Integer offset = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
+            Integer length = thumbnailDirectory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
+            if (offset != null && length != null) {
+                try {
+                    byte[] thumbnailData = reader.getBytes(tiffHeaderOffset + offset, length);
+                    thumbnailDirectory.setThumbnailData(thumbnailData);
+                } catch (BufferBoundsException ex) {
+                    directory.addError("Invalid thumbnail data specification: " + ex.getMessage());
+                }
+            }
+        }
     }
 
     /**
      * Process one of the nested Tiff IFD directories.
+     * <p/>
+     * Header
      * 2 bytes: number of tags
-     * for each tag
-     *   2 bytes: tag type
-     *   2 bytes: format code
-     *   4 bytes: component count
+     * <p/>
+     * Then for each tag
+     * 2 bytes: tag type
+     * 2 bytes: format code
+     * 4 bytes: component count
      */
-    private void processDirectory(Directory directory, HashMap processedDirectoryOffsets, int dirStartOffset, int tiffHeaderOffset)
+    private void processDirectory(@NotNull Directory directory, @NotNull Set<Integer> processedDirectoryOffsets, int dirStartOffset, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull final BufferReader reader) throws BufferBoundsException
     {
         // check for directories we've already visited to avoid stack overflows when recursive/cyclic directory structures exist
-        if (processedDirectoryOffsets.containsKey(new Integer(dirStartOffset)))
+        if (processedDirectoryOffsets.contains(Integer.valueOf(dirStartOffset)))
             return;
 
         // remember that we've visited this directory so that we don't visit it again later
-        processedDirectoryOffsets.put(new Integer(dirStartOffset), "processed");
-
-        if (dirStartOffset>=_data.length || dirStartOffset<0) {
-            directory.addError("Ignored directory marked to start outside data segement");
-            return;
-        }
-
-        if (!isDirectoryLengthValid(dirStartOffset, tiffHeaderOffset)) {
+        processedDirectoryOffsets.add(dirStartOffset);
+
+        if (dirStartOffset >= reader.getLength() || dirStartOffset < 0) {
+            directory.addError("Ignored directory marked to start outside data segment");
+            return;
+        }
+
+        // First two bytes in the IFD are the number of tags in this directory
+        int dirTagCount = reader.getUInt16(dirStartOffset);
+
+        int dirLength = (2 + (12 * dirTagCount) + 4);
+        if (dirLength + dirStartOffset > reader.getLength()) {
             directory.addError("Illegally sized directory");
             return;
         }
 
-        // First two bytes in the IFD are the number of tags in this directory
-        int dirTagCount = get16Bits(dirStartOffset);
-
         // Handle each tag in this directory
-        for (int tagNumber = 0; tagNumber<dirTagCount; tagNumber++)
-        {
+        for (int tagNumber = 0; tagNumber < dirTagCount; tagNumber++) {
             final int tagOffset = calculateTagOffset(dirStartOffset, tagNumber);
 
             // 2 bytes for the tag type
-            final int tagType = get16Bits(tagOffset);
+            final int tagType = reader.getUInt16(tagOffset);
 
             // 2 bytes for the format code
-            final int formatCode = get16Bits(tagOffset + 2);
-            if (formatCode<1 || formatCode>MAX_FORMAT_CODE) {
-                directory.addError("Invalid format code: " + formatCode);
-                continue;
+            final int formatCode = reader.getUInt16(tagOffset + 2);
+            if (formatCode < 1 || formatCode > MAX_FORMAT_CODE) {
+                // This error suggests that we are processing at an incorrect index and will generate
+                // rubbish until we go out of bounds (which may be a while).  Exit now.
+                directory.addError("Invalid TIFF tag format code: " + formatCode);
+                return;
             }
 
             // 4 bytes dictate the number of components in this tag's data
-            final int componentCount = get32Bits(tagOffset + 4);
-            if (componentCount<0) {
-                directory.addError("Negative component count in EXIF");
+            final int componentCount = reader.getInt32(tagOffset + 4);
+            if (componentCount < 0) {
+                directory.addError("Negative TIFF tag component count");
                 continue;
             }
             // each component may have more than one byte... calculate the total number of bytes
             final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode];
-            final int tagValueOffset = calculateTagValueOffset(byteCount, tagOffset, tiffHeaderOffset);
-            if (tagValueOffset<0 || tagValueOffset > _data.length) {
-                directory.addError("Illegal pointer offset value in EXIF");
+            final int tagValueOffset;
+            if (byteCount > 4) {
+                // If it's bigger than 4 bytes, the dir entry contains an offset.
+                // dirEntryOffset must be passed, as some makernote implementations (e.g. FujiFilm) incorrectly use an
+                // offset relative to the start of the makernote itself, not the TIFF segment.
+                final int offsetVal = reader.getInt32(tagOffset + 8);
+                if (offsetVal + byteCount > reader.getLength()) {
+                    // Bogus pointer offset and / or byteCount value
+                    directory.addError("Illegal TIFF tag pointer offset");
+                    continue;
+                }
+                tagValueOffset = tiffHeaderOffset + offsetVal;
+            } else {
+                // 4 bytes or less and value is in the dir entry itself
+                tagValueOffset = tagOffset + 8;
+            }
+
+            if (tagValueOffset < 0 || tagValueOffset > reader.getLength()) {
+                directory.addError("Illegal TIFF tag pointer offset");
                 continue;
             }
@@ -312,29 +269,33 @@
             // Check that this tag isn't going to allocate outside the bounds of the data array.
             // This addresses an uncommon OutOfMemoryError.
-            if (byteCount < 0 || tagValueOffset + byteCount > _data.length)
-            {
+            if (byteCount < 0 || tagValueOffset + byteCount > reader.getLength()) {
                 directory.addError("Illegal number of bytes: " + byteCount);
                 continue;
             }
 
-            // Calculate the value as an offset for cases where the tag represents directory
-            final int subdirOffset = tiffHeaderOffset + get32Bits(tagValueOffset);
-
             switch (tagType) {
-                case TAG_EXIF_OFFSET:
-                    processDirectory(_metadata.getDirectory(ExifDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset);
+                case TAG_EXIF_SUB_IFD_OFFSET: {
+                    final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
+                    processDirectory(metadata.getOrCreateDirectory(ExifSubIFDDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
                     continue;
-                case TAG_INTEROP_OFFSET:
-                    processDirectory(_metadata.getDirectory(ExifInteropDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset);
+                }
+                case TAG_INTEROP_OFFSET: {
+                    final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
+                    processDirectory(metadata.getOrCreateDirectory(ExifInteropDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
                     continue;
-                case TAG_GPS_INFO_OFFSET:
-                    processDirectory(_metadata.getDirectory(GpsDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset);
+                }
+                case TAG_GPS_INFO_OFFSET: {
+                    final int subdirOffset = tiffHeaderOffset + reader.getInt32(tagValueOffset);
+                    processDirectory(metadata.getOrCreateDirectory(GpsDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
                     continue;
-                case TAG_MAKER_NOTE:
-                    processMakerNote(tagValueOffset, processedDirectoryOffsets, tiffHeaderOffset);
+                }
+                case TAG_MAKER_NOTE_OFFSET: {
+                    processMakerNote(tagValueOffset, processedDirectoryOffsets, tiffHeaderOffset, metadata, reader);
                     continue;
-                default:
-                    processTag(directory, tagType, tagValueOffset, componentCount, formatCode);
+                }
+                default: {
+                    processTag(directory, tagType, tagValueOffset, componentCount, formatCode, reader);
                     break;
+                }
             }
         }
@@ -342,8 +303,8 @@
         // at the end of each IFD is an optional link to the next IFD
         final int finalTagOffset = calculateTagOffset(dirStartOffset, dirTagCount);
-        int nextDirectoryOffset = get32Bits(finalTagOffset);
-        if (nextDirectoryOffset!=0) {
+        int nextDirectoryOffset = reader.getInt32(finalTagOffset);
+        if (nextDirectoryOffset != 0) {
             nextDirectoryOffset += tiffHeaderOffset;
-            if (nextDirectoryOffset>=_data.length) {
+            if (nextDirectoryOffset >= reader.getLength()) {
                 // Last 4 bytes of IFD reference another IFD with an address that is out of bounds
                 // Note this could have been caused by jhead 1.3 cropping too much
@@ -353,36 +314,35 @@
                 return;
             }
-            // the next directory is of same type as this one
-            processDirectory(directory, processedDirectoryOffsets, nextDirectoryOffset, tiffHeaderOffset);
-        }
-    }
-
-    private void processMakerNote(int subdirOffset, HashMap processedDirectoryOffsets, int tiffHeaderOffset)
+            // TODO in Exif, the only known 'follower' IFD is the thumbnail one, however this may not be the case
+            final ExifThumbnailDirectory nextDirectory = metadata.getOrCreateDirectory(ExifThumbnailDirectory.class);
+            processDirectory(nextDirectory, processedDirectoryOffsets, nextDirectoryOffset, tiffHeaderOffset, metadata, reader);
+        }
+    }
+
+    private void processMakerNote(int subdirOffset, @NotNull Set<Integer> processedDirectoryOffsets, int tiffHeaderOffset, @NotNull final Metadata metadata, @NotNull BufferReader reader) throws BufferBoundsException
     {
         // Determine the camera model and makernote format
-        Directory exifDirectory = _metadata.getDirectory(ExifDirectory.class);
-
-        if (exifDirectory==null)
-            return;
-
-        String cameraModel = exifDirectory.getString(ExifDirectory.TAG_MAKE);
-        final String firstTwoChars = new String(_data, subdirOffset, 2);
-        final String firstThreeChars = new String(_data, subdirOffset, 3);
-        final String firstFourChars = new String(_data, subdirOffset, 4);
-        final String firstFiveChars = new String(_data, subdirOffset, 5);
-        final String firstSixChars = new String(_data, subdirOffset, 6);
-        final String firstSevenChars = new String(_data, subdirOffset, 7);
-        final String firstEightChars = new String(_data, subdirOffset, 8);
-        if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars))
-        {
+        Directory ifd0Directory = metadata.getDirectory(ExifIFD0Directory.class);
+
+        if (ifd0Directory==null)
+            return;
+
+        String cameraModel = ifd0Directory.getString(ExifIFD0Directory.TAG_MAKE);
+
+        //final String firstTwoChars = reader.getString(subdirOffset, 2);
+        final String firstThreeChars = reader.getString(subdirOffset, 3);
+        final String firstFourChars = reader.getString(subdirOffset, 4);
+        final String firstFiveChars = reader.getString(subdirOffset, 5);
+        final String firstSixChars = reader.getString(subdirOffset, 6);
+        final String firstSevenChars = reader.getString(subdirOffset, 7);
+        final String firstEightChars = reader.getString(subdirOffset, 8);
+        final String firstTwelveChars = reader.getString(subdirOffset, 12);
+
+        if ("OLYMP".equals(firstFiveChars) || "EPSON".equals(firstFiveChars) || "AGFA".equals(firstFourChars)) {
             // Olympus Makernote
-            // Epson and Agfa use Olypus maker note standard, see:
-            //     http://www.ozhiker.com/electronics/pjmt/jpeg_info/
-            processDirectory(_metadata.getDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset);
-        }
-        else if (cameraModel!=null && cameraModel.trim().toUpperCase().startsWith("NIKON"))
-        {
-            if ("Nikon".equals(firstFiveChars))
-            {
+            // Epson and Agfa use Olympus maker note standard: http://www.ozhiker.com/electronics/pjmt/jpeg_info/
+            processDirectory(metadata.getOrCreateDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset, metadata, reader);
+        } else if (cameraModel != null && cameraModel.trim().toUpperCase().startsWith("NIKON")) {
+            if ("Nikon".equals(firstFiveChars)) {
                 /* There are two scenarios here:
                  * Type 1:                  **
@@ -393,76 +353,62 @@
                  * :0010: 00 08 00 1E 00 01 00 07-00 00 00 04 30 32 30 30 ............0200
                  */
-                if (_data[subdirOffset+6]==1)
-                    processDirectory(_metadata.getDirectory(NikonType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset);
-                else if (_data[subdirOffset+6]==2)
-                    processDirectory(_metadata.getDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 18, subdirOffset + 10);
-                else
-                    exifDirectory.addError("Unsupported makernote data ignored.");
-            }
+                switch (reader.getUInt8(subdirOffset + 6)) {
+                    case 1:
+                        processDirectory(metadata.getOrCreateDirectory(NikonType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 8, tiffHeaderOffset, metadata, reader);
+                        break;
+                    case 2:
+                        processDirectory(metadata.getOrCreateDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 18, subdirOffset + 10, metadata, reader);
+                        break;
+                    default:
+                        ifd0Directory.addError("Unsupported Nikon makernote data ignored.");
+                        break;
+                }
+            } else {
+                // The IFD begins with the first MakerNote byte (no ASCII name).  This occurs with CoolPix 775, E990 and D1 models.
+                processDirectory(metadata.getOrCreateDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
+            }
+        } else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars)) {
+            processDirectory(metadata.getOrCreateDirectory(SonyType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset, metadata, reader);
+        } else if ("SIGMA\u0000\u0000\u0000".equals(firstEightChars) || "FOVEON\u0000\u0000".equals(firstEightChars)) {
+            processDirectory(metadata.getOrCreateDirectory(SigmaMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 10, tiffHeaderOffset, metadata, reader);
+        } else if ("SEMC MS\u0000\u0000\u0000\u0000\u0000".equals(firstTwelveChars)) {
+            // force MM for this directory
+            boolean isMotorola = reader.isMotorolaByteOrder();
+            reader.setMotorolaByteOrder(true);
+            // skip 12 byte header + 2 for "MM" + 6
+            processDirectory(metadata.getOrCreateDirectory(SonyType6MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset, metadata, reader);
+            reader.setMotorolaByteOrder(isMotorola);
+        } else if ("KDK".equals(firstThreeChars)) {
+            processDirectory(metadata.getOrCreateDirectory(KodakMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset, metadata, reader);
+        } else if ("Canon".equalsIgnoreCase(cameraModel)) {
+            processDirectory(metadata.getOrCreateDirectory(CanonMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
+        } else if (cameraModel != null && cameraModel.toUpperCase().startsWith("CASIO")) {
+            if ("QVC\u0000\u0000\u0000".equals(firstSixChars))
+                processDirectory(metadata.getOrCreateDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, tiffHeaderOffset, metadata, reader);
             else
-            {
-                // The IFD begins with the first MakerNote byte (no ASCII name).  This occurs with CoolPix 775, E990 and D1 models.
-                processDirectory(_metadata.getDirectory(NikonType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset);
-            }
-        }
-        else if ("SONY CAM".equals(firstEightChars) || "SONY DSC".equals(firstEightChars))
-        {
-            processDirectory(_metadata.getDirectory(SonyMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset);
-        }
-        else if ("KDK".equals(firstThreeChars))
-        {
-            processDirectory(_metadata.getDirectory(KodakMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 20, tiffHeaderOffset);
-        }
-        else if ("Canon".equalsIgnoreCase(cameraModel))
-        {
-            processDirectory(_metadata.getDirectory(CanonMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset);
-        }
-        else if (cameraModel!=null && cameraModel.toUpperCase().startsWith("CASIO"))
-        {
-            if ("QVC\u0000\u0000\u0000".equals(firstSixChars))
-                processDirectory(_metadata.getDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, tiffHeaderOffset);
-            else
-                processDirectory(_metadata.getDirectory(CasioType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset);
-        }
-        else if ("FUJIFILM".equals(firstEightChars) || "Fujifilm".equalsIgnoreCase(cameraModel))
-        {
-            // TODO make this field a passed parameter, to avoid threading issues
-            boolean byteOrderBefore = _isMotorollaByteOrder;
+                processDirectory(metadata.getOrCreateDirectory(CasioType1MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
+        } else if ("FUJIFILM".equals(firstEightChars) || "Fujifilm".equalsIgnoreCase(cameraModel)) {
+            boolean byteOrderBefore = reader.isMotorolaByteOrder();
             // bug in fujifilm makernote ifd means we temporarily use Intel byte ordering
-            _isMotorollaByteOrder = false;
+            reader.setMotorolaByteOrder(false);
             // the 4 bytes after "FUJIFILM" in the makernote point to the start of the makernote
             // IFD, though the offset is relative to the start of the makernote, not the TIFF
             // header (like everywhere else)
-            int ifdStart = subdirOffset + get32Bits(subdirOffset + 8);
-            processDirectory(_metadata.getDirectory(FujifilmMakernoteDirectory.class), processedDirectoryOffsets, ifdStart, tiffHeaderOffset);
-            _isMotorollaByteOrder = byteOrderBefore;
-        }
-        else if (cameraModel!=null && cameraModel.toUpperCase().startsWith("MINOLTA"))
-        {
+            int ifdStart = subdirOffset + reader.getInt32(subdirOffset + 8);
+            processDirectory(metadata.getOrCreateDirectory(FujifilmMakernoteDirectory.class), processedDirectoryOffsets, ifdStart, tiffHeaderOffset, metadata, reader);
+            reader.setMotorolaByteOrder(byteOrderBefore);
+        } else if (cameraModel != null && cameraModel.toUpperCase().startsWith("MINOLTA")) {
             // Cases seen with the model starting with MINOLTA in capitals seem to have a valid Olympus makernote
             // area that commences immediately.
-            processDirectory(_metadata.getDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset);
-        }
-        else if ("KC".equals(firstTwoChars) || "MINOL".equals(firstFiveChars) || "MLY".equals(firstThreeChars) || "+M+M+M+M".equals(firstEightChars))
-        {
-            // This Konica data is not understood.  Header identified in accordance with information at this site:
-            // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html
-            // TODO determine how to process the information described at the above website
-            exifDirectory.addError("Unsupported Konica/Minolta data ignored.");
-        }
-        else if ("KYOCERA".equals(firstSevenChars))
-        {
+            processDirectory(metadata.getOrCreateDirectory(OlympusMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, tiffHeaderOffset, metadata, reader);
+        } else if ("KYOCERA".equals(firstSevenChars)) {
             // http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html
-            processDirectory(_metadata.getDirectory(KyoceraMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 22, tiffHeaderOffset);
-        }
-        else if ("Panasonic\u0000\u0000\u0000".equals(new String(_data, subdirOffset, 12)))
-        {
+            processDirectory(metadata.getOrCreateDirectory(KyoceraMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 22, tiffHeaderOffset, metadata, reader);
+        } else if ("Panasonic\u0000\u0000\u0000".equals(reader.getString(subdirOffset, 12))) {
             // NON-Standard TIFF IFD Data using Panasonic Tags. There is no Next-IFD pointer after the IFD
             // Offsets are relative to the start of the TIFF header at the beginning of the EXIF segment
             // more information here: http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html
-            processDirectory(_metadata.getDirectory(PanasonicMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset);
-        }
-        else if ("AOC\u0000".equals(firstFourChars))
-        {
+            processDirectory(metadata.getOrCreateDirectory(PanasonicMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 12, tiffHeaderOffset, metadata, reader);
+        } else if ("AOC\u0000".equals(firstFourChars)) {
             // NON-Standard TIFF IFD Data using Casio Type 2 Tags
             // IFD has no Next-IFD pointer at end of IFD, and
@@ -470,8 +416,6 @@
             // Observed for:
             // - Pentax ist D
-            processDirectory(_metadata.getDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, subdirOffset);
-        }
-        else if (cameraModel!=null && (cameraModel.toUpperCase().startsWith("PENTAX") || cameraModel.toUpperCase().startsWith("ASAHI")))
-        {
+            processDirectory(metadata.getOrCreateDirectory(CasioType2MakernoteDirectory.class), processedDirectoryOffsets, subdirOffset + 6, subdirOffset, metadata, reader);
+        } else if (cameraModel != null && (cameraModel.toUpperCase().startsWith("PENTAX") || cameraModel.toUpperCase().startsWith("ASAHI"))) {
             // NON-Standard TIFF IFD Data using Pentax Tags
             // IFD has no Next-IFD pointer at end of IFD, and
@@ -480,88 +424,137 @@
             // - PENTAX Optio 330
             // - PENTAX Optio 430
-            processDirectory(_metadata.getDirectory(PentaxMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, subdirOffset);
-        }
-        else
-        {
+            processDirectory(metadata.getOrCreateDirectory(PentaxMakernoteDirectory.class), processedDirectoryOffsets, subdirOffset, subdirOffset, metadata, reader);
+//        } else if ("KC".equals(firstTwoChars) || "MINOL".equals(firstFiveChars) || "MLY".equals(firstThreeChars) || "+M+M+M+M".equals(firstEightChars)) {
+//            // This Konica data is not understood.  Header identified in accordance with information at this site:
+//            // http://www.ozhiker.com/electronics/pjmt/jpeg_info/minolta_mn.html
+//            // TODO add support for minolta/konica cameras
+//            exifDirectory.addError("Unsupported Konica/Minolta data ignored.");
+        } else {
             // TODO how to store makernote data when it's not from a supported camera model?
             // this is difficult as the starting offset is not known.  we could look for it...
-            exifDirectory.addError("Unsupported makernote data ignored.");
-        }
-    }
-
-    private boolean isDirectoryLengthValid(int dirStartOffset, int tiffHeaderOffset)
-    {
-        int dirTagCount = get16Bits(dirStartOffset);
-        int dirLength = (2 + (12 * dirTagCount) + 4);
-        if (dirLength + dirStartOffset + tiffHeaderOffset>=_data.length) {
-            // Note: Files that had thumbnails trimmed with jhead 1.3 or earlier might trigger this
-            return false;
-        }
-        return true;
-    }
-
-    private void processTag(Directory directory, int tagType, int tagValueOffset, int componentCount, int formatCode)
+        }
+    }
+
+    private void processTag(@NotNull Directory directory, int tagType, int tagValueOffset, int componentCount, int formatCode, @NotNull final BufferReader reader) throws BufferBoundsException
     {
         // Directory simply stores raw values
         // The display side uses a Descriptor class per directory to turn the raw values into 'pretty' descriptions
-        switch (formatCode)
-        {
+        switch (formatCode) {
             case FMT_UNDEFINED:
                 // this includes exif user comments
-                final byte[] tagBytes = new byte[componentCount];
-                final int byteCount = componentCount * BYTES_PER_FORMAT[formatCode];
-                for (int i=0; i<byteCount; i++)
-                    tagBytes[i] = _data[tagValueOffset + i];
-                directory.setByteArray(tagType, tagBytes);
+                directory.setByteArray(tagType, reader.getBytes(tagValueOffset, componentCount));
                 break;
             case FMT_STRING:
-                directory.setString(tagType, readString(tagValueOffset, componentCount));
+                String string = reader.getNullTerminatedString(tagValueOffset, componentCount);
+                directory.setString(tagType, string);
+/*
+                // special handling for certain known tags, proposed by Yuri Binev but left out for now,
+                // as it gives the false impression that the image was captured in the same timezone
+                // in which the string is parsed
+                if (tagType==ExifSubIFDDirectory.TAG_DATETIME ||
+                    tagType==ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL ||
+                    tagType==ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED) {
+                    String[] datePatterns = {
+                        "yyyy:MM:dd HH:mm:ss",
+                        "yyyy:MM:dd HH:mm",
+                        "yyyy-MM-dd HH:mm:ss",
+                        "yyyy-MM-dd HH:mm"};
+                    for (String datePattern : datePatterns) {
+                        try {
+                            DateFormat parser = new SimpleDateFormat(datePattern);
+                            Date date = parser.parse(string);
+                            directory.setDate(tagType, date);
+                            break;
+                        } catch (ParseException ex) {
+                            // simply try the next pattern
+                        }
+                    }
+                }
+*/
                 break;
             case FMT_SRATIONAL:
+                if (componentCount == 1) {
+                    directory.setRational(tagType, new Rational(reader.getInt32(tagValueOffset), reader.getInt32(tagValueOffset + 4)));
+                } else if (componentCount > 1) {
+                    Rational[] rationals = new Rational[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        rationals[i] = new Rational(reader.getInt32(tagValueOffset + (8 * i)), reader.getInt32(tagValueOffset + 4 + (8 * i)));
+                    directory.setRationalArray(tagType, rationals);
+                }
+                break;
             case FMT_URATIONAL:
-                if (componentCount==1) {
-                    Rational rational = new Rational(get32Bits(tagValueOffset), get32Bits(tagValueOffset + 4));
-                    directory.setRational(tagType, rational);
-                } else {
+                if (componentCount == 1) {
+                    directory.setRational(tagType, new Rational(reader.getUInt32(tagValueOffset), reader.getUInt32(tagValueOffset + 4)));
+                } else if (componentCount > 1) {
                     Rational[] rationals = new Rational[componentCount];
-                    for (int i = 0; i<componentCount; i++)
-                        rationals[i] = new Rational(get32Bits(tagValueOffset + (8 * i)), get32Bits(tagValueOffset + 4 + (8 * i)));
+                    for (int i = 0; i < componentCount; i++)
+                        rationals[i] = new Rational(reader.getUInt32(tagValueOffset + (8 * i)), reader.getUInt32(tagValueOffset + 4 + (8 * i)));
                     directory.setRationalArray(tagType, rationals);
                 }
                 break;
+            case FMT_SINGLE:
+                if (componentCount == 1) {
+                    directory.setFloat(tagType, reader.getFloat32(tagValueOffset));
+                } else {
+                    float[] floats = new float[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        floats[i] = reader.getFloat32(tagValueOffset + (i * 4));
+                    directory.setFloatArray(tagType, floats);
+                }
+                break;
+            case FMT_DOUBLE:
+                if (componentCount == 1) {
+                    directory.setDouble(tagType, reader.getDouble64(tagValueOffset));
+                } else {
+                    double[] doubles = new double[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        doubles[i] = reader.getDouble64(tagValueOffset + (i * 4));
+                    directory.setDoubleArray(tagType, doubles);
+                }
+                break;
+
+            //
+            // Note that all integral types are stored as int32 internally (the largest supported by TIFF)
+            //
+
             case FMT_SBYTE:
+                if (componentCount == 1) {
+                    directory.setInt(tagType, reader.getInt8(tagValueOffset));
+                } else {
+                    int[] bytes = new int[componentCount];
+                    for (int i = 0; i < componentCount; i++)
+                        bytes[i] = reader.getInt8(tagValueOffset + i);
+                    directory.setIntArray(tagType, bytes);
+                }
+                break;
             case FMT_BYTE:
-                if (componentCount==1) {
-                    // this may need to be a byte, but I think casting to int is fine
-                    int b = _data[tagValueOffset];
-                    directory.setInt(tagType, b);
+                if (componentCount == 1) {
+                    directory.setInt(tagType, reader.getUInt8(tagValueOffset));
                 } else {
                     int[] bytes = new int[componentCount];
-                    for (int i = 0; i<componentCount; i++)
-                        bytes[i] = _data[tagValueOffset + i];
+                    for (int i = 0; i < componentCount; i++)
+                        bytes[i] = reader.getUInt8(tagValueOffset + i);
                     directory.setIntArray(tagType, bytes);
                 }
                 break;
-            case FMT_SINGLE:
-            case FMT_DOUBLE:
-                if (componentCount==1) {
-                    int i = _data[tagValueOffset];
+            case FMT_USHORT:
+                if (componentCount == 1) {
+                    int i = reader.getUInt16(tagValueOffset);
                     directory.setInt(tagType, i);
                 } else {
                     int[] ints = new int[componentCount];
-                    for (int i = 0; i<componentCount; i++)
-                        ints[i] = _data[tagValueOffset + i];
+                    for (int i = 0; i < componentCount; i++)
+                        ints[i] = reader.getUInt16(tagValueOffset + (i * 2));
                     directory.setIntArray(tagType, ints);
                 }
                 break;
-            case FMT_USHORT:
             case FMT_SSHORT:
-                if (componentCount==1) {
-                    int i = get16Bits(tagValueOffset);
+                if (componentCount == 1) {
+                    int i = reader.getInt16(tagValueOffset);
                     directory.setInt(tagType, i);
                 } else {
                     int[] ints = new int[componentCount];
-                    for (int i = 0; i<componentCount; i++)
-                        ints[i] = get16Bits(tagValueOffset + (i * 2));
+                    for (int i = 0; i < componentCount; i++)
+                        ints[i] = reader.getInt16(tagValueOffset + (i * 2));
                     directory.setIntArray(tagType, ints);
                 }
@@ -569,11 +562,12 @@
             case FMT_SLONG:
             case FMT_ULONG:
-                if (componentCount==1) {
-                    int i = get32Bits(tagValueOffset);
+                // NOTE 'long' in this case means 32 bit, not 64
+                if (componentCount == 1) {
+                    int i = reader.getInt32(tagValueOffset);
                     directory.setInt(tagType, i);
                 } else {
                     int[] ints = new int[componentCount];
-                    for (int i = 0; i<componentCount; i++)
-                        ints[i] = get32Bits(tagValueOffset + (i * 4));
+                    for (int i = 0; i < componentCount; i++)
+                        ints[i] = reader.getInt32(tagValueOffset + (i * 4));
                     directory.setIntArray(tagType, ints);
                 }
@@ -584,39 +578,9 @@
     }
 
-    private int calculateTagValueOffset(int byteCount, int dirEntryOffset, int tiffHeaderOffset)
-    {
-        if (byteCount>4) {
-            // If its bigger than 4 bytes, the dir entry contains an offset.
-            // dirEntryOffset must be passed, as some makernote implementations (e.g. FujiFilm) incorrectly use an
-            // offset relative to the start of the makernote itself, not the TIFF segment.
-            final int offsetVal = get32Bits(dirEntryOffset + 8);
-            if (offsetVal + byteCount>_data.length) {
-                // Bogus pointer offset and / or bytecount value
-                return -1; // signal error
-            }
-            return tiffHeaderOffset + offsetVal;
-        } else {
-            // 4 bytes or less and value is in the dir entry itself
-            return dirEntryOffset + 8;
-        }
-    }
-
-    /**
-     * Creates a String from the _data buffer starting at the specified offset,
-     * and ending where byte=='\0' or where length==maxLength.
-     */
-    private String readString(int offset, int maxLength)
-    {
-        int length = 0;
-        while ((offset + length)<_data.length && _data[offset + length]!='\0' && length<maxLength)
-            length++;
-
-        return new String(_data, offset, length);
-    }
-
     /**
      * Determine the offset at which a given InteropArray entry begins within the specified IFD.
+     *
      * @param dirStartOffset the offset at which the IFD starts
-     * @param entryNumber the zero-based entry number
+     * @param entryNumber    the zero-based entry number
      */
     private int calculateTagOffset(int dirStartOffset, int entryNumber)
@@ -626,43 +590,3 @@
         return dirStartOffset + 2 + (12 * entryNumber);
     }
-
-    /**
-     * Get a 16 bit value from file's native byte order.  Between 0x0000 and 0xFFFF.
-     */
-    private int get16Bits(int offset)
-    {
-        if (offset<0 || offset+2>_data.length)
-            throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index " + offset + " where max index is " + (_data.length - 1) + ")");
-
-        if (_isMotorollaByteOrder) {
-            // Motorola - MSB first
-            return (_data[offset] << 8 & 0xFF00) | (_data[offset + 1] & 0xFF);
-        } else {
-            // Intel ordering - LSB first
-            return (_data[offset + 1] << 8 & 0xFF00) | (_data[offset] & 0xFF);
-        }
-    }
-
-    /**
-     * Get a 32 bit value from file's native byte order.
-     */
-    private int get32Bits(int offset)
-    {
-        if (offset<0 || offset+4>_data.length)
-            throw new ArrayIndexOutOfBoundsException("attempt to read data outside of exif segment (index " + offset + " where max index is " + (_data.length - 1) + ")");
-
-        if (_isMotorollaByteOrder) {
-            // Motorola - MSB first
-            return (_data[offset] << 24 & 0xFF000000) |
-                    (_data[offset + 1] << 16 & 0xFF0000) |
-                    (_data[offset + 2] << 8 & 0xFF00) |
-                    (_data[offset + 3] & 0xFF);
-        } else {
-            // Intel ordering - LSB first
-            return (_data[offset + 3] << 24 & 0xFF000000) |
-                    (_data[offset + 2] << 16 & 0xFF0000) |
-                    (_data[offset + 1] << 8 & 0xFF00) |
-                    (_data[offset] & 0xFF);
-        }
-    }
 }
Index: trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/ExifSubIFDDescriptor.java	(revision 6127)
@@ -0,0 +1,931 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+package com.drew.metadata.exif;
+
+import com.drew.imaging.PhotographicConversions;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+import java.io.UnsupportedEncodingException;
+import java.text.DecimalFormat;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Provides human-readable string representations of tag values stored in a <code>ExifSubIFDDirectory</code>.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ExifSubIFDDescriptor extends TagDescriptor<ExifSubIFDDirectory>
+{
+    /**
+     * Dictates whether rational values will be represented in decimal format in instances
+     * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3).
+     */
+    private final boolean _allowDecimalRepresentationOfRationals = true;
+
+    @NotNull
+    private static final java.text.DecimalFormat SimpleDecimalFormatter = new DecimalFormat("0.#");
+
+    public ExifSubIFDDescriptor(@NotNull ExifSubIFDDirectory directory)
+    {
+        super(directory);
+    }
+
+    // Note for the potential addition of brightness presentation in eV:
+    // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv),
+    // you must add SensitivityValue(Sv).
+    // Ev=BV+Sv   Sv=log2(ISOSpeedRating/3.125)
+    // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32.
+
+    /**
+     * Returns a descriptive value of the the specified tag for this image.
+     * Where possible, known values will be substituted here in place of the raw
+     * tokens actually kept in the Exif segment.  If no substitution is
+     * available, the value provided by getString(int) will be returned.
+     * @param tagType the tag to find a description for
+     * @return a description of the image's value for the specified tag, or
+     *         <code>null</code> if the tag hasn't been defined.
+     */
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE:
+                return getNewSubfileTypeDescription();
+            case ExifSubIFDDirectory.TAG_SUBFILE_TYPE:
+                return getSubfileTypeDescription();
+            case ExifSubIFDDirectory.TAG_THRESHOLDING:
+                return getThresholdingDescription();
+            case ExifSubIFDDirectory.TAG_FILL_ORDER:
+                return getFillOrderDescription();
+            case ExifSubIFDDirectory.TAG_EXPOSURE_TIME:
+                return getExposureTimeDescription();
+            case ExifSubIFDDirectory.TAG_SHUTTER_SPEED:
+                return getShutterSpeedDescription();
+            case ExifSubIFDDirectory.TAG_FNUMBER:
+                return getFNumberDescription();
+            case ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL:
+                return getCompressedAverageBitsPerPixelDescription();
+            case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE:
+                return getSubjectDistanceDescription();
+            case ExifSubIFDDirectory.TAG_METERING_MODE:
+                return getMeteringModeDescription();
+            case ExifSubIFDDirectory.TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case ExifSubIFDDirectory.TAG_FLASH:
+                return getFlashDescription();
+            case ExifSubIFDDirectory.TAG_FOCAL_LENGTH:
+                return getFocalLengthDescription();
+            case ExifSubIFDDirectory.TAG_COLOR_SPACE:
+                return getColorSpaceDescription();
+            case ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH:
+                return getExifImageWidthDescription();
+            case ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT:
+                return getExifImageHeightDescription();
+            case ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT:
+                return getFocalPlaneResolutionUnitDescription();
+            case ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES:
+                return getFocalPlaneXResolutionDescription();
+            case ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES:
+                return getFocalPlaneYResolutionDescription();
+            case ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE:
+                return getBitsPerSampleDescription();
+            case ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION:
+                return getPhotometricInterpretationDescription();
+            case ExifSubIFDDirectory.TAG_ROWS_PER_STRIP:
+                return getRowsPerStripDescription();
+            case ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS:
+                return getStripByteCountsDescription();
+            case ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL:
+                return getSamplesPerPixelDescription();
+            case ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION:
+                return getPlanarConfigurationDescription();
+            case ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING:
+                return getYCbCrSubsamplingDescription();
+            case ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM:
+                return getExposureProgramDescription();
+            case ExifSubIFDDirectory.TAG_APERTURE:
+                return getApertureValueDescription();
+            case ExifSubIFDDirectory.TAG_MAX_APERTURE:
+                return getMaxApertureValueDescription();
+            case ExifSubIFDDirectory.TAG_SENSING_METHOD:
+                return getSensingMethodDescription();
+            case ExifSubIFDDirectory.TAG_EXPOSURE_BIAS:
+                return getExposureBiasDescription();
+            case ExifSubIFDDirectory.TAG_FILE_SOURCE:
+                return getFileSourceDescription();
+            case ExifSubIFDDirectory.TAG_SCENE_TYPE:
+                return getSceneTypeDescription();
+            case ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION:
+                return getComponentConfigurationDescription();
+            case ExifSubIFDDirectory.TAG_EXIF_VERSION:
+                return getExifVersionDescription();
+            case ExifSubIFDDirectory.TAG_FLASHPIX_VERSION:
+                return getFlashPixVersionDescription();
+            case ExifSubIFDDirectory.TAG_ISO_EQUIVALENT:
+                return getIsoEquivalentDescription();
+            case ExifSubIFDDirectory.TAG_USER_COMMENT:
+                return getUserCommentDescription();
+            case ExifSubIFDDirectory.TAG_CUSTOM_RENDERED:
+                return getCustomRenderedDescription();
+            case ExifSubIFDDirectory.TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE:
+                return getWhiteBalanceModeDescription();
+            case ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO:
+                return getDigitalZoomRatioDescription();
+            case ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH:
+                return get35mmFilmEquivFocalLengthDescription();
+            case ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE:
+                return getSceneCaptureTypeDescription();
+            case ExifSubIFDDirectory.TAG_GAIN_CONTROL:
+                return getGainControlDescription();
+            case ExifSubIFDDirectory.TAG_CONTRAST:
+                return getContrastDescription();
+            case ExifSubIFDDirectory.TAG_SATURATION:
+                return getSaturationDescription();
+            case ExifSubIFDDirectory.TAG_SHARPNESS:
+                return getSharpnessDescription();
+            case ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE:
+                return getSubjectDistanceRangeDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getNewSubfileTypeDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_NEW_SUBFILE_TYPE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Full-resolution image";
+            case 2: return "Reduced-resolution image";
+            case 3: return "Single page of multi-page reduced-resolution image";
+            case 4: return "Transparency mask";
+            case 5: return "Transparency mask of reduced-resolution image";
+            case 6: return "Transparency mask of multi-page image";
+            case 7: return "Transparency mask of reduced-resolution multi-page image";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSubfileTypeDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SUBFILE_TYPE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Full-resolution image";
+            case 2: return "Reduced-resolution image";
+            case 3: return "Single page of multi-page image";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getThresholdingDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_THRESHOLDING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "No dithering or halftoning";
+            case 2: return "Ordered dither or halftone";
+            case 3: return "Randomized dither";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFillOrderDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FILL_ORDER);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Normal";
+            case 2: return "Reversed";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSubjectDistanceRangeDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE_RANGE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Macro";
+            case 2: return "Close view";
+            case 3: return "Distant view";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SHARPNESS);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "None";
+            case 1: return "Low";
+            case 2: return "Hard";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SATURATION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "None";
+            case 1: return "Low saturation";
+            case 2: return "High saturation";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_CONTRAST);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "None";
+            case 1: return "Soft";
+            case 2: return "Hard";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getGainControlDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_GAIN_CONTROL);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "None";
+            case 1: return "Low gain up";
+            case 2: return "Low gain down";
+            case 3: return "High gain up";
+            case 4: return "High gain down";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSceneCaptureTypeDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_CAPTURE_TYPE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Standard";
+            case 1: return "Landscape";
+            case 2: return "Portrait";
+            case 3: return "Night scene";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String get35mmFilmEquivFocalLengthDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_35MM_FILM_EQUIV_FOCAL_LENGTH);
+        if (value==null)
+            return null;
+        if (value==0)
+            return "Unknown";
+        else
+            return SimpleDecimalFormatter.format(value) + "mm";
+    }
+
+    @Nullable
+    public String getDigitalZoomRatioDescription()
+    {
+        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_DIGITAL_ZOOM_RATIO);
+        if (value==null)
+            return null;
+        if (value.getNumerator()==0)
+            return "Digital zoom not used.";
+        return SimpleDecimalFormatter.format(value.doubleValue());
+    }
+
+    @Nullable
+    public String getWhiteBalanceModeDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_WHITE_BALANCE_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Auto white balance";
+            case 1: return "Manual white balance";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXPOSURE_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Auto exposure";
+            case 1: return "Manual exposure";
+            case 2: return "Auto bracket";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getCustomRenderedDescription()
+    {
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_CUSTOM_RENDERED);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Normal process";
+            case 1: return "Custom process";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getUserCommentDescription()
+    {
+        byte[] commentBytes = _directory.getByteArray(ExifSubIFDDirectory.TAG_USER_COMMENT);
+        if (commentBytes==null)
+            return null;
+        if (commentBytes.length == 0)
+            return "";
+
+        final Map<String, String> encodingMap = new HashMap<String, String>();
+        encodingMap.put("ASCII",    System.getProperty("file.encoding")); // Someone suggested "ISO-8859-1".
+        encodingMap.put("UNICODE",  "UTF-16LE");
+        encodingMap.put("JIS",      "Shift-JIS"); // We assume this charset for now.  Another suggestion is "JIS".
+
+        try {
+            if (commentBytes.length >= 10) {
+                String firstTenBytesString = new String(commentBytes, 0, 10);
+
+                // try each encoding name
+                for (Map.Entry<String, String> pair : encodingMap.entrySet()) {
+                    String encodingName = pair.getKey();
+                    String charset = pair.getValue();
+                    if (firstTenBytesString.startsWith(encodingName)) {
+                        // skip any null or blank characters commonly present after the encoding name, up to a limit of 10 from the start
+                        for (int j = encodingName.length(); j < 10; j++) {
+                            byte b = commentBytes[j];
+                            if (b != '\0' && b != ' ')
+                                return new String(commentBytes, j, commentBytes.length - j, charset).trim();
+                        }
+                        return new String(commentBytes, 10, commentBytes.length - 10, charset).trim();
+                    }
+                }
+            }
+            // special handling fell through, return a plain string representation
+            return new String(commentBytes, System.getProperty("file.encoding")).trim();
+        } catch (UnsupportedEncodingException ex) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public String getIsoEquivalentDescription()
+    {
+        // Have seen an exception here from files produced by ACDSEE that stored an int[] here with two values
+        Integer isoEquiv = _directory.getInteger(ExifSubIFDDirectory.TAG_ISO_EQUIVALENT);
+        if (isoEquiv==null)
+            return null;
+        // There used to be a check here that multiplied ISO values < 50 by 200.
+        // Issue 36 shows a smart-phone image from a Samsung Galaxy S2 with ISO-40.
+        return Integer.toString(isoEquiv);
+    }
+
+    @Nullable
+    public String getExifVersionDescription()
+    {
+        int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_EXIF_VERSION);
+        if (ints==null)
+            return null;
+        return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2);
+    }
+
+    @Nullable
+    public String getFlashPixVersionDescription()
+    {
+        int[] ints = _directory.getIntArray(ExifSubIFDDirectory.TAG_FLASHPIX_VERSION);
+        if (ints==null)
+            return null;
+        return ExifSubIFDDescriptor.convertBytesToVersionString(ints, 2);
+    }
+
+    @Nullable
+    public String getSceneTypeDescription()
+    {
+        Integer sceneType = _directory.getInteger(ExifSubIFDDirectory.TAG_SCENE_TYPE);
+        if (sceneType==null)
+            return null;
+        return sceneType == 1
+                ? "Directly photographed image"
+                : "Unknown (" + sceneType + ")";
+    }
+
+    @Nullable
+    public String getFileSourceDescription()
+    {
+        Integer fileSource = _directory.getInteger(ExifSubIFDDirectory.TAG_FILE_SOURCE);
+        if (fileSource==null)
+            return null;
+        return fileSource == 3
+                ? "Digital Still Camera (DSC)"
+                : "Unknown (" + fileSource + ")";
+    }
+
+    @Nullable
+    public String getExposureBiasDescription()
+    {
+        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_EXPOSURE_BIAS);
+        if (value==null)
+            return null;
+        return value.toSimpleString(true) + " EV";
+    }
+
+    @Nullable
+    public String getMaxApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(ExifSubIFDDirectory.TAG_MAX_APERTURE);
+        if (aperture==null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return "F" + SimpleDecimalFormatter.format(fStop);
+    }
+
+    @Nullable
+    public String getApertureValueDescription()
+    {
+        Double aperture = _directory.getDoubleObject(ExifSubIFDDirectory.TAG_APERTURE);
+        if (aperture==null)
+            return null;
+        double fStop = PhotographicConversions.apertureToFStop(aperture);
+        return "F" + SimpleDecimalFormatter.format(fStop);
+    }
+
+    @Nullable
+    public String getExposureProgramDescription()
+    {
+        // '1' means manual control, '2' program normal, '3' aperture priority,
+        // '4' shutter priority, '5' program creative (slow program),
+        // '6' program action(high-speed program), '7' portrait mode, '8' landscape mode.
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXPOSURE_PROGRAM);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Manual control";
+            case 2: return "Program normal";
+            case 3: return "Aperture priority";
+            case 4: return "Shutter priority";
+            case 5: return "Program creative (slow program)";
+            case 6: return "Program action (high-speed program)";
+            case 7: return "Portrait mode";
+            case 8: return "Landscape mode";
+            default:
+                return "Unknown program (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getYCbCrSubsamplingDescription()
+    {
+        int[] positions = _directory.getIntArray(ExifSubIFDDirectory.TAG_YCBCR_SUBSAMPLING);
+        if (positions==null)
+            return null;
+        if (positions[0] == 2 && positions[1] == 1) {
+            return "YCbCr4:2:2";
+        } else if (positions[0] == 2 && positions[1] == 2) {
+            return "YCbCr4:2:0";
+        } else {
+            return "(Unknown)";
+        }
+    }
+
+    @Nullable
+    public String getPlanarConfigurationDescription()
+    {
+        // When image format is no compression YCbCr, this value shows byte aligns of YCbCr
+        // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling
+        // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr
+        // plane format.
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_PLANAR_CONFIGURATION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Chunky (contiguous for each subsampling pixel)";
+            case 2: return "Separate (Y-plane/Cb-plane/Cr-plane format)";
+            default:
+                return "Unknown configuration";
+        }
+    }
+
+    @Nullable
+    public String getSamplesPerPixelDescription()
+    {
+        String value = _directory.getString(ExifSubIFDDirectory.TAG_SAMPLES_PER_PIXEL);
+        return value==null ? null : value + " samples/pixel";
+    }
+
+    @Nullable
+    public String getRowsPerStripDescription()
+    {
+        final String value = _directory.getString(ExifSubIFDDirectory.TAG_ROWS_PER_STRIP);
+        return value==null ? null : value + " rows/strip";
+    }
+
+    @Nullable
+    public String getStripByteCountsDescription()
+    {
+        final String value = _directory.getString(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
+        return value==null ? null : value + " bytes";
+    }
+
+    @Nullable
+    public String getPhotometricInterpretationDescription()
+    {
+        // Shows the color space of the image data components
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "WhiteIsZero";
+            case 1: return "BlackIsZero";
+            case 2: return "RGB";
+            case 3: return "RGB Palette";
+            case 4: return "Transparency Mask";
+            case 5: return "CMYK";
+            case 6: return "YCbCr";
+            case 8: return "CIELab";
+            case 9: return "ICCLab";
+            case 10: return "ITULab";
+            case 32803: return "Color Filter Array";
+            case 32844: return "Pixar LogL";
+            case 32845: return "Pixar LogLuv";
+            case 32892: return "Linear Raw";
+            default:
+                return "Unknown colour space";
+        }
+    }
+
+    @Nullable
+    public String getBitsPerSampleDescription()
+    {
+        String value = _directory.getString(ExifSubIFDDirectory.TAG_BITS_PER_SAMPLE);
+        return value==null ? null : value + " bits/component/pixel";
+    }
+
+    @Nullable
+    public String getFocalPlaneXResolutionDescription()
+    {
+        Rational rational = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_PLANE_X_RES);
+        if (rational==null)
+            return null;
+        final String unit = getFocalPlaneResolutionUnitDescription();
+        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
+            + (unit==null ? "" : " " + unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getFocalPlaneYResolutionDescription()
+    {
+        Rational rational = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_PLANE_Y_RES);
+        if (rational==null)
+            return null;
+        final String unit = getFocalPlaneResolutionUnitDescription();
+        return rational.getReciprocal().toSimpleString(_allowDecimalRepresentationOfRationals)
+            + (unit==null ? "" : " " + unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getFocalPlaneResolutionUnitDescription()
+    {
+        // Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit,
+        // '2' inch, '3' centimeter.
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FOCAL_PLANE_UNIT);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "(No unit)";
+            case 2: return "Inches";
+            case 3: return "cm";
+            default:
+                return "";
+        }
+    }
+
+    @Nullable
+    public String getExifImageWidthDescription()
+    {
+        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH);
+        if (value==null)
+            return null;
+        return value + " pixels";
+    }
+
+    @Nullable
+    public String getExifImageHeightDescription()
+    {
+        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT);
+        if (value==null)
+            return null;
+        return value + " pixels";
+    }
+
+    @Nullable
+    public String getColorSpaceDescription()
+    {
+        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_COLOR_SPACE);
+        if (value==null)
+            return null;
+        if (value == 1)
+            return "sRGB";
+        if (value == 65535)
+            return "Undefined";
+        return "Unknown";
+    }
+
+    @Nullable
+    public String getFocalLengthDescription()
+    {
+        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_FOCAL_LENGTH);
+        if (value==null)
+            return null;
+        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " mm";
+    }
+
+    @Nullable
+    public String getFlashDescription()
+    {
+        /*
+         * This is a bitmask.
+         * 0 = flash fired
+         * 1 = return detected
+         * 2 = return able to be detected
+         * 3 = unknown
+         * 4 = auto used
+         * 5 = unknown
+         * 6 = red eye reduction used
+         */
+
+        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_FLASH);
+
+        if (value==null)
+            return null;
+
+        StringBuilder sb = new StringBuilder();
+
+        if ((value & 0x1)!=0)
+            sb.append("Flash fired");
+        else
+            sb.append("Flash did not fire");
+
+        // check if we're able to detect a return, before we mention it
+        if ((value & 0x4)!=0)
+        {
+            if ((value & 0x2)!=0)
+                sb.append(", return detected");
+            else
+                sb.append(", return not detected");
+        }
+
+        if ((value & 0x10)!=0)
+            sb.append(", auto");
+
+        if ((value & 0x40)!=0)
+            sb.append(", red-eye reduction");
+
+        return sb.toString();
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        // '0' means unknown, '1' daylight, '2' fluorescent, '3' tungsten, '10' flash,
+        // '17' standard light A, '18' standard light B, '19' standard light C, '20' D55,
+        // '21' D65, '22' D75, '255' other.
+        final Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_WHITE_BALANCE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Daylight";
+            case 2: return "Florescent";
+            case 3: return "Tungsten";
+            case 10: return "Flash";
+            case 17: return "Standard light";
+            case 18: return "Standard light (B)";
+            case 19: return "Standard light (C)";
+            case 20: return "D55";
+            case 21: return "D65";
+            case 22: return "D75";
+            case 255: return "(Other)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getMeteringModeDescription()
+    {
+        // '0' means unknown, '1' average, '2' center weighted average, '3' spot
+        // '4' multi-spot, '5' multi-segment, '6' partial, '255' other
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_METERING_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Unknown";
+            case 1: return "Average";
+            case 2: return "Center weighted average";
+            case 3: return "Spot";
+            case 4: return "Multi-spot";
+            case 5: return "Multi-segment";
+            case 6: return "Partial";
+            case 255: return "(Other)";
+            default:
+                return "";
+        }
+    }
+
+    @Nullable
+    public String getSubjectDistanceDescription()
+    {
+        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_SUBJECT_DISTANCE);
+        if (value==null)
+            return null;
+        java.text.DecimalFormat formatter = new DecimalFormat("0.0##");
+        return formatter.format(value.doubleValue()) + " metres";
+    }
+
+    @Nullable
+    public String getCompressedAverageBitsPerPixelDescription()
+    {
+        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL);
+        if (value==null)
+            return null;
+        String ratio = value.toSimpleString(_allowDecimalRepresentationOfRationals);
+        if (value.isInteger() && value.intValue() == 1) {
+            return ratio + " bit/pixel";
+        } else {
+            return ratio + " bits/pixel";
+        }
+    }
+
+    @Nullable
+    public String getExposureTimeDescription()
+    {
+        String value = _directory.getString(ExifSubIFDDirectory.TAG_EXPOSURE_TIME);
+        return value==null ? null : value + " sec";
+    }
+
+    @Nullable
+    public String getShutterSpeedDescription()
+    {
+        // I believe this method to now be stable, but am leaving some alternative snippets of
+        // code in here, to assist anyone who's looking into this (given that I don't have a public CVS).
+
+//        float apexValue = _directory.getFloat(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+//        int apexPower = (int)Math.pow(2.0, apexValue);
+//        return "1/" + apexPower + " sec";
+        // TODO test this method
+        // thanks to Mark Edwards for spotting and patching a bug in the calculation of this
+        // description (spotted bug using a Canon EOS 300D)
+        // thanks also to Gli Blr for spotting this bug
+        Float apexValue = _directory.getFloatObject(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+        if (apexValue==null)
+            return null;
+        if (apexValue<=1) {
+            float apexPower = (float)(1/(Math.exp(apexValue*Math.log(2))));
+            long apexPower10 = Math.round((double)apexPower * 10.0);
+            float fApexPower = (float) apexPower10 / 10.0f;
+            return fApexPower + " sec";
+        } else {
+            int apexPower = (int)((Math.exp(apexValue*Math.log(2))));
+            return "1/" + apexPower + " sec";
+        }
+
+/*
+        // This alternative implementation offered by Bill Richards
+        // TODO determine which is the correct / more-correct implementation
+        double apexValue = _directory.getDouble(ExifSubIFDDirectory.TAG_SHUTTER_SPEED);
+        double apexPower = Math.pow(2.0, apexValue);
+
+        StringBuffer sb = new StringBuffer();
+        if (apexPower > 1)
+            apexPower = Math.floor(apexPower);
+
+        if (apexPower < 1) {
+            sb.append((int)Math.round(1/apexPower));
+        } else {
+            sb.append("1/");
+            sb.append((int)apexPower);
+        }
+        sb.append(" sec");
+        return sb.toString();
+*/
+
+    }
+
+    @Nullable
+    public String getFNumberDescription()
+    {
+        Rational value = _directory.getRational(ExifSubIFDDirectory.TAG_FNUMBER);
+        if (value==null)
+            return null;
+        return "F" + SimpleDecimalFormatter.format(value.doubleValue());
+    }
+
+    @Nullable
+    public String getSensingMethodDescription()
+    {
+        // '1' Not defined, '2' One-chip color area sensor, '3' Two-chip color area sensor
+        // '4' Three-chip color area sensor, '5' Color sequential area sensor
+        // '7' Trilinear sensor '8' Color sequential linear sensor,  'Other' reserved
+        Integer value = _directory.getInteger(ExifSubIFDDirectory.TAG_SENSING_METHOD);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "(Not defined)";
+            case 2: return "One-chip color area sensor";
+            case 3: return "Two-chip color area sensor";
+            case 4: return "Three-chip color area sensor";
+            case 5: return "Color sequential area sensor";
+            case 7: return "Trilinear sensor";
+            case 8: return "Color sequential linear sensor";
+            default:
+                return "";
+        }
+    }
+
+    @Nullable
+    public String getComponentConfigurationDescription()
+    {
+        int[] components = _directory.getIntArray(ExifSubIFDDirectory.TAG_COMPONENTS_CONFIGURATION);
+        if (components==null)
+            return null;
+        String[] componentStrings = {"", "Y", "Cb", "Cr", "R", "G", "B"};
+        StringBuilder componentConfig = new StringBuilder();
+        for (int i = 0; i < Math.min(4, components.length); i++) {
+            int j = components[i];
+            if (j > 0 && j < componentStrings.length) {
+                componentConfig.append(componentStrings[j]);
+            }
+        }
+        return componentConfig.toString();
+    }
+}
Index: trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/ExifSubIFDDirectory.java	(revision 6127)
@@ -0,0 +1,647 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes Exif tags from the SubIFD directory.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ExifSubIFDDirectory extends Directory
+{
+    /**
+     * The actual aperture value of lens when the image was taken. Unit is APEX.
+     * To convert this value to ordinary F-number (F-stop), calculate this value's
+     * power of root 2 (=1.4142). For example, if the ApertureValue is '5',
+     * F-number is 1.4142^5 = F5.6.
+     */
+    public static final int TAG_APERTURE = 0x9202;
+    /**
+     * When image format is no compression, this value shows the number of bits
+     * per component for each pixel. Usually this value is '8,8,8'.
+     */
+    public static final int TAG_BITS_PER_SAMPLE = 0x0102;
+
+    /**
+     * Shows the color space of the image data components.
+     * 0 = WhiteIsZero
+     * 1 = BlackIsZero
+     * 2 = RGB
+     * 3 = RGB Palette
+     * 4 = Transparency Mask
+     * 5 = CMYK
+     * 6 = YCbCr
+     * 8 = CIELab
+     * 9 = ICCLab
+     * 10 = ITULab
+     * 32803 = Color Filter Array
+     * 32844 = Pixar LogL
+     * 32845 = Pixar LogLuv
+     * 34892 = Linear Raw
+     */
+    public static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106;
+
+    /**
+     * 1 = No dithering or halftoning
+     * 2 = Ordered dither or halftone
+     * 3 = Randomized dither
+     */
+    public static final int TAG_THRESHOLDING = 0x0107;
+
+    /**
+     * 1 = Normal
+     * 2 = Reversed
+     */
+    public static final int TAG_FILL_ORDER = 0x010A;
+    public static final int TAG_DOCUMENT_NAME = 0x010D;
+
+    /** The position in the file of raster data. */
+    public static final int TAG_STRIP_OFFSETS = 0x0111;
+    /** Each pixel is composed of this many samples. */
+    public static final int TAG_SAMPLES_PER_PIXEL = 0x0115;
+    /** The raster is codified by a single block of data holding this many rows. */
+    public static final int TAG_ROWS_PER_STRIP = 0x116;
+    /** The size of the raster data in bytes. */
+    public static final int TAG_STRIP_BYTE_COUNTS = 0x0117;
+    public static final int TAG_MIN_SAMPLE_VALUE = 0x0118;
+    public static final int TAG_MAX_SAMPLE_VALUE = 0x0119;
+    /**
+     * When image format is no compression YCbCr, this value shows byte aligns of
+     * YCbCr data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for
+     * each subsampling pixel. If value is '2', Y/Cb/Cr value is separated and
+     * stored to Y plane/Cb plane/Cr plane format.
+     */
+    public static final int TAG_PLANAR_CONFIGURATION = 0x011C;
+    public static final int TAG_YCBCR_SUBSAMPLING = 0x0212;
+
+    /**
+     * The new subfile type tag.
+     * 0 = Full-resolution Image
+     * 1 = Reduced-resolution image
+     * 2 = Single page of multi-page image
+     * 3 = Single page of multi-page reduced-resolution image
+     * 4 = Transparency mask
+     * 5 = Transparency mask of reduced-resolution image
+     * 6 = Transparency mask of multi-page image
+     * 7 = Transparency mask of reduced-resolution multi-page image
+     */
+    public static final int TAG_NEW_SUBFILE_TYPE = 0x00FE;
+    /**
+     * The old subfile type tag.
+     * 1 = Full-resolution image (Main image)
+     * 2 = Reduced-resolution image (Thumbnail)
+     * 3 = Single page of multi-page image
+     */
+    public static final int TAG_SUBFILE_TYPE = 0x00FF;
+    public static final int TAG_TRANSFER_FUNCTION = 0x012D;
+    public static final int TAG_PREDICTOR = 0x013D;
+    public static final int TAG_TILE_WIDTH = 0x0142;
+    public static final int TAG_TILE_LENGTH = 0x0143;
+    public static final int TAG_TILE_OFFSETS = 0x0144;
+    public static final int TAG_TILE_BYTE_COUNTS = 0x0145;
+    public static final int TAG_JPEG_TABLES = 0x015B;
+    public static final int TAG_CFA_REPEAT_PATTERN_DIM = 0x828D;
+    /** There are two definitions for CFA pattern, I don't know the difference... */
+    public static final int TAG_CFA_PATTERN_2 = 0x828E;
+    public static final int TAG_BATTERY_LEVEL = 0x828F;
+    public static final int TAG_IPTC_NAA = 0x83BB;
+    public static final int TAG_INTER_COLOR_PROFILE = 0x8773;
+    public static final int TAG_SPECTRAL_SENSITIVITY = 0x8824;
+    /**
+     * Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524.
+     * <p/>
+     * OECF is the relationship between the camera optical input and the image values.
+     * <p/>
+     * The values are:
+     * <ul>
+     *   <li>Two shorts, indicating respectively number of columns, and number of rows.</li>
+     *   <li>For each column, the column name in a null-terminated ASCII string.</li>
+     *   <li>For each cell, an SRATIONAL value.</li>
+     * </ul>
+     */
+    public static final int TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION = 0x8828;
+    public static final int TAG_INTERLACE = 0x8829;
+    public static final int TAG_TIME_ZONE_OFFSET = 0x882A;
+    public static final int TAG_SELF_TIMER_MODE = 0x882B;
+    public static final int TAG_FLASH_ENERGY = 0x920B;
+    public static final int TAG_SPATIAL_FREQ_RESPONSE = 0x920C;
+    public static final int TAG_NOISE = 0x920D;
+    public static final int TAG_IMAGE_NUMBER = 0x9211;
+    public static final int TAG_SECURITY_CLASSIFICATION = 0x9212;
+    public static final int TAG_IMAGE_HISTORY = 0x9213;
+    public static final int TAG_SUBJECT_LOCATION = 0x9214;
+    /** There are two definitions for exposure index, I don't know the difference... */
+    public static final int TAG_EXPOSURE_INDEX_2 = 0x9215;
+    public static final int TAG_TIFF_EP_STANDARD_ID = 0x9216;
+    public static final int TAG_FLASH_ENERGY_2 = 0xA20B;
+    public static final int TAG_SPATIAL_FREQ_RESPONSE_2 = 0xA20C;
+    public static final int TAG_SUBJECT_LOCATION_2 = 0xA214;
+    public static final int TAG_PAGE_NAME = 0x011D;
+    /**
+     * Exposure time (reciprocal of shutter speed). Unit is second.
+     */
+    public static final int TAG_EXPOSURE_TIME = 0x829A;
+    /**
+     * The actual F-number(F-stop) of lens when the image was taken.
+     */
+    public static final int TAG_FNUMBER = 0x829D;
+    /**
+     * Exposure program that the camera used when image was taken. '1' means
+     * manual control, '2' program normal, '3' aperture priority, '4' shutter
+     * priority, '5' program creative (slow program), '6' program action
+     * (high-speed program), '7' portrait mode, '8' landscape mode.
+     */
+    public static final int TAG_EXPOSURE_PROGRAM = 0x8822;
+    public static final int TAG_ISO_EQUIVALENT = 0x8827;
+    public static final int TAG_EXIF_VERSION = 0x9000;
+    public static final int TAG_DATETIME_ORIGINAL = 0x9003;
+    public static final int TAG_DATETIME_DIGITIZED = 0x9004;
+    public static final int TAG_COMPONENTS_CONFIGURATION = 0x9101;
+    /**
+     * Average (rough estimate) compression level in JPEG bits per pixel.
+     * */
+    public static final int TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL = 0x9102;
+    /**
+     * Shutter speed by APEX value. To convert this value to ordinary 'Shutter Speed';
+     * calculate this value's power of 2, then reciprocal. For example, if the
+     * ShutterSpeedValue is '4', shutter speed is 1/(24)=1/16 second.
+     */
+    public static final int TAG_SHUTTER_SPEED = 0x9201;
+    public static final int TAG_BRIGHTNESS_VALUE = 0x9203;
+    public static final int TAG_EXPOSURE_BIAS = 0x9204;
+    /**
+     * Maximum aperture value of lens. You can convert to F-number by calculating
+     * power of root 2 (same process of ApertureValue:0x9202).
+     * The actual aperture value of lens when the image was taken. To convert this
+     * value to ordinary f-number(f-stop), calculate the value's power of root 2
+     * (=1.4142). For example, if the ApertureValue is '5', f-number is 1.41425^5 = F5.6.
+     */
+    public static final int TAG_MAX_APERTURE = 0x9205;
+    /**
+     * Indicates the distance the autofocus camera is focused to.  Tends to be less accurate as distance increases.
+     */
+    public static final int TAG_SUBJECT_DISTANCE = 0x9206;
+    /**
+     * Exposure metering method. '0' means unknown, '1' average, '2' center
+     * weighted average, '3' spot, '4' multi-spot, '5' multi-segment, '6' partial,
+     * '255' other.
+     */
+    public static final int TAG_METERING_MODE = 0x9207;
+
+    public static final int TAG_LIGHT_SOURCE = 0x9208;
+    /**
+     * White balance (aka light source). '0' means unknown, '1' daylight,
+     * '2' fluorescent, '3' tungsten, '10' flash, '17' standard light A,
+     * '18' standard light B, '19' standard light C, '20' D55, '21' D65,
+     * '22' D75, '255' other.
+     */
+    public static final int TAG_WHITE_BALANCE = 0x9208;
+    /**
+     * 0x0  = 0000000 = No Flash
+     * 0x1  = 0000001 = Fired
+     * 0x5  = 0000101 = Fired, Return not detected
+     * 0x7  = 0000111 = Fired, Return detected
+     * 0x9  = 0001001 = On
+     * 0xd  = 0001101 = On, Return not detected
+     * 0xf  = 0001111 = On, Return detected
+     * 0x10 = 0010000 = Off
+     * 0x18 = 0011000 = Auto, Did not fire
+     * 0x19 = 0011001 = Auto, Fired
+     * 0x1d = 0011101 = Auto, Fired, Return not detected
+     * 0x1f = 0011111 = Auto, Fired, Return detected
+     * 0x20 = 0100000 = No flash function
+     * 0x41 = 1000001 = Fired, Red-eye reduction
+     * 0x45 = 1000101 = Fired, Red-eye reduction, Return not detected
+     * 0x47 = 1000111 = Fired, Red-eye reduction, Return detected
+     * 0x49 = 1001001 = On, Red-eye reduction
+     * 0x4d = 1001101 = On, Red-eye reduction, Return not detected
+     * 0x4f = 1001111 = On, Red-eye reduction, Return detected
+     * 0x59 = 1011001 = Auto, Fired, Red-eye reduction
+     * 0x5d = 1011101 = Auto, Fired, Red-eye reduction, Return not detected
+     * 0x5f = 1011111 = Auto, Fired, Red-eye reduction, Return detected
+     *        6543210 (positions)
+     *
+     * This is a bitmask.
+     * 0 = flash fired
+     * 1 = return detected
+     * 2 = return able to be detected
+     * 3 = unknown
+     * 4 = auto used
+     * 5 = unknown
+     * 6 = red eye reduction used
+     */
+    public static final int TAG_FLASH = 0x9209;
+    /**
+     * Focal length of lens used to take image.  Unit is millimeter.
+     * Nice digital cameras actually save the focal length as a function of how far they are zoomed in.
+     */
+    public static final int TAG_FOCAL_LENGTH = 0x920A;
+    public static final int TAG_USER_COMMENT = 0x9286;
+    public static final int TAG_SUBSECOND_TIME = 0x9290;
+    public static final int TAG_SUBSECOND_TIME_ORIGINAL = 0x9291;
+    public static final int TAG_SUBSECOND_TIME_DIGITIZED = 0x9292;
+    public static final int TAG_FLASHPIX_VERSION = 0xA000;
+    /**
+     * Defines Color Space. DCF image must use sRGB color space so value is
+     * always '1'. If the picture uses the other color space, value is
+     * '65535':Uncalibrated.
+     */
+    public static final int TAG_COLOR_SPACE = 0xA001;
+    public static final int TAG_EXIF_IMAGE_WIDTH = 0xA002;
+    public static final int TAG_EXIF_IMAGE_HEIGHT = 0xA003;
+    public static final int TAG_RELATED_SOUND_FILE = 0xA004;
+    public static final int TAG_FOCAL_PLANE_X_RES = 0xA20E;
+    public static final int TAG_FOCAL_PLANE_Y_RES = 0xA20F;
+    /**
+     * Unit of FocalPlaneXResolution/FocalPlaneYResolution. '1' means no-unit,
+     * '2' inch, '3' centimeter.
+     *
+     * Note: Some of Fujifilm's digicam(e.g.FX2700,FX2900,Finepix4700Z/40i etc)
+     * uses value '3' so it must be 'centimeter', but it seems that they use a
+     * '8.3mm?'(1/3in.?) to their ResolutionUnit. Fuji's BUG? Finepix4900Z has
+     * been changed to use value '2' but it doesn't match to actual value also.
+     */
+    public static final int TAG_FOCAL_PLANE_UNIT = 0xA210;
+    public static final int TAG_EXPOSURE_INDEX = 0xA215;
+    public static final int TAG_SENSING_METHOD = 0xA217;
+    public static final int TAG_FILE_SOURCE = 0xA300;
+    public static final int TAG_SCENE_TYPE = 0xA301;
+    public static final int TAG_CFA_PATTERN = 0xA302;
+
+    // these tags new with Exif 2.2 (?) [A401 - A4
+    /**
+     * This tag indicates the use of special processing on image data, such as rendering
+     * geared to output. When special processing is performed, the reader is expected to
+     * disable or minimize any further processing.
+     * Tag = 41985 (A401.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal process
+     *   1 = Custom process
+     *   Other = reserved
+     */
+    public static final int TAG_CUSTOM_RENDERED = 0xA401;
+
+    /**
+     * This tag indicates the exposure mode set when the image was shot. In auto-bracketing
+     * mode, the camera shoots a series of frames of the same scene at different exposure settings.
+     * Tag = 41986 (A402.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = Auto exposure
+     *   1 = Manual exposure
+     *   2 = Auto bracket
+     *   Other = reserved
+     */
+    public static final int TAG_EXPOSURE_MODE = 0xA402;
+
+    /**
+     * This tag indicates the white balance mode set when the image was shot.
+     * Tag = 41987 (A403.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = Auto white balance
+     *   1 = Manual white balance
+     *   Other = reserved
+     */
+    public static final int TAG_WHITE_BALANCE_MODE = 0xA403;
+
+    /**
+     * This tag indicates the digital zoom ratio when the image was shot. If the
+     * numerator of the recorded value is 0, this indicates that digital zoom was
+     * not used.
+     * Tag = 41988 (A404.H)
+     * Type = RATIONAL
+     * Count = 1
+     * Default = none
+     */
+    public static final int TAG_DIGITAL_ZOOM_RATIO = 0xA404;
+
+    /**
+     * This tag indicates the equivalent focal length assuming a 35mm film camera,
+     * in mm. A value of 0 means the focal length is unknown. Note that this tag
+     * differs from the FocalLength tag.
+     * Tag = 41989 (A405.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     */
+    public static final int TAG_35MM_FILM_EQUIV_FOCAL_LENGTH = 0xA405;
+
+    /**
+     * This tag indicates the type of scene that was shot. It can also be used to
+     * record the mode in which the image was shot. Note that this differs from
+     * the scene type (SceneType) tag.
+     * Tag = 41990 (A406.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Standard
+     *   1 = Landscape
+     *   2 = Portrait
+     *   3 = Night scene
+     *   Other = reserved
+     */
+    public static final int TAG_SCENE_CAPTURE_TYPE = 0xA406;
+
+    /**
+     * This tag indicates the degree of overall image gain adjustment.
+     * Tag = 41991 (A407.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = None
+     *   1 = Low gain up
+     *   2 = High gain up
+     *   3 = Low gain down
+     *   4 = High gain down
+     *   Other = reserved
+     */
+    public static final int TAG_GAIN_CONTROL = 0xA407;
+
+    /**
+     * This tag indicates the direction of contrast processing applied by the camera
+     * when the image was shot.
+     * Tag = 41992 (A408.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Soft
+     *   2 = Hard
+     *   Other = reserved
+     */
+    public static final int TAG_CONTRAST = 0xA408;
+
+    /**
+     * This tag indicates the direction of saturation processing applied by the camera
+     * when the image was shot.
+     * Tag = 41993 (A409.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Low saturation
+     *   2 = High saturation
+     *   Other = reserved
+     */
+    public static final int TAG_SATURATION = 0xA409;
+
+    /**
+     * This tag indicates the direction of sharpness processing applied by the camera
+     * when the image was shot.
+     * Tag = 41994 (A40A.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = 0
+     *   0 = Normal
+     *   1 = Soft
+     *   2 = Hard
+     *   Other = reserved
+     */
+    public static final int TAG_SHARPNESS = 0xA40A;
+
+    // TODO support this tag (I haven't seen a camera's actual implementation of this yet)
+
+    /**
+     * This tag indicates information on the picture-taking conditions of a particular
+     * camera model. The tag is used only to indicate the picture-taking conditions in
+     * the reader.
+     * Tag = 41995 (A40B.H)
+     * Type = UNDEFINED
+     * Count = Any
+     * Default = none
+     *
+     * The information is recorded in the format shown below. The data is recorded
+     * in Unicode using SHORT type for the number of display rows and columns and
+     * UNDEFINED type for the camera settings. The Unicode (UCS-2) string including
+     * Signature is NULL terminated. The specifics of the Unicode string are as given
+     * in ISO/IEC 10464-1.
+     *
+     *      Length  Type        Meaning
+     *      ------+-----------+------------------
+     *      2       SHORT       Display columns
+     *      2       SHORT       Display rows
+     *      Any     UNDEFINED   Camera setting-1
+     *      Any     UNDEFINED   Camera setting-2
+     *      :       :           :
+     *      Any     UNDEFINED   Camera setting-n
+     */
+    public static final int TAG_DEVICE_SETTING_DESCRIPTION = 0xA40B;
+
+    /**
+     * This tag indicates the distance to the subject.
+     * Tag = 41996 (A40C.H)
+     * Type = SHORT
+     * Count = 1
+     * Default = none
+     *   0 = unknown
+     *   1 = Macro
+     *   2 = Close view
+     *   3 = Distant view
+     *   Other = reserved
+     */
+    public static final int TAG_SUBJECT_DISTANCE_RANGE = 0xA40C;
+
+    /**
+     * This tag indicates an identifier assigned uniquely to each image. It is
+     * recorded as an ASCII string equivalent to hexadecimal notation and 128-bit
+     * fixed length.
+     * Tag = 42016 (A420.H)
+     * Type = ASCII
+     * Count = 33
+     * Default = none
+     */
+    public static final int TAG_IMAGE_UNIQUE_ID = 0xA420;
+
+    /** String. */
+    public static final int TAG_CAMERA_OWNER_NAME = 0xA430;
+    /** String. */
+    public static final int TAG_BODY_SERIAL_NUMBER = 0xA431;
+    /** An array of four Rational64u numbers giving focal and aperture ranges. */
+    public static final int TAG_LENS_SPECIFICATION = 0xA432;
+    /** String. */
+    public static final int TAG_LENS_MAKE = 0xA433;
+    /** String. */
+    public static final int TAG_LENS_MODEL = 0xA434;
+    /** String. */
+    public static final int TAG_LENS_SERIAL_NUMBER = 0xA435;
+    /** Rational64u. */
+    public static final int TAG_GAMMA = 0xA500;
+
+    public static final int TAG_LENS = 0xFDEA;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_FILL_ORDER, "Fill Order");
+        _tagNameMap.put(TAG_DOCUMENT_NAME, "Document Name");
+        // TODO why don't these tags have fields associated with them?
+        _tagNameMap.put(0x1000, "Related Image File Format");
+        _tagNameMap.put(0x1001, "Related Image Width");
+        _tagNameMap.put(0x1002, "Related Image Length");
+        _tagNameMap.put(0x0156, "Transfer Range");
+        _tagNameMap.put(0x0200, "JPEG Proc");
+        _tagNameMap.put(TAG_COMPRESSED_AVERAGE_BITS_PER_PIXEL, "Compressed Bits Per Pixel");
+        _tagNameMap.put(0x927C, "Maker Note");
+        _tagNameMap.put(0xA005, "Interoperability Offset");
+
+        _tagNameMap.put(TAG_NEW_SUBFILE_TYPE, "New Subfile Type");
+        _tagNameMap.put(TAG_SUBFILE_TYPE, "Subfile Type");
+        _tagNameMap.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample");
+        _tagNameMap.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation");
+        _tagNameMap.put(TAG_THRESHOLDING, "Thresholding");
+        _tagNameMap.put(TAG_STRIP_OFFSETS, "Strip Offsets");
+        _tagNameMap.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel");
+        _tagNameMap.put(TAG_ROWS_PER_STRIP, "Rows Per Strip");
+        _tagNameMap.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts");
+        _tagNameMap.put(TAG_PAGE_NAME, "Page Name");
+        _tagNameMap.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration");
+        _tagNameMap.put(TAG_TRANSFER_FUNCTION, "Transfer Function");
+        _tagNameMap.put(TAG_PREDICTOR, "Predictor");
+        _tagNameMap.put(TAG_TILE_WIDTH, "Tile Width");
+        _tagNameMap.put(TAG_TILE_LENGTH, "Tile Length");
+        _tagNameMap.put(TAG_TILE_OFFSETS, "Tile Offsets");
+        _tagNameMap.put(TAG_TILE_BYTE_COUNTS, "Tile Byte Counts");
+        _tagNameMap.put(TAG_JPEG_TABLES, "JPEG Tables");
+        _tagNameMap.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling");
+        _tagNameMap.put(TAG_CFA_REPEAT_PATTERN_DIM, "CFA Repeat Pattern Dim");
+        _tagNameMap.put(TAG_CFA_PATTERN_2, "CFA Pattern");
+        _tagNameMap.put(TAG_BATTERY_LEVEL, "Battery Level");
+        _tagNameMap.put(TAG_EXPOSURE_TIME, "Exposure Time");
+        _tagNameMap.put(TAG_FNUMBER, "F-Number");
+        _tagNameMap.put(TAG_IPTC_NAA, "IPTC/NAA");
+        _tagNameMap.put(TAG_INTER_COLOR_PROFILE, "Inter Color Profile");
+        _tagNameMap.put(TAG_EXPOSURE_PROGRAM, "Exposure Program");
+        _tagNameMap.put(TAG_SPECTRAL_SENSITIVITY, "Spectral Sensitivity");
+        _tagNameMap.put(TAG_ISO_EQUIVALENT, "ISO Speed Ratings");
+        _tagNameMap.put(TAG_OPTO_ELECTRIC_CONVERSION_FUNCTION, "Opto-electric Conversion Function (OECF)");
+        _tagNameMap.put(TAG_INTERLACE, "Interlace");
+        _tagNameMap.put(TAG_TIME_ZONE_OFFSET, "Time Zone Offset");
+        _tagNameMap.put(TAG_SELF_TIMER_MODE, "Self Timer Mode");
+        _tagNameMap.put(TAG_EXIF_VERSION, "Exif Version");
+        _tagNameMap.put(TAG_DATETIME_ORIGINAL, "Date/Time Original");
+        _tagNameMap.put(TAG_DATETIME_DIGITIZED, "Date/Time Digitized");
+        _tagNameMap.put(TAG_COMPONENTS_CONFIGURATION, "Components Configuration");
+        _tagNameMap.put(TAG_SHUTTER_SPEED, "Shutter Speed Value");
+        _tagNameMap.put(TAG_APERTURE, "Aperture Value");
+        _tagNameMap.put(TAG_BRIGHTNESS_VALUE, "Brightness Value");
+        _tagNameMap.put(TAG_EXPOSURE_BIAS, "Exposure Bias Value");
+        _tagNameMap.put(TAG_MAX_APERTURE, "Max Aperture Value");
+        _tagNameMap.put(TAG_SUBJECT_DISTANCE, "Subject Distance");
+        _tagNameMap.put(TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(TAG_LIGHT_SOURCE, "Light Source");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_FLASH, "Flash");
+        _tagNameMap.put(TAG_FOCAL_LENGTH, "Focal Length");
+        _tagNameMap.put(TAG_FLASH_ENERGY, "Flash Energy");
+        _tagNameMap.put(TAG_SPATIAL_FREQ_RESPONSE, "Spatial Frequency Response");
+        _tagNameMap.put(TAG_NOISE, "Noise");
+        _tagNameMap.put(TAG_IMAGE_NUMBER, "Image Number");
+        _tagNameMap.put(TAG_SECURITY_CLASSIFICATION, "Security Classification");
+        _tagNameMap.put(TAG_IMAGE_HISTORY, "Image History");
+        _tagNameMap.put(TAG_SUBJECT_LOCATION, "Subject Location");
+        _tagNameMap.put(TAG_EXPOSURE_INDEX, "Exposure Index");
+        _tagNameMap.put(TAG_TIFF_EP_STANDARD_ID, "TIFF/EP Standard ID");
+        _tagNameMap.put(TAG_USER_COMMENT, "User Comment");
+        _tagNameMap.put(TAG_SUBSECOND_TIME, "Sub-Sec Time");
+        _tagNameMap.put(TAG_SUBSECOND_TIME_ORIGINAL, "Sub-Sec Time Original");
+        _tagNameMap.put(TAG_SUBSECOND_TIME_DIGITIZED, "Sub-Sec Time Digitized");
+        _tagNameMap.put(TAG_FLASHPIX_VERSION, "FlashPix Version");
+        _tagNameMap.put(TAG_COLOR_SPACE, "Color Space");
+        _tagNameMap.put(TAG_EXIF_IMAGE_WIDTH, "Exif Image Width");
+        _tagNameMap.put(TAG_EXIF_IMAGE_HEIGHT, "Exif Image Height");
+        _tagNameMap.put(TAG_RELATED_SOUND_FILE, "Related Sound File");
+        // 0x920B in TIFF/EP
+        _tagNameMap.put(TAG_FLASH_ENERGY_2, "Flash Energy");
+        // 0x920C in TIFF/EP
+        _tagNameMap.put(TAG_SPATIAL_FREQ_RESPONSE_2, "Spatial Frequency Response");
+        // 0x920E in TIFF/EP
+        _tagNameMap.put(TAG_FOCAL_PLANE_X_RES, "Focal Plane X Resolution");
+        // 0x920F in TIFF/EP
+        _tagNameMap.put(TAG_FOCAL_PLANE_Y_RES, "Focal Plane Y Resolution");
+        // 0x9210 in TIFF/EP
+        _tagNameMap.put(TAG_FOCAL_PLANE_UNIT, "Focal Plane Resolution Unit");
+        // 0x9214 in TIFF/EP
+        _tagNameMap.put(TAG_SUBJECT_LOCATION_2, "Subject Location");
+        // 0x9215 in TIFF/EP
+        _tagNameMap.put(TAG_EXPOSURE_INDEX_2, "Exposure Index");
+        // 0x9217 in TIFF/EP
+        _tagNameMap.put(TAG_SENSING_METHOD, "Sensing Method");
+        _tagNameMap.put(TAG_FILE_SOURCE, "File Source");
+        _tagNameMap.put(TAG_SCENE_TYPE, "Scene Type");
+        _tagNameMap.put(TAG_CFA_PATTERN, "CFA Pattern");
+
+        _tagNameMap.put(TAG_CUSTOM_RENDERED, "Custom Rendered");
+        _tagNameMap.put(TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(TAG_WHITE_BALANCE_MODE, "White Balance Mode");
+        _tagNameMap.put(TAG_DIGITAL_ZOOM_RATIO, "Digital Zoom Ratio");
+        _tagNameMap.put(TAG_35MM_FILM_EQUIV_FOCAL_LENGTH, "Focal Length 35");
+        _tagNameMap.put(TAG_SCENE_CAPTURE_TYPE, "Scene Capture Type");
+        _tagNameMap.put(TAG_GAIN_CONTROL, "Gain Control");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_DEVICE_SETTING_DESCRIPTION, "Device Setting Description");
+        _tagNameMap.put(TAG_SUBJECT_DISTANCE_RANGE, "Subject Distance Range");
+        _tagNameMap.put(TAG_IMAGE_UNIQUE_ID, "Unique Image ID");
+        
+        _tagNameMap.put(TAG_CAMERA_OWNER_NAME, "Camera Owner Name");
+        _tagNameMap.put(TAG_BODY_SERIAL_NUMBER, "Body Serial Number");
+        _tagNameMap.put(TAG_LENS_SPECIFICATION, "Lens Specification");
+        _tagNameMap.put(TAG_LENS_MAKE, "Lens Make");
+        _tagNameMap.put(TAG_LENS_MODEL, "Lens Model");
+        _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
+        _tagNameMap.put(TAG_GAMMA, "Gamma");
+
+        _tagNameMap.put(TAG_MIN_SAMPLE_VALUE, "Minimum sample value");
+        _tagNameMap.put(TAG_MAX_SAMPLE_VALUE, "Maximum sample value");
+
+        _tagNameMap.put(TAG_LENS, "Lens");
+    }
+
+    public ExifSubIFDDirectory()
+    {
+        this.setDescriptor(new ExifSubIFDDescriptor(this));
+    }
+
+    @NotNull
+    public String getName()
+    {
+        return "Exif SubIFD";
+    }
+
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/ExifThumbnailDescriptor.java	(revision 6127)
@@ -0,0 +1,351 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a <code>ExifThumbnailDirectory</code>.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ExifThumbnailDescriptor extends TagDescriptor<ExifThumbnailDirectory>
+{
+    /**
+     * Dictates whether rational values will be represented in decimal format in instances
+     * where decimal notation is elegant (such as 1/2 -> 0.5, but not 1/3).
+     */
+    private final boolean _allowDecimalRepresentationOfRationals = true;
+
+    public ExifThumbnailDescriptor(@NotNull ExifThumbnailDirectory directory)
+    {
+        super(directory);
+    }
+
+    // Note for the potential addition of brightness presentation in eV:
+    // Brightness of taken subject. To calculate Exposure(Ev) from BrightnessValue(Bv),
+    // you must add SensitivityValue(Sv).
+    // Ev=BV+Sv   Sv=log2(ISOSpeedRating/3.125)
+    // ISO100:Sv=5, ISO200:Sv=6, ISO400:Sv=7, ISO125:Sv=5.32.
+
+    /**
+     * Returns a descriptive value of the the specified tag for this image.
+     * Where possible, known values will be substituted here in place of the raw
+     * tokens actually kept in the Exif segment.  If no substitution is
+     * available, the value provided by getString(int) will be returned.
+     * @param tagType the tag to find a description for
+     * @return a description of the image's value for the specified tag, or
+     *         <code>null</code> if the tag hasn't been defined.
+     */
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case ExifThumbnailDirectory.TAG_ORIENTATION:
+                return getOrientationDescription();
+            case ExifThumbnailDirectory.TAG_RESOLUTION_UNIT:
+                return getResolutionDescription();
+            case ExifThumbnailDirectory.TAG_YCBCR_POSITIONING:
+                return getYCbCrPositioningDescription();
+            case ExifThumbnailDirectory.TAG_X_RESOLUTION:
+                return getXResolutionDescription();
+            case ExifThumbnailDirectory.TAG_Y_RESOLUTION:
+                return getYResolutionDescription();
+            case ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET:
+                return getThumbnailOffsetDescription();
+            case ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH:
+                return getThumbnailLengthDescription();
+            case ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_WIDTH:
+                return getThumbnailImageWidthDescription();
+            case ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT:
+                return getThumbnailImageHeightDescription();
+            case ExifThumbnailDirectory.TAG_BITS_PER_SAMPLE:
+                return getBitsPerSampleDescription();
+            case ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION:
+                return getCompressionDescription();
+            case ExifThumbnailDirectory.TAG_PHOTOMETRIC_INTERPRETATION:
+                return getPhotometricInterpretationDescription();
+            case ExifThumbnailDirectory.TAG_ROWS_PER_STRIP:
+                return getRowsPerStripDescription();
+            case ExifThumbnailDirectory.TAG_STRIP_BYTE_COUNTS:
+                return getStripByteCountsDescription();
+            case ExifThumbnailDirectory.TAG_SAMPLES_PER_PIXEL:
+                return getSamplesPerPixelDescription();
+            case ExifThumbnailDirectory.TAG_PLANAR_CONFIGURATION:
+                return getPlanarConfigurationDescription();
+            case ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING:
+                return getYCbCrSubsamplingDescription();
+            case ExifThumbnailDirectory.TAG_REFERENCE_BLACK_WHITE:
+                return getReferenceBlackWhiteDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getReferenceBlackWhiteDescription()
+    {
+        int[] ints = _directory.getIntArray(ExifThumbnailDirectory.TAG_REFERENCE_BLACK_WHITE);
+        if (ints==null)
+            return null;
+        int blackR = ints[0];
+        int whiteR = ints[1];
+        int blackG = ints[2];
+        int whiteG = ints[3];
+        int blackB = ints[4];
+        int whiteB = ints[5];
+        return "[" + blackR + "," + blackG + "," + blackB + "] " +
+               "[" + whiteR + "," + whiteG + "," + whiteB + "]";
+    }
+
+    @Nullable
+    public String getYCbCrSubsamplingDescription()
+    {
+        int[] positions = _directory.getIntArray(ExifThumbnailDirectory.TAG_YCBCR_SUBSAMPLING);
+        if (positions==null || positions.length < 2)
+            return null;
+        if (positions[0] == 2 && positions[1] == 1) {
+            return "YCbCr4:2:2";
+        } else if (positions[0] == 2 && positions[1] == 2) {
+            return "YCbCr4:2:0";
+        } else {
+            return "(Unknown)";
+        }
+    }
+
+    @Nullable
+    public String getPlanarConfigurationDescription()
+    {
+        // When image format is no compression YCbCr, this value shows byte aligns of YCbCr
+        // data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for each subsampling
+        // pixel. If value is '2', Y/Cb/Cr value is separated and stored to Y plane/Cb plane/Cr
+        // plane format.
+        Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_PLANAR_CONFIGURATION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Chunky (contiguous for each subsampling pixel)";
+            case 2: return "Separate (Y-plane/Cb-plane/Cr-plane format)";
+            default:
+                return "Unknown configuration";
+        }
+    }
+
+    @Nullable
+    public String getSamplesPerPixelDescription()
+    {
+        String value = _directory.getString(ExifThumbnailDirectory.TAG_SAMPLES_PER_PIXEL);
+        return value==null ? null : value + " samples/pixel";
+    }
+
+    @Nullable
+    public String getRowsPerStripDescription()
+    {
+        final String value = _directory.getString(ExifThumbnailDirectory.TAG_ROWS_PER_STRIP);
+        return value==null ? null : value + " rows/strip";
+    }
+
+    @Nullable
+    public String getStripByteCountsDescription()
+    {
+        final String value = _directory.getString(ExifThumbnailDirectory.TAG_STRIP_BYTE_COUNTS);
+        return value==null ? null : value + " bytes";
+    }
+
+    @Nullable
+    public String getPhotometricInterpretationDescription()
+    {
+        // Shows the color space of the image data components
+        Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "WhiteIsZero";
+            case 1: return "BlackIsZero";
+            case 2: return "RGB";
+            case 3: return "RGB Palette";
+            case 4: return "Transparency Mask";
+            case 5: return "CMYK";
+            case 6: return "YCbCr";
+            case 8: return "CIELab";
+            case 9: return "ICCLab";
+            case 10: return "ITULab";
+            case 32803: return "Color Filter Array";
+            case 32844: return "Pixar LogL";
+            case 32845: return "Pixar LogLuv";
+            case 32892: return "Linear Raw";
+            default:
+                return "Unknown colour space";
+        }
+    }
+
+    @Nullable
+    public String getCompressionDescription()
+    {
+        Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_THUMBNAIL_COMPRESSION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Uncompressed";
+            case 2: return "CCITT 1D";
+            case 3: return "T4/Group 3 Fax";
+            case 4: return "T6/Group 4 Fax";
+            case 5: return "LZW";
+            case 6: return "JPEG (old-style)";
+            case 7: return "JPEG";
+            case 8: return "Adobe Deflate";
+            case 9: return "JBIG B&W";
+            case 10: return "JBIG Color";
+            case 32766: return "Next";
+            case 32771: return "CCIRLEW";
+            case 32773: return "PackBits";
+            case 32809: return "Thunderscan";
+            case 32895: return "IT8CTPAD";
+            case 32896: return "IT8LW";
+            case 32897: return "IT8MP";
+            case 32898: return "IT8BL";
+            case 32908: return "PixarFilm";
+            case 32909: return "PixarLog";
+            case 32946: return "Deflate";
+            case 32947: return "DCS";
+            case 32661: return "JBIG";
+            case 32676: return "SGILog";
+            case 32677: return "SGILog24";
+            case 32712: return "JPEG 2000";
+            case 32713: return "Nikon NEF Compressed";
+            default:
+                return "Unknown compression";
+        }
+    }
+
+    @Nullable
+    public String getBitsPerSampleDescription()
+    {
+        String value = _directory.getString(ExifThumbnailDirectory.TAG_BITS_PER_SAMPLE);
+        return value==null ? null : value + " bits/component/pixel";
+    }
+
+    @Nullable
+    public String getThumbnailImageWidthDescription()
+    {
+        String value = _directory.getString(ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
+        return value==null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getThumbnailImageHeightDescription()
+    {
+        String value = _directory.getString(ExifThumbnailDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
+        return value==null ? null : value + " pixels";
+    }
+
+    @Nullable
+    public String getThumbnailLengthDescription()
+    {
+        String value = _directory.getString(ExifThumbnailDirectory.TAG_THUMBNAIL_LENGTH);
+        return value==null ? null : value + " bytes";
+    }
+
+    @Nullable
+    public String getThumbnailOffsetDescription()
+    {
+        String value = _directory.getString(ExifThumbnailDirectory.TAG_THUMBNAIL_OFFSET);
+        return value==null ? null : value + " bytes";
+    }
+
+    @Nullable
+    public String getYResolutionDescription()
+    {
+        Rational value = _directory.getRational(ExifThumbnailDirectory.TAG_Y_RESOLUTION);
+        if (value==null)
+            return null;
+        final String unit = getResolutionDescription();
+        return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
+                " dots per " +
+                (unit==null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getXResolutionDescription()
+    {
+        Rational value = _directory.getRational(ExifThumbnailDirectory.TAG_X_RESOLUTION);
+        if (value==null)
+            return null;
+        final String unit = getResolutionDescription();
+        return value.toSimpleString(_allowDecimalRepresentationOfRationals) +
+                " dots per " +
+                (unit==null ? "unit" : unit.toLowerCase());
+    }
+
+    @Nullable
+    public String getYCbCrPositioningDescription()
+    {
+        Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_YCBCR_POSITIONING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Center of pixel array";
+            case 2: return "Datum point";
+            default:
+                return String.valueOf(value);
+        }
+    }
+
+    @Nullable
+    public String getOrientationDescription()
+    {
+        Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_ORIENTATION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Top, left side (Horizontal / normal)";
+            case 2: return "Top, right side (Mirror horizontal)";
+            case 3: return "Bottom, right side (Rotate 180)";
+            case 4: return "Bottom, left side (Mirror vertical)";
+            case 5: return "Left side, top (Mirror horizontal and rotate 270 CW)";
+            case 6: return "Right side, top (Rotate 90 CW)";
+            case 7: return "Right side, bottom (Mirror horizontal and rotate 90 CW)";
+            case 8: return "Left side, bottom (Rotate 270 CW)";
+            default:
+                return String.valueOf(value);
+        }
+    }
+
+    @Nullable
+    public String getResolutionDescription()
+    {
+        // '1' means no-unit, '2' means inch, '3' means centimeter. Default value is '2'(inch)
+        Integer value = _directory.getInteger(ExifThumbnailDirectory.TAG_RESOLUTION_UNIT);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "(No unit)";
+            case 2: return "Inch";
+            case 3: return "cm";
+            default:
+                return "";
+        }
+    }
+}
Index: trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/ExifThumbnailDirectory.java	(revision 6127)
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Directory;
+import com.drew.metadata.MetadataException;
+
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.HashMap;
+
+/**
+ * One of several Exif directories.  Otherwise known as IFD1, this directory holds information about an embedded thumbnail image.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class ExifThumbnailDirectory extends Directory
+{
+    public static final int TAG_THUMBNAIL_IMAGE_WIDTH = 0x0100;
+    public static final int TAG_THUMBNAIL_IMAGE_HEIGHT = 0x0101;
+
+    /**
+     * When image format is no compression, this value shows the number of bits
+     * per component for each pixel. Usually this value is '8,8,8'.
+     */
+    public static final int TAG_BITS_PER_SAMPLE = 0x0102;
+
+    /**
+     * Shows compression method for Thumbnail.
+     * 1 = Uncompressed
+     * 2 = CCITT 1D
+     * 3 = T4/Group 3 Fax
+     * 4 = T6/Group 4 Fax
+     * 5 = LZW
+     * 6 = JPEG (old-style)
+     * 7 = JPEG
+     * 8 = Adobe Deflate
+     * 9 = JBIG B&W
+     * 10 = JBIG Color
+     * 32766 = Next
+     * 32771 = CCIRLEW
+     * 32773 = PackBits
+     * 32809 = Thunderscan
+     * 32895 = IT8CTPAD
+     * 32896 = IT8LW
+     * 32897 = IT8MP
+     * 32898 = IT8BL
+     * 32908 = PixarFilm
+     * 32909 = PixarLog
+     * 32946 = Deflate
+     * 32947 = DCS
+     * 34661 = JBIG
+     * 34676 = SGILog
+     * 34677 = SGILog24
+     * 34712 = JPEG 2000
+     * 34713 = Nikon NEF Compressed
+     */
+    public static final int TAG_THUMBNAIL_COMPRESSION = 0x0103;
+
+    /**
+     * Shows the color space of the image data components.
+     * 0 = WhiteIsZero
+     * 1 = BlackIsZero
+     * 2 = RGB
+     * 3 = RGB Palette
+     * 4 = Transparency Mask
+     * 5 = CMYK
+     * 6 = YCbCr
+     * 8 = CIELab
+     * 9 = ICCLab
+     * 10 = ITULab
+     * 32803 = Color Filter Array
+     * 32844 = Pixar LogL
+     * 32845 = Pixar LogLuv
+     * 34892 = Linear Raw
+     */
+    public static final int TAG_PHOTOMETRIC_INTERPRETATION = 0x0106;
+
+    /** The position in the file of raster data. */
+    public static final int TAG_STRIP_OFFSETS = 0x0111;
+    public static final int TAG_ORIENTATION = 0x0112;
+    /** Each pixel is composed of this many samples. */
+    public static final int TAG_SAMPLES_PER_PIXEL = 0x0115;
+    /** The raster is codified by a single block of data holding this many rows. */
+    public static final int TAG_ROWS_PER_STRIP = 0x116;
+    /** The size of the raster data in bytes. */
+    public static final int TAG_STRIP_BYTE_COUNTS = 0x0117;
+    /**
+     * When image format is no compression YCbCr, this value shows byte aligns of
+     * YCbCr data. If value is '1', Y/Cb/Cr value is chunky format, contiguous for
+     * each subsampling pixel. If value is '2', Y/Cb/Cr value is separated and
+     * stored to Y plane/Cb plane/Cr plane format.
+     */
+    public static final int TAG_X_RESOLUTION = 0x011A;
+    public static final int TAG_Y_RESOLUTION = 0x011B;
+    public static final int TAG_PLANAR_CONFIGURATION = 0x011C;
+    public static final int TAG_RESOLUTION_UNIT = 0x0128;
+    /** The offset to thumbnail image bytes. */
+    public static final int TAG_THUMBNAIL_OFFSET = 0x0201;
+    /** The size of the thumbnail image data in bytes. */
+    public static final int TAG_THUMBNAIL_LENGTH = 0x0202;
+    public static final int TAG_YCBCR_COEFFICIENTS = 0x0211;
+    public static final int TAG_YCBCR_SUBSAMPLING = 0x0212;
+    public static final int TAG_YCBCR_POSITIONING = 0x0213;
+    public static final int TAG_REFERENCE_BLACK_WHITE = 0x0214;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_THUMBNAIL_IMAGE_WIDTH, "Thumbnail Image Width");
+        _tagNameMap.put(TAG_THUMBNAIL_IMAGE_HEIGHT, "Thumbnail Image Height");
+        _tagNameMap.put(TAG_BITS_PER_SAMPLE, "Bits Per Sample");
+        _tagNameMap.put(TAG_THUMBNAIL_COMPRESSION, "Thumbnail Compression");
+        _tagNameMap.put(TAG_PHOTOMETRIC_INTERPRETATION, "Photometric Interpretation");
+        _tagNameMap.put(TAG_STRIP_OFFSETS, "Strip Offsets");
+        _tagNameMap.put(TAG_ORIENTATION, "Orientation");
+        _tagNameMap.put(TAG_SAMPLES_PER_PIXEL, "Samples Per Pixel");
+        _tagNameMap.put(TAG_ROWS_PER_STRIP, "Rows Per Strip");
+        _tagNameMap.put(TAG_STRIP_BYTE_COUNTS, "Strip Byte Counts");
+        _tagNameMap.put(TAG_X_RESOLUTION, "X Resolution");
+        _tagNameMap.put(TAG_Y_RESOLUTION, "Y Resolution");
+        _tagNameMap.put(TAG_PLANAR_CONFIGURATION, "Planar Configuration");
+        _tagNameMap.put(TAG_RESOLUTION_UNIT, "Resolution Unit");
+        _tagNameMap.put(TAG_THUMBNAIL_OFFSET, "Thumbnail Offset");
+        _tagNameMap.put(TAG_THUMBNAIL_LENGTH, "Thumbnail Length");
+        _tagNameMap.put(TAG_YCBCR_COEFFICIENTS, "YCbCr Coefficients");
+        _tagNameMap.put(TAG_YCBCR_SUBSAMPLING, "YCbCr Sub-Sampling");
+        _tagNameMap.put(TAG_YCBCR_POSITIONING, "YCbCr Positioning");
+        _tagNameMap.put(TAG_REFERENCE_BLACK_WHITE, "Reference Black/White");
+    }
+
+    @Nullable
+    private byte[] _thumbnailData;
+
+    public ExifThumbnailDirectory()
+    {
+        this.setDescriptor(new ExifThumbnailDescriptor(this));
+    }
+
+    @NotNull
+    public String getName()
+    {
+        return "Exif Thumbnail";
+    }
+
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    public boolean hasThumbnailData()
+    {
+        return _thumbnailData != null;
+    }
+
+    @Nullable
+    public byte[] getThumbnailData()
+    {
+        return _thumbnailData;
+    }
+
+    public void setThumbnailData(@Nullable byte[] data)
+    {
+        _thumbnailData = data;
+    }
+
+    public void writeThumbnail(@NotNull String filename) throws MetadataException, IOException
+    {
+        byte[] data = _thumbnailData;
+
+        if (data==null)
+            throw new MetadataException("No thumbnail data exists.");
+
+        FileOutputStream stream = null;
+        try {
+            stream = new FileOutputStream(filename);
+            stream.write(data);
+        } finally {
+            if (stream!=null)
+                stream.close();
+        }
+    }
+
+/*
+    // This thumbnail extraction code is not complete, and is included to assist anyone who feels like looking into
+    // it.  Please share any progress with the original author, and hence the community.  Thanks.
+
+    public Image getThumbnailImage() throws MetadataException
+    {
+        if (!hasThumbnailData())
+            return null;
+
+        int compression = 0;
+        try {
+            compression = this.getInt(ExifSubIFDDirectory.TAG_COMPRESSION);
+        } catch (Throwable e) {
+            this.addError("Unable to determine thumbnail type " + e.getMessage());
+        }
+
+        final byte[] thumbnailBytes = getThumbnailData();
+
+        if (compression == ExifSubIFDDirectory.COMPRESSION_JPEG)
+        {
+            // JPEG Thumbnail
+            // operate directly on thumbnailBytes
+            return decodeBytesAsImage(thumbnailBytes);
+        }
+        else if (compression == ExifSubIFDDirectory.COMPRESSION_NONE)
+        {
+            // uncompressed thumbnail (raw RGB data)
+            if (!this.containsTag(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION))
+                return null;
+
+            try
+            {
+                // If the image is RGB format, then convert it to a bitmap
+                final int photometricInterpretation = this.getInt(ExifSubIFDDirectory.TAG_PHOTOMETRIC_INTERPRETATION);
+                if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_RGB)
+                {
+                    // RGB
+                    Image image = createImageFromRawRgb(thumbnailBytes);
+                    return image;
+                }
+                else if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_YCBCR)
+                {
+                    // YCbCr
+                    Image image = createImageFromRawYCbCr(thumbnailBytes);
+                    return image;
+                }
+                else if (photometricInterpretation == ExifSubIFDDirectory.PHOTOMETRIC_INTERPRETATION_MONOCHROME)
+                {
+                    // Monochrome
+                    return null;
+                }
+            } catch (Throwable e) {
+                this.addError("Unable to extract thumbnail: " + e.getMessage());
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Handle the YCbCr thumbnail encoding used by Ricoh RDC4200/4300, Fuji DS-7/300 and DX-5/7/9 cameras.
+     *
+     * At DX-5/7/9, YCbCrSubsampling(0x0212) has values of '2,1', PlanarConfiguration(0x011c) has a value '1'. So the
+     * data align of this image is below.
+     *
+     * Y(0,0),Y(1,0),Cb(0,0),Cr(0,0), Y(2,0),Y(3,0),Cb(2,0),Cr(3.0), Y(4,0),Y(5,0),Cb(4,0),Cr(4,0). . . .
+     *
+     * The numbers in parenthesis are pixel coordinates. DX series' YCbCrCoefficients(0x0211) has values '0.299/0.587/0.114',
+     * ReferenceBlackWhite(0x0214) has values '0,255,128,255,128,255'. Therefore to convert from Y/Cb/Cr to RGB is;
+     *
+     * B(0,0)=(Cb-128)*(2-0.114*2)+Y(0,0)
+     * R(0,0)=(Cr-128)*(2-0.299*2)+Y(0,0)
+     * G(0,0)=(Y(0,0)-0.114*B(0,0)-0.299*R(0,0))/0.587
+     *
+     * Horizontal subsampling is a value '2', so you can calculate B(1,0)/R(1,0)/G(1,0) by using the Y(1,0) and Cr(0,0)/Cb(0,0).
+     * Repeat this conversion by value of ImageWidth(0x0100) and ImageLength(0x0101).
+     *
+     * @param thumbnailBytes
+     * @return
+     * @throws com.drew.metadata.MetadataException
+     * /
+    private Image createImageFromRawYCbCr(byte[] thumbnailBytes) throws MetadataException
+    {
+        /*
+            Y  =  0.257R + 0.504G + 0.098B + 16
+            Cb = -0.148R - 0.291G + 0.439B + 128
+            Cr =  0.439R - 0.368G - 0.071B + 128
+
+            G = 1.164(Y-16) - 0.391(Cb-128) - 0.813(Cr-128)
+            R = 1.164(Y-16) + 1.596(Cr-128)
+            B = 1.164(Y-16) + 2.018(Cb-128)
+
+            R, G and B range from 0 to 255.
+            Y ranges from 16 to 235.
+            Cb and Cr range from 16 to 240.
+
+            http://www.faqs.org/faqs/graphics/colorspace-faq/
+        * /
+
+        int length = thumbnailBytes.length; // this.getInt(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
+        final int imageWidth = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
+        final int imageHeight = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
+//        final int headerLength = 54;
+//        byte[] result = new byte[length + headerLength];
+//        // Add a windows BMP header described:
+//        // http://www.onicos.com/staff/iz/formats/bmp.html
+//        result[0] = 'B';
+//        result[1] = 'M'; // File Type identifier
+//        result[3] = (byte)(result.length / 256);
+//        result[2] = (byte)result.length;
+//        result[10] = (byte)headerLength;
+//        result[14] = 40; // MS Windows BMP header
+//        result[18] = (byte)imageWidth;
+//        result[22] = (byte)imageHeight;
+//        result[26] = 1;  // 1 Plane
+//        result[28] = 24; // Colour depth
+//        result[34] = (byte)length;
+//        result[35] = (byte)(length / 256);
+
+        final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
+
+        // order is YCbCr and image is upside down, bitmaps are BGR
+////        for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3)
+//        {
+//            final int y =  thumbnailBytes[dataOffset - 2] & 0xFF;
+//            final int cb = thumbnailBytes[dataOffset - 1] & 0xFF;
+//            final int cr = thumbnailBytes[dataOffset] & 0xFF;
+//            if (y<16 || y>235 || cb<16 || cb>240 || cr<16 || cr>240)
+//                "".toString();
+//
+//            int g = (int)(1.164*(y-16) - 0.391*(cb-128) - 0.813*(cr-128));
+//            int r = (int)(1.164*(y-16) + 1.596*(cr-128));
+//            int b = (int)(1.164*(y-16) + 2.018*(cb-128));
+//
+////            result[i] = (byte)b;
+////            result[i + 1] = (byte)g;
+////            result[i + 2] = (byte)r;
+//
+//            // TODO compose the image here
+//            image.setRGB(1, 2, 3);
+//        }
+
+        return image;
+    }
+
+    /**
+     * Creates a thumbnail image in (Windows) BMP format from raw RGB data.
+     * @param thumbnailBytes
+     * @return
+     * @throws com.drew.metadata.MetadataException
+     * /
+    private Image createImageFromRawRgb(byte[] thumbnailBytes) throws MetadataException
+    {
+        final int length = thumbnailBytes.length; // this.getInt(ExifSubIFDDirectory.TAG_STRIP_BYTE_COUNTS);
+        final int imageWidth = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_WIDTH);
+        final int imageHeight = this.getInt(ExifSubIFDDirectory.TAG_THUMBNAIL_IMAGE_HEIGHT);
+//        final int headerLength = 54;
+//        final byte[] result = new byte[length + headerLength];
+//        // Add a windows BMP header described:
+//        // http://www.onicos.com/staff/iz/formats/bmp.html
+//        result[0] = 'B';
+//        result[1] = 'M'; // File Type identifier
+//        result[3] = (byte)(result.length / 256);
+//        result[2] = (byte)result.length;
+//        result[10] = (byte)headerLength;
+//        result[14] = 40; // MS Windows BMP header
+//        result[18] = (byte)imageWidth;
+//        result[22] = (byte)imageHeight;
+//        result[26] = 1;  // 1 Plane
+//        result[28] = 24; // Colour depth
+//        result[34] = (byte)length;
+//        result[35] = (byte)(length / 256);
+
+        final BufferedImage image = new BufferedImage(imageWidth, imageHeight, BufferedImage.TYPE_INT_RGB);
+
+        // order is RGB and image is upside down, bitmaps are BGR
+//        for (int i = headerLength, dataOffset = length; i<result.length; i += 3, dataOffset -= 3)
+//        {
+//            byte b = thumbnailBytes[dataOffset - 2];
+//            byte g = thumbnailBytes[dataOffset - 1];
+//            byte r = thumbnailBytes[dataOffset];
+//
+//            // TODO compose the image here
+//            image.setRGB(1, 2, 3);
+//        }
+
+        return image;
+    }
+*/
+}
Index: trunk/src/com/drew/metadata/exif/FujifilmMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/FujifilmMakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/FujifilmMakernoteDescriptor.java	(revision 6127)
@@ -1,34 +1,42 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 27-Nov-2002 10:12:05 using IntelliJ IDEA.
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
 import com.drew.lang.Rational;
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
+ * Provides human-readable string representations of tag values stored in a <code>FujifilmMakernoteDirectory</code>.
+ * <p/>
  * Fujifilm's digicam added the MakerNote tag from the Year2000's model (e.g.Finepix1400,
  * Finepix4700). It uses IFD format and start from ASCII character 'FUJIFILM', and next 4
  * bytes(value 0x000c) points the offset to first IFD entry. Example of actual data
  * structure is shown below.
- *
+ * <p/>
+ * <pre><code>
  * :0000: 46 55 4A 49 46 49 4C 4D-0C 00 00 00 0F 00 00 00 :0000: FUJIFILM........
  * :0010: 07 00 04 00 00 00 30 31-33 30 00 10 02 00 08 00 :0010: ......0130......
- *
+ * </code></pre>
+ * <p/>
  * There are two big differences to the other manufacturers.
  * - Fujifilm's Exif data uses Motorola align, but MakerNote ignores it and uses Intel
@@ -37,13 +45,16 @@
  *   of TIFF header (same as the other IFD), but Fujifilm counts it from the first byte
  *   of MakerNote itself.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class FujifilmMakernoteDescriptor extends TagDescriptor
+public class FujifilmMakernoteDescriptor extends TagDescriptor<FujifilmMakernoteDirectory>
 {
-    public FujifilmMakernoteDescriptor(Directory directory)
+    public FujifilmMakernoteDescriptor(@NotNull FujifilmMakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
@@ -52,5 +63,5 @@
             case FujifilmMakernoteDirectory.TAG_FUJIFILM_WHITE_BALANCE:
                 return getWhiteBalanceDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_COLOR:
+            case FujifilmMakernoteDirectory.TAG_FUJIFILM_COLOR_SATURATION:
                 return getColorDescription();
             case FujifilmMakernoteDirectory.TAG_FUJIFILM_TONE:
@@ -64,5 +75,5 @@
             case FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_MODE:
                 return getFocusModeDescription();
-            case FujifilmMakernoteDirectory.TAG_FUJIFILM_SLOW_SYNCHRO:
+            case FujifilmMakernoteDirectory.TAG_FUJIFILM_SLOW_SYNCH:
                 return getSlowSyncDescription();
             case FujifilmMakernoteDirectory.TAG_FUJIFILM_PICTURE_MODE:
@@ -77,68 +88,157 @@
                 return getAutoExposureWarningDescription();
             default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getAutoExposureWarningDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_AE_WARNING)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_AE_WARNING);
-        switch (value) {
-            case 0:
-                return "AE good";
-            case 1:
-                return "Over exposed (>1/1000s @ F11)";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getFocusWarningDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_WARNING)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_WARNING);
-        switch (value) {
-            case 0:
-                return "Auto focus good";
-            case 1:
-                return "Out of focus";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getBlurWarningDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_BLUR_WARNING)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_BLUR_WARNING);
-        switch (value) {
-            case 0:
-                return "No blur warning";
-            case 1:
-                return "Blur warning";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getContinuousTakingOrAutoBrackettingDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_CONTINUOUS_TAKING_OR_AUTO_BRACKETTING)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_CONTINUOUS_TAKING_OR_AUTO_BRACKETTING);
-        switch (value) {
-            case 0:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_SHARPNESS);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Softest";
+            case 2:
+                return "Soft";
+            case 3:
+                return "Normal";
+            case 4:
+                return "Hard";
+            case 5:
+                return "Hardest";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_WHITE_BALANCE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Auto";
+            case 256:
+                return "Daylight";
+            case 512:
+                return "Cloudy";
+            case 768:
+                return "DaylightColor-fluorescence";
+            case 769:
+                return "DaywhiteColor-fluorescence";
+            case 770:
+                return "White-fluorescence";
+            case 1024:
+                return "Incandescence";
+            case 3840:
+                return "Custom white balance";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColorDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_COLOR_SATURATION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Normal (STD)";
+            case 256:
+                return "High (HARD)";
+            case 512:
+                return "Low (ORG)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getToneDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_TONE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Normal (STD)";
+            case 256:
+                return "High (HARD)";
+            case 512:
+                return "Low (ORG)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_FLASH_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Auto";
+            case 1:
+                return "On";
+            case 2:
                 return "Off";
-            case 1:
-                return "On";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getPictureModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_PICTURE_MODE)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_PICTURE_MODE);
+            case 3:
+                return "Red-eye reduction";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashStrengthDescription()
+    {
+        Rational value = _directory.getRational(FujifilmMakernoteDirectory.TAG_FUJIFILM_FLASH_STRENGTH);
+        if (value==null)
+            return null;
+        return value.toSimpleString(false) + " EV (Apex)";
+    }
+
+    @Nullable
+    public String getMacroDescription()
+    {
+        return getOnOffDescription(FujifilmMakernoteDirectory.TAG_FUJIFILM_MACRO);
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_MODE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Auto focus";
+            case 1:
+                return "Manual focus";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSlowSyncDescription()
+    {
+        return getOnOffDescription(FujifilmMakernoteDirectory.TAG_FUJIFILM_SLOW_SYNCH);
+    }
+
+    @Nullable
+    public String getPictureModeDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_PICTURE_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -165,8 +265,65 @@
     }
 
-    public String getSlowSyncDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_SLOW_SYNCHRO)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_SLOW_SYNCHRO);
+    @Nullable
+    public String getContinuousTakingOrAutoBrackettingDescription()
+    {
+        return getOnOffDescription(FujifilmMakernoteDirectory.TAG_FUJIFILM_CONTINUOUS_TAKING_OR_AUTO_BRACKETTING);
+    }
+
+    @Nullable
+    public String getBlurWarningDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_BLUR_WARNING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "No blur warning";
+            case 1:
+                return "Blur warning";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFocusWarningDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_WARNING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Auto focus good";
+            case 1:
+                return "Out of focus";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoExposureWarningDescription()
+    {
+        final Integer value = _directory.getInteger(FujifilmMakernoteDirectory.TAG_FUJIFILM_AE_WARNING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0:
+                return "AE good";
+            case 1:
+                return "Over exposed (>1/1000s @ F11)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+
+    @Nullable
+    private String getOnOffDescription(final int tagType)
+    {
+        final Integer value = _directory.getInteger(tagType);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -178,134 +335,3 @@
         }
     }
-
-    public String getFocusModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_MODE)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_FOCUS_MODE);
-        switch (value) {
-            case 0:
-                return "Auto focus";
-            case 1:
-                return "Manual focus";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getMacroDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_MACRO)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_MACRO);
-        switch (value) {
-            case 0:
-                return "Off";
-            case 1:
-                return "On";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getFlashStrengthDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_FLASH_STRENGTH)) return null;
-        Rational value = _directory.getRational(FujifilmMakernoteDirectory.TAG_FUJIFILM_FLASH_STRENGTH);
-        return value.toSimpleString(false) + " EV (Apex)";
-    }
-
-    public String getFlashModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_FLASH_MODE)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_FLASH_MODE);
-        switch (value) {
-            case 0:
-                return "Auto";
-            case 1:
-                return "On";
-            case 2:
-                return "Off";
-            case 3:
-                return "Red-eye reduction";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getToneDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_TONE)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_TONE);
-        switch (value) {
-            case 0:
-                return "Normal (STD)";
-            case 256:
-                return "High (HARD)";
-            case 512:
-                return "Low (ORG)";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getColorDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_COLOR)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_COLOR);
-        switch (value) {
-            case 0:
-                return "Normal (STD)";
-            case 256:
-                return "High";
-            case 512:
-                return "Low (ORG)";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getWhiteBalanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_WHITE_BALANCE)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_WHITE_BALANCE);
-        switch (value) {
-            case 0:
-                return "Auto";
-            case 256:
-                return "Daylight";
-            case 512:
-                return "Cloudy";
-            case 768:
-                return "DaylightColor-fluorescence";
-            case 769:
-                return "DaywhiteColor-fluorescence";
-            case 770:
-                return "White-fluorescence";
-            case 1024:
-                return "Incandenscense";
-            case 3840:
-                return "Custom white balance";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getSharpnessDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(FujifilmMakernoteDirectory.TAG_FUJIFILM_SHARPNESS)) return null;
-        int value = _directory.getInt(FujifilmMakernoteDirectory.TAG_FUJIFILM_SHARPNESS);
-        switch (value) {
-            case 1:
-                return "Softest";
-            case 2:
-                return "Soft";
-            case 3:
-                return "Normal";
-            case 4:
-                return "Hard";
-            case 5:
-                return "Hardest";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
 }
Index: trunk/src/com/drew/metadata/exif/FujifilmMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/FujifilmMakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/FujifilmMakernoteDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,49 +27,52 @@
 
 /**
+ * Describes tags specific to Fujifilm cameras.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class FujifilmMakernoteDirectory extends Directory
 {
     public static final int TAG_FUJIFILM_MAKERNOTE_VERSION = 0x0000;
-    public static final int TAG_FUJIFILM_QUALITY = 0x1000;
-    public static final int TAG_FUJIFILM_SHARPNESS = 0x1001;
-    public static final int TAG_FUJIFILM_WHITE_BALANCE = 0x1002;
-    public static final int TAG_FUJIFILM_COLOR = 0x1003;
-    public static final int TAG_FUJIFILM_TONE = 0x1004;
-    public static final int TAG_FUJIFILM_FLASH_MODE = 0x1010;
-    public static final int TAG_FUJIFILM_FLASH_STRENGTH = 0x1011;
-    public static final int TAG_FUJIFILM_MACRO = 0x1020;
-    public static final int TAG_FUJIFILM_FOCUS_MODE = 0x1021;
-    public static final int TAG_FUJIFILM_SLOW_SYNCHRO = 0x1030;
-    public static final int TAG_FUJIFILM_PICTURE_MODE = 0x1031;
-    public static final int TAG_FUJIFILM_UNKNOWN_1 = 0x1032;
-    public static final int TAG_FUJIFILM_CONTINUOUS_TAKING_OR_AUTO_BRACKETTING = 0x1100;
-    public static final int TAG_FUJIFILM_UNKNOWN_2 = 0x1200;
-    public static final int TAG_FUJIFILM_BLUR_WARNING = 0x1300;
-    public static final int TAG_FUJIFILM_FOCUS_WARNING = 0x1301;
-    public static final int TAG_FUJIFILM_AE_WARNING = 0x1302;
+    public static final int TAG_FUJIFILM_QUALITY = 0x1000; // 4096
+    public static final int TAG_FUJIFILM_SHARPNESS = 0x1001; // 4097
+    public static final int TAG_FUJIFILM_WHITE_BALANCE = 0x1002; // 4098
+    public static final int TAG_FUJIFILM_COLOR_SATURATION = 0x1003; // 4099
+    public static final int TAG_FUJIFILM_TONE = 0x1004; // 4100
+    public static final int TAG_FUJIFILM_FLASH_MODE = 0x1010; // 4112
+    public static final int TAG_FUJIFILM_FLASH_STRENGTH = 0x1011; // 4113
+    public static final int TAG_FUJIFILM_MACRO = 0x1020; // 4128
+    public static final int TAG_FUJIFILM_FOCUS_MODE = 0x1021; // 4129
+    public static final int TAG_FUJIFILM_SLOW_SYNCH = 0x1030; // 4144
+    public static final int TAG_FUJIFILM_PICTURE_MODE = 0x1031; // 4145
+    public static final int TAG_FUJIFILM_UNKNOWN_1 = 0x1032; // 4146
+    public static final int TAG_FUJIFILM_CONTINUOUS_TAKING_OR_AUTO_BRACKETTING = 0x1100; // 4352
+    public static final int TAG_FUJIFILM_UNKNOWN_2 = 0x1200; // 4608
+    public static final int TAG_FUJIFILM_BLUR_WARNING = 0x1300; // 4864
+    public static final int TAG_FUJIFILM_FOCUS_WARNING = 0x1301; // 4865
+    public static final int TAG_FUJIFILM_AE_WARNING = 0x1302; // 4866
 
-    protected static final HashMap tagNameMap = new HashMap();
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        tagNameMap.put(new Integer(TAG_FUJIFILM_AE_WARNING), "AE Warning");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_BLUR_WARNING), "Blur Warning");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_COLOR), "Color");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_CONTINUOUS_TAKING_OR_AUTO_BRACKETTING), "Continuous Taking Or Auto Bracketting");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_FLASH_MODE), "Flash Mode");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_FLASH_STRENGTH), "Flash Strength");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_FOCUS_MODE), "Focus Mode");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_FOCUS_WARNING), "Focus Warning");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_MACRO), "Macro");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_MAKERNOTE_VERSION), "Makernote Version");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_PICTURE_MODE), "Picture Mode");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_QUALITY), "Quality");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_SHARPNESS), "Sharpness");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_SLOW_SYNCHRO), "Slow Synchro");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_TONE), "Tone");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_UNKNOWN_1), "Makernote Unknown 1");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_UNKNOWN_2), "Makernote Unknown 2");
-        tagNameMap.put(new Integer(TAG_FUJIFILM_WHITE_BALANCE), "White Balance");
+        _tagNameMap.put(TAG_FUJIFILM_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_FUJIFILM_QUALITY, "Quality");
+        _tagNameMap.put(TAG_FUJIFILM_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_FUJIFILM_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_FUJIFILM_COLOR_SATURATION, "Color Saturation");
+        _tagNameMap.put(TAG_FUJIFILM_TONE, "Tone (Contrast)");
+        _tagNameMap.put(TAG_FUJIFILM_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_FUJIFILM_FLASH_STRENGTH, "Flash Strength");
+        _tagNameMap.put(TAG_FUJIFILM_MACRO, "Macro");
+        _tagNameMap.put(TAG_FUJIFILM_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_FUJIFILM_SLOW_SYNCH, "Slow Synch");
+        _tagNameMap.put(TAG_FUJIFILM_PICTURE_MODE, "Picture Mode");
+        _tagNameMap.put(TAG_FUJIFILM_UNKNOWN_1, "Makernote Unknown 1");
+        _tagNameMap.put(TAG_FUJIFILM_CONTINUOUS_TAKING_OR_AUTO_BRACKETTING, "Continuous Taking Or Auto Bracketting");
+        _tagNameMap.put(TAG_FUJIFILM_UNKNOWN_2, "Makernote Unknown 2");
+        _tagNameMap.put(TAG_FUJIFILM_BLUR_WARNING, "Blur Warning");
+        _tagNameMap.put(TAG_FUJIFILM_FOCUS_WARNING, "Focus Warning");
+        _tagNameMap.put(TAG_FUJIFILM_AE_WARNING, "AE Warning");
     }
 
@@ -74,4 +82,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -79,7 +88,8 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
-        return tagNameMap;
+        return _tagNameMap;
     }
 }
Index: trunk/src/com/drew/metadata/exif/GpsDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/GpsDescriptor.java	(revision 6127)
@@ -1,38 +1,50 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 12-Nov-2002 22:27:52 using IntelliJ IDEA.
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.GeoLocation;
 import com.drew.lang.Rational;
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
+import java.text.DecimalFormat;
+
 /**
- *
+ * Provides human-readable string representations of tag values stored in a <code>GpsDirectory</code>.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class GpsDescriptor extends TagDescriptor
+public class GpsDescriptor extends TagDescriptor<GpsDirectory>
 {
-    public GpsDescriptor(Directory directory)
+    public GpsDescriptor(@NotNull GpsDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
+            case GpsDirectory.TAG_GPS_VERSION_ID:
+                return getGpsVersionIdDescription();
             case GpsDirectory.TAG_GPS_ALTITUDE:
                 return getGpsAltitudeDescription();
@@ -57,79 +69,100 @@
             case GpsDirectory.TAG_GPS_TIME_STAMP:
                 return getGpsTimeStampDescription();
+            case GpsDirectory.TAG_GPS_LONGITUDE:
                 // three rational numbers -- displayed in HH"MM"SS.ss
-            case GpsDirectory.TAG_GPS_LONGITUDE:
                 return getGpsLongitudeDescription();
             case GpsDirectory.TAG_GPS_LATITUDE:
+                // three rational numbers -- displayed in HH"MM"SS.ss
                 return getGpsLatitudeDescription();
+            case GpsDirectory.TAG_GPS_DIFFERENTIAL:
+                return getGpsDifferentialDescription();
             default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getGpsLatitudeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_LATITUDE)) return null;
-        return getHoursMinutesSecondsDescription(GpsDirectory.TAG_GPS_LATITUDE);
-    }
-
-    public String getGpsLongitudeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_LONGITUDE)) return null;
-        return getHoursMinutesSecondsDescription(GpsDirectory.TAG_GPS_LONGITUDE);
-    }
-
-    public String getHoursMinutesSecondsDescription(int tagType) throws MetadataException
-    {
-        Rational[] components = _directory.getRationalArray(tagType);
-        // TODO create an HoursMinutesSecods class ??
-        int deg = components[0].intValue();
-        float min = components[1].floatValue();
-        float sec = components[2].floatValue();
-        // carry fractions of minutes into seconds -- thanks Colin Briton
-        sec += (min % 1) * 60;
-        return String.valueOf(deg) + "\"" + String.valueOf((int)min) + "'" + String.valueOf(sec);
-    }
-
-    public String getGpsTimeStampDescription() throws MetadataException
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    private String getGpsVersionIdDescription()
+    {
+        return convertBytesToVersionString(_directory.getIntArray(GpsDirectory.TAG_GPS_VERSION_ID), 1);
+    }
+
+    @Nullable
+    public String getGpsLatitudeDescription()
+    {
+        GeoLocation location = _directory.getGeoLocation();
+
+        if (location == null)
+            return null;
+
+        return GeoLocation.decimalToDegreesMinutesSecondsString(location.getLatitude());
+    }
+
+    @Nullable
+    public String getGpsLongitudeDescription()
+    {
+        GeoLocation location = _directory.getGeoLocation();
+
+        if (location == null)
+            return null;
+
+        return GeoLocation.decimalToDegreesMinutesSecondsString(location.getLongitude());
+    }
+
+    @Nullable
+    public String getGpsTimeStampDescription()
     {
         // time in hour, min, sec
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_TIME_STAMP)) return null;
         int[] timeComponents = _directory.getIntArray(GpsDirectory.TAG_GPS_TIME_STAMP);
-        StringBuffer sbuffer = new StringBuffer();
-        sbuffer.append(timeComponents[0]);
-        sbuffer.append(":");
-        sbuffer.append(timeComponents[1]);
-        sbuffer.append(":");
-        sbuffer.append(timeComponents[2]);
-        sbuffer.append(" UTC");
-        return sbuffer.toString();
-    }
-
+        if (timeComponents==null)
+            return null;
+        StringBuilder description = new StringBuilder();
+        description.append(timeComponents[0]);
+        description.append(":");
+        description.append(timeComponents[1]);
+        description.append(":");
+        description.append(timeComponents[2]);
+        description.append(" UTC");
+        return description.toString();
+    }
+
+    @Nullable
     public String getGpsDestinationReferenceDescription()
     {
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_DEST_DISTANCE_REF)) return null;
-        String destRef = _directory.getString(GpsDirectory.TAG_GPS_DEST_DISTANCE_REF).trim();
-        if ("K".equalsIgnoreCase(destRef)) {
+        final String value = _directory.getString(GpsDirectory.TAG_GPS_DEST_DISTANCE_REF);
+        if (value==null)
+            return null;
+        String distanceRef = value.trim();
+        if ("K".equalsIgnoreCase(distanceRef)) {
             return "kilometers";
-        } else if ("M".equalsIgnoreCase(destRef)) {
+        } else if ("M".equalsIgnoreCase(distanceRef)) {
             return "miles";
-        } else if ("N".equalsIgnoreCase(destRef)) {
+        } else if ("N".equalsIgnoreCase(distanceRef)) {
             return "knots";
         } else {
-            return "Unknown (" + destRef + ")";
-        }
-    }
-
+            return "Unknown (" + distanceRef + ")";
+        }
+    }
+
+    @Nullable
     public String getGpsDirectionDescription(int tagType)
     {
-        if (!_directory.containsTag(tagType)) return null;
-        String gpsDirection = _directory.getString(tagType).trim();
-        return gpsDirection + " degrees";
-    }
-
+        Rational angle = _directory.getRational(tagType);
+        // provide a decimal version of rational numbers in the description, to avoid strings like "35334/199 degrees"
+        String value = angle != null
+                ? new DecimalFormat("0.##").format(angle.doubleValue())
+                : _directory.getString(tagType);
+        if (value==null || value.trim().length()==0)
+            return null;
+        return value.trim() + " degrees";
+    }
+
+    @Nullable
     public String getGpsDirectionReferenceDescription(int tagType)
     {
-        if (!_directory.containsTag(tagType)) return null;
-        String gpsDistRef = _directory.getString(tagType).trim();
+        final String value = _directory.getString(tagType);
+        if (value==null)
+            return null;
+        String gpsDistRef = value.trim();
         if ("T".equalsIgnoreCase(gpsDistRef)) {
             return "True direction";
@@ -141,8 +174,11 @@
     }
 
+    @Nullable
     public String getGpsSpeedRefDescription()
     {
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_SPEED_REF)) return null;
-        String gpsSpeedRef = _directory.getString(GpsDirectory.TAG_GPS_SPEED_REF).trim();
+        final String value = _directory.getString(GpsDirectory.TAG_GPS_SPEED_REF);
+        if (value==null)
+            return null;
+        String gpsSpeedRef = value.trim();
         if ("K".equalsIgnoreCase(gpsSpeedRef)) {
             return "kph";
@@ -156,8 +192,11 @@
     }
 
+    @Nullable
     public String getGpsMeasureModeDescription()
     {
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_MEASURE_MODE)) return null;
-        String gpsSpeedMeasureMode = _directory.getString(GpsDirectory.TAG_GPS_MEASURE_MODE).trim();
+        final String value = _directory.getString(GpsDirectory.TAG_GPS_MEASURE_MODE);
+        if (value==null)
+            return null;
+        String gpsSpeedMeasureMode = value.trim();
         if ("2".equalsIgnoreCase(gpsSpeedMeasureMode)) {
             return "2-dimensional measurement";
@@ -169,12 +208,15 @@
     }
 
+    @Nullable
     public String getGpsStatusDescription()
     {
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_STATUS)) return null;
-        String gpsStatus = _directory.getString(GpsDirectory.TAG_GPS_STATUS).trim();
+        final String value = _directory.getString(GpsDirectory.TAG_GPS_STATUS);
+        if (value==null)
+            return null;
+        String gpsStatus = value.trim();
         if ("A".equalsIgnoreCase(gpsStatus)) {
-            return "Measurement in progess";
+            return "Active (Measurement in progress)";
         } else if ("V".equalsIgnoreCase(gpsStatus)) {
-            return "Measurement Interoperability";
+            return "Void (Measurement Interoperability)";
         } else {
             return "Unknown (" + gpsStatus + ")";
@@ -182,20 +224,48 @@
     }
 
-    public String getGpsAltitudeRefDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_ALTITUDE_REF)) return null;
-        int alititudeRef = _directory.getInt(GpsDirectory.TAG_GPS_ALTITUDE_REF);
-        if (alititudeRef == 0) {
+    @Nullable
+    public String getGpsAltitudeRefDescription()
+    {
+        Integer value = _directory.getInteger(GpsDirectory.TAG_GPS_ALTITUDE_REF);
+        if (value==null)
+            return null;
+        if (value == 0)
             return "Sea level";
-        } else {
-            return "Unknown (" + alititudeRef + ")";
-        }
-    }
-
-    public String getGpsAltitudeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(GpsDirectory.TAG_GPS_ALTITUDE)) return null;
-        String alititude = _directory.getRational(GpsDirectory.TAG_GPS_ALTITUDE).toSimpleString(true);
-        return alititude + " metres";
+        if (value == 1)
+            return "Below sea level";
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getGpsAltitudeDescription()
+    {
+        final Rational value = _directory.getRational(GpsDirectory.TAG_GPS_ALTITUDE);
+        if (value==null)
+            return null;
+        return value.intValue() + " metres";
+    }
+
+    @Nullable
+    public String getGpsDifferentialDescription()
+    {
+        final Integer value = _directory.getInteger(GpsDirectory.TAG_GPS_DIFFERENTIAL);
+        if (value==null)
+            return null;
+        if (value == 0)
+            return "No Correction";
+        if (value == 1)
+            return "Differential Corrected";
+        return "Unknown (" + value + ")";
+    }
+
+    @Nullable
+    public String getDegreesMinutesSecondsDescription()
+    {
+        GeoLocation location = _directory.getGeoLocation();
+
+        if (location == null)
+            return null;
+
+        return location.toDMSString();
     }
 }
Index: trunk/src/com/drew/metadata/exif/GpsDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/GpsDirectory.java	(revision 6127)
@@ -1,20 +1,28 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 26-Nov-2002 11:00:52 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.GeoLocation;
+import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 
@@ -22,5 +30,7 @@
 
 /**
+ * Describes Exif tags that contain Global Positioning System (GPS) data.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class GpsDirectory extends Directory
@@ -81,35 +91,46 @@
     public static final int TAG_GPS_DEST_DISTANCE = 0x001A;
 
-    protected static final HashMap tagNameMap = new HashMap();
+    /** Values of "GPS", "CELLID", "WLAN" or "MANUAL" by the EXIF spec. */
+    public static final int TAG_GPS_PROCESSING_METHOD = 0x001B;
+    public static final int TAG_GPS_AREA_INFORMATION = 0x001C;
+    public static final int TAG_GPS_DATE_STAMP = 0x001D;
+    public static final int TAG_GPS_DIFFERENTIAL = 0x001E;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        tagNameMap.put(new Integer(TAG_GPS_VERSION_ID), "GPS Version ID");
-        tagNameMap.put(new Integer(TAG_GPS_LATITUDE_REF), "GPS Latitude Ref");
-        tagNameMap.put(new Integer(TAG_GPS_LATITUDE), "GPS Latitude");
-        tagNameMap.put(new Integer(TAG_GPS_LONGITUDE_REF), "GPS Longitude Ref");
-        tagNameMap.put(new Integer(TAG_GPS_LONGITUDE), "GPS Longitude");
-        tagNameMap.put(new Integer(TAG_GPS_ALTITUDE_REF), "GPS Altitude Ref");
-        tagNameMap.put(new Integer(TAG_GPS_ALTITUDE), "GPS Altitude");
-        tagNameMap.put(new Integer(TAG_GPS_TIME_STAMP), "GPS Time-Stamp");
-        tagNameMap.put(new Integer(TAG_GPS_SATELLITES), "GPS Satellites");
-        tagNameMap.put(new Integer(TAG_GPS_STATUS), "GPS Status");
-        tagNameMap.put(new Integer(TAG_GPS_MEASURE_MODE), "GPS Measure Mode");
-        tagNameMap.put(new Integer(TAG_GPS_DOP), "GPS DOP");
-        tagNameMap.put(new Integer(TAG_GPS_SPEED_REF), "GPS Speed Ref");
-        tagNameMap.put(new Integer(TAG_GPS_SPEED), "GPS Speed");
-        tagNameMap.put(new Integer(TAG_GPS_TRACK_REF), "GPS Track Ref");
-        tagNameMap.put(new Integer(TAG_GPS_TRACK), "GPS Track");
-        tagNameMap.put(new Integer(TAG_GPS_IMG_DIRECTION_REF), "GPS Img Direction Ref");
-        tagNameMap.put(new Integer(TAG_GPS_IMG_DIRECTION_REF), "GPS Img Direction");
-        tagNameMap.put(new Integer(TAG_GPS_MAP_DATUM), "GPS Map Datum");
-        tagNameMap.put(new Integer(TAG_GPS_DEST_LATITUDE_REF), "GPS Dest Latitude Ref");
-        tagNameMap.put(new Integer(TAG_GPS_DEST_LATITUDE), "GPS Dest Latitude");
-        tagNameMap.put(new Integer(TAG_GPS_DEST_LONGITUDE_REF), "GPS Dest Longitude Ref");
-        tagNameMap.put(new Integer(TAG_GPS_DEST_LONGITUDE), "GPS Dest Longitude");
-        tagNameMap.put(new Integer(TAG_GPS_DEST_BEARING_REF), "GPS Dest Bearing Ref");
-        tagNameMap.put(new Integer(TAG_GPS_DEST_BEARING), "GPS Dest Bearing");
-        tagNameMap.put(new Integer(TAG_GPS_DEST_DISTANCE_REF), "GPS Dest Distance Ref");
-        tagNameMap.put(new Integer(TAG_GPS_DEST_DISTANCE), "GPS Dest Distance");
+        _tagNameMap.put(TAG_GPS_VERSION_ID, "GPS Version ID");
+        _tagNameMap.put(TAG_GPS_LATITUDE_REF, "GPS Latitude Ref");
+        _tagNameMap.put(TAG_GPS_LATITUDE, "GPS Latitude");
+        _tagNameMap.put(TAG_GPS_LONGITUDE_REF, "GPS Longitude Ref");
+        _tagNameMap.put(TAG_GPS_LONGITUDE, "GPS Longitude");
+        _tagNameMap.put(TAG_GPS_ALTITUDE_REF, "GPS Altitude Ref");
+        _tagNameMap.put(TAG_GPS_ALTITUDE, "GPS Altitude");
+        _tagNameMap.put(TAG_GPS_TIME_STAMP, "GPS Time-Stamp");
+        _tagNameMap.put(TAG_GPS_SATELLITES, "GPS Satellites");
+        _tagNameMap.put(TAG_GPS_STATUS, "GPS Status");
+        _tagNameMap.put(TAG_GPS_MEASURE_MODE, "GPS Measure Mode");
+        _tagNameMap.put(TAG_GPS_DOP, "GPS DOP");
+        _tagNameMap.put(TAG_GPS_SPEED_REF, "GPS Speed Ref");
+        _tagNameMap.put(TAG_GPS_SPEED, "GPS Speed");
+        _tagNameMap.put(TAG_GPS_TRACK_REF, "GPS Track Ref");
+        _tagNameMap.put(TAG_GPS_TRACK, "GPS Track");
+        _tagNameMap.put(TAG_GPS_IMG_DIRECTION_REF, "GPS Img Direction Ref");
+        _tagNameMap.put(TAG_GPS_IMG_DIRECTION, "GPS Img Direction");
+        _tagNameMap.put(TAG_GPS_MAP_DATUM, "GPS Map Datum");
+        _tagNameMap.put(TAG_GPS_DEST_LATITUDE_REF, "GPS Dest Latitude Ref");
+        _tagNameMap.put(TAG_GPS_DEST_LATITUDE, "GPS Dest Latitude");
+        _tagNameMap.put(TAG_GPS_DEST_LONGITUDE_REF, "GPS Dest Longitude Ref");
+        _tagNameMap.put(TAG_GPS_DEST_LONGITUDE, "GPS Dest Longitude");
+        _tagNameMap.put(TAG_GPS_DEST_BEARING_REF, "GPS Dest Bearing Ref");
+        _tagNameMap.put(TAG_GPS_DEST_BEARING, "GPS Dest Bearing");
+        _tagNameMap.put(TAG_GPS_DEST_DISTANCE_REF, "GPS Dest Distance Ref");
+        _tagNameMap.put(TAG_GPS_DEST_DISTANCE, "GPS Dest Distance");
+        _tagNameMap.put(TAG_GPS_PROCESSING_METHOD, "GPS Processing Method");
+        _tagNameMap.put(TAG_GPS_AREA_INFORMATION, "GPS Area Information");
+        _tagNameMap.put(TAG_GPS_DATE_STAMP, "GPS Date Stamp");
+        _tagNameMap.put(TAG_GPS_DIFFERENTIAL, "GPS Differential");
     }
 
@@ -119,4 +140,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -124,7 +146,40 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
-        return tagNameMap;
+        return _tagNameMap;
+    }
+
+    /**
+     * Parses various tags in an attempt to obtain a single object representing the latitude and longitude
+     * at which this image was captured.
+     *
+     * @return The geographical location of this image, if possible, otherwise null
+     */
+    @Nullable
+    public GeoLocation getGeoLocation()
+    {
+        Rational[] latitudes = getRationalArray(GpsDirectory.TAG_GPS_LATITUDE);
+        Rational[] longitudes = getRationalArray(GpsDirectory.TAG_GPS_LONGITUDE);
+        String latitudeRef = getString(GpsDirectory.TAG_GPS_LATITUDE_REF);
+        String longitudeRef = getString(GpsDirectory.TAG_GPS_LONGITUDE_REF);
+
+        // Make sure we have the required values
+        if (latitudes == null || latitudes.length != 3)
+            return null;
+        if (longitudes == null || longitudes.length != 3)
+            return null;
+        if (latitudeRef == null || longitudeRef == null)
+            return null;
+
+        Double lat = GeoLocation.degreesMinutesSecondsToDecimal(latitudes[0], latitudes[1], latitudes[2], latitudeRef.equalsIgnoreCase("S"));
+        Double lon = GeoLocation.degreesMinutesSecondsToDecimal(longitudes[0], longitudes[1], longitudes[2], longitudeRef.equalsIgnoreCase("W"));
+
+        // This can return null, in cases where the conversion was not possible
+        if (lat == null || lon == null)
+            return null;
+
+        return new GeoLocation(lat, lon);
     }
 }
Index: trunk/src/com/drew/metadata/exif/KodakMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/KodakMakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/KodakMakernoteDescriptor.java	(revision 6127)
@@ -1,36 +1,39 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.TagDescriptor;
 
 /**
- * Provides human-readable string versions of the tags stored in a KodakMakernoteDirectory.
+ * Provides human-readable string representations of tag values stored in a <code>KodakMakernoteDirectory</code>.
+ * 
  * Thanks to David Carson for the initial version of this class.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class KodakMakernoteDescriptor extends TagDescriptor
+public class KodakMakernoteDescriptor extends TagDescriptor<KodakMakernoteDirectory>
 {
-	public KodakMakernoteDescriptor(Directory directory)
-	{
-		super(directory);
-	}
-	
-	public String getDescription(int tagType) throws MetadataException
+    public KodakMakernoteDescriptor(@NotNull KodakMakernoteDirectory directory)
     {
-		return _directory.getString(tagType);
-	}
+        super(directory);
+    }
 }
Index: trunk/src/com/drew/metadata/exif/KodakMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/KodakMakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/KodakMakernoteDirectory.java	(revision 6127)
@@ -1,18 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -21,17 +28,27 @@
 /**
  * Describes tags specific to Kodak cameras.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class KodakMakernoteDirectory extends Directory
 {
-	protected static final HashMap _tagNameMap = new HashMap();
-	
-	public String getName()
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    public KodakMakernoteDirectory()
     {
-		return "Kodak Makernote";
-	}
+        this.setDescriptor(new KodakMakernoteDescriptor(this));
+    }
 
-	protected HashMap getTagNameMap()
+    @NotNull
+    public String getName()
     {
-		return _tagNameMap;
-	}
+        return "Kodak Makernote";
+    }
+
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
 }
Index: trunk/src/com/drew/metadata/exif/KyoceraMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/KyoceraMakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/KyoceraMakernoteDescriptor.java	(revision 6127)
@@ -1,40 +1,49 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
- * Provides human-readable string versions of the tags stored in a KyoceraMakernoteDirectory.
- *
+ * Provides human-readable string representations of tag values stored in a <code>KyoceraMakernoteDirectory</code>.
+ * <p/>
  * Some information about this makernote taken from here:
  * http://www.ozhiker.com/electronics/pjmt/jpeg_info/kyocera_mn.html
- *
+ * <p/>
  * Most manufacturer's MakerNote counts the "offset to data" from the first byte
  * of TIFF header (same as the other IFD), but Kyocera (along with Fujifilm) counts
  * it from the first byte of MakerNote itself.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class KyoceraMakernoteDescriptor extends TagDescriptor
+public class KyoceraMakernoteDescriptor extends TagDescriptor<KyoceraMakernoteDirectory>
 {
-    public KyoceraMakernoteDescriptor(Directory directory)
+    public KyoceraMakernoteDescriptor(@NotNull KyoceraMakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
@@ -44,19 +53,23 @@
                 return getProprietaryThumbnailDataDescription();
             default:
-                return _directory.getString(tagType);
+                return super.getDescription(tagType);
         }
     }
 
-    public String getPrintImageMatchingInfoDescription() throws MetadataException
+    @Nullable
+    public String getPrintImageMatchingInfoDescription()
     {
-        if (!_directory.containsTag(KyoceraMakernoteDirectory.TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO)) return null;
         byte[] bytes = _directory.getByteArray(KyoceraMakernoteDirectory.TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO);
+        if (bytes==null)
+            return null;
         return "(" + bytes.length + " bytes)";
     }
 
-    public String getProprietaryThumbnailDataDescription() throws MetadataException
+    @Nullable
+    public String getProprietaryThumbnailDataDescription()
     {
-        if (!_directory.containsTag(KyoceraMakernoteDirectory.TAG_KYOCERA_PROPRIETARY_THUMBNAIL)) return null;
         byte[] bytes = _directory.getByteArray(KyoceraMakernoteDirectory.TAG_KYOCERA_PROPRIETARY_THUMBNAIL);
+        if (bytes==null)
+            return null;
         return "(" + bytes.length + " bytes)";
     }
Index: trunk/src/com/drew/metadata/exif/KyoceraMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/KyoceraMakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/KyoceraMakernoteDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,5 +27,7 @@
 
 /**
+ * Describes tags specific to Kyocera and Contax cameras.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class KyoceraMakernoteDirectory extends Directory
@@ -29,10 +36,11 @@
     public static final int TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
-    protected static final HashMap tagNameMap = new HashMap();
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        tagNameMap.put(new Integer(TAG_KYOCERA_PROPRIETARY_THUMBNAIL), "Proprietary Thumbnail Format Data");
-        tagNameMap.put(new Integer(TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO), "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_KYOCERA_PROPRIETARY_THUMBNAIL, "Proprietary Thumbnail Format Data");
+        _tagNameMap.put(TAG_KYOCERA_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
     }
 
@@ -42,4 +50,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -47,7 +56,8 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
-        return tagNameMap;
+        return _tagNameMap;
     }
 }
Index: trunk/src/com/drew/metadata/exif/NikonType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/NikonType1MakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/NikonType1MakernoteDescriptor.java	(revision 6127)
@@ -1,28 +1,35 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
 import com.drew.lang.Rational;
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
- * Provides human-readable string versions of the tags stored in a NikonType1MakernoteDirectory.
+ * Provides human-readable string representations of tag values stored in a <code>NikonType1MakernoteDirectory</code>.
+ * <p/>
  * Type-1 is for E-Series cameras prior to (not including) E990.  For example: E700, E800, E900,
  * E900S, E910, E950.
- *
+ * <p/>
  * MakerNote starts from ASCII string "Nikon". Data format is the same as IFD, but it starts from
  * offset 0x08. This is the same as Olympus except start string. Example of actual data
@@ -32,13 +39,16 @@
  * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
  * </code></pre>
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class NikonType1MakernoteDescriptor extends TagDescriptor
+public class NikonType1MakernoteDescriptor extends TagDescriptor<NikonType1MakernoteDirectory>
 {
-    public NikonType1MakernoteDescriptor(Directory directory)
+    public NikonType1MakernoteDescriptor(@NotNull NikonType1MakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
@@ -60,12 +70,14 @@
                 return getConverterDescription();
             default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getConverterDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CONVERTER)) return null;
-        int value = _directory.getInt(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CONVERTER);
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getConverterDescription()
+    {
+        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CONVERTER);
+        if (value == null)
+            return null;
         switch (value) {
             case 0:
@@ -78,8 +90,10 @@
     }
 
-    public String getDigitalZoomDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_DIGITAL_ZOOM)) return null;
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
         Rational value = _directory.getRational(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_DIGITAL_ZOOM);
+        if (value == null)
+            return null;
         if (value.getNumerator() == 0) {
             return "No digital zoom";
@@ -88,8 +102,10 @@
     }
 
-    public String getFocusDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_FOCUS)) return null;
+    @Nullable
+    public String getFocusDescription()
+    {
         Rational value = _directory.getRational(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_FOCUS);
+        if (value == null)
+            return null;
         if (value.getNumerator() == 1 && value.getDenominator() == 0) {
             return "Infinite";
@@ -98,8 +114,10 @@
     }
 
-    public String getWhiteBalanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_WHITE_BALANCE)) return null;
-        int value = _directory.getInt(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_WHITE_BALANCE);
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_WHITE_BALANCE);
+        if (value == null)
+            return null;
         switch (value) {
             case 0:
@@ -110,7 +128,7 @@
                 return "Daylight";
             case 3:
-                return "Incandescense";
-            case 4:
-                return "Flourescence";
+                return "Incandescence";
+            case 4:
+                return "Florescence";
             case 5:
                 return "Cloudy";
@@ -122,8 +140,10 @@
     }
 
-    public String getCcdSensitivityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CCD_SENSITIVITY)) return null;
-        int value = _directory.getInt(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CCD_SENSITIVITY);
+    @Nullable
+    public String getCcdSensitivityDescription()
+    {
+        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_CCD_SENSITIVITY);
+        if (value == null)
+            return null;
         switch (value) {
             case 0:
@@ -140,8 +160,10 @@
     }
 
-    public String getImageAdjustmentDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_IMAGE_ADJUSTMENT)) return null;
-        int value = _directory.getInt(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_IMAGE_ADJUSTMENT);
+    @Nullable
+    public String getImageAdjustmentDescription()
+    {
+        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_IMAGE_ADJUSTMENT);
+        if (value == null)
+            return null;
         switch (value) {
             case 0:
@@ -160,8 +182,10 @@
     }
 
-    public String getColorModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_COLOR_MODE)) return null;
-        int value = _directory.getInt(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_COLOR_MODE);
+    @Nullable
+    public String getColorModeDescription()
+    {
+        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_COLOR_MODE);
+        if (value == null)
+            return null;
         switch (value) {
             case 1:
@@ -174,8 +198,10 @@
     }
 
-    public String getQualityDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_QUALITY)) return null;
-        int value = _directory.getInt(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_QUALITY);
+    @Nullable
+    public String getQualityDescription()
+    {
+        Integer value = _directory.getInteger(NikonType1MakernoteDirectory.TAG_NIKON_TYPE1_QUALITY);
+        if (value == null)
+            return null;
         switch (value) {
             case 1:
Index: trunk/src/com/drew/metadata/exif/NikonType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/NikonType1MakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/NikonType1MakernoteDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,5 +27,5 @@
 
 /**
- * Contains values specific to Nikon cameras.  Type-1 is for E-Series cameras prior to (not including) E990.
+ * Describes tags specific to Nikon (type 1) cameras.  Type-1 is for E-Series cameras prior to (not including) E990.
  *
  * There are 3 formats of Nikon's MakerNote. MakerNote of E700/E800/E900/E900S/E910/E950
@@ -32,4 +37,6 @@
  * :0010: 00 00 EC 02 00 00 03 00-03 00 01 00 00 00 06 00 ................
  * </code></pre>
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class NikonType1MakernoteDirectory extends Directory
@@ -47,19 +54,20 @@
     public static final int TAG_NIKON_TYPE1_UNKNOWN_3 = 0x0F00;
 
-    protected static final HashMap _tagNameMap = new HashMap();
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_CCD_SENSITIVITY), "CCD Sensitivity");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_COLOR_MODE), "Color Mode");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_DIGITAL_ZOOM), "Digital Zoom");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_CONVERTER), "Fisheye Converter");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_FOCUS), "Focus");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_IMAGE_ADJUSTMENT), "Image Adjustment");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_QUALITY), "Quality");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_UNKNOWN_1), "Makernote Unknown 1");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_UNKNOWN_2), "Makernote Unknown 2");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_UNKNOWN_3), "Makernote Unknown 3");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE1_WHITE_BALANCE), "White Balance");
+        _tagNameMap.put(TAG_NIKON_TYPE1_CCD_SENSITIVITY, "CCD Sensitivity");
+        _tagNameMap.put(TAG_NIKON_TYPE1_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_NIKON_TYPE1_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_NIKON_TYPE1_CONVERTER, "Fisheye Converter");
+        _tagNameMap.put(TAG_NIKON_TYPE1_FOCUS, "Focus");
+        _tagNameMap.put(TAG_NIKON_TYPE1_IMAGE_ADJUSTMENT, "Image Adjustment");
+        _tagNameMap.put(TAG_NIKON_TYPE1_QUALITY, "Quality");
+        _tagNameMap.put(TAG_NIKON_TYPE1_UNKNOWN_1, "Makernote Unknown 1");
+        _tagNameMap.put(TAG_NIKON_TYPE1_UNKNOWN_2, "Makernote Unknown 2");
+        _tagNameMap.put(TAG_NIKON_TYPE1_UNKNOWN_3, "Makernote Unknown 3");
+        _tagNameMap.put(TAG_NIKON_TYPE1_WHITE_BALANCE, "White Balance");
     }
 
@@ -69,4 +77,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -74,5 +83,6 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
         return _tagNameMap;
Index: trunk/src/com/drew/metadata/exif/NikonType2MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/NikonType2MakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/NikonType2MakernoteDescriptor.java	(revision 6127)
@@ -1,45 +1,58 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
 import com.drew.lang.Rational;
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.StringUtil;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
 
 /**
- * Provides human-readable string versions of the tags stored in a NikonType2MakernoteDirectory.
+ * Provides human-readable string representations of tag values stored in a <code>NikonType2MakernoteDirectory</code>.
+ *
  * Type-2 applies to the E990 and D-series cameras such as the D1, D70 and D100.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class NikonType2MakernoteDescriptor extends TagDescriptor
+public class NikonType2MakernoteDescriptor extends TagDescriptor<NikonType2MakernoteDirectory>
 {
-    public NikonType2MakernoteDescriptor(Directory directory)
+    public NikonType2MakernoteDescriptor(@NotNull NikonType2MakernoteDirectory directory)
     {
         super(directory);
     }
 
-    private NikonType2MakernoteDirectory getMakernoteDirectory()
-    {
-        return (NikonType2MakernoteDirectory)_directory;
-    }
-
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType)
         {
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_PROGRAM_SHIFT:
+                return getProgramShiftDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_EXPOSURE_DIFFERENCE:
+                return getExposureDifferenceDescription();
             case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS:
                 return getLensDescription();
@@ -50,21 +63,209 @@
             case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_AUTO_FLASH_COMPENSATION:
                 return getAutoFlashCompensationDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FLASH_EXPOSURE_COMPENSATION:
+                return getFlashExposureCompensationDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FLASH_BRACKET_COMPENSATION:
+                return getFlashBracketCompensationDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_EXPOSURE_TUNING:
+                return getExposureTuningDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS_STOPS:
+                return getLensStopsDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_COLOR_SPACE:
+                return getColorSpaceDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_ACTIVE_D_LIGHTING:
+                return getActiveDLightingDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_VIGNETTE_CONTROL:
+                return getVignetteControlDescription();
             case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_ISO_1:
                 return getIsoSettingDescription();
             case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_DIGITAL_ZOOM:
                 return getDigitalZoomDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FLASH_USED:
+                return getFlashUsedDescription();
             case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_AF_FOCUS_POSITION:
                 return getAutoFocusPositionDescription();
             case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FIRMWARE_VERSION:
-                return getAutoFirmwareVersionDescription();
+                return getFirmwareVersionDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS_TYPE:
+                return getLensTypeDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_SHOOTING_MODE:
+                return getShootingModeDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_NEF_COMPRESSION:
+                return getNEFCompressionDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_HIGH_ISO_NOISE_REDUCTION:
+                return getHighISONoiseReductionDescription();
+            case NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_POWER_UP_TIME:
+                return getPowerUpTimeDescription();
             default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getAutoFocusPositionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_AF_FOCUS_POSITION)) return null;
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getPowerUpTimeDescription()
+    {
+        Long value = _directory.getLongObject(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_POWER_UP_TIME);
+        if (value==null)
+            return null; // TODO have observed a byte[8] here which is likely some kind of date (ticks as long?)
+        return new Date(value).toString();
+    }
+
+    @Nullable
+    public String getHighISONoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_HIGH_ISO_NOISE_REDUCTION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Minimal";
+            case 2: return "Low";
+            case 4: return "Normal";
+            case 6: return "High";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashUsedDescription()
+    {
+        Integer value = _directory.getInteger(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FLASH_USED);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Flash Not Used";
+            case 1: return "Manual Flash";
+            case 3: return "Flash Not Ready";
+            case 7: return "External Flash";
+            case 8: return "Fired, Commander Mode";
+            case 9: return "Fired, TTL Mode";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getNEFCompressionDescription()
+    {
+        Integer value = _directory.getInteger(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_NEF_COMPRESSION);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "Lossy (Type 1)";
+            case 3: return "Uncompressed";
+            case 7: return "Lossless";
+            case 8: return "Lossy (Type 2)";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getShootingModeDescription()
+    {
+        Integer value = _directory.getInteger(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_SHOOTING_MODE);
+        if (value==null)
+            return null;
+        Collection<String> bits = new ArrayList<String>();
+
+        if ((value&1)==1)
+            bits.add("Continuous");
+        else
+            bits.add("Single Frame");
+
+        if ((value&2)==2)
+            bits.add("Delay");
+        // Don't know about 3
+        if ((value&8)==8)
+            bits.add("PC Control");
+        if ((value&16)==16)
+            bits.add("Exposure Bracketing");
+        if ((value&32)==32)
+            bits.add("Auto ISO");
+        if ((value&64)==64)
+            bits.add("White-Balance Bracketing");
+        if ((value&128)==128)
+            bits.add("IR Control");
+
+        return StringUtil.join(bits, ", ");
+    }
+
+    @Nullable
+    public String getLensTypeDescription()
+    {
+        Integer value = _directory.getInteger(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS_TYPE);
+        if (value==null)
+            return null;
+
+        Collection<String> bits = new ArrayList<String>();
+
+        // TODO validate these values, as 14 is labelled as AF-C elsewhere but appears here as AF-D-G-VR
+
+        if ((value&1)==1)
+            bits.add("MF");
+        else
+            bits.add("AF");
+
+        if ((value&2)==2)
+            bits.add("D");
+
+        if ((value&4)==4)
+            bits.add("G");
+
+        if ((value&8)==8)
+            bits.add("VR");
+
+        return StringUtil.join(bits, ", ");
+    }
+
+    @Nullable
+    public String getColorSpaceDescription()
+    {
+        Integer value = _directory.getInteger(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_COLOR_SPACE);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 1: return "sRGB";
+            case 2: return "Adobe RGB";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getActiveDLightingDescription()
+    {
+        Integer value = _directory.getInteger(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_ACTIVE_D_LIGHTING);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Light";
+            case 3: return "Normal";
+            case 5: return "High";
+            case 7: return "Extra High";
+            case 65535: return "Auto";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getVignetteControlDescription()
+    {
+        Integer value = _directory.getInteger(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_VIGNETTE_CONTROL);
+        if (value==null)
+            return null;
+        switch (value) {
+            case 0: return "Off";
+            case 1: return "Low";
+            case 3: return "Normal";
+            case 5: return "High";
+            default: return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAutoFocusPositionDescription()
+    {
         int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_AF_FOCUS_POSITION);
+        if (values==null)
+            return null;
         if (values.length != 4 || values[0] != 0 || values[2] != 0 || values[3] != 0) {
             return "Unknown (" + _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_AF_FOCUS_POSITION) + ")";
@@ -86,84 +287,137 @@
     }
 
-    public String getDigitalZoomDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_DIGITAL_ZOOM)) return null;
-        Rational rational = _directory.getRational(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_DIGITAL_ZOOM);
-        if (rational.intValue() == 1) {
-            return "No digital zoom";
-        }
-        return rational.toSimpleString(true) + "x digital zoom";
-    }
-
-    public String getIsoSettingDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_ISO_1)) return null;
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Rational value = _directory.getRational(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_DIGITAL_ZOOM);
+        if (value==null)
+            return null;
+        return value.intValue() == 1
+                ? "No digital zoom"
+                : value.toSimpleString(true) + "x digital zoom";
+    }
+
+    @Nullable
+    public String getProgramShiftDescription()
+    {
+        int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_PROGRAM_SHIFT);
+        return getEVDescription(values);
+    }
+
+    @Nullable
+    public String getExposureDifferenceDescription()
+    {
+        int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_EXPOSURE_DIFFERENCE);
+        return getEVDescription(values);
+    }
+
+    @NotNull
+    public String getAutoFlashCompensationDescription()
+    {
+        int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_AUTO_FLASH_COMPENSATION);
+        return getEVDescription(values);
+    }
+
+    @NotNull
+    public String getFlashExposureCompensationDescription()
+    {
+        int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FLASH_EXPOSURE_COMPENSATION);
+        return getEVDescription(values);
+    }
+
+    @NotNull
+    public String getFlashBracketCompensationDescription()
+    {
+        int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FLASH_BRACKET_COMPENSATION);
+        return getEVDescription(values);
+    }
+
+    @NotNull
+    public String getExposureTuningDescription()
+    {
+        int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_EXPOSURE_TUNING);
+        return getEVDescription(values);
+    }
+
+    @NotNull
+    public String getLensStopsDescription()
+    {
+        int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS_STOPS);
+        return getEVDescription(values);
+    }
+
+    @Nullable
+    private static String getEVDescription(@Nullable int[] values)
+    {
+        if (values==null)
+            return null;
+        if (values.length<3 || values[2]==0)
+            return null;
+        final DecimalFormat decimalFormat = new DecimalFormat("0.##");
+        double ev = values[0] * values[1] / (double)values[2];
+        return decimalFormat.format(ev) + " EV";
+    }
+
+    @Nullable
+    public String getIsoSettingDescription()
+    {
         int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_ISO_1);
-        if (values[0] != 0 || values[1] == 0) {
+        if (values==null)
+            return null;
+        if (values[0] != 0 || values[1] == 0)
             return "Unknown (" + _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_ISO_1) + ")";
-        }
         return "ISO " + values[1];
     }
 
-    public String getAutoFlashCompensationDescription() throws MetadataException
-    {
-        Rational ev = getMakernoteDirectory().getAutoFlashCompensation();
-
-        if (ev==null)
-            return "Unknown";
-
-        DecimalFormat decimalFormat = new DecimalFormat("0.##");
-        return decimalFormat.format(ev.floatValue()) + " EV";
-    }
-
-    public String getLensDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS))
-            return null;
-
-        Rational[] lensValues = _directory.getRationalArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS);
-
-        if (lensValues.length!=4)
+    @Nullable
+    public String getLensDescription()
+    {
+        Rational[] values = _directory.getRationalArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS);
+
+        if (values==null)
+            return null;
+
+        if (values.length<4)
             return _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_LENS);
 
-        StringBuffer description = new StringBuffer();
-        description.append(lensValues[0].intValue());
+        StringBuilder description = new StringBuilder();
+        description.append(values[0].intValue());
         description.append('-');
-        description.append(lensValues[1].intValue());
+        description.append(values[1].intValue());
         description.append("mm f/");
-        description.append(lensValues[2].floatValue());
+        description.append(values[2].floatValue());
         description.append('-');
-        description.append(lensValues[3].floatValue());
+        description.append(values[3].floatValue());
 
         return description.toString();
     }
 
+    @Nullable
     public String getHueAdjustmentDescription()
     {
-        if (!_directory.containsTag(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT))
-            return null;
-
-        return _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT) + " degrees";
-    }
-
+        final String value = _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT);
+        if (value==null)
+            return null;
+        return value + " degrees";
+    }
+
+    @Nullable
     public String getColorModeDescription()
     {
-        if (!_directory.containsTag(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_COLOR_MODE))
-            return null;
-
-        String raw = _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_COLOR_MODE);
-        if (raw.startsWith("MODE1"))
+        String value = _directory.getString(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_CAMERA_COLOR_MODE);
+        if (value==null)
+            return null;
+        if (value.startsWith("MODE1"))
             return "Mode I (sRGB)";
-
-        return raw;
-    }
-
-    public String getAutoFirmwareVersionDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FIRMWARE_VERSION))
-            return null;
-
-        int[] ints = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FIRMWARE_VERSION);
-        return ExifDescriptor.convertBytesToVersionString(ints);
+        return value;
+    }
+
+    @Nullable
+    public String getFirmwareVersionDescription()
+    {
+        int[] values = _directory.getIntArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_FIRMWARE_VERSION);
+        if (values==null)
+            return null;
+        return ExifSubIFDDescriptor.convertBytesToVersionString(values, 2);
     }
 }
Index: trunk/src/com/drew/metadata/exif/NikonType2MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/NikonType2MakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/NikonType2MakernoteDirectory.java	(revision 6127)
@@ -1,23 +1,26 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   metadata_extractor [at] drewnoakes [dot] com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 3-Oct-2002 10:10:47 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.lang.Rational;
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
 
 import java.util.HashMap;
@@ -26,17 +29,19 @@
  * Describes tags specific to Nikon (type 2) cameras.  Type-2 applies to the E990 and D-series cameras such as the E990, D1,
  * D70 and D100.
- *
+ * <p/>
  * Thanks to Fabrizio Giudici for publishing his reverse-engineering of the D100 makernote data.
  * http://www.timelesswanderings.net/equipment/D100/NEF.html
- *
+ * <p/>
  * Note that the camera implements image protection (locking images) via the file's 'readonly' attribute.  Similarly
  * image hiding uses the 'hidden' attribute (observed on the D70).  Consequently, these values are not available here.
- *
+ * <p/>
  * Additional sample images have been observed, and their tag values recorded in javadoc comments for each tag's field.
  * New tags have subsequently been added since Fabrizio's observations.
- *
+ * <p/>
  * In earlier models (such as the E990 and D1), this directory begins at the first byte of the makernote IFD.  In
  * later models, the IFD was given the standard prefix to indicate the camera models (most other manufacturers also
  * provide this prefix to aid in software decoding).
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class NikonType2MakernoteDirectory extends Directory
@@ -57,75 +62,125 @@
 
     /**
-     * Values observed
-     * - COLOR (seen in the D1X)
+     * The camera's color mode, as an uppercase string.  Examples include:
+     * <ul>
+     * <li><code>B & W</code></li>
+     * <li><code>COLOR</code></li>
+     * <li><code>COOL</code></li>
+     * <li><code>SEPIA</code></li>
+     * <li><code>VIVID</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_COLOR_MODE = 0x0003;
 
     /**
-     * Values observed
-     * - FILE
-     * - RAW
-     * - NORMAL
-     * - FINE
+     * The camera's quality setting, as an uppercase string.  Examples include:
+     * <ul>
+     * <li><code>BASIC</code></li>
+     * <li><code>FINE</code></li>
+     * <li><code>NORMAL</code></li>
+     * <li><code>RAW</code></li>
+     * <li><code>RAW2.7M</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_QUALITY_AND_FILE_FORMAT = 0x0004;
 
     /**
-     * The white balance as set in the camera.
-     *
-     * Values observed
-     * - AUTO
-     * - SUNNY (D70)
-     * - FLASH (D1X)
-     * (presumably also SHADOW / INCANDESCENT / FLUORESCENT / CLOUDY)
+     * The camera's white balance setting, as an uppercase string.  Examples include:
+     *
+     * <ul>
+     * <li><code>AUTO</code></li>
+     * <li><code>CLOUDY</code></li>
+     * <li><code>FLASH</code></li>
+     * <li><code>FLUORESCENT</code></li>
+     * <li><code>INCANDESCENT</code></li>
+     * <li><code>PRESET</code></li>
+     * <li><code>PRESET0</code></li>
+     * <li><code>PRESET1</code></li>
+     * <li><code>PRESET3</code></li>
+     * <li><code>SUNNY</code></li>
+     * <li><code>WHITE PRESET</code></li>
+     * <li><code>4350K</code></li>
+     * <li><code>5000K</code></li>
+     * <li><code>DAY WHITE FL</code></li>
+     * <li><code>SHADE</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE  = 0x0005;
 
     /**
-     * The sharpening as set in the camera.
-     *
-     * Values observed
-     * - AUTO
-     * - NORMAL (D70)
-     * - NONE (D1X)
+     * The camera's sharpening setting, as an uppercase string.  Examples include:
+     *
+     * <ul>
+     * <li><code>AUTO</code></li>
+     * <li><code>HIGH</code></li>
+     * <li><code>LOW</code></li>
+     * <li><code>NONE</code></li>
+     * <li><code>NORMAL</code></li>
+     * <li><code>MED.H</code></li>
+     * <li><code>MED.L</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_CAMERA_SHARPENING = 0x0006;
 
     /**
-     * The auto-focus type used by the camera.
-     *
-     * Values observed
-     * - AF-S
-     * - AF-C
-     * - MANUAL
+     * The camera's auto-focus mode, as an uppercase string.  Examples include:
+     *
+     * <ul>
+     * <li><code>AF-C</code></li>
+     * <li><code>AF-S</code></li>
+     * <li><code>MANUAL</code></li>
+     * <li><code>AF-A</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_AF_TYPE = 0x0007;
 
     /**
-     * Values observed
-     * - NORMAL
-     * - RED-EYE
-     *
-     * Note: when TAG_NIKON_TYPE2_AUTO_FLASH_MODE is blank, Nikon Browser displays "Flash Sync Mode: Not Attached"
+     * The camera's flash setting, as an uppercase string.  Examples include:
+     *
+     * <ul>
+     * <li><code></code></li>
+     * <li><code>NORMAL</code></li>
+     * <li><code>RED-EYE</code></li>
+     * <li><code>SLOW</code></li>
+     * <li><code>NEW_TTL</code></li>
+     * <li><code>REAR</code></li>
+     * <li><code>REAR SLOW</code></li>
+     * </ul>
+     * Note: when TAG_NIKON_TYPE2_AUTO_FLASH_MODE is blank (whitespace), Nikon Browser displays "Flash Sync Mode: Not Attached"
      */
     public static final int TAG_NIKON_TYPE2_FLASH_SYNC_MODE = 0x0008;
 
     /**
-     * Values observed
-     * - Built-in,TTL
-     * - Optional,TTL (with speedlight SB800, flash sync mode as NORMAL.  NikonBrowser reports Auto Flash Comp: 0 EV -- which tag is that?) (D70)
-     * - NEW_TTL (Nikon Browser interprets as "D-TTL")
-     * - (blank -- accompanied FlashSyncMode of NORMAL) (D70)
+     * The type of flash used in the photograph, as a string.  Examples include:
+     * 
+     * <ul>
+     * <li><code></code></li>
+     * <li><code>Built-in,TTL</code></li>
+     * <li><code>NEW_TTL</code> Nikon Browser interprets as "D-TTL"</li>
+     * <li><code>Built-in,M</code></li>
+     * <li><code>Optional,TTL</code> with speedlight SB800, flash sync mode as "NORMAL"</li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_AUTO_FLASH_MODE = 0x0009;
 
     /**
-     * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
+     * An unknown tag, as a rational.  Several values given here:
+     * http://gvsoft.homedns.org/exif/makernote-nikon-type2.html#0x000b
      */
     public static final int TAG_NIKON_TYPE2_UNKNOWN_34 = 0x000A;
 
     /**
-     * Values observed
-     * - 0
+     * The camera's white balance bias setting, as an uint16 array having either one or two elements.
+     *
+     * <ul>
+     * <li><code>0</code></li>
+     * <li><code>1</code></li>
+     * <li><code>-3</code></li>
+     * <li><code>-2</code></li>
+     * <li><code>-1</code></li>
+     * <li><code>0,0</code></li>
+     * <li><code>1,0</code></li>
+     * <li><code>5,-5</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE_FINE = 0x000B;
@@ -143,98 +198,256 @@
 
     /**
-     * Values observed
-     * - 0,1,6,0 (hex)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_1 = 0x000D;
-
-    /**
-     * Values observed
-     * - 0,1,c,0 (hex)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_2 = 0x000E;
+     * The camera's program shift setting, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>0,1,3,0</code> = 0 EV</li>
+     * <li><code>1,1,3,0</code> = 0.33 EV</li>
+     * <li><code>-3,1,3,0</code> = -1 EV</li>
+     * <li><code>1,1,2,0</code> = 0.5 EV</li>
+     * <li><code>2,1,6,0</code> = 0.33 EV</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_PROGRAM_SHIFT = 0x000D;
+
+    /**
+     * The exposure difference, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>-105,1,12,0</code> = -8.75 EV</li>
+     * <li><code>-72,1,12,0</code> = -6.00 EV</li>
+     * <li><code>-11,1,12,0</code> = -0.92 EV</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_EXPOSURE_DIFFERENCE = 0x000E;
+
+    /**
+     * The camera's ISO mode, as an uppercase string.
+     *
+     * <ul>
+     * <li><code>AUTO</code></code></li>
+     * <li><code>MANUAL</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_ISO_MODE = 0x000F;
 
     /**
      * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
      */
-    public static final int TAG_NIKON_TYPE2_ISO_SELECTION = 0x000F;
-
-    /**
-     * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
-     */
     public static final int TAG_NIKON_TYPE2_DATA_DUMP = 0x0010;
 
     /**
-     * Values observed
-     * - 914
-     * - 1379 (D70)
-     * - 2781 (D1X)
-     * - 6942 (D100)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_3 = 0x0011;
-
-    /**
-     * Values observed
-     * - (no value -- blank)
+     * Preview to another IFD (?)
+     * <p/>
+     * Details here: http://gvsoft.homedns.org/exif/makernote-nikon-2-tag0x0011.html
+     * // TODO if this is another IFD, decode it
+     */
+    public static final int TAG_NIKON_TYPE2_PREVIEW_IFD = 0x0011;
+
+    /**
+     * The flash compensation, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>-18,1,6,0</code> = -3 EV</li>
+     * <li><code>4,1,6,0</code> = 0.67 EV</li>
+     * <li><code>6,1,6,0</code> = 1 EV</li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_AUTO_FLASH_COMPENSATION = 0x0012;
 
     /**
-     * Values observed
-     * - 0 250
-     * - 0 400
-     */
-    public static final int TAG_NIKON_TYPE2_ISO_2 = 0x0013;
-
-    /**
-     * Values observed
+     * The requested ISO value, as an array of two integers.
+     *
+     * <ul>
+     * <li><code>0,0</code></li>
+     * <li><code>0,125</code></li>
+     * <li><code>1,2500</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_ISO_REQUESTED = 0x0013;
+
+    /**
+     * Defines the photo corner coordinates, in 8 bytes.  Treated as four 16-bit integers, they
+     * decode as: top-left (x,y); bot-right (x,y)
      * - 0 0 49163 53255
      * - 0 0 3008 2000 (the image dimensions were 3008x2000) (D70)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_21 = 0x0016;
-
-    /**
-     * Values observed
-     * - (blank)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_22 = 0x0017;
-
-    /**
-     * Values observed
-     * - (blank)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_23 = 0x0018;
-
-    /**
-     * Values observed
-     * - 0
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_24 = 0x0019;
-
-    /**
-     * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
+     * <ul>
+     * <li><code>0,0,4288,2848</code> The max resolution of the D300 camera</li>
+     * <li><code>0,0,3008,2000</code> The max resolution of the D70 camera</li>
+     * <li><code>0,0,4256,2832</code> The max resolution of the D3 camera</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_IMAGE_BOUNDARY = 0x0016;
+
+    /**
+     * The flash exposure compensation, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>0,0,0,0</code> = 0 EV</li>
+     * <li><code>0,1,6,0</code> = 0 EV</li>
+     * <li><code>4,1,6,0</code> = 0.67 EV</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_FLASH_EXPOSURE_COMPENSATION = 0x0017;
+
+    /**
+     * The flash bracket compensation, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>0,0,0,0</code> = 0 EV</li>
+     * <li><code>0,1,6,0</code> = 0 EV</li>
+     * <li><code>4,1,6,0</code> = 0.67 EV</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_FLASH_BRACKET_COMPENSATION = 0x0018;
+
+    /**
+     * The AE bracket compensation, as a rational number.
+     *
+     * <ul>
+     * <li><code>0/0</code></li>
+     * <li><code>0/1</code></li>
+     * <li><code>0/6</code></li>
+     * <li><code>4/6</code></li>
+     * <li><code>6/6</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_AE_BRACKET_COMPENSATION = 0x0019;
+
+    /**
+     * Flash mode, as a string.
+     *
+     * <ul>
+     * <li><code></code></li>
+     * <li><code>Red Eye Reduction</code></li>
+     * <li><code>D-Lighting</code></li>
+     * <li><code>Distortion control</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_FLASH_MODE = 0x001a;
+
+    public static final int TAG_NIKON_TYPE2_CROP_HIGH_SPEED = 0x001b;
+    public static final int TAG_NIKON_TYPE2_EXPOSURE_TUNING = 0x001c;
+
+    /**
+     * The camera's serial number, as a string.
+     * Note that D200 is always blank, and D50 is always <code>"D50"</code>.
+     */
+    public static final int TAG_NIKON_TYPE2_CAMERA_SERIAL_NUMBER = 0x001d;
+
+    /**
+     * The camera's color space setting.
+     *
+     * <ul>
+     * <li><code>1</code> sRGB</li>
+     * <li><code>2</code> Adobe RGB</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_COLOR_SPACE = 0x001e;
+    public static final int TAG_NIKON_TYPE2_VR_INFO = 0x001f;
+    public static final int TAG_NIKON_TYPE2_IMAGE_AUTHENTICATION = 0x0020;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_35 = 0x0021;
+
+    /**
+     * The active D-Lighting setting.
+     *
+     * <ul>
+     * <li><code>0</code> Off</li>
+     * <li><code>1</code> Low</li>
+     * <li><code>3</code> Normal</li>
+     * <li><code>5</code> High</li>
+     * <li><code>7</code> Extra High</li>
+     * <li><code>65535</code> Auto</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_ACTIVE_D_LIGHTING = 0x0022;
+    public static final int TAG_NIKON_TYPE2_PICTURE_CONTROL = 0x0023;
+    public static final int TAG_NIKON_TYPE2_WORLD_TIME = 0x0024;
+    public static final int TAG_NIKON_TYPE2_ISO_INFO = 0x0025;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_36 = 0x0026;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_37 = 0x0027;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_38 = 0x0028;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_39 = 0x0029;
+
+    /**
+     * The camera's vignette control setting.
+     *
+     * <ul>
+     * <li><code>0</code> Off</li>
+     * <li><code>1</code> Low</li>
+     * <li><code>3</code> Normal</li>
+     * <li><code>5</code> High</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_VIGNETTE_CONTROL = 0x002a;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_40 = 0x002b;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_41 = 0x002c;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_42 = 0x002d;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_43 = 0x002e;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_44 = 0x002f;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_45 = 0x0030;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_46 = 0x0031;
+
+    /**
+     * The camera's image adjustment setting, as a string.
+     *
+     * <ul>
+     * <li><code>AUTO</code></li>
+     * <li><code>CONTRAST(+)</code></li>
+     * <li><code>CONTRAST(-)</code></li>
+     * <li><code>NORMAL</code></li>
+     * <li><code>B & W</code></li>
+     * <li><code>BRIGHTNESS(+)</code></li>
+     * <li><code>BRIGHTNESS(-)</code></li>
+     * <li><code>SEPIA</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_IMAGE_ADJUSTMENT = 0x0080;
 
     /**
-     * The tone compensation as set in the camera.
-     *
-     * Values observed
-     * - AUTO
-     * - NORMAL (D1X, D100)
+     * The camera's tone compensation setting, as a string.
+     *
+     * <ul>
+     * <li><code>NORMAL</code></li>
+     * <li><code>LOW</code></li>
+     * <li><code>MED.L</code></li>
+     * <li><code>MED.H</code></li>
+     * <li><code>HIGH</code></li>
+     * <li><code>AUTO</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_CAMERA_TONE_COMPENSATION = 0x0081;
 
     /**
-     * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
+     * A description of any auxiliary lens, as a string.
+     *
+     * <ul>
+     * <li><code>OFF</code></li>
+     * <li><code>FISHEYE 1</code></li>
+     * <li><code>FISHEYE 2</code></li>
+     * <li><code>TELEPHOTO 2</code></li>
+     * <li><code>WIDE ADAPTER</code></li>
+     * </ul>
      */
     public static final int TAG_NIKON_TYPE2_ADAPTER = 0x0082;
 
     /**
-     * Values observed
-     * - 6
-     * - 6 (D70)
-     * - 2 (D1X)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_4 = 0x0083;
+     * The type of lens used, as a byte.
+     *
+     * <ul>
+     * <li><code>0x00</code> AF</li>
+     * <li><code>0x01</code> MF</li>
+     * <li><code>0x02</code> D</li>
+     * <li><code>0x06</code> G, D</li>
+     * <li><code>0x08</code> VR</li>
+     * <li><code>0x0a</code> VR, D</li>
+     * <li><code>0x0e</code> VR, G, D</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_LENS_TYPE = 0x0083;
 
     /**
@@ -260,249 +473,433 @@
 
     /**
-     * Added during merge of Type2 & Type3.  May apply to earlier models, such as E990 and D1.
+     * The amount of digital zoom used.
      */
     public static final int TAG_NIKON_TYPE2_DIGITAL_ZOOM = 0x0086;
 
     /**
+     * Whether the flash was used in this image.
+     *
+     * <ul>
+     * <li><code>0</code> Flash Not Used</li>
+     * <li><code>1</code> Manual Flash</li>
+     * <li><code>3</code> Flash Not Ready</li>
+     * <li><code>7</code> External Flash</li>
+     * <li><code>8</code> Fired, Commander Mode</li>
+     * <li><code>9</code> Fired, TTL Mode</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_FLASH_USED = 0x0087;
+
+    /**
+     * The position of the autofocus target.
+     */
+    public static final int TAG_NIKON_TYPE2_AF_FOCUS_POSITION = 0x0088;
+
+    /**
+     * The camera's shooting mode.
+     * <p/>
+     * A bit-array with:
+     * <ul>
+     * <li><code>0</code> Single Frame</li>
+     * <li><code>1</code> Continuous</li>
+     * <li><code>2</code> Delay</li>
+     * <li><code>8</code> PC Control</li>
+     * <li><code>16</code> Exposure Bracketing</li>
+     * <li><code>32</code> Auto ISO</li>
+     * <li><code>64</code> White-Balance Bracketing</li>
+     * <li><code>128</code> IR Control</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_SHOOTING_MODE = 0x0089;
+
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_20 = 0x008A;
+
+    /**
+     * Lens stops, as an array of four integers.
+     * The value, in EV, is calculated as <code>a*b/c</code>.
+     *
+     * <ul>
+     * <li><code>64,1,12,0</code> = 5.33 EV</li>
+     * <li><code>72,1,12,0</code> = 6 EV</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_LENS_STOPS = 0x008B;
+
+    public static final int TAG_NIKON_TYPE2_CONTRAST_CURVE = 0x008C;
+
+    /**
+     * The color space as set in the camera, as a string.
+     *
+     * <ul>
+     * <li><code>MODE1</code> = Mode 1 (sRGB)</li>
+     * <li><code>MODE1a</code> = Mode 1 (sRGB)</li>
+     * <li><code>MODE2</code> = Mode 2 (Adobe RGB)</li>
+     * <li><code>MODE3</code> = Mode 2 (sRGB): Higher Saturation</li>
+     * <li><code>MODE3a</code> = Mode 2 (sRGB): Higher Saturation</li>
+     * <li><code>B & W</code> = B & W</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_CAMERA_COLOR_MODE = 0x008D;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_47 = 0x008E;
+
+    /**
+     * The camera's scene mode, as a string.  Examples include:
+     * <ul>
+     * <li><code>BEACH/SNOW</code></li>
+     * <li><code>CLOSE UP</code></li>
+     * <li><code>NIGHT PORTRAIT</code></li>
+     * <li><code>PORTRAIT</code></li>
+     * <li><code>ANTI-SHAKE</code></li>
+     * <li><code>BACK LIGHT</code></li>
+     * <li><code>BEST FACE</code></li>
+     * <li><code>BEST</code></li>
+     * <li><code>COPY</code></li>
+     * <li><code>DAWN/DUSK</code></li>
+     * <li><code>FACE-PRIORITY</code></li>
+     * <li><code>FIREWORKS</code></li>
+     * <li><code>FOOD</code></li>
+     * <li><code>HIGH SENS.</code></li>
+     * <li><code>LAND SCAPE</code></li>
+     * <li><code>MUSEUM</code></li>
+     * <li><code>PANORAMA ASSIST</code></li>
+     * <li><code>PARTY/INDOOR</code></li>
+     * <li><code>SCENE AUTO</code></li>
+     * <li><code>SMILE</code></li>
+     * <li><code>SPORT</code></li>
+     * <li><code>SPORT CONT.</code></li>
+     * <li><code>SUNSET</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_SCENE_MODE = 0x008F;
+
+    /**
+     * The lighting type, as a string.  Examples include:
+     * <ul>
+     * <li><code></code></li>
+     * <li><code>NATURAL</code></li>
+     * <li><code>SPEEDLIGHT</code></li>
+     * <li><code>COLORED</code></li>
+     * <li><code>MIXED</code></li>
+     * <li><code>NORMAL</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_LIGHT_SOURCE = 0x0090;
+
+    /**
+     * Advertised as ASCII, but actually isn't.  A variable number of bytes (eg. 18 to 533).  Actual number of bytes
+     * appears fixed for a given camera model.
+     */
+    public static final int TAG_NIKON_TYPE2_SHOT_INFO = 0x0091;
+
+    /**
+     * The hue adjustment as set in the camera.  Values observed are either 0 or 3.
+     */
+    public static final int TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT = 0x0092;
+    /**
+     * The NEF (RAW) compression.  Examples include:
+     * <ul>
+     * <li><code>1</code> Lossy (Type 1)</li>
+     * <li><code>2</code> Uncompressed</li>
+     * <li><code>3</code> Lossless</li>
+     * <li><code>4</code> Lossy (Type 2)</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_NEF_COMPRESSION = 0x0093;
+    
+    /**
+     * The saturation level, as a signed integer.  Examples include:
+     * <ul>
+     * <li><code>+3</code></li>
+     * <li><code>+2</code></li>
+     * <li><code>+1</code></li>
+     * <li><code>0</code> Normal</li>
+     * <li><code>-1</code></li>
+     * <li><code>-2</code></li>
+     * <li><code>-3</code> (B&W)</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_SATURATION = 0x0094;
+
+    /**
+     * The type of noise reduction, as a string.  Examples include:
+     * <ul>
+     * <li><code>OFF</code></li>
+     * <li><code>FPNR</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_NOISE_REDUCTION = 0x0095;
+    public static final int TAG_NIKON_TYPE2_LINEARIZATION_TABLE = 0x0096;
+    public static final int TAG_NIKON_TYPE2_COLOR_BALANCE = 0x0097;
+    public static final int TAG_NIKON_TYPE2_LENS_DATA = 0x0098;
+
+    /** The NEF (RAW) thumbnail size, as an integer array with two items representing [width,height]. */
+    public static final int TAG_NIKON_TYPE2_NEF_THUMBNAIL_SIZE = 0x0099;
+
+    /** The sensor pixel size, as a pair of rational numbers. */
+    public static final int TAG_NIKON_TYPE2_SENSOR_PIXEL_SIZE = 0x009A;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_10 = 0x009B;
+    public static final int TAG_NIKON_TYPE2_SCENE_ASSIST = 0x009C;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_11 = 0x009D;
+    public static final int TAG_NIKON_TYPE2_RETOUCH_HISTORY = 0x009E;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_12 = 0x009F;
+
+    /**
+     * The camera serial number, as a string.
+     * <ul>
+     * <li><code>NO= 00002539</code></li>
+     * <li><code>NO= -1000d71</code></li>
+     * <li><code>PKG597230621263</code></li>
+     * <li><code>PKG5995671330625116</code></li>
+     * <li><code>PKG49981281631130677</code></li>
+     * <li><code>BU672230725063</code></li>
+     * <li><code>NO= 200332c7</code></li>
+     * <li><code>NO= 30045efe</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_CAMERA_SERIAL_NUMBER_2 = 0x00A0;
+
+    public static final int TAG_NIKON_TYPE2_IMAGE_DATA_SIZE = 0x00A2;
+
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_27 = 0x00A3;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_28 = 0x00A4;
+    public static final int TAG_NIKON_TYPE2_IMAGE_COUNT = 0x00A5;
+    public static final int TAG_NIKON_TYPE2_DELETED_IMAGE_COUNT = 0x00A6;
+
+    /** The number of total shutter releases.  This value increments for each exposure (observed on D70). */
+    public static final int TAG_NIKON_TYPE2_EXPOSURE_SEQUENCE_NUMBER = 0x00A7;
+
+    public static final int TAG_NIKON_TYPE2_FLASH_INFO = 0x00A8;
+    /**
+     * The camera's image optimisation, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>NORMAL</code></li>
+     *     <li><code>CUSTOM</code></li>
+     *     <li><code>BLACK AND WHITE</code></li>
+     *     <li><code>LAND SCAPE</code></li>
+     *     <li><code>MORE VIVID</code></li>
+     *     <li><code>PORTRAIT</code></li>
+     *     <li><code>SOFT</code></li>
+     *     <li><code>VIVID</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_IMAGE_OPTIMISATION = 0x00A9;
+
+    /**
+     * The camera's saturation level, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>NORMAL</code></li>
+     *     <li><code>AUTO</code></li>
+     *     <li><code>ENHANCED</code></li>
+     *     <li><code>MODERATE</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_SATURATION_2 = 0x00AA;
+
+    /**
+     * The camera's digital vari-program setting, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>AUTO</code></li>
+     *     <li><code>AUTO(FLASH OFF)</code></li>
+     *     <li><code>CLOSE UP</code></li>
+     *     <li><code>LANDSCAPE</code></li>
+     *     <li><code>NIGHT PORTRAIT</code></li>
+     *     <li><code>PORTRAIT</code></li>
+     *     <li><code>SPORT</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_DIGITAL_VARI_PROGRAM = 0x00AB;
+
+    /**
+     * The camera's digital vari-program setting, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>VR-ON</code></li>
+     *     <li><code>VR-OFF</code></li>
+     *     <li><code>VR-HYBRID</code></li>
+     *     <li><code>VR-ACTIVE</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_IMAGE_STABILISATION = 0x00AC;
+
+    /**
+     * The camera's digital vari-program setting, as a string.
+     * <ul>
+     *     <li><code></code></li>
+     *     <li><code>HYBRID</code></li>
+     *     <li><code>STANDARD</code></li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_AF_RESPONSE = 0x00AD;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_29 = 0x00AE;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_30 = 0x00AF;
+    public static final int TAG_NIKON_TYPE2_MULTI_EXPOSURE = 0x00B0;
+
+    /**
+     * The camera's high ISO noise reduction setting, as an integer.
+     * <ul>
+     *     <li><code>0</code> Off</li>
+     *     <li><code>1</code> Minimal</li>
+     *     <li><code>2</code> Low</li>
+     *     <li><code>4</code> Normal</li>
+     *     <li><code>6</code> High</li>
+     * </ul>
+     */
+    public static final int TAG_NIKON_TYPE2_HIGH_ISO_NOISE_REDUCTION = 0x00B1;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_31 = 0x00B2;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_32 = 0x00B3;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_33 = 0x00B4;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_48 = 0x00B5;
+    public static final int TAG_NIKON_TYPE2_POWER_UP_TIME = 0x00B6;
+    public static final int TAG_NIKON_TYPE2_AF_INFO_2 = 0x00B7;
+    public static final int TAG_NIKON_TYPE2_FILE_INFO = 0x00B8;
+    public static final int TAG_NIKON_TYPE2_AF_TUNE = 0x00B9;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_49 = 0x00BB;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_50 = 0x00BD;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_51 = 0x0103;
+    public static final int TAG_NIKON_TYPE2_PRINT_IM = 0x0E00;
+
+    /**
+     * Data about changes set by Nikon Capture Editor.
+     *
      * Values observed
-     * - 0
-     * - 9
-     * - 3 (D1X)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_5 = 0x0087;
-
-    /**
-     * Values observed
-     * -
-     */
-    public static final int TAG_NIKON_TYPE2_AF_FOCUS_POSITION = 0x0088;
-
-    /**
-     * Values observed
-     * - 0
-     * - 1
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_7 = 0x0089;
-
-    /**
-     * Values observed
-     * - 0
-     * - 0
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_20 = 0x008A;
-
-    /**
-     * Values observed
-     * - 48,1,c,0 (hex) (D100)
-     * - @ <hex>
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_8 = 0x008B;
-
-    /**
-     * Unknown.  Fabrizio believes this may be a lookup table for the user-defined curve.
-     *
-     * Values observed
-     * - (blank) (D1X)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_9 = 0x008C;
-
-    /**
-     * The color space as set in the camera.
-     *
-     * Values observed
-     * - MODE1
-     * - Mode I (sRGB) (D70)
-     * - MODE2 (D1X, D100)
-     */
-    public static final int TAG_NIKON_TYPE2_CAMERA_COLOR_MODE = 0x008D;
-
-    /**
-     * Values observed
-     * - NATURAL
-     * - SPEEDLIGHT (D70, D1X)
-     */
-    public static final int TAG_NIKON_TYPE2_LIGHT_SOURCE = 0x0090;
-
-    /**
-     * Values observed
-     * - 0100 <hex>
-     * - 0103 (D70)
-     * - 0100 (D1X)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_11 = 0x0091;
-
-    /**
-     * The hue adjustment as set in the camera.
-     *
-     * Values observed
-     * - 0
-     */
-    public static final int TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT = 0x0092;
-
-    /**
-     * Values observed
-     * - OFF
-     */
-    public static final int TAG_NIKON_TYPE2_NOISE_REDUCTION = 0x0095;
-
-    /**
-     * Values observed
-     * - 0100 <hex>
-     * - 0103 <hex>
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_12 = 0x0097;
-
-    /**
-     * Values observed
-     * - 0100 <hex>
-     * - 0101 <hex>
-     * - 0100 <hex> (D1X)
-     * - 30,31,30,30,0,0,b,48,7c,7c,24,24,5,15,24,0,0,0,0,0 (hex) (D100)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_13 = 0x0098;
-
-    /**
-     * Values observed
-     * - 2014 662 (D1X)
-     * - 1517,1012 (D100)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_14 = 0x0099;
-
-    /**
-     * Values observed
-     * - 78/10 78/10
-     * - 78/10 78/10 (D70)
-     * - 59/10 59/5 (D1X)
-     * - 7.8,7.8 (D100)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_15 = 0x009A;
-
-    /**
-     * Values observed
-     * - NO= 00002539
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_25 = 0x00A0;
-
-    /**
-     * Values observed
-     * - 1564851
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_26 = 0x00A2;
-
-    /**
-     * Values observed
-     * - 0
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_27 = 0x00A3;
-
-    /**
-     * This appears to be a sequence number to indentify the exposure.  This value seems to increment
-     * for consecutive exposures (observed on D70).
-     *
-     * Values observed
-     * - 5062
-     */
-    public static final int TAG_NIKON_TYPE2_EXPOSURE_SEQUENCE_NUMBER = 0x00A7;
-
-    /**
-     * Values observed
-     * - 0100 (D70)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_32 = 0x00A8;
-
-    /**
-     * Values observed
-     * - NORMAL (D70)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_33 = 0x00A9;
-
-    /**
-     * Nikon Browser suggests this value represents Saturation...
-     * Values observed
-     * - NORMAL (D70)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_29 = 0x00AA;
-
-    /**
-     * Values observed
-     * - AUTO (D70)
-     * - (blank) (D70)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_30 = 0x00AB;
-
-    /**
-     * Data about changes set by Nikon Capture Editor.
-     *
-     * Values observed
-     */
-    public static final int TAG_NIKON_TYPE2_CAPTURE_EDITOR_DATA = 0x0E01;
-
-    /**
-     * Values observed
-     * - 1473
-     * - 7036 (D100)
-     */
-    public static final int TAG_NIKON_TYPE2_UNKNOWN_16 = 0x0E10;
-
-    protected static final HashMap _tagNameMap = new HashMap();
+     */
+    public static final int TAG_NIKON_TYPE2_NIKON_CAPTURE_DATA = 0x0E01;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_52 = 0x0E05;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_53 = 0x0E08;
+    public static final int TAG_NIKON_TYPE2_NIKON_CAPTURE_VERSION = 0x0E09;
+    public static final int TAG_NIKON_TYPE2_NIKON_CAPTURE_OFFSETS = 0x0E0E;
+    public static final int TAG_NIKON_TYPE2_NIKON_SCAN = 0x0E10;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_54 = 0x0E19;
+    public static final int TAG_NIKON_TYPE2_NEF_BIT_DEPTH = 0x0E22;
+    public static final int TAG_NIKON_TYPE2_UNKNOWN_55 = 0x0E23;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_FIRMWARE_VERSION), "Firmware Version");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_ISO_1), "ISO");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_QUALITY_AND_FILE_FORMAT), "Quality & File Format");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE), "White Balance");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_CAMERA_SHARPENING), "Sharpening");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_AF_TYPE), "AF Type");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE_FINE), "White Balance Fine");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE_RB_COEFF), "White Balance RB Coefficients");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_ISO_2), "ISO");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_ISO_SELECTION), "ISO Selection");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_DATA_DUMP), "Data Dump");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_IMAGE_ADJUSTMENT), "Image Adjustment");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_CAMERA_TONE_COMPENSATION), "Tone Compensation");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_ADAPTER), "Adapter");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_LENS), "Lens");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_MANUAL_FOCUS_DISTANCE), "Manual Focus Distance");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_DIGITAL_ZOOM), "Digital Zoom");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_CAMERA_COLOR_MODE), "Colour Mode");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT), "Camera Hue Adjustment");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_NOISE_REDUCTION), "Noise Reduction");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_CAPTURE_EDITOR_DATA), "Capture Editor Data");
-
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_1), "Unknown 01");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_2), "Unknown 02");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_3), "Unknown 03");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_4), "Unknown 04");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_5), "Unknown 05");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_AF_FOCUS_POSITION), "AF Focus Position");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_7), "Unknown 07");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_8), "Unknown 08");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_9), "Unknown 09");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_LIGHT_SOURCE), "Light source");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_11), "Unknown 11");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_12), "Unknown 12");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_13), "Unknown 13");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_14), "Unknown 14");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_15), "Unknown 15");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_16), "Unknown 16");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_FLASH_SYNC_MODE), "Flash Sync Mode");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_AUTO_FLASH_MODE), "Auto Flash Mode");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_AUTO_FLASH_COMPENSATION), "Auto Flash Compensation");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_EXPOSURE_SEQUENCE_NUMBER), "Exposure Sequence Number");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_COLOR_MODE), "Color Mode");
-
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_20), "Unknown 20");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_21), "Unknown 21");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_22), "Unknown 22");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_23), "Unknown 23");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_24), "Unknown 24");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_25), "Unknown 25");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_26), "Unknown 26");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_27), "Unknown 27");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_29), "Unknown 29");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_30), "Unknown 30");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_32), "Unknown 32");
-        _tagNameMap.put(new Integer(TAG_NIKON_TYPE2_UNKNOWN_33), "Unknown 33");
+        _tagNameMap.put(TAG_NIKON_TYPE2_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_NIKON_TYPE2_ISO_1, "ISO");
+        _tagNameMap.put(TAG_NIKON_TYPE2_QUALITY_AND_FILE_FORMAT, "Quality & File Format");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_SHARPENING, "Sharpening");
+        _tagNameMap.put(TAG_NIKON_TYPE2_AF_TYPE, "AF Type");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE_FINE, "White Balance Fine");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_WHITE_BALANCE_RB_COEFF, "White Balance RB Coefficients");
+        _tagNameMap.put(TAG_NIKON_TYPE2_ISO_REQUESTED, "ISO");
+        _tagNameMap.put(TAG_NIKON_TYPE2_ISO_MODE, "ISO Mode");
+        _tagNameMap.put(TAG_NIKON_TYPE2_DATA_DUMP, "Data Dump");
+
+        _tagNameMap.put(TAG_NIKON_TYPE2_PROGRAM_SHIFT, "Program Shift");
+        _tagNameMap.put(TAG_NIKON_TYPE2_EXPOSURE_DIFFERENCE, "Exposure Difference");
+        _tagNameMap.put(TAG_NIKON_TYPE2_PREVIEW_IFD, "Preview IFD");
+        _tagNameMap.put(TAG_NIKON_TYPE2_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_NIKON_TYPE2_FLASH_USED, "Flash Used");
+        _tagNameMap.put(TAG_NIKON_TYPE2_AF_FOCUS_POSITION, "AF Focus Position");
+        _tagNameMap.put(TAG_NIKON_TYPE2_SHOOTING_MODE, "Shooting Mode");
+        _tagNameMap.put(TAG_NIKON_TYPE2_LENS_STOPS, "Lens Stops");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CONTRAST_CURVE, "Contrast Curve");
+        _tagNameMap.put(TAG_NIKON_TYPE2_LIGHT_SOURCE, "Light source");
+        _tagNameMap.put(TAG_NIKON_TYPE2_SHOT_INFO, "Shot Info");
+        _tagNameMap.put(TAG_NIKON_TYPE2_COLOR_BALANCE, "Color Balance");
+        _tagNameMap.put(TAG_NIKON_TYPE2_LENS_DATA, "Lens Data");
+        _tagNameMap.put(TAG_NIKON_TYPE2_NEF_THUMBNAIL_SIZE, "NEF Thumbnail Size");
+        _tagNameMap.put(TAG_NIKON_TYPE2_SENSOR_PIXEL_SIZE, "Sensor Pixel Size");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_10, "Unknown 10");
+        _tagNameMap.put(TAG_NIKON_TYPE2_SCENE_ASSIST, "Scene Assist");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_11, "Unknown 11");
+        _tagNameMap.put(TAG_NIKON_TYPE2_RETOUCH_HISTORY, "Retouch History");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_12, "Unknown 12");
+        _tagNameMap.put(TAG_NIKON_TYPE2_FLASH_SYNC_MODE, "Flash Sync Mode");
+        _tagNameMap.put(TAG_NIKON_TYPE2_AUTO_FLASH_MODE, "Auto Flash Mode");
+        _tagNameMap.put(TAG_NIKON_TYPE2_AUTO_FLASH_COMPENSATION, "Auto Flash Compensation");
+        _tagNameMap.put(TAG_NIKON_TYPE2_EXPOSURE_SEQUENCE_NUMBER, "Exposure Sequence Number");
+        _tagNameMap.put(TAG_NIKON_TYPE2_COLOR_MODE, "Color Mode");
+
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_20, "Unknown 20");
+        _tagNameMap.put(TAG_NIKON_TYPE2_IMAGE_BOUNDARY, "Image Boundary");
+        _tagNameMap.put(TAG_NIKON_TYPE2_FLASH_EXPOSURE_COMPENSATION, "Flash Exposure Compensation");
+        _tagNameMap.put(TAG_NIKON_TYPE2_FLASH_BRACKET_COMPENSATION, "Flash Bracket Compensation");
+        _tagNameMap.put(TAG_NIKON_TYPE2_AE_BRACKET_COMPENSATION, "AE Bracket Compensation");
+        _tagNameMap.put(TAG_NIKON_TYPE2_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CROP_HIGH_SPEED, "Crop High Speed");
+        _tagNameMap.put(TAG_NIKON_TYPE2_EXPOSURE_TUNING, "Exposure Tuning");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_SERIAL_NUMBER, "Camera Serial Number");
+        _tagNameMap.put(TAG_NIKON_TYPE2_COLOR_SPACE, "Color Space");
+        _tagNameMap.put(TAG_NIKON_TYPE2_VR_INFO, "VR Info");
+        _tagNameMap.put(TAG_NIKON_TYPE2_IMAGE_AUTHENTICATION, "Image Authentication");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_35, "Unknown 35");
+        _tagNameMap.put(TAG_NIKON_TYPE2_ACTIVE_D_LIGHTING, "Active D-Lighting");
+        _tagNameMap.put(TAG_NIKON_TYPE2_PICTURE_CONTROL, "Picture Control");
+        _tagNameMap.put(TAG_NIKON_TYPE2_WORLD_TIME, "World Time");
+        _tagNameMap.put(TAG_NIKON_TYPE2_ISO_INFO, "ISO Info");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_36, "Unknown 36");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_37, "Unknown 37");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_38, "Unknown 38");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_39, "Unknown 39");
+        _tagNameMap.put(TAG_NIKON_TYPE2_VIGNETTE_CONTROL, "Vignette Control");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_40, "Unknown 40");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_41, "Unknown 41");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_42, "Unknown 42");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_43, "Unknown 43");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_44, "Unknown 44");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_45, "Unknown 45");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_46, "Unknown 46");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_47, "Unknown 47");
+        _tagNameMap.put(TAG_NIKON_TYPE2_SCENE_MODE, "Scene Mode");
+
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_SERIAL_NUMBER_2, "Camera Serial Number");
+        _tagNameMap.put(TAG_NIKON_TYPE2_IMAGE_DATA_SIZE, "Image Data Size");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_27, "Unknown 27");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_28, "Unknown 28");
+        _tagNameMap.put(TAG_NIKON_TYPE2_IMAGE_COUNT, "Image Count");
+        _tagNameMap.put(TAG_NIKON_TYPE2_DELETED_IMAGE_COUNT, "Deleted Image Count");
+        _tagNameMap.put(TAG_NIKON_TYPE2_SATURATION_2, "Saturation");
+        _tagNameMap.put(TAG_NIKON_TYPE2_DIGITAL_VARI_PROGRAM, "Digital Vari Program");
+        _tagNameMap.put(TAG_NIKON_TYPE2_IMAGE_STABILISATION, "Image Stabilisation");
+        _tagNameMap.put(TAG_NIKON_TYPE2_AF_RESPONSE, "AF Response");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_29, "Unknown 29");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_30, "Unknown 30");
+        _tagNameMap.put(TAG_NIKON_TYPE2_MULTI_EXPOSURE, "Multi Exposure");
+        _tagNameMap.put(TAG_NIKON_TYPE2_HIGH_ISO_NOISE_REDUCTION, "High ISO Noise Reduction");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_31, "Unknown 31");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_32, "Unknown 32");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_33, "Unknown 33");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_48, "Unknown 48");
+        _tagNameMap.put(TAG_NIKON_TYPE2_POWER_UP_TIME, "Power Up Time");
+        _tagNameMap.put(TAG_NIKON_TYPE2_AF_INFO_2, "AF Info 2");
+        _tagNameMap.put(TAG_NIKON_TYPE2_FILE_INFO, "File Info");
+        _tagNameMap.put(TAG_NIKON_TYPE2_AF_TUNE, "AF Tune");
+        _tagNameMap.put(TAG_NIKON_TYPE2_FLASH_INFO, "Flash Info");
+        _tagNameMap.put(TAG_NIKON_TYPE2_IMAGE_OPTIMISATION, "Image Optimisation");
+
+        _tagNameMap.put(TAG_NIKON_TYPE2_IMAGE_ADJUSTMENT, "Image Adjustment");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_TONE_COMPENSATION, "Tone Compensation");
+        _tagNameMap.put(TAG_NIKON_TYPE2_ADAPTER, "Adapter");
+        _tagNameMap.put(TAG_NIKON_TYPE2_LENS, "Lens");
+        _tagNameMap.put(TAG_NIKON_TYPE2_MANUAL_FOCUS_DISTANCE, "Manual Focus Distance");
+        _tagNameMap.put(TAG_NIKON_TYPE2_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_COLOR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_NIKON_TYPE2_CAMERA_HUE_ADJUSTMENT, "Camera Hue Adjustment");
+        _tagNameMap.put(TAG_NIKON_TYPE2_NEF_COMPRESSION, "NEF Compression");
+        _tagNameMap.put(TAG_NIKON_TYPE2_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_NIKON_TYPE2_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_NIKON_TYPE2_LINEARIZATION_TABLE, "Linearization Table");
+        _tagNameMap.put(TAG_NIKON_TYPE2_NIKON_CAPTURE_DATA, "Nikon Capture Data");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_49, "Unknown 49");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_50, "Unknown 50");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_51, "Unknown 51");
+        _tagNameMap.put(TAG_NIKON_TYPE2_PRINT_IM, "Print IM");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_52, "Unknown 52");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_53, "Unknown 53");
+        _tagNameMap.put(TAG_NIKON_TYPE2_NIKON_CAPTURE_VERSION, "Nikon Capture Version");
+        _tagNameMap.put(TAG_NIKON_TYPE2_NIKON_CAPTURE_OFFSETS, "Nikon Capture Offsets");
+        _tagNameMap.put(TAG_NIKON_TYPE2_NIKON_SCAN, "Nikon Scan");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_54, "Unknown 54");
+        _tagNameMap.put(TAG_NIKON_TYPE2_NEF_BIT_DEPTH, "NEF Bit Depth");
+        _tagNameMap.put(TAG_NIKON_TYPE2_UNKNOWN_55, "Unknown 55");
     }
 
@@ -512,24 +909,5 @@
     }
 
-    public Rational getAutoFlashCompensation() throws MetadataException
-    {
-        if (!containsTag(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_AUTO_FLASH_COMPENSATION))
-            return null;
-
-        byte[] bytes = getByteArray(NikonType2MakernoteDirectory.TAG_NIKON_TYPE2_AUTO_FLASH_COMPENSATION);
-        return CalculateFlashCompensationFromBytes(bytes);
-    }
-
-    public static Rational CalculateFlashCompensationFromBytes(byte[] bytes)
-    {
-        if (bytes.length==3)
-        {
-            byte denominator = bytes[2];
-            int numerator = (int)bytes[0] * bytes[1];
-            return new Rational(numerator, denominator);
-        }
-        return null;
-    }
-
+    @NotNull
     public String getName()
     {
@@ -537,5 +915,6 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
         return _tagNameMap;
Index: trunk/src/com/drew/metadata/exif/OlympusMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/OlympusMakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/OlympusMakernoteDescriptor.java	(revision 6127)
@@ -1,33 +1,42 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
- * Provides human-readable string versions of the tags stored in an OlympusMakernoteDirectory.
+ * Provides human-readable string representations of tag values stored in a <code>OlympusMakernoteDirectory</code>.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class OlympusMakernoteDescriptor extends TagDescriptor
+public class OlympusMakernoteDescriptor extends TagDescriptor<OlympusMakernoteDirectory>
 {
-    public OlympusMakernoteDescriptor(Directory directory)
+    public OlympusMakernoteDescriptor(@NotNull OlympusMakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) {
@@ -41,12 +50,14 @@
                 return getDigiZoomRatioDescription();
             default:
-                return _directory.getString(tagType);
+                return super.getDescription(tagType);
         }
     }
 
-    public String getDigiZoomRatioDescription() throws MetadataException
+    @Nullable
+    public String getDigiZoomRatioDescription()
     {
-        if (!_directory.containsTag(OlympusMakernoteDirectory.TAG_OLYMPUS_DIGI_ZOOM_RATIO)) return null;
-        int value = _directory.getInt(OlympusMakernoteDirectory.TAG_OLYMPUS_DIGI_ZOOM_RATIO);
+        Integer value = _directory.getInteger(OlympusMakernoteDirectory.TAG_OLYMPUS_DIGI_ZOOM_RATIO);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -59,8 +70,10 @@
     }
 
-    public String getMacroModeDescription() throws MetadataException
+    @Nullable
+    public String getMacroModeDescription()
     {
-        if (!_directory.containsTag(OlympusMakernoteDirectory.TAG_OLYMPUS_MACRO_MODE)) return null;
-        int value = _directory.getInt(OlympusMakernoteDirectory.TAG_OLYMPUS_MACRO_MODE);
+        Integer value = _directory.getInteger(OlympusMakernoteDirectory.TAG_OLYMPUS_MACRO_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 0:
@@ -73,8 +86,10 @@
     }
 
-    public String getJpegQualityDescription() throws MetadataException
+    @Nullable
+    public String getJpegQualityDescription()
     {
-        if (!_directory.containsTag(OlympusMakernoteDirectory.TAG_OLYMPUS_JPEG_QUALITY)) return null;
-        int value = _directory.getInt(OlympusMakernoteDirectory.TAG_OLYMPUS_JPEG_QUALITY);
+        Integer value = _directory.getInteger(OlympusMakernoteDirectory.TAG_OLYMPUS_JPEG_QUALITY);
+        if (value==null)
+            return null;
         switch (value) {
             case 1:
@@ -89,9 +104,13 @@
     }
 
-    public String getSpecialModeDescription() throws MetadataException
+    @Nullable
+    public String getSpecialModeDescription()
     {
-        if (!_directory.containsTag(OlympusMakernoteDirectory.TAG_OLYMPUS_SPECIAL_MODE)) return null;
         int[] values = _directory.getIntArray(OlympusMakernoteDirectory.TAG_OLYMPUS_SPECIAL_MODE);
-        StringBuffer desc = new StringBuffer();
+        if (values==null)
+            return null;
+        if (values.length < 1)
+            return "";
+        StringBuilder desc = new StringBuilder();
         switch (values[0]) {
             case 0:
@@ -111,4 +130,7 @@
                 break;
         }
+
+        if (values.length < 2)
+            return desc.toString();
         desc.append(" - ");
         switch (values[1]) {
@@ -117,5 +139,5 @@
                 break;
             case 1:
-                desc.append("1st in a sequnce");
+                desc.append("1st in a sequence");
                 break;
             case 2:
@@ -130,4 +152,7 @@
                 break;
         }
+        if (values.length < 3)
+            return desc.toString();
+        desc.append(" - ");
         switch (values[2]) {
             case 1:
Index: trunk/src/com/drew/metadata/exif/OlympusMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/OlympusMakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/OlympusMakernoteDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,42 +27,24 @@
 
 /**
- * The Olympus makernote is used by many manufacturers, and as such contains some tags that appear specific to
- * those manufacturers.  Other users include Konica, Minolta and Epson. 
+ * The Olympus makernote is used by many manufacturers (Konica, Minolta and Epson...), and as such contains some tags
+ * that appear specific to those manufacturers.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class OlympusMakernoteDirectory extends Directory
 {
-    /**
-     * Used by Konica / Minolta cameras.
-     */
+    /** Used by Konica / Minolta cameras. */
     public static final int TAG_OLYMPUS_MAKERNOTE_VERSION = 0x0000;
-
-    /**
-     * Used by Konica / Minolta cameras.
-     */
+    /** Used by Konica / Minolta cameras. */
     public static final int TAG_OLYMPUS_CAMERA_SETTINGS_1 = 0x0001;
-
-    /**
-     * Alternate Camera Settings Tag. Used by Konica / Minolta cameras.
-     */
+    /** Alternate Camera Settings Tag. Used by Konica / Minolta cameras. */
     public static final int TAG_OLYMPUS_CAMERA_SETTINGS_2 = 0x0003;
-
-    /**
-     * Used by Konica / Minolta cameras.
-     */
+    /** Used by Konica / Minolta cameras. */
     public static final int TAG_OLYMPUS_COMPRESSED_IMAGE_SIZE = 0x0040;
-
-    /**
-     * Used by Konica / Minolta cameras.
-     */
+    /** Used by Konica / Minolta cameras. */
     public static final int TAG_OLYMPUS_MINOLTA_THUMBNAIL_OFFSET_1 = 0x0081;
-
-    /**
-     * Alternate Thumbnail Offset. Used by Konica / Minolta cameras.
-     */
+    /** Alternate Thumbnail Offset. Used by Konica / Minolta cameras. */
     public static final int TAG_OLYMPUS_MINOLTA_THUMBNAIL_OFFSET_2 = 0x0088;
-
-    /**
-     * Length of thumbnail in bytes. Used by Konica / Minolta cameras.
-     */
+    /** Length of thumbnail in bytes. Used by Konica / Minolta cameras. */
     public static final int TAG_OLYMPUS_MINOLTA_THUMBNAIL_LENGTH = 0x0089;
 
@@ -84,5 +71,5 @@
     /**
      * Not 100% sure about this tag.
-     *
+     * <p/>
      * Used by Konica / Minolta cameras.
      * 0 = Raw
@@ -119,37 +106,12 @@
     public static final int TAG_OLYMPUS_MACRO_MODE = 0x0202;
 
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_UNKNOWN_1 = 0x0203;
 
-    /**
-     * Zoom Factor (0 or 1 = normal)
-     */
+    /** Zoom Factor (0 or 1 = normal) */
     public static final int TAG_OLYMPUS_DIGI_ZOOM_RATIO = 0x0204;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_UNKNOWN_2 = 0x0205;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_UNKNOWN_3 = 0x0206;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_FIRMWARE_VERSION = 0x0207;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_PICT_INFO = 0x0208;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_CAMERA_ID = 0x0209;
 
@@ -166,7 +128,5 @@
     public static final int TAG_OLYMPUS_IMAGE_HEIGHT = 0x020C;
 
-    /**
-     * A string. Used by Epson cameras.
-     */
+    /** A string. Used by Epson cameras. */
     public static final int TAG_OLYMPUS_ORIGINAL_MANUFACTURER_MODEL = 0x020D;
 
@@ -177,169 +137,79 @@
     public static final int TAG_OLYMPUS_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
 
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_DATA_DUMP = 0x0F00;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_FLASH_MODE = 0x1004;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_BRACKET = 0x1006;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_FOCUS_MODE = 0x100B;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_FOCUS_DISTANCE = 0x100C;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_ZOOM = 0x100D;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_MACRO_FOCUS = 0x100E;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_SHARPNESS = 0x100F;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_COLOUR_MATRIX = 0x1011;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_BLACK_LEVEL = 0x1012;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_WHITE_BALANCE = 0x1015;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_RED_BIAS = 0x1017;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_BLUE_BIAS = 0x1018;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_SERIAL_NUMBER = 0x101A;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_FLASH_BIAS = 0x1023;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_CONTRAST = 0x1029;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_SHARPNESS_FACTOR = 0x102A;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_COLOUR_CONTROL = 0x102B;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_VALID_BITS = 0x102C;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_CORING_FILTER = 0x102D;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_FINAL_WIDTH = 0x102E;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_FINAL_HEIGHT = 0x102F;
-
-    /**
-     *
-     */
     public static final int TAG_OLYMPUS_COMPRESSION_RATIO = 0x1034;
 
-    protected static final HashMap tagNameMap = new HashMap();
-
-    static
-    {
-        tagNameMap.put(new Integer(TAG_OLYMPUS_SPECIAL_MODE), "Special Mode");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_JPEG_QUALITY), "Jpeg Quality");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_MACRO_MODE), "Macro");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_UNKNOWN_1), "Makernote Unknown 1");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_DIGI_ZOOM_RATIO), "DigiZoom Ratio");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_UNKNOWN_2), "Makernote Unknown 2");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_UNKNOWN_3), "Makernote Unknown 3");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_FIRMWARE_VERSION), "Firmware Version");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_PICT_INFO), "Pict Info");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_CAMERA_ID), "Camera Id");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_DATA_DUMP), "Data Dump");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_MAKERNOTE_VERSION), "Makernote Version");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_CAMERA_SETTINGS_1), "Camera Settings");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_CAMERA_SETTINGS_2), "Camera Settings");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_COMPRESSED_IMAGE_SIZE), "Compressed Image Size");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_MINOLTA_THUMBNAIL_OFFSET_1), "Thumbnail Offset");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_MINOLTA_THUMBNAIL_OFFSET_2), "Thumbnail Offset");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_MINOLTA_THUMBNAIL_LENGTH), "Thumbnail Length");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_COLOUR_MODE), "Colour Mode");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_IMAGE_QUALITY_1), "Image Quality");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_IMAGE_QUALITY_2), "Image Quality");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_IMAGE_HEIGHT), "Image Height");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_ORIGINAL_MANUFACTURER_MODEL), "Original Manufacturer Model");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_PRINT_IMAGE_MATCHING_INFO), "Print Image Matching (PIM) Info");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_FLASH_MODE), "Flash Mode");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_BRACKET), "Bracket");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_FOCUS_MODE), "Focus Mode");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_FOCUS_DISTANCE), "Focus Distance");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_ZOOM), "Zoom");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_MACRO_FOCUS), "Macro Focus");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_SHARPNESS), "Sharpness");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_COLOUR_MATRIX), "Colour Matrix");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_BLACK_LEVEL), "Black Level");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_WHITE_BALANCE), "White Balance");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_RED_BIAS), "Red Bias");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_BLUE_BIAS), "Blue Bias");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_SERIAL_NUMBER), "Serial Number");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_FLASH_BIAS), "Flash Bias");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_CONTRAST), "Contrast");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_SHARPNESS_FACTOR), "Sharpness Factor");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_COLOUR_CONTROL), "Colour Control");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_VALID_BITS), "Valid Bits");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_CORING_FILTER), "Coring Filter");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_FINAL_WIDTH), "Final Width");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_FINAL_HEIGHT), "Final Height");
-        tagNameMap.put(new Integer(TAG_OLYMPUS_COMPRESSION_RATIO), "Compression Ratio");
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static {
+        _tagNameMap.put(TAG_OLYMPUS_SPECIAL_MODE, "Special Mode");
+        _tagNameMap.put(TAG_OLYMPUS_JPEG_QUALITY, "Jpeg Quality");
+        _tagNameMap.put(TAG_OLYMPUS_MACRO_MODE, "Macro");
+        _tagNameMap.put(TAG_OLYMPUS_UNKNOWN_1, "Makernote Unknown 1");
+        _tagNameMap.put(TAG_OLYMPUS_DIGI_ZOOM_RATIO, "DigiZoom Ratio");
+        _tagNameMap.put(TAG_OLYMPUS_UNKNOWN_2, "Makernote Unknown 2");
+        _tagNameMap.put(TAG_OLYMPUS_UNKNOWN_3, "Makernote Unknown 3");
+        _tagNameMap.put(TAG_OLYMPUS_FIRMWARE_VERSION, "Firmware Version");
+        _tagNameMap.put(TAG_OLYMPUS_PICT_INFO, "Pict Info");
+        _tagNameMap.put(TAG_OLYMPUS_CAMERA_ID, "Camera Id");
+        _tagNameMap.put(TAG_OLYMPUS_DATA_DUMP, "Data Dump");
+        _tagNameMap.put(TAG_OLYMPUS_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_OLYMPUS_CAMERA_SETTINGS_1, "Camera Settings");
+        _tagNameMap.put(TAG_OLYMPUS_CAMERA_SETTINGS_2, "Camera Settings");
+        _tagNameMap.put(TAG_OLYMPUS_COMPRESSED_IMAGE_SIZE, "Compressed Image Size");
+        _tagNameMap.put(TAG_OLYMPUS_MINOLTA_THUMBNAIL_OFFSET_1, "Thumbnail Offset");
+        _tagNameMap.put(TAG_OLYMPUS_MINOLTA_THUMBNAIL_OFFSET_2, "Thumbnail Offset");
+        _tagNameMap.put(TAG_OLYMPUS_MINOLTA_THUMBNAIL_LENGTH, "Thumbnail Length");
+        _tagNameMap.put(TAG_OLYMPUS_COLOUR_MODE, "Colour Mode");
+        _tagNameMap.put(TAG_OLYMPUS_IMAGE_QUALITY_1, "Image Quality");
+        _tagNameMap.put(TAG_OLYMPUS_IMAGE_QUALITY_2, "Image Quality");
+        _tagNameMap.put(TAG_OLYMPUS_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_OLYMPUS_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_OLYMPUS_ORIGINAL_MANUFACTURER_MODEL, "Original Manufacturer Model");
+        _tagNameMap.put(TAG_OLYMPUS_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_OLYMPUS_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_OLYMPUS_BRACKET, "Bracket");
+        _tagNameMap.put(TAG_OLYMPUS_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_OLYMPUS_FOCUS_DISTANCE, "Focus Distance");
+        _tagNameMap.put(TAG_OLYMPUS_ZOOM, "Zoom");
+        _tagNameMap.put(TAG_OLYMPUS_MACRO_FOCUS, "Macro Focus");
+        _tagNameMap.put(TAG_OLYMPUS_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_OLYMPUS_COLOUR_MATRIX, "Colour Matrix");
+        _tagNameMap.put(TAG_OLYMPUS_BLACK_LEVEL, "Black Level");
+        _tagNameMap.put(TAG_OLYMPUS_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_OLYMPUS_RED_BIAS, "Red Bias");
+        _tagNameMap.put(TAG_OLYMPUS_BLUE_BIAS, "Blue Bias");
+        _tagNameMap.put(TAG_OLYMPUS_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_OLYMPUS_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(TAG_OLYMPUS_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_OLYMPUS_SHARPNESS_FACTOR, "Sharpness Factor");
+        _tagNameMap.put(TAG_OLYMPUS_COLOUR_CONTROL, "Colour Control");
+        _tagNameMap.put(TAG_OLYMPUS_VALID_BITS, "Valid Bits");
+        _tagNameMap.put(TAG_OLYMPUS_CORING_FILTER, "Coring Filter");
+        _tagNameMap.put(TAG_OLYMPUS_FINAL_WIDTH, "Final Width");
+        _tagNameMap.put(TAG_OLYMPUS_FINAL_HEIGHT, "Final Height");
+        _tagNameMap.put(TAG_OLYMPUS_COMPRESSION_RATIO, "Compression Ratio");
     }
 
@@ -349,4 +219,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -354,7 +225,8 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
-        return tagNameMap;
+        return _tagNameMap;
     }
 }
Index: trunk/src/com/drew/metadata/exif/PanasonicMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicMakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/PanasonicMakernoteDescriptor.java	(revision 6127)
@@ -1,82 +1,1100 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.BufferBoundsException;
+import com.drew.lang.BufferReader;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Age;
+import com.drew.metadata.Face;
 import com.drew.metadata.TagDescriptor;
 
+import java.io.UnsupportedEncodingException;
+
 /**
- * Provides human-readable string versions of the tags stored in a PanasonicMakernoteDirectory.
+ * Provides human-readable string representations of tag values stored in a <code>PanasonicMakernoteDirectory</code>.
+ * <p/>
+ * Some information about this makernote taken from here:
+ * <ul>
+ * <li><a href="http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html">http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html</a></li>
+ * <li><a href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html">http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Panasonic.html</a></li>
+ * </ul>
  *
- * Some information about this makernote taken from here:
- * http://www.ozhiker.com/electronics/pjmt/jpeg_info/panasonic_mn.html
+ * @author Drew Noakes http://drewnoakes.com, Philipp Sandhaus
  */
-public class PanasonicMakernoteDescriptor extends TagDescriptor
+public class PanasonicMakernoteDescriptor extends TagDescriptor<PanasonicMakernoteDirectory>
 {
-    public PanasonicMakernoteDescriptor(Directory directory)
+    public PanasonicMakernoteDescriptor(@NotNull PanasonicMakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
-    {
-        switch (tagType)
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case PanasonicMakernoteDirectory.TAG_QUALITY_MODE:
+                return getQualityModeDescription();
+            case PanasonicMakernoteDirectory.TAG_VERSION:
+                return getVersionDescription();
+            case PanasonicMakernoteDirectory.TAG_WHITE_BALANCE:
+                return getWhiteBalanceDescription();
+            case PanasonicMakernoteDirectory.TAG_FOCUS_MODE:
+                return getFocusModeDescription();
+            case PanasonicMakernoteDirectory.TAG_AF_AREA_MODE:
+                return getAfAreaModeDescription();
+            case PanasonicMakernoteDirectory.TAG_IMAGE_STABILIZATION:
+                return getImageStabilizationDescription();
+            case PanasonicMakernoteDirectory.TAG_MACRO_MODE:
+                return getMacroModeDescription();
+            case PanasonicMakernoteDirectory.TAG_RECORD_MODE:
+                return getRecordModeDescription();
+            case PanasonicMakernoteDirectory.TAG_AUDIO:
+                return getAudioDescription();
+            case PanasonicMakernoteDirectory.TAG_UNKNOWN_DATA_DUMP:
+                return getUnknownDataDumpDescription();
+            case PanasonicMakernoteDirectory.TAG_COLOR_EFFECT:
+                return getColorEffectDescription();
+            case PanasonicMakernoteDirectory.TAG_UPTIME:
+                return getUptimeDescription();
+            case PanasonicMakernoteDirectory.TAG_BURST_MODE:
+                return getBurstModeDescription();
+            case PanasonicMakernoteDirectory.TAG_CONTRAST_MODE:
+                return getContrastModeDescription();
+            case PanasonicMakernoteDirectory.TAG_NOISE_REDUCTION:
+                return getNoiseReductionDescription();
+            case PanasonicMakernoteDirectory.TAG_SELF_TIMER:
+                return getSelfTimerDescription();
+            case PanasonicMakernoteDirectory.TAG_ROTATION:
+                return getRotationDescription();
+            case PanasonicMakernoteDirectory.TAG_AF_ASSIST_LAMP:
+                return getAfAssistLampDescription();
+            case PanasonicMakernoteDirectory.TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case PanasonicMakernoteDirectory.TAG_OPTICAL_ZOOM_MODE:
+                return getOpticalZoomModeDescription();
+            case PanasonicMakernoteDirectory.TAG_CONVERSION_LENS:
+                return getConversionLensDescription();
+            case PanasonicMakernoteDirectory.TAG_CONTRAST:
+                return getContrastDescription();
+            case PanasonicMakernoteDirectory.TAG_WORLD_TIME_LOCATION:
+                return getWorldTimeLocationDescription();
+            case PanasonicMakernoteDirectory.TAG_ADVANCED_SCENE_MODE:
+                return getAdvancedSceneModeDescription();
+            case PanasonicMakernoteDirectory.TAG_FACE_DETECTION_INFO:
+                return getDetectedFacesDescription();
+            case PanasonicMakernoteDirectory.TAG_TRANSFORM:
+                return getTransformDescription();
+			case PanasonicMakernoteDirectory.TAG_TRANSFORM_1:
+	            return getTransform1Description();
+            case PanasonicMakernoteDirectory.TAG_INTELLIGENT_EXPOSURE:
+                return getIntelligentExposureDescription();
+            case PanasonicMakernoteDirectory.TAG_FLASH_WARNING:
+                return getFlashWarningDescription();
+            case PanasonicMakernoteDirectory.TAG_COUNTRY:
+                return getCountryDescription();
+            case PanasonicMakernoteDirectory.TAG_STATE:
+                return getStateDescription();
+            case PanasonicMakernoteDirectory.TAG_CITY:
+                return getCityDescription();
+            case PanasonicMakernoteDirectory.TAG_LANDMARK:
+                return getLandmarkDescription();
+            case PanasonicMakernoteDirectory.TAG_INTELLIGENT_RESOLUTION:
+                return getIntelligentResolutionDescription();
+            case PanasonicMakernoteDirectory.TAG_FACE_RECOGNITION_INFO:
+                return getRecognizedFacesDescription();
+            case PanasonicMakernoteDirectory.TAG_PRINT_IMAGE_MATCHING_INFO:
+                return getPrintImageMatchingInfoDescription();
+            case PanasonicMakernoteDirectory.TAG_SCENE_MODE:
+                return getSceneModeDescription();
+            case PanasonicMakernoteDirectory.TAG_FLASH_FIRED:
+                return getFlashFiredDescription();
+            case PanasonicMakernoteDirectory.TAG_TEXT_STAMP:
+		        return getTextStampDescription();
+			case PanasonicMakernoteDirectory.TAG_TEXT_STAMP_1:
+	             return getTextStamp1Description();
+			case PanasonicMakernoteDirectory.TAG_TEXT_STAMP_2:
+		         return getTextStamp2Description();
+			case PanasonicMakernoteDirectory.TAG_TEXT_STAMP_3:
+			     return getTextStamp3Description();
+            case PanasonicMakernoteDirectory.TAG_MAKERNOTE_VERSION:
+                return getMakernoteVersionDescription();
+            case PanasonicMakernoteDirectory.TAG_EXIF_VERSION:
+                return getExifVersionDescription();
+            case PanasonicMakernoteDirectory.TAG_INTERNAL_SERIAL_NUMBER:
+                return getInternalSerialNumberDescription();
+            case PanasonicMakernoteDirectory.TAG_TITLE:
+	            return getTitleDescription();
+			case PanasonicMakernoteDirectory.TAG_BABY_NAME:
+	            return getBabyNameDescription();
+			case PanasonicMakernoteDirectory.TAG_LOCATION:
+	            return getLocationDescription();
+			case PanasonicMakernoteDirectory.TAG_BABY_AGE:
+		        return getBabyAgeDescription();
+			case PanasonicMakernoteDirectory.TAG_BABY_AGE_1:
+		        return getBabyAge1Description();
+			default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getPrintImageMatchingInfoDescription()
+    {
+        byte[] values = _directory.getByteArray(PanasonicMakernoteDirectory.TAG_PRINT_IMAGE_MATCHING_INFO);
+        if (values == null)
+            return null;
+        return "(" + values.length + " bytes)";
+    }
+
+    @Nullable
+    public String getTextStampDescription()
+    {
+        return getOnOffDescription(PanasonicMakernoteDirectory.TAG_TEXT_STAMP);
+    }
+
+	@Nullable
+    public String getTextStamp1Description()
+    {
+        return getOnOffDescription(PanasonicMakernoteDirectory.TAG_TEXT_STAMP_1);
+    }
+
+	@Nullable
+    public String getTextStamp2Description()
+    {
+        return getOnOffDescription(PanasonicMakernoteDirectory.TAG_TEXT_STAMP_2);
+    }
+
+	@Nullable
+    public String getTextStamp3Description()
+    {
+        return getOnOffDescription(PanasonicMakernoteDirectory.TAG_TEXT_STAMP_3);
+    }
+
+	@Nullable
+    public String getMacroModeDescription()
+    {
+        return getOnOffDescription(PanasonicMakernoteDirectory.TAG_MACRO_MODE);
+    }
+
+    @Nullable
+    public String getFlashFiredDescription()
+    {
+        return getOnOffDescription(PanasonicMakernoteDirectory.TAG_FLASH_FIRED);
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_IMAGE_STABILIZATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 2:
+                return "On, Mode 1";
+            case 3:
+                return "Off";
+            case 4:
+                return "On, Mode 2";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAudioDescription()
+    {
+        return getOnOffDescription(PanasonicMakernoteDirectory.TAG_AUDIO);
+    }
+
+    @Nullable
+    public String getTransformDescription()
+    {
+        return getTransformDescription(PanasonicMakernoteDirectory.TAG_TRANSFORM);
+    }
+
+    @Nullable
+    public String getTransform1Description()
+    {
+        return getTransformDescription(PanasonicMakernoteDirectory.TAG_TRANSFORM_1);
+    }
+
+    @Nullable
+    private String getTransformDescription(int tag)
+    {
+        byte[] values = _directory.getByteArray(tag);
+        if (values == null)
+            return null;
+
+        BufferReader reader = new ByteArrayReader(values);
+
+        try
         {
-            case PanasonicMakernoteDirectory.TAG_PANASONIC_MACRO_MODE:
-                return getMacroModeDescription();
-            case PanasonicMakernoteDirectory.TAG_PANASONIC_RECORD_MODE:
-                return getRecordModeDescription();
-            case PanasonicMakernoteDirectory.TAG_PANASONIC_PRINT_IMAGE_MATCHING_INFO:
-                return getPrintImageMatchingInfoDescription();
-            default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getPrintImageMatchingInfoDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PanasonicMakernoteDirectory.TAG_PANASONIC_PRINT_IMAGE_MATCHING_INFO)) return null;
-        byte[] bytes = _directory.getByteArray(PanasonicMakernoteDirectory.TAG_PANASONIC_PRINT_IMAGE_MATCHING_INFO);
-        return "(" + bytes.length + " bytes)";
-    }
-
-    public String getMacroModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PanasonicMakernoteDirectory.TAG_PANASONIC_MACRO_MODE)) return null;
-        int value = _directory.getInt(PanasonicMakernoteDirectory.TAG_PANASONIC_MACRO_MODE);
-        switch (value) {
-            case 1:
+            int val1 = reader.getUInt16(0);
+            int val2 = reader.getUInt16(2);
+
+            if (val1 == -1 && val2 == 1)
+                return "Slim Low";
+            if (val1 == -3 && val2 == 2)
+                return "Slim High";
+            if (val1 == 0 && val2 == 0)
+                return "Off";
+            if (val1 == 1 && val2 == 1)
+                return "Stretch Low";
+            if (val1 == 3 && val2 == 2)
+                return "Stretch High";
+
+            return "Unknown (" + val1 + " " + val2 + ")";
+        } catch (BufferBoundsException e) {
+            return null   ;
+        }
+    }
+
+    @Nullable
+    public String getIntelligentExposureDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_INTELLIGENT_EXPOSURE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Off";
+            case 1:
+                return "Low";
+            case 2:
+                return "Standard";
+            case 3:
+                return "High";
+
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFlashWarningDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_FLASH_WARNING);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "No";
+            case 1:
+                return "Yes (Flash required but disabled)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+	
+    @Nullable
+    public String getCountryDescription()
+    {
+        return getTextDescription(PanasonicMakernoteDirectory.TAG_COUNTRY);
+    }
+
+    @Nullable
+    public String getStateDescription()
+    {
+        return getTextDescription(PanasonicMakernoteDirectory.TAG_STATE);
+    }
+
+    @Nullable
+    public String getCityDescription()
+    {
+        return getTextDescription(PanasonicMakernoteDirectory.TAG_CITY);
+    }
+
+    @Nullable
+    public String getLandmarkDescription()
+    {
+        return getTextDescription(PanasonicMakernoteDirectory.TAG_LANDMARK);
+    }
+
+	@Nullable
+    public String getTitleDescription()
+    {
+        return getTextDescription(PanasonicMakernoteDirectory.TAG_TITLE);
+    }
+
+	@Nullable
+    public String getBabyNameDescription()
+    {
+        return getTextDescription(PanasonicMakernoteDirectory.TAG_BABY_NAME);
+    }
+
+	@Nullable
+    public String getLocationDescription()
+    {
+        return getTextDescription(PanasonicMakernoteDirectory.TAG_LOCATION);
+    }
+
+    @Nullable
+    public String getIntelligentResolutionDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_INTELLIGENT_RESOLUTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Off";
+            case 2:
+                return "Auto";
+            case 3:
                 return "On";
-            case 2:
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getContrastDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_CONTRAST);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Normal";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getWorldTimeLocationDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_WORLD_TIME_LOCATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Home";
+            case 2:
+                return "Destination";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAdvancedSceneModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_ADVANCED_SCENE_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Normal";
+            case 2:
+                return "Outdoor/Illuminations/Flower/HDR Art";
+            case 3:
+                return "Indoor/Architecture/Objects/HDR B&W";
+            case 4:
+                return "Creative";
+            case 5:
+                return "Auto";
+            case 7:
+                return "Expressive";
+            case 8:
+                return "Retro";
+            case 9:
+                return "Pure";
+            case 10:
+                return "Elegant";
+            case 12:
+                return "Monochrome";
+            case 13:
+                return "Dynamic Art";
+            case 14:
+                return "Silhouette";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getUnknownDataDumpDescription()
+    {
+        byte[] value = _directory.getByteArray(PanasonicMakernoteDirectory.TAG_UNKNOWN_DATA_DUMP);
+        if (value == null)
+            return null;
+        return "[" + value.length + " bytes]";
+    }
+
+    @Nullable
+    public String getColorEffectDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_COLOR_EFFECT);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
                 return "Off";
-            default:
-                return "Unknown (" + value + ")";
-        }
-    }
-
-    public String getRecordModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PanasonicMakernoteDirectory.TAG_PANASONIC_RECORD_MODE)) return null;
-        int value = _directory.getInt(PanasonicMakernoteDirectory.TAG_PANASONIC_RECORD_MODE);
-        switch (value) {
-            case 1:
+            case 2:
+                return "Warm";
+            case 3:
+                return "Cool";
+            case 4:
+                return "Black & White";
+            case 5:
+                return "Sepia";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getUptimeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_UPTIME);
+        if (value == null)
+            return null;
+        return value / 100f + " s";
+    }
+
+    @Nullable
+    public String getBurstModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_BURST_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Off";
+            case 1:
+                return "On";
+            case 2:
+                return "Infinite";
+            case 4:
+                return "Unlimited";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getContrastModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_CONTRAST_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0x0:
                 return "Normal";
+            case 0x1:
+                return "Low";
+            case 0x2:
+                return "High";
+            case 0x6:
+                return "Medium Low";
+            case 0x7:
+                return "Medium High";
+            case 0x100:
+                return "Low";
+            case 0x110:
+                return "Normal";
+            case 0x120:
+                return "High";
+
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_NOISE_REDUCTION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Standard (0)";
+            case 1:
+                return "Low (-1)";
+            case 2:
+                return "High (+1)";
+            case 3:
+                return "Lowest (-2)";
+            case 4:
+                return "Highest (+2)";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+
+    @Nullable
+    public String getSelfTimerDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_SELF_TIMER);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Off";
+            case 2:
+                return "10 s";
+            case 3:
+                return "2 s";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getRotationDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_ROTATION);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Horizontal";
+            case 3:
+                return "Rotate 180";
+            case 6:
+                return "Rotate 90 CW";
+            case 8:
+                return "Rotate 270 CW";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAfAssistLampDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_AF_ASSIST_LAMP);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Fired";
+            case 2:
+                return "Enabled but not used";
+            case 3:
+                return "Disabled but required";
+            case 4:
+                return "Disabled and not required";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_COLOR_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0:
+                return "Normal";
+            case 1:
+                return "Natural";
+            case 2:
+                return "Vivid";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getOpticalZoomModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_OPTICAL_ZOOM_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Standard";
+            case 2:
+                return "Extended";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getConversionLensDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_CONVERSION_LENS);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Off";
+            case 2:
+                return "Wide";
+            case 3:
+                return "Telephoto";
+            case 4:
+                return "Macro";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getDetectedFacesDescription()
+    {
+        return buildFacesDescription(_directory.getDetectedFaces());
+    }
+
+    @Nullable
+    public String getRecognizedFacesDescription()
+    {
+        return buildFacesDescription(_directory.getRecognizedFaces());
+    }
+
+    @Nullable
+    private String buildFacesDescription(@Nullable Face[] faces)
+    {
+        if (faces == null)
+            return null;
+
+        StringBuilder result = new StringBuilder();
+
+        for (int i = 0; i < faces.length; i++)
+            result.append("Face ").append(i + 1).append(": ").append(faces[i].toString()).append("\n");
+
+        if (result.length() > 0)
+            return result.substring(0, result.length() - 1);
+
+        return null;
+    }
+
+    @Nullable
+    public String getRecordModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_RECORD_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Normal";
             case 2:
                 return "Portrait";
+            case 3:
+                return "Scenery";
+            case 4:
+                return "Sports";
+            case 5:
+                return "Night Portrait";
+            case 6:
+                return "Program";
+            case 7:
+                return "Aperture Priority";
+            case 8:
+                return "Shutter Priority";
             case 9:
                 return "Macro";
+            case 10:
+                return "Spot";
+            case 11:
+                return "Manual";
+            case 12:
+                return "Movie Preview";
+            case 13:
+                return "Panning";
+            case 14:
+                return "Simple";
+            case 15:
+                return "Color Effects";
+            case 16:
+                return "Self Portrait";
+            case 17:
+                return "Economy";
+            case 18:
+                return "Fireworks";
+            case 19:
+                return "Party";
+            case 20:
+                return "Snow";
+            case 21:
+                return "Night Scenery";
+            case 22:
+                return "Food";
+            case 23:
+                return "Baby";
+            case 24:
+                return "Soft Skin";
+            case 25:
+                return "Candlelight";
+            case 26:
+                return "Starry Night";
+            case 27:
+                return "High Sensitivity";
+            case 28:
+                return "Panorama Assist";
+            case 29:
+                return "Underwater";
+            case 30:
+                return "Beach";
+            case 31:
+                return "Aerial Photo";
+            case 32:
+                return "Sunset";
+            case 33:
+                return "Pet";
+            case 34:
+                return "Intelligent ISO";
+            case 35:
+                return "Clipboard";
+            case 36:
+                return "High Speed Continuous Shooting";
+            case 37:
+                return "Intelligent Auto";
+            case 39:
+                return "Multi-aspect";
+            case 41:
+                return "Transform";
+            case 42:
+                return "Flash Burst";
+            case 43:
+                return "Pin Hole";
+            case 44:
+                return "Film Grain";
+            case 45:
+                return "My Color";
+            case 46:
+                return "Photo Frame";
+            case 51:
+                return "HDR";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getSceneModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_SCENE_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Normal";
+            case 2:
+                return "Portrait";
+            case 3:
+                return "Scenery";
+            case 4:
+                return "Sports";
+            case 5:
+                return "Night Portrait";
+            case 6:
+                return "Program";
+            case 7:
+                return "Aperture Priority";
+            case 8:
+                return "Shutter Priority";
+            case 9:
+                return "Macro";
+            case 10:
+                return "Spot";
+            case 11:
+                return "Manual";
+            case 12:
+                return "Movie Preview";
+            case 13:
+                return "Panning";
+            case 14:
+                return "Simple";
+            case 15:
+                return "Color Effects";
+            case 16:
+                return "Self Portrait";
+            case 17:
+                return "Economy";
+            case 18:
+                return "Fireworks";
+            case 19:
+                return "Party";
+            case 20:
+                return "Snow";
+            case 21:
+                return "Night Scenery";
+            case 22:
+                return "Food";
+            case 23:
+                return "Baby";
+            case 24:
+                return "Soft Skin";
+            case 25:
+                return "Candlelight";
+            case 26:
+                return "Starry Night";
+            case 27:
+                return "High Sensitivity";
+            case 28:
+                return "Panorama Assist";
+            case 29:
+                return "Underwater";
+            case 30:
+                return "Beach";
+            case 31:
+                return "Aerial Photo";
+            case 32:
+                return "Sunset";
+            case 33:
+                return "Pet";
+            case 34:
+                return "Intelligent ISO";
+            case 35:
+                return "Clipboard";
+            case 36:
+                return "High Speed Continuous Shooting";
+            case 37:
+                return "Intelligent Auto";
+            case 39:
+                return "Multi-aspect";
+            case 41:
+                return "Transform";
+            case 42:
+                return "Flash Burst";
+            case 43:
+                return "Pin Hole";
+            case 44:
+                return "Film Grain";
+            case 45:
+                return "My Color";
+            case 46:
+                return "Photo Frame";
+            case 51:
+                return "HDR";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_FOCUS_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Auto";
+            case 2:
+                return "Manual";
+            case 4:
+                return "Auto, Focus Button";
+            case 5:
+                return "Auto, Continuous";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getAfAreaModeDescription()
+    {
+        int[] value = _directory.getIntArray(PanasonicMakernoteDirectory.TAG_AF_AREA_MODE);
+        if (value == null || value.length < 2)
+            return null;
+        switch (value[0]) {
+            case 0:
+                switch (value[1]) {
+                    case 1:
+                        return "Spot Mode On";
+                    case 16:
+                        return "Spot Mode Off";
+                    default:
+                        return "Unknown (" + value[0] + " " + value[1] + ")";
+                }
+            case 1:
+                switch (value[1]) {
+                    case 0:
+                        return "Spot Focusing";
+                    case 1:
+                        return "5-area";
+                    default:
+                        return "Unknown (" + value[0] + " " + value[1] + ")";
+                }
+            case 16:
+                switch (value[1]) {
+                    case 0:
+                        return "1-area";
+                    case 16:
+                        return "1-area (high speed)";
+                    default:
+                        return "Unknown (" + value[0] + " " + value[1] + ")";
+                }
+            case 32:
+                switch (value[1]) {
+                    case 0:
+                        return "Auto or Face Detect";
+                    case 1:
+                        return "3-area (left)";
+                    case 2:
+                        return "3-area (center)";
+                    case 3:
+                        return "3-area (right)";
+                    default:
+                        return "Unknown (" + value[0] + " " + value[1] + ")";
+                }
+            case 64:
+                return "Face Detect";
+            default:
+                return "Unknown (" + value[0] + " " + value[1] + ")";
+        }
+    }
+
+    @Nullable
+    public String getQualityModeDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_QUALITY_MODE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 2:
+                return "High";
+            case 3:
+                return "Normal";
+            case 6:
+                return "Very High";
+            case 7:
+                return "Raw";
+            case 9:
+                return "Motion Picture";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+    @Nullable
+    public String getVersionDescription()
+    {
+        return convertBytesToVersionString(_directory.getIntArray(PanasonicMakernoteDirectory.TAG_VERSION), 2);
+    }
+
+    @Nullable
+    public String getMakernoteVersionDescription()
+    {
+        return convertBytesToVersionString(_directory.getIntArray(PanasonicMakernoteDirectory.TAG_MAKERNOTE_VERSION), 2);
+    }
+
+    @Nullable
+    public String getExifVersionDescription()
+    {
+        return convertBytesToVersionString(_directory.getIntArray(PanasonicMakernoteDirectory.TAG_EXIF_VERSION), 2);
+    }
+
+    @Nullable
+    public String getInternalSerialNumberDescription()
+    {
+        final byte[] bytes = _directory.getByteArray(PanasonicMakernoteDirectory.TAG_INTERNAL_SERIAL_NUMBER);
+
+        if (bytes==null)
+            return null;
+
+        int length = bytes.length;
+        for (int index = 0; index < bytes.length; index++) {
+            int i = bytes[index] & 0xFF;
+            if (i == 0 || i > 0x7F) {
+                length = index;
+                break;
+            }
+        }
+
+        return new String(bytes, 0, length);
+    }
+
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(PanasonicMakernoteDirectory.TAG_WHITE_BALANCE);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Auto";
+            case 2:
+                return "Daylight";
+            case 3:
+                return "Cloudy";
+            case 4:
+                return "Incandescent";
+            case 5:
+                return "Manual";
+            case 8:
+                return "Flash";
+            case 10:
+                return "Black & White";
+            case 11:
+                return "Manual";
+            case 12:
+                return "Shade";
+            default:
+                return "Unknown (" + value + ")";
+        }
+    }
+
+	@Nullable
+	public String getBabyAgeDescription()
+    {
+        final Age age = _directory.getAge(PanasonicMakernoteDirectory.TAG_BABY_AGE);
+        if (age==null)
+            return null;
+        return age.toFriendlyString();
+	}
+	
+	@Nullable
+	public String getBabyAge1Description()
+    {
+        final Age age = _directory.getAge(PanasonicMakernoteDirectory.TAG_BABY_AGE_1);
+        if (age==null)
+            return null;
+        return age.toFriendlyString();
+	}
+
+	@Nullable
+	private String getTextDescription(int tag)
+    {
+		byte[] values = _directory.getByteArray(tag);
+        if (values == null)
+            return null;
+        try {
+            return new String(values, "ASCII").trim();
+        } catch (UnsupportedEncodingException e) {
+            return null;
+        }
+	}
+
+    @Nullable
+    private String getOnOffDescription(int tag)
+    {
+        Integer value = _directory.getInteger(tag);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 1:
+                return "Off";
+            case 2:
+                return "On";
             default:
                 return "Unknown (" + value + ")";
Index: trunk/src/com/drew/metadata/exif/PanasonicMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PanasonicMakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/PanasonicMakernoteDirectory.java	(revision 6127)
@@ -1,53 +1,546 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.BufferBoundsException;
+import com.drew.lang.BufferReader;
+import com.drew.lang.ByteArrayReader;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.Age;
 import com.drew.metadata.Directory;
+import com.drew.metadata.Face;
 
 import java.util.HashMap;
 
 /**
+ * Describes tags specific to Panasonic and Leica cameras.
  *
+ * @author Drew Noakes http://drewnoakes.com, Philipp Sandhaus
  */
 public class PanasonicMakernoteDirectory extends Directory
 {
-    public static final int TAG_PANASONIC_QUALITY_MODE = 0x0001;
-    public static final int TAG_PANASONIC_VERSION = 0x0002;
-    /**
-     * 1 = On
-     * 2 = Off
-     */
-    public static final int TAG_PANASONIC_MACRO_MODE = 0x001C;
-    /**
-     * 1 = Normal
-     * 2 = Portrait
-     * 9 = Macro 
-     */
-    public static final int TAG_PANASONIC_RECORD_MODE = 0x001F;
-    public static final int TAG_PANASONIC_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
-
-    protected static final HashMap tagNameMap = new HashMap();
+
+    /**
+     * <br>
+     * 2 = High            <br>
+     * 3 = Normal          <br>
+     * 6 = Very High       <br>
+     * 7 = Raw             <br>
+     * 9 = Motion Picture  <br>
+     */
+    public static final int TAG_QUALITY_MODE = 0x0001;
+    public static final int TAG_VERSION = 0x0002;
+    
+    /**                    
+     * <br>
+     * 1 = Auto            <br>
+     * 2 = Daylight        <br>
+     * 3 = Cloudy          <br>
+     * 4 = Incandescent    <br>
+     * 5 = Manual          <br>
+     * 8 = Flash           <br>
+     * 10 = Black & White  <br>
+     * 11 = Manual         <br>
+     * 12 = Shade          <br>
+     */
+    public static final int TAG_WHITE_BALANCE = 0x0003;
+
+
+    /**                        
+     * <br>
+     * 1 = Auto                <br>
+     * 2 = Manual              <br>
+     * 4 =  Auto, Focus Button <br>
+     * 5 = Auto, Continuous    <br>
+     */
+    public static final int TAG_FOCUS_MODE = 0x0007;
+
+    /**
+     * <br>
+     * 2 bytes                         <br>
+     * (DMC-FZ10)                      <br>
+     * '0 1' = Spot Mode On            <br>
+     * '0 16' = Spot Mode Off          <br>
+     * '(other models)                 <br>
+     * 16 = Normal?                    <br>
+     * '0 1' = 9-area                  <br>
+     * '0 16' = 3-area (high speed)    <br>
+     * '1 0' = Spot Focusing           <br>
+     * '1 1' = 5-area                  <br>
+     * '16 0' = 1-area                 <br>
+     * '16 16' = 1-area (high speed)   <br>
+     * '32 0' = Auto or Face Detect    <br>
+     * '32 1' = 3-area (left)?         <br>
+     * '32 2' = 3-area (center)?       <br>
+     * '32 3' = 3-area (right)?        <br>
+     * '64 0' = Face Detect            <br>
+     */
+    public static final int TAG_AF_AREA_MODE = 0x000f;
+
+    /**
+     * <br>
+     * 2 = On, Mode 1   <br>
+     * 3 = Off          <br>
+     * 4 = On, Mode 2   <br>
+     */
+    public static final int TAG_IMAGE_STABILIZATION = 0x001a;
+
+    /**
+     * <br>
+     * 1 = On    <br>
+     * 2 = Off   <br>
+     */
+    public static final int TAG_MACRO_MODE = 0x001C;
+
+    /**
+     * <br>
+     * 1 = Normal                            <br>
+     * 2 = Portrait                          <br>
+     * 3 = Scenery                           <br>
+     * 4 = Sports                            <br>
+     * 5 = Night Portrait                    <br>
+     * 6 = Program                           <br>
+     * 7 = Aperture Priority                 <br>
+     * 8 = Shutter Priority                  <br>
+     * 9 = Macro                             <br>
+     * 10= Spot                              <br>
+     * 11= Manual                            <br>
+     * 12= Movie Preview                     <br>
+     * 13= Panning                           <br>
+     * 14= Simple                            <br>
+     * 15= Color Effects                     <br>
+     * 16= Self Portrait                     <br>
+     * 17= Economy                           <br>
+     * 18= Fireworks                         <br>
+     * 19= Party                             <br>
+     * 20= Snow                              <br>
+     * 21= Night Scenery                     <br>
+     * 22= Food                              <br>
+     * 23= Baby                              <br>
+     * 24= Soft Skin                         <br>
+     * 25= Candlelight                       <br>
+     * 26= Starry Night                      <br>
+     * 27= High Sensitivity                  <br>
+     * 28= Panorama Assist                   <br>
+     * 29= Underwater                        <br>
+     * 30= Beach                             <br>
+     * 31= Aerial Photo                      <br>
+     * 32= Sunset                            <br>
+     * 33= Pet                               <br>
+     * 34= Intelligent ISO                   <br>
+     * 35= Clipboard                         <br>
+     * 36= High Speed Continuous Shooting    <br>
+     * 37= Intelligent Auto                  <br>
+     * 39= Multi-aspect                      <br>
+     * 41= Transform                         <br>
+     * 42= Flash Burst                       <br>
+     * 43= Pin Hole                          <br>
+     * 44= Film Grain                        <br>
+     * 45= My Color                          <br>
+     * 46= Photo Frame                       <br>
+     * 51= HDR                               <br>
+     */
+    public static final int TAG_RECORD_MODE = 0x001F;
+    
+    /**
+     * 1 = Yes <br>
+     * 2 = No  <br>
+     */
+    public static final int TAG_AUDIO = 0x0020;
+
+    /**
+     * No idea, what this is
+     */
+    public static final int TAG_UNKNOWN_DATA_DUMP = 0x0021;
+    
+    public static final int TAG_WHITE_BALANCE_BIAS = 0x0023;
+    public static final int TAG_FLASH_BIAS = 0x0024;
+    
+    /**
+     * this number is unique, and contains the date of manufacture,
+     * but is not the same as the number printed on the camera body
+     */
+    public static final int TAG_INTERNAL_SERIAL_NUMBER = 0x0025;
+
+    /**
+     * Panasonic Exif Version
+     */
+    public static final int TAG_EXIF_VERSION = 0x0026;
+    
+    
+    /**
+     * 1 = Off           <br>
+     * 2 = Warm          <br>
+     * 3 = Cool          <br>
+     * 4 = Black & White <br>
+     * 5 = Sepia         <br>
+     */
+    public static final int TAG_COLOR_EFFECT = 0x0028;
+
+    /**
+     * 4 Bytes <br>
+     * Time in 1/100 s from when the camera was powered on to when the
+     * image is written to memory card
+     */
+    public static final int TAG_UPTIME = 0x0029;
+
+
+    /**
+     * 0 = Off        <br>
+     * 1 = On         <br>
+     * 2 = Infinite   <br>
+     * 4 = Unlimited  <br>
+     */
+    public static final int TAG_BURST_MODE = 0x002a;
+    
+    public static final int TAG_SEQUENCE_NUMBER = 0x002b;
+    
+    /**
+     * (this decoding seems to work for some models such as the LC1, LX2, FZ7, FZ8, FZ18 and FZ50, but may not be correct for other models such as the FX10, G1, L1, L10 and LC80) <br>
+     * 0x0 = Normal                                            <br>
+     * 0x1 = Low                                               <br>
+     * 0x2 = High                                              <br>
+     * 0x6 = Medium Low                                        <br>
+     * 0x7 = Medium High                                       <br>
+     * 0x100 = Low                                             <br>
+     * 0x110 = Normal                                          <br>
+     * 0x120 = High                                            <br>
+     * (these values are used by the GF1)                      <br>
+     * 0 = -2                                                  <br>
+     * 1 = -1                                                  <br>
+     * 2 = Normal                                              <br>
+     * 3 = +1                                                  <br>
+     * 4 = +2                                                  <br>
+     * 7 = Nature (Color Film)                                 <br>
+     * 12 = Smooth (Color Film) or Pure (My Color)             <br>
+     * 17 = Dynamic (B&W Film)                                 <br>
+     * 22 = Smooth (B&W Film)                                  <br>
+     * 27 = Dynamic (Color Film)                               <br>
+     * 32 = Vibrant (Color Film) or Expressive (My Color)      <br>
+     * 33 = Elegant (My Color)                                 <br>
+     * 37 = Nostalgic (Color Film)                             <br>
+     * 41 = Dynamic Art (My Color)                             <br>
+     * 42 = Retro (My Color)                                   <br>
+     */
+    public static final int TAG_CONTRAST_MODE = 0x002c;
+    
+    
+    /**
+     * 0 = Standard      <br>
+     * 1 = Low (-1)      <br>
+     * 2 = High (+1)     <br>
+     * 3 = Lowest (-2)   <br>
+     * 4 = Highest (+2)  <br>
+     */
+    public static final int TAG_NOISE_REDUCTION = 0x002d;
+
+    /**
+     * 1 = Off   <br>
+     * 2 = 10 s  <br>
+     * 3 = 2 s   <br>
+     */
+    public static final int TAG_SELF_TIMER = 0x002e;
+
+    /**
+     * 1 = 0 DG    <br>
+     * 3 = 180 DG  <br>
+     * 6 =  90 DG  <br>
+     * 8 = 270 DG  <br>
+     */
+    public static final int TAG_ROTATION = 0x0030;
+
+    /**
+     * 1 = Fired <br>
+     * 2 = Enabled nut not used <br>
+     * 3 = Disabled but required <br>
+     * 4 = Disabled and not required
+     */
+    public static final int TAG_AF_ASSIST_LAMP = 0x0031;
+    
+    /**
+     * 0 = Normal <br>
+     * 1 = Natural<br>
+     * 2 = Vivid
+     * 
+     */
+    public static final int TAG_COLOR_MODE = 0x0032;
+    
+    public static final int TAG_BABY_AGE = 0x0033;
+    
+    /**
+     *  1 = Standard <br>
+     *  2 = Extended
+     */
+    public static final int TAG_OPTICAL_ZOOM_MODE = 0x0034;
+    
+    /**
+     * 1 = Off <br>
+     * 2 = Wide <br>
+     * 3 = Telephoto <br>
+     * 4 = Macro
+     */
+    public static final int TAG_CONVERSION_LENS = 0x0035;
+    
+    public static final int TAG_TRAVEL_DAY = 0x0036;
+    
+    /**
+     * 0 = Normal 
+     */ 
+    public static final int TAG_CONTRAST = 0x0039;
+    
+    /**
+     * <br>
+     * 1 = Home <br>
+     * 2 = Destination 
+     */ 
+    public static final int TAG_WORLD_TIME_LOCATION = 0x003a;
+    
+    /**
+     * 1 = Off   <br>
+     * 2 = On 
+     */
+    public static final int TAG_TEXT_STAMP = 0x003b;
+
+	public static final int TAG_PROGRAM_ISO = 0x003c;
+    
+    /**
+     * <br>
+     * 1 = Normal                               <br>
+     * 2 = Outdoor/Illuminations/Flower/HDR Art <br>
+     * 3 = Indoor/Architecture/Objects/HDR B&W  <br>
+     * 4 = Creative                             <br>
+     * 5 = Auto                                 <br>
+     * 7 = Expressive                           <br>
+     * 8 = Retro                                <br>
+     * 9 = Pure                                 <br>
+     * 10 = Elegant                             <br>
+     * 12 = Monochrome                          <br>
+     * 13 = Dynamic Art                         <br>
+     * 14 = Silhouette                          <br>
+     */
+    public static final int TAG_ADVANCED_SCENE_MODE = 0x003d;
+    
+    /**
+     * 1 = Off   <br>
+     * 2 = On 
+     */
+    public static final int TAG_TEXT_STAMP_1 = 0x003e;
+	
+    public static final int TAG_FACES_DETECTED = 0x003f;
+
+    public static final int TAG_SATURATION = 0x0040;
+    public static final int TAG_SHARPNESS = 0x0041;
+    public static final int TAG_FILM_MODE = 0x0042;
+
+    /**
+	 * WB adjust AB. Positive is a shift toward blue.
+	 */
+	public static final int TAG_WB_ADJUST_AB = 0x0046;
+    /**
+	 * WB adjust GM. Positive is a shift toward green.
+	 */
+	public static final int TAG_WB_ADJUST_GM = 0x0047;
+	
+
+    public static final int TAG_AF_POINT_POSITION = 0x004d;
+    
+    
+    /**
+     * <br>
+     * Integer (16Bit) Indexes:                                             <br>
+     * 0  Number Face Positions (maybe less than Faces Detected)            <br>
+     * 1-4 Face Position 1                                                  <br>
+     * 5-8 Face Position 2                                                  <br>
+     * and so on                                                            <br>
+     *                                                                      <br>
+     * The four Integers are interpreted as follows:                        <br>
+     * (XYWH)  X,Y Center of Face,  (W,H) Width and Height                  <br>
+     * All values are in respect to double the size of the thumbnail image  <br>
+     *
+     */
+    public static final int TAG_FACE_DETECTION_INFO = 0x004e;
+    public static final int TAG_LENS_TYPE = 0x0051;
+    public static final int TAG_LENS_SERIAL_NUMBER = 0x0052;
+    public static final int TAG_ACCESSORY_TYPE = 0x0053;
+    
+    /**
+     * (decoded as two 16-bit signed integers) 
+     * '-1 1' = Slim Low 
+     * '-3 2' = Slim High 
+     * '0 0' = Off 
+     * '1 1' = Stretch Low 
+     * '3 2' = Stretch High  
+     */
+    public static final int TAG_TRANSFORM = 0x0059;
+    
+    /**
+    * 0 = Off <br>
+    * 1 = Low <br>
+    * 2 = Standard <br>
+    * 3 = High
+    */
+    public static final int TAG_INTELLIGENT_EXPOSURE = 0x005d;
+    
+    /**
+	  * Info at http://www.ozhiker.com/electronics/pjmt/jpeg_info/pim.html
+	  * 
+     */
+	public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+
+
+
+    /**                                                                                    
+     * Byte Indexes:                                                                       <br>
+     *  0    Int (2  Byte) Number of Recognized Faces                                      <br>
+     *  4    String(20 Byte)    Recognized Face 1 Name                                     <br>
+     * 24    4 Int (8 Byte)     Recognized Face 1 Position  (Same Format as Face Detection)  <br>
+     * 32    String(20 Byte)    Recognized Face 1 Age                                      <br>
+     * 52    String(20 Byte)    Recognized Face 2 Name                                     <br>
+     * 72    4 Int (8 Byte)     Recognized Face 2 Position  (Same Format as Face Detection)  <br>
+     * 80    String(20 Byte)    Recognized Face 2 Age                                      <br>
+     *                                                                                     <br>
+     * And so on                                                                           <br>
+     *                                                                                     <br>
+     * The four Integers are interpreted as follows:                                       <br>
+     * (XYWH)  X,Y Center of Face,  (W,H) Width and Height                                 <br>
+     * All values are in respect to double the size of the thumbnail image                 <br>
+     *
+     */
+    public static final int TAG_FACE_RECOGNITION_INFO = 0x0061;
+
+    /**
+    * 0 = No <br>
+    * 1 = Yes 
+    */
+    public static final int TAG_FLASH_WARNING = 0x0062;
+    public static final int TAG_RECOGNIZED_FACE_FLAGS = 0x0063;
+    public static final int TAG_TITLE = 0x0065;
+	public static final int TAG_BABY_NAME = 0x0066;
+	public static final int TAG_LOCATION = 0x0067;
+	public static final int TAG_COUNTRY = 0x0069;
+    public static final int TAG_STATE = 0x006b;
+    public static final int TAG_CITY = 0x006d;
+    public static final int TAG_LANDMARK = 0x006f;
+    
+    /**
+     * 0 = Off <br>
+     * 2 = Auto <br>
+     * 3 = On 
+     */
+    public static final int TAG_INTELLIGENT_RESOLUTION = 0x0070;
+    
+    public static final int TAG_MAKERNOTE_VERSION = 0x8000;
+    public static final int TAG_SCENE_MODE = 0x8001;
+    public static final int TAG_WB_RED_LEVEL = 0x8004;
+    public static final int TAG_WB_GREEN_LEVEL = 0x8005;
+    public static final int TAG_WB_BLUE_LEVEL = 0x8006;
+    public static final int TAG_FLASH_FIRED = 0x8007;
+    public static final int TAG_TEXT_STAMP_2 = 0x8008;
+	public static final int TAG_TEXT_STAMP_3 = 0x8009;
+	public static final int TAG_BABY_AGE_1 = 0x8010;
+	
+	/**
+     * (decoded as two 16-bit signed integers) 
+     * '-1 1' = Slim Low 
+     * '-3 2' = Slim High 
+     * '0 0' = Off 
+     * '1 1' = Stretch Low 
+     * '3 2' = Stretch High  
+     */
+    public static final int TAG_TRANSFORM_1 = 0x8012;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        tagNameMap.put(new Integer(TAG_PANASONIC_QUALITY_MODE), "Quality Mode");
-        tagNameMap.put(new Integer(TAG_PANASONIC_VERSION), "Version");
-        tagNameMap.put(new Integer(TAG_PANASONIC_MACRO_MODE), "Macro Mode");
-        tagNameMap.put(new Integer(TAG_PANASONIC_RECORD_MODE), "Record Mode");
-        tagNameMap.put(new Integer(TAG_PANASONIC_PRINT_IMAGE_MATCHING_INFO), "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_QUALITY_MODE, "Quality Mode");
+        _tagNameMap.put(TAG_VERSION, "Version");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_AF_AREA_MODE, "AF Area Mode");
+        _tagNameMap.put(TAG_IMAGE_STABILIZATION, "Image Stabilization");
+        _tagNameMap.put(TAG_MACRO_MODE, "Macro Mode");
+        _tagNameMap.put(TAG_RECORD_MODE, "Record Mode");
+        _tagNameMap.put(TAG_AUDIO, "Audio");
+        _tagNameMap.put(TAG_INTERNAL_SERIAL_NUMBER, "Internal Serial Number");
+        _tagNameMap.put(TAG_UNKNOWN_DATA_DUMP, "Unknown Data Dump");
+        _tagNameMap.put(TAG_WHITE_BALANCE_BIAS, "White Balance Bias");
+        _tagNameMap.put(TAG_FLASH_BIAS, "Flash Bias");
+        _tagNameMap.put(TAG_EXIF_VERSION, "Exif Version");
+        _tagNameMap.put(TAG_COLOR_EFFECT, "Color Effect");
+        _tagNameMap.put(TAG_UPTIME, "Camera Uptime");
+        _tagNameMap.put(TAG_BURST_MODE, "Burst Mode");
+        _tagNameMap.put(TAG_SEQUENCE_NUMBER, "Sequence Number");
+        _tagNameMap.put(TAG_CONTRAST_MODE, "Contrast Mode");
+        _tagNameMap.put(TAG_NOISE_REDUCTION, "Noise Reduction");
+        _tagNameMap.put(TAG_SELF_TIMER, "Self Timer");
+        _tagNameMap.put(TAG_ROTATION, "Rotation");
+        _tagNameMap.put(TAG_AF_ASSIST_LAMP, "AF Assist Lamp");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_BABY_AGE, "Baby Age");
+        _tagNameMap.put(TAG_OPTICAL_ZOOM_MODE, "Optical Zoom Mode");
+        _tagNameMap.put(TAG_CONVERSION_LENS, "Conversion Lens");
+        _tagNameMap.put(TAG_TRAVEL_DAY, "Travel Day");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_WORLD_TIME_LOCATION, "World Time Location");
+        _tagNameMap.put(TAG_TEXT_STAMP, "Text Stamp");
+        _tagNameMap.put(TAG_PROGRAM_ISO, "Program ISO");
+		_tagNameMap.put(TAG_ADVANCED_SCENE_MODE, "Advanced Scene Mode");
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_FACES_DETECTED, "Number of Detected Faces");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_FILM_MODE, "Film Mode");
+        _tagNameMap.put(TAG_WB_ADJUST_AB, "White Balance Adjust (AB)");
+		_tagNameMap.put(TAG_WB_ADJUST_GM, "White Balance Adjust (GM)");
+		_tagNameMap.put(TAG_AF_POINT_POSITION, "Af Point Position");
+        _tagNameMap.put(TAG_FACE_DETECTION_INFO, "Face Detection Info");
+        _tagNameMap.put(TAG_LENS_TYPE, "Lens Type");
+        _tagNameMap.put(TAG_LENS_SERIAL_NUMBER, "Lens Serial Number");
+        _tagNameMap.put(TAG_ACCESSORY_TYPE, "Accessory Type");
+        _tagNameMap.put(TAG_TRANSFORM, "Transform");
+        _tagNameMap.put(TAG_INTELLIGENT_EXPOSURE, "Intelligent Exposure");
+        _tagNameMap.put(TAG_FACE_RECOGNITION_INFO, "Face Recognition Info");
+        _tagNameMap.put(TAG_FLASH_WARNING, "Flash Warning");
+        _tagNameMap.put(TAG_RECOGNIZED_FACE_FLAGS, "Recognized Face Flags");
+		_tagNameMap.put(TAG_TITLE, "Title");
+		_tagNameMap.put(TAG_BABY_NAME, "Baby Name");
+		_tagNameMap.put(TAG_LOCATION, "Location");
+		_tagNameMap.put(TAG_COUNTRY, "Country");
+        _tagNameMap.put(TAG_STATE, "State");
+        _tagNameMap.put(TAG_CITY, "City");
+        _tagNameMap.put(TAG_LANDMARK, "Landmark");
+        _tagNameMap.put(TAG_INTELLIGENT_RESOLUTION, "Intelligent Resolution");
+        _tagNameMap.put(TAG_MAKERNOTE_VERSION, "Makernote Version");
+        _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+        _tagNameMap.put(TAG_WB_RED_LEVEL, "White Balance (Red)");
+        _tagNameMap.put(TAG_WB_GREEN_LEVEL, "White Balance (Green)");
+        _tagNameMap.put(TAG_WB_BLUE_LEVEL, "White Balance (Blue)");
+        _tagNameMap.put(TAG_FLASH_FIRED, "Flash Fired");
+		_tagNameMap.put(TAG_TEXT_STAMP_1, "Text Stamp 1");
+		_tagNameMap.put(TAG_TEXT_STAMP_2, "Text Stamp 2");
+		_tagNameMap.put(TAG_TEXT_STAMP_3, "Text Stamp 3");
+		_tagNameMap.put(TAG_BABY_AGE_1, "Baby Age 1");
+		_tagNameMap.put(TAG_TRANSFORM_1, "Transform 1");
     }
 
@@ -57,4 +550,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -62,7 +556,87 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
-        return tagNameMap;
+        return _tagNameMap;
     }
+
+    @Nullable
+    public Face[] getDetectedFaces()
+    {
+        byte[] bytes = getByteArray(PanasonicMakernoteDirectory.TAG_FACE_DETECTION_INFO);
+        if (bytes==null)
+            return null;
+
+        BufferReader reader = new ByteArrayReader(bytes);
+        reader.setMotorolaByteOrder(false);
+        
+        try {
+            int faceCount = reader.getUInt16(0);
+            if (faceCount==0)
+                return null;
+            Face[] faces = new Face[faceCount];
+
+            for (int i = 0; i < faceCount; i++) {
+                int offset = 2 + i * 8;
+                faces[i] = new Face(
+                        reader.getUInt16(offset),
+                        reader.getUInt16(offset + 2),
+                        reader.getUInt16(offset + 4),
+                        reader.getUInt16(offset + 6)
+                        , null, null);
+            }
+            return faces;
+        } catch (BufferBoundsException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    public Face[] getRecognizedFaces()
+    {
+        byte[] bytes = getByteArray(PanasonicMakernoteDirectory.TAG_FACE_RECOGNITION_INFO);
+        if (bytes == null)
+            return null;
+
+        BufferReader reader = new ByteArrayReader(bytes);
+        reader.setMotorolaByteOrder(false);
+
+        try {
+            int faceCount = reader.getUInt16(0);
+            if (faceCount==0)
+                return null;
+            Face[] faces = new Face[faceCount];
+
+            for (int i = 0; i < faceCount; i++) {
+                int offset = 4 + i * 44;
+                String name = reader.getString(offset, 20, "ASCII").trim();
+                String age = reader.getString(offset + 28, 20, "ASCII").trim();
+                faces[i] = new Face(
+                        reader.getUInt16(offset + 20),
+                        reader.getUInt16(offset + 22),
+                        reader.getUInt16(offset + 24),
+                        reader.getUInt16(offset + 26),
+                        name,
+                        Age.fromPanasonicString(age));
+            }
+            return faces;
+        } catch (BufferBoundsException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Attempts to convert the underlying string value (as stored in the directory) into an Age object.
+     * @param tag The tag identifier.
+     * @return The parsed Age object, or null if the tag was empty of the value unable to be parsed.
+     */
+	@Nullable
+	public Age getAge(int tag)
+    {
+        final String ageString = getString(tag);
+        if (ageString==null)
+            return null;
+        return Age.fromPanasonicString(ageString);
+	}
 }
Index: trunk/src/com/drew/metadata/exif/PentaxMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PentaxMakernoteDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/PentaxMakernoteDescriptor.java	(revision 6127)
@@ -1,36 +1,45 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
- * Provides human-readable string versions of the tags stored in PentaxMakernoteDirectory.
- *
+ * Provides human-readable string representations of tag values stored in a <code>PentaxMakernoteDirectory</code>.
+ * <p/>
  * Some information about this makernote taken from here:
  * http://www.ozhiker.com/electronics/pjmt/jpeg_info/pentax_mn.html
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class PentaxMakernoteDescriptor extends TagDescriptor
+public class PentaxMakernoteDescriptor extends TagDescriptor<PentaxMakernoteDirectory>
 {
-    public PentaxMakernoteDescriptor(Directory directory)
+    public PentaxMakernoteDescriptor(@NotNull PentaxMakernoteDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType) 
@@ -59,12 +68,14 @@
                 return getColourDescription();
             default:
-                return _directory.getString(tagType);
-        }
-    }
-
-    public String getColourDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_COLOUR)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_COLOUR);
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getColourDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_COLOUR);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -76,8 +87,10 @@
     }
 
-    public String getIsoSpeedDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_ISO_SPEED)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_ISO_SPEED);
+    @Nullable
+    public String getIsoSpeedDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_ISO_SPEED);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -91,8 +104,10 @@
     }
 
-    public String getSaturationDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_SATURATION)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_SATURATION);
+    @Nullable
+    public String getSaturationDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_SATURATION);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -104,8 +119,10 @@
     }
 
-    public String getContrastDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_CONTRAST)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_CONTRAST);
+    @Nullable
+    public String getContrastDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_CONTRAST);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -117,8 +134,10 @@
     }
 
-    public String getSharpnessDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_SHARPNESS)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_SHARPNESS);
+    @Nullable
+    public String getSharpnessDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_SHARPNESS);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -130,8 +149,10 @@
     }
 
-    public String getDigitalZoomDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_DIGITAL_ZOOM)) return null;
-        float value = _directory.getFloat(PentaxMakernoteDirectory.TAG_PENTAX_DIGITAL_ZOOM);
+    @Nullable
+    public String getDigitalZoomDescription()
+    {
+        Float value = _directory.getFloatObject(PentaxMakernoteDirectory.TAG_PENTAX_DIGITAL_ZOOM);
+        if (value==null)
+            return null;
         if (value==0)
             return "Off";
@@ -139,8 +160,10 @@
     }
 
-    public String getWhiteBalanceDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_WHITE_BALANCE)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_WHITE_BALANCE);
+    @Nullable
+    public String getWhiteBalanceDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_WHITE_BALANCE);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -155,8 +178,10 @@
     }
 
-    public String getFlashModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_FLASH_MODE)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_FLASH_MODE);
+    @Nullable
+    public String getFlashModeDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_FLASH_MODE);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -169,8 +194,10 @@
     }
 
-    public String getFocusModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_FOCUS_MODE)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_FOCUS_MODE);
+    @Nullable
+    public String getFocusModeDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_FOCUS_MODE);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -181,8 +208,10 @@
     }
 
-    public String getQualityLevelDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_QUALITY_LEVEL)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_QUALITY_LEVEL);
+    @Nullable
+    public String getQualityLevelDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_QUALITY_LEVEL);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -194,8 +223,10 @@
     }
 
-    public String getCaptureModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PENTAX_CAPTURE_MODE)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PENTAX_CAPTURE_MODE);
+    @Nullable
+    public String getCaptureModeDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PENTAX_CAPTURE_MODE);
+        if (value==null)
+            return null;
         switch (value)
         {
@@ -209,15 +240,17 @@
 
 /*
-    public String getPrintImageMatchingInfoDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PANASONIC_PRINT_IMAGE_MATCHING_INFO)) return null;
+    public String getPrintImageMatchingInfoDescription()
+    {
         byte[] bytes = _directory.getByteArray(PentaxMakernoteDirectory.TAG_PANASONIC_PRINT_IMAGE_MATCHING_INFO);
+        if (bytes==null)
+            return null;
         return "(" + bytes.length + " bytes)";
     }
 
-    public String getMacroModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PANASONIC_MACRO_MODE)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PANASONIC_MACRO_MODE);
+    public String getMacroModeDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PANASONIC_MACRO_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 1:
@@ -230,8 +263,9 @@
     }
 
-    public String getRecordModeDescription() throws MetadataException
-    {
-        if (!_directory.containsTag(PentaxMakernoteDirectory.TAG_PANASONIC_RECORD_MODE)) return null;
-        int value = _directory.getInt(PentaxMakernoteDirectory.TAG_PANASONIC_RECORD_MODE);
+    public String getRecordModeDescription()
+    {
+        Integer value = _directory.getInteger(PentaxMakernoteDirectory.TAG_PANASONIC_RECORD_MODE);
+        if (value==null)
+            return null;
         switch (value) {
             case 1:
Index: trunk/src/com/drew/metadata/exif/PentaxMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/PentaxMakernoteDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/exif/PentaxMakernoteDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 27-Nov-2002 10:10:47 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.exif;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,5 +27,7 @@
 
 /**
- * Directory for metadata specific to Pentax and Asahi cameras.
+ * Describes tags specific to Pentax and Asahi cameras.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class PentaxMakernoteDirectory extends Directory
@@ -122,22 +129,23 @@
     public static final int TAG_PENTAX_DAYLIGHT_SAVINGS = 0x1001;
 
-    protected static final HashMap tagNameMap = new HashMap();
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        tagNameMap.put(new Integer(TAG_PENTAX_CAPTURE_MODE), "Capture Mode");
-        tagNameMap.put(new Integer(TAG_PENTAX_QUALITY_LEVEL), "Quality Level");
-        tagNameMap.put(new Integer(TAG_PENTAX_FOCUS_MODE), "Focus Mode");
-        tagNameMap.put(new Integer(TAG_PENTAX_FLASH_MODE), "Flash Mode");
-        tagNameMap.put(new Integer(TAG_PENTAX_WHITE_BALANCE), "White Balance");
-        tagNameMap.put(new Integer(TAG_PENTAX_DIGITAL_ZOOM), "Digital Zoom");
-        tagNameMap.put(new Integer(TAG_PENTAX_SHARPNESS), "Sharpness");
-        tagNameMap.put(new Integer(TAG_PENTAX_CONTRAST), "Contrast");
-        tagNameMap.put(new Integer(TAG_PENTAX_SATURATION), "Saturation");
-        tagNameMap.put(new Integer(TAG_PENTAX_ISO_SPEED), "ISO Speed");
-        tagNameMap.put(new Integer(TAG_PENTAX_COLOUR), "Colour");
-        tagNameMap.put(new Integer(TAG_PENTAX_PRINT_IMAGE_MATCHING_INFO), "Print Image Matching (PIM) Info");
-        tagNameMap.put(new Integer(TAG_PENTAX_TIME_ZONE), "Time Zone");
-        tagNameMap.put(new Integer(TAG_PENTAX_DAYLIGHT_SAVINGS), "Daylight Savings");
+        _tagNameMap.put(TAG_PENTAX_CAPTURE_MODE, "Capture Mode");
+        _tagNameMap.put(TAG_PENTAX_QUALITY_LEVEL, "Quality Level");
+        _tagNameMap.put(TAG_PENTAX_FOCUS_MODE, "Focus Mode");
+        _tagNameMap.put(TAG_PENTAX_FLASH_MODE, "Flash Mode");
+        _tagNameMap.put(TAG_PENTAX_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_PENTAX_DIGITAL_ZOOM, "Digital Zoom");
+        _tagNameMap.put(TAG_PENTAX_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_PENTAX_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_PENTAX_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_PENTAX_ISO_SPEED, "ISO Speed");
+        _tagNameMap.put(TAG_PENTAX_COLOUR, "Colour");
+        _tagNameMap.put(TAG_PENTAX_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching (PIM) Info");
+        _tagNameMap.put(TAG_PENTAX_TIME_ZONE, "Time Zone");
+        _tagNameMap.put(TAG_PENTAX_DAYLIGHT_SAVINGS, "Daylight Savings");
     }
 
@@ -147,4 +155,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -152,7 +161,8 @@
     }
 
-    protected HashMap getTagNameMap()
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
     {
-        return tagNameMap;
+        return _tagNameMap;
     }
 }
Index: trunk/src/com/drew/metadata/exif/SigmaMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/SigmaMakernoteDescriptor.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/SigmaMakernoteDescriptor.java	(revision 6127)
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a <code>SigmaMakernoteDirectory</code>.
+ * 
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class SigmaMakernoteDescriptor extends TagDescriptor<SigmaMakernoteDirectory>
+{
+    public SigmaMakernoteDescriptor(@NotNull SigmaMakernoteDirectory directory)
+    {
+        super(directory);
+    }
+}
Index: trunk/src/com/drew/metadata/exif/SigmaMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/SigmaMakernoteDirectory.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/SigmaMakernoteDirectory.java	(revision 6127)
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Sigma / Foveon cameras.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class SigmaMakernoteDirectory extends Directory
+{
+    public static final int TAG_SERIAL_NUMBER = 2;
+    public static final int TAG_DRIVE_MODE = 3;
+    public static final int TAG_RESOLUTION_MODE = 4;
+    public static final int TAG_AUTO_FOCUS_MODE = 5;
+    public static final int TAG_FOCUS_SETTING = 6;
+    public static final int TAG_WHITE_BALANCE = 7;
+    public static final int TAG_EXPOSURE_MODE = 8;
+    public static final int TAG_METERING_MODE = 9;
+    public static final int TAG_LENS_RANGE = 10;
+    public static final int TAG_COLOR_SPACE = 11;
+    public static final int TAG_EXPOSURE = 12;
+    public static final int TAG_CONTRAST = 13;
+    public static final int TAG_SHADOW = 14;
+    public static final int TAG_HIGHLIGHT = 15;
+    public static final int TAG_SATURATION = 16;
+    public static final int TAG_SHARPNESS = 17;
+    public static final int TAG_FILL_LIGHT = 18;
+    public static final int TAG_COLOR_ADJUSTMENT = 20;
+    public static final int TAG_ADJUSTMENT_MODE = 21;
+    public static final int TAG_QUALITY = 22;
+    public static final int TAG_FIRMWARE = 23;
+    public static final int TAG_SOFTWARE = 24;
+    public static final int TAG_AUTO_BRACKET = 25;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_SERIAL_NUMBER, "Serial Number");
+        _tagNameMap.put(TAG_DRIVE_MODE, "Drive Mode");
+        _tagNameMap.put(TAG_RESOLUTION_MODE, "Resolution Mode");
+        _tagNameMap.put(TAG_AUTO_FOCUS_MODE, "Auto Focus Mode");
+        _tagNameMap.put(TAG_FOCUS_SETTING, "Focus Setting");
+        _tagNameMap.put(TAG_WHITE_BALANCE, "White Balance");
+        _tagNameMap.put(TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(TAG_METERING_MODE, "Metering Mode");
+        _tagNameMap.put(TAG_LENS_RANGE, "Lens Range");
+        _tagNameMap.put(TAG_COLOR_SPACE, "Color Space");
+        _tagNameMap.put(TAG_EXPOSURE, "Exposure");
+        _tagNameMap.put(TAG_CONTRAST, "Contrast");
+        _tagNameMap.put(TAG_SHADOW, "Shadow");
+        _tagNameMap.put(TAG_HIGHLIGHT, "Highlight");
+        _tagNameMap.put(TAG_SATURATION, "Saturation");
+        _tagNameMap.put(TAG_SHARPNESS, "Sharpness");
+        _tagNameMap.put(TAG_FILL_LIGHT, "Fill Light");
+        _tagNameMap.put(TAG_COLOR_ADJUSTMENT, "Color Adjustment");
+        _tagNameMap.put(TAG_ADJUSTMENT_MODE, "Adjustment Mode");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_FIRMWARE, "Firmware");
+        _tagNameMap.put(TAG_SOFTWARE, "Software");
+        _tagNameMap.put(TAG_AUTO_BRACKET, "Auto Bracket");
+    }
+
+
+    public SigmaMakernoteDirectory()
+    {
+        this.setDescriptor(new SigmaMakernoteDescriptor(this));
+    }
+
+    @NotNull
+    public String getName()
+    {
+        return "Sigma Makernote";
+    }
+
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/exif/SonyMakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/SonyMakernoteDescriptor.java	(revision 6002)
+++ 	(revision )
@@ -1,36 +1,0 @@
-/*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- */
-package com.drew.metadata.exif;
-
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
-import com.drew.metadata.TagDescriptor;
-
-/**
- * Provides human-readable string versions of the tags stored in a SonyMakernoteDirectory.
- * Thanks to David Carson for the initial version of this class.
- */
-public class SonyMakernoteDescriptor extends TagDescriptor
-{
-    public SonyMakernoteDescriptor(Directory directory)
-    {
-        super(directory);
-    }
-
-    public String getDescription(int tagType) throws MetadataException
-    {
-        return _directory.getString(tagType);
-    }
-}
Index: trunk/src/com/drew/metadata/exif/SonyMakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/SonyMakernoteDirectory.java	(revision 6002)
+++ 	(revision )
@@ -1,37 +1,0 @@
-/*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- */
-package com.drew.metadata.exif;
-
-import com.drew.metadata.Directory;
-
-import java.util.HashMap;
-
-/**
- * Describes tags specific to Sony cameras.
- */
-public class SonyMakernoteDirectory extends Directory
-{
-	protected static final HashMap _tagNameMap = new HashMap();
-	
-	public String getName()
-    {
-		return "Sony Makernote";
-	}
-
-	protected HashMap getTagNameMap()
-    {
-		return _tagNameMap;
-	}
-}
Index: trunk/src/com/drew/metadata/exif/SonyType1MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/SonyType1MakernoteDescriptor.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/SonyType1MakernoteDescriptor.java	(revision 6127)
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a <code>SonyType1MakernoteDirectory</code>.
+ * Thanks to David Carson for the initial version of this class.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class SonyType1MakernoteDescriptor extends TagDescriptor<SonyType1MakernoteDirectory>
+{
+    public SonyType1MakernoteDescriptor(@NotNull SonyType1MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case SonyType1MakernoteDirectory.TAG_COLOR_TEMPERATURE:
+                return getColorTemperatureDescription();
+            case SonyType1MakernoteDirectory.TAG_SCENE_MODE:
+                return getSceneModeDescription();
+            case SonyType1MakernoteDirectory.TAG_ZONE_MATCHING:
+                return getZoneMatchingDescription();
+            case SonyType1MakernoteDirectory.TAG_DYNAMIC_RANGE_OPTIMISER:
+                return getDynamicRangeOptimizerDescription();
+            case SonyType1MakernoteDirectory.TAG_IMAGE_STABILISATION:
+                return getImageStabilizationDescription();
+            // Unfortunately it seems that there is no definite mapping between a lens ID and a lens model
+            // http://gvsoft.homedns.org/exif/makernote-sony-type1.html#0xb027
+//            case SonyType1MakernoteDirectory.TAG_LENS_ID:
+//                return getLensIDDescription();
+            case SonyType1MakernoteDirectory.TAG_COLOR_MODE:
+                return getColorModeDescription();
+            case SonyType1MakernoteDirectory.TAG_MACRO:
+                return getMacroDescription();
+            case SonyType1MakernoteDirectory.TAG_EXPOSURE_MODE:
+                return getExposureModeDescription();
+            case SonyType1MakernoteDirectory.TAG_QUALITY:
+                return getQualityDescription();
+            case SonyType1MakernoteDirectory.TAG_ANTI_BLUR:
+                return getAntiBlurDescription();
+            case SonyType1MakernoteDirectory.TAG_LONG_EXPOSURE_NOISE_REDUCTION:
+                return getLongExposureNoiseReductionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getColorTemperatureDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_COLOR_TEMPERATURE);
+        if (value==null)
+            return null;
+        if (value==0)
+            return "Auto";
+        int kelvin = ((value & 0x00FF0000) >> 8) | ((value & 0xFF000000) >> 24);
+        return String.format("%d K", kelvin);
+    }
+
+    @Nullable
+    public String getSceneModeDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_SCENE_MODE);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "Standard";
+            case 1: return "Portrait";
+            case 2: return "Text";
+            case 3: return "Night Scene";
+            case 4: return "Sunset";
+            case 5: return "Sports";
+            case 6: return "Landscape";
+            case 7: return "Night Portrait";
+            case 8: return "Macro";
+            case 9: return "Super Macro";
+            case 16: return "Auto";
+            case 17: return "Night View/Portrait";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getZoneMatchingDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_ZONE_MATCHING);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "ISO Setting Used";
+            case 1: return "High Key";
+            case 2: return "Low Key";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getDynamicRangeOptimizerDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_DYNAMIC_RANGE_OPTIMISER);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "Off";
+            case 1: return "Standard";
+            case 2: return "Advanced Auto";
+            case 8: return "Advanced LV1";
+            case 9: return "Advanced LV2";
+            case 10: return "Advanced LV3";
+            case 11: return "Advanced LV4";
+            case 12: return "Advanced LV5";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getImageStabilizationDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_IMAGE_STABILISATION);
+        if (value==null)
+            return null;
+        // Different non-zero 'on' values have been observed.  Do they mean different things?
+        return value == 0 ? "Off" : "On";
+    }
+
+    @Nullable
+    public String getColorModeDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_COLOR_MODE);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "Standard";
+            case 1: return "Vivid";
+            case 2: return "Portrait";
+            case 3: return "Landscape";
+            case 4: return "Sunset";
+            case 5: return "Night Portrait";
+            case 6: return "Black & White";
+            case 7: return "Adobe RGB";
+            case 12:
+            case 100: return "Neutral";
+            case 101: return "Clear";
+            case 102: return "Deep";
+            case 103: return "Light";
+            case 104: return "Night View";
+            case 105: return "Autumn Leaves";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getMacroDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_MACRO);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "Off";
+            case 1: return "On";
+            case 2: return "Magnifying Glass/Super Macro";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getExposureModeDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_EXPOSURE_MODE);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "Auto";
+            case 5: return "Landscape";
+            case 6: return "Program";
+            case 7: return "Aperture Priority";
+            case 8: return "Shutter Priority";
+            case 9: return "Night Scene";
+            case 15: return "Manual";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getQualityDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_QUALITY);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "Normal";
+            case 1: return "Fine";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getAntiBlurDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_ANTI_BLUR);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "Off";
+            case 1: return "On (Continuous)";
+            case 2: return "On (Shooting)";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+
+    @Nullable
+    public String getLongExposureNoiseReductionDescription()
+    {
+        Integer value = _directory.getInteger(SonyType1MakernoteDirectory.TAG_LONG_EXPOSURE_NOISE_REDUCTION);
+        if (value==null)
+            return null;
+        switch (value){
+            case 0: return "Off";
+            case 1: return "On";
+            case 0xFFFF: return "N/A";
+            default: return String.format("Unknown (%d)", value);
+        }
+    }
+}
Index: trunk/src/com/drew/metadata/exif/SonyType1MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/SonyType1MakernoteDirectory.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/SonyType1MakernoteDirectory.java	(revision 6127)
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Sony cameras that use the Sony Type 1 makernote tags.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class SonyType1MakernoteDirectory extends Directory
+{
+    public static final int TAG_PRINT_IMAGE_MATCHING_INFO = 0x0E00;
+    public static final int TAG_PREVIEW_IMAGE = 0x2001;
+    public static final int TAG_COLOR_MODE_SETTING = 0xb020;
+    public static final int TAG_COLOR_TEMPERATURE = 0xb021;
+    public static final int TAG_SCENE_MODE = 0xb023;
+    public static final int TAG_ZONE_MATCHING = 0xb024;
+    public static final int TAG_DYNAMIC_RANGE_OPTIMISER = 0xb025;
+    public static final int TAG_IMAGE_STABILISATION = 0xb026;
+    public static final int TAG_LENS_ID = 0xb027;
+    public static final int TAG_MINOLTA_MAKER_NOTE = 0xb028;
+    public static final int TAG_COLOR_MODE = 0xb029;
+    public static final int TAG_MACRO = 0xb040;
+    public static final int TAG_EXPOSURE_MODE = 0xb041;
+    public static final int TAG_QUALITY = 0xb047;
+    public static final int TAG_ANTI_BLUR = 0xb04B;
+    public static final int TAG_LONG_EXPOSURE_NOISE_REDUCTION = 0xb04E;
+    public static final int TAG_NO_PRINT = 0xFFFF;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_PRINT_IMAGE_MATCHING_INFO, "Print Image Matching Info");
+        _tagNameMap.put(TAG_PREVIEW_IMAGE, "Preview Image");
+        _tagNameMap.put(TAG_COLOR_MODE_SETTING, "Color Mode Setting");
+        _tagNameMap.put(TAG_COLOR_TEMPERATURE, "Color Temperature");
+        _tagNameMap.put(TAG_SCENE_MODE, "Scene Mode");
+        _tagNameMap.put(TAG_ZONE_MATCHING, "Zone Matching");
+        _tagNameMap.put(TAG_DYNAMIC_RANGE_OPTIMISER, "Dynamic Range Optimizer");
+        _tagNameMap.put(TAG_IMAGE_STABILISATION, "Image Stabilisation");
+        _tagNameMap.put(TAG_LENS_ID, "Lens ID");
+        _tagNameMap.put(TAG_MINOLTA_MAKER_NOTE, "Minolta Maker Note");
+        _tagNameMap.put(TAG_COLOR_MODE, "Color Mode");
+        _tagNameMap.put(TAG_MACRO, "Macro");
+        _tagNameMap.put(TAG_EXPOSURE_MODE, "Exposure Mode");
+        _tagNameMap.put(TAG_QUALITY, "Quality");
+        _tagNameMap.put(TAG_ANTI_BLUR, "Anti Blur");
+        _tagNameMap.put(TAG_LONG_EXPOSURE_NOISE_REDUCTION, "Long Exposure Noise Reduction");
+        _tagNameMap.put(TAG_NO_PRINT, "No Print");
+    }
+
+    public SonyType1MakernoteDirectory()
+    {
+        this.setDescriptor(new SonyType1MakernoteDescriptor(this));
+    }
+
+    @NotNull
+    public String getName()
+    {
+        return "Sony Makernote";
+    }
+
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/exif/SonyType6MakernoteDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/exif/SonyType6MakernoteDescriptor.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/SonyType6MakernoteDescriptor.java	(revision 6127)
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
+import com.drew.metadata.TagDescriptor;
+
+/**
+ * Provides human-readable string representations of tag values stored in a <code>SonyType6MakernoteDirectory</code>.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class SonyType6MakernoteDescriptor extends TagDescriptor<SonyType6MakernoteDirectory>
+{
+    public SonyType6MakernoteDescriptor(@NotNull SonyType6MakernoteDirectory directory)
+    {
+        super(directory);
+    }
+
+    @Nullable
+    public String getDescription(int tagType)
+    {
+        switch (tagType) {
+            case SonyType6MakernoteDirectory.TAG_MAKER_NOTE_THUMB_VERSION:
+                return getMakerNoteThumbVersionDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getMakerNoteThumbVersionDescription()
+    {
+        int[] values = _directory.getIntArray(SonyType6MakernoteDirectory.TAG_MAKER_NOTE_THUMB_VERSION);
+        return convertBytesToVersionString(values, 2);
+    }
+}
Index: trunk/src/com/drew/metadata/exif/SonyType6MakernoteDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/exif/SonyType6MakernoteDirectory.java	(revision 6127)
+++ trunk/src/com/drew/metadata/exif/SonyType6MakernoteDirectory.java	(revision 6127)
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
+ */
+
+package com.drew.metadata.exif;
+
+import com.drew.lang.annotations.NotNull;
+import com.drew.metadata.Directory;
+
+import java.util.HashMap;
+
+/**
+ * Describes tags specific to Sony cameras that use the Sony Type 6 makernote tags.
+ *
+ * @author Drew Noakes http://drewnoakes.com
+ */
+public class SonyType6MakernoteDirectory extends Directory
+{
+    public static final int TAG_MAKER_NOTE_THUMB_OFFSET = 0x0513;
+    public static final int TAG_MAKER_NOTE_THUMB_LENGTH = 0x0514;
+    public static final int TAG_UNKNOWN_1 = 0x0515;
+    public static final int TAG_MAKER_NOTE_THUMB_VERSION = 0x2000;
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
+
+    static
+    {
+        _tagNameMap.put(TAG_MAKER_NOTE_THUMB_OFFSET, "Maker Note Thumb Offset");
+        _tagNameMap.put(TAG_MAKER_NOTE_THUMB_LENGTH, "Maker Note Thumb Length");
+        _tagNameMap.put(TAG_UNKNOWN_1, "Sony-6-0x0203");
+        _tagNameMap.put(TAG_MAKER_NOTE_THUMB_VERSION, "Maker Note Thumb Version");
+    }
+
+    public SonyType6MakernoteDirectory()
+    {
+        this.setDescriptor(new SonyType6MakernoteDescriptor(this));
+    }
+
+    @NotNull
+    public String getName()
+    {
+        return "Sony Makernote";
+    }
+
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+}
Index: trunk/src/com/drew/metadata/iptc/IptcDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/iptc/IptcDescriptor.java	(revision 6127)
@@ -1,36 +1,242 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 21-Nov-2002 17:58:19 using IntelliJ IDEA.
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.iptc;
 
-import com.drew.metadata.Directory;
+import com.drew.lang.StringUtil;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
- *
+ * Provides human-readable string representations of tag values stored in a <code>IptcDirectory</code>.
+ * <p/>
+ * As the IPTC directory already stores values as strings, this class simply returns the tag's value.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class IptcDescriptor extends TagDescriptor
+public class IptcDescriptor extends TagDescriptor<IptcDirectory>
 {
-    public IptcDescriptor(Directory directory)
+    public IptcDescriptor(@NotNull IptcDirectory directory)
     {
         super(directory);
     }
 
+    @Nullable
     public String getDescription(int tagType)
     {
-        return _directory.getString(tagType);
+        switch (tagType) {
+            case IptcDirectory.TAG_FILE_FORMAT:
+                return getFileFormatDescription();
+            case IptcDirectory.TAG_KEYWORDS:
+                return getKeywordsDescription();
+            default:
+                return super.getDescription(tagType);
+        }
+    }
+
+    @Nullable
+    public String getFileFormatDescription()
+    {
+        Integer value = _directory.getInteger(IptcDirectory.TAG_FILE_FORMAT);
+        if (value == null)
+            return null;
+        switch (value) {
+            case 0: return "No ObjectData";
+            case 1: return "IPTC-NAA Digital Newsphoto Parameter Record";
+            case 2: return "IPTC7901 Recommended Message Format";
+            case 3: return "Tagged Image File Format (Adobe/Aldus Image data)";
+            case 4: return "Illustrator (Adobe Graphics data)";
+            case 5: return "AppleSingle (Apple Computer Inc)";
+            case 6: return "NAA 89-3 (ANPA 1312)";
+            case 7: return "MacBinary II";
+            case 8: return "IPTC Unstructured Character Oriented File Format (UCOFF)";
+            case 9: return "United Press International ANPA 1312 variant";
+            case 10: return "United Press International Down-Load Message";
+            case 11: return "JPEG File Interchange (JFIF)";
+            case 12: return "Photo-CD Image-Pac (Eastman Kodak)";
+            case 13: return "Bit Mapped Graphics File [.BMP] (Microsoft)";
+            case 14: return "Digital Audio File [.WAV] (Microsoft & Creative Labs)";
+            case 15: return "Audio plus Moving Video [.AVI] (Microsoft)";
+            case 16: return "PC DOS/Windows Executable Files [.COM][.EXE]";
+            case 17: return "Compressed Binary File [.ZIP] (PKWare Inc)";
+            case 18: return "Audio Interchange File Format AIFF (Apple Computer Inc)";
+            case 19: return "RIFF Wave (Microsoft Corporation)";
+            case 20: return "Freehand (Macromedia/Aldus)";
+            case 21: return "Hypertext Markup Language [.HTML] (The Internet Society)";
+            case 22: return "MPEG 2 Audio Layer 2 (Musicom), ISO/IEC";
+            case 23: return "MPEG 2 Audio Layer 3, ISO/IEC";
+            case 24: return "Portable Document File [.PDF] Adobe";
+            case 25: return "News Industry Text Format (NITF)";
+            case 26: return "Tape Archive [.TAR]";
+            case 27: return "Tidningarnas Telegrambyra NITF version (TTNITF DTD)";
+            case 28: return "Ritzaus Bureau NITF version (RBNITF DTD)";
+            case 29: return "Corel Draw [.CDR]";
+        }
+        return String.format("Unknown (%d)", value);
+    }
+
+    @Nullable
+    public String getByLineDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_BY_LINE);
+    }
+
+    @Nullable
+    public String getByLineTitleDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_BY_LINE_TITLE);
+    }
+
+    @Nullable
+    public String getCaptionDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CAPTION);
+    }
+
+    @Nullable
+    public String getCategoryDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CATEGORY);
+    }
+
+    @Nullable
+    public String getCityDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CITY);
+    }
+
+    @Nullable
+    public String getCopyrightNoticeDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_COPYRIGHT_NOTICE);
+    }
+
+    @Nullable
+    public String getCountryOrPrimaryLocationDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME);
+    }
+
+    @Nullable
+    public String getCreditDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CREDIT);
+    }
+
+    @Nullable
+    public String getDateCreatedDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_DATE_CREATED);
+    }
+
+    @Nullable
+    public String getHeadlineDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_HEADLINE);
+    }
+
+    @Nullable
+    public String getKeywordsDescription()
+    {
+        final String[] keywords = _directory.getStringArray(IptcDirectory.TAG_KEYWORDS);
+        if (keywords==null)
+            return null;
+        return StringUtil.join(keywords, ";");
+    }
+
+    @Nullable
+    public String getObjectNameDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_OBJECT_NAME);
+    }
+
+    @Nullable
+    public String getOriginalTransmissionReferenceDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_ORIGINAL_TRANSMISSION_REFERENCE);
+    }
+
+    @Nullable
+    public String getOriginatingProgramDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_ORIGINATING_PROGRAM);
+    }
+
+    @Nullable
+    public String getProvinceOrStateDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_PROVINCE_OR_STATE);
+    }
+
+    @Nullable
+    public String getRecordVersionDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_APPLICATION_RECORD_VERSION);
+    }
+
+    @Nullable
+    public String getReleaseDateDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_RELEASE_DATE);
+    }
+
+    @Nullable
+    public String getReleaseTimeDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_RELEASE_TIME);
+    }
+
+    @Nullable
+    public String getSourceDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_SOURCE);
+    }
+
+    @Nullable
+    public String getSpecialInstructionsDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_SPECIAL_INSTRUCTIONS);
+    }
+
+    @Nullable
+    public String getSupplementalCategoriesDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_SUPPLEMENTAL_CATEGORIES);
+    }
+
+    @Nullable
+    public String getTimeCreatedDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_TIME_CREATED);
+    }
+
+    @Nullable
+    public String getUrgencyDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_URGENCY);
+    }
+
+    @Nullable
+    public String getWriterDescription()
+    {
+        return _directory.getString(IptcDirectory.TAG_CAPTION_WRITER);
     }
 }
Index: trunk/src/com/drew/metadata/iptc/IptcDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/iptc/IptcDirectory.java	(revision 6127)
@@ -1,82 +1,207 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created by dnoakes on 26-Nov-2002 01:26:39 using IntelliJ IDEA.
+ * Copyright 2002-2012 Drew Noakes
+ *
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
+ *
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.iptc;
 
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.List;
 
 /**
- *
+ * Describes tags used by the International Press Telecommunications Council (IPTC) metadata format.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class IptcDirectory extends Directory
 {
-    public static final int TAG_RECORD_VERSION = 0x0200;
-    public static final int TAG_CAPTION = 0x0278;
-    public static final int TAG_WRITER = 0x027a;
-    public static final int TAG_HEADLINE = 0x0269;
-    public static final int TAG_SPECIAL_INSTRUCTIONS = 0x0228;
-    public static final int TAG_BY_LINE = 0x0250;
-    public static final int TAG_BY_LINE_TITLE = 0x0255;
-    public static final int TAG_CREDIT = 0x026e;
-    public static final int TAG_SOURCE = 0x0273;
-    public static final int TAG_OBJECT_NAME = 0x0205;
-    public static final int TAG_DATE_CREATED = 0x0237;
-    public static final int TAG_CITY = 0x025a;
-    public static final int TAG_PROVINCE_OR_STATE = 0x025f;
-    public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION = 0x0265;
-    public static final int TAG_ORIGINAL_TRANSMISSION_REFERENCE = 0x0267;
-    public static final int TAG_CATEGORY = 0x020f;
-    public static final int TAG_SUPPLEMENTAL_CATEGORIES = 0x0214;
-    public static final int TAG_URGENCY = 0x0200 | 10;
-    public static final int TAG_KEYWORDS = 0x0200 | 25;
-    public static final int TAG_COPYRIGHT_NOTICE = 0x0274;
-    public static final int TAG_RELEASE_DATE = 0x0200 | 30;
-    public static final int TAG_RELEASE_TIME = 0x0200 | 35;
-    public static final int TAG_TIME_CREATED = 0x0200 | 60;
-    public static final int TAG_ORIGINATING_PROGRAM = 0x0200 | 65;
-
-    protected static final HashMap tagNameMap = new HashMap();
+    // IPTC EnvelopeRecord Tags
+    public static final int TAG_ENVELOPE_RECORD_VERSION          = 0x0100; // 0 + 0x0100
+    public static final int TAG_DESTINATION                      = 0x0105; // 5
+    public static final int TAG_FILE_FORMAT                      = 0x0114; // 20
+    public static final int TAG_FILE_VERSION                     = 0x0116; // 22
+    public static final int TAG_SERVICE_ID                       = 0x011E; // 30
+    public static final int TAG_ENVELOPE_NUMBER                  = 0x0128; // 40
+    public static final int TAG_PRODUCT_ID                       = 0x0132; // 50
+    public static final int TAG_ENVELOPE_PRIORITY                = 0x013C; // 60
+    public static final int TAG_DATE_SENT                        = 0x0146; // 70
+    public static final int TAG_TIME_SENT                        = 0x0150; // 80
+    public static final int TAG_CODED_CHARACTER_SET              = 0x015A; // 90
+    public static final int TAG_UNIQUE_OBJECT_NAME               = 0x0164; // 100
+    public static final int TAG_ARM_IDENTIFIER                   = 0x0178; // 120
+    public static final int TAG_ARM_VERSION                      = 0x017a; // 122
+
+    // IPTC ApplicationRecord Tags
+    public static final int TAG_APPLICATION_RECORD_VERSION       = 0x0200; // 0 + 0x0200
+    public static final int TAG_OBJECT_TYPE_REFERENCE            = 0x0203; // 3
+    public static final int TAG_OBJECT_ATTRIBUTE_REFERENCE       = 0x0204; // 4
+    public static final int TAG_OBJECT_NAME                      = 0x0205; // 5
+    public static final int TAG_EDIT_STATUS                      = 0x0207; // 7
+    public static final int TAG_EDITORIAL_UPDATE                 = 0x0208; // 8
+    public static final int TAG_URGENCY                          = 0X020A; // 10
+    public static final int TAG_SUBJECT_REFERENCE                = 0X020C; // 12
+    public static final int TAG_CATEGORY                         = 0x020F; // 15
+    public static final int TAG_SUPPLEMENTAL_CATEGORIES          = 0x0214; // 20
+    public static final int TAG_FIXTURE_ID                       = 0x0216; // 22
+    public static final int TAG_KEYWORDS                         = 0x0219; // 25
+    public static final int TAG_CONTENT_LOCATION_CODE            = 0x021A; // 26
+    public static final int TAG_CONTENT_LOCATION_NAME            = 0x021B; // 27
+    public static final int TAG_RELEASE_DATE                     = 0X021E; // 30
+    public static final int TAG_RELEASE_TIME                     = 0x0223; // 35
+    public static final int TAG_EXPIRATION_DATE                  = 0x0225; // 37
+    public static final int TAG_EXPIRATION_TIME                  = 0x0226; // 38
+    public static final int TAG_SPECIAL_INSTRUCTIONS             = 0x0228; // 40
+    public static final int TAG_ACTION_ADVISED                   = 0x022A; // 42
+    public static final int TAG_REFERENCE_SERVICE                = 0x022D; // 45
+    public static final int TAG_REFERENCE_DATE                   = 0x022F; // 47
+    public static final int TAG_REFERENCE_NUMBER                 = 0x0232; // 50
+    public static final int TAG_DATE_CREATED                     = 0x0237; // 55
+    public static final int TAG_TIME_CREATED                     = 0X023C; // 60
+    public static final int TAG_DIGITAL_DATE_CREATED             = 0x023E; // 62
+    public static final int TAG_DIGITAL_TIME_CREATED             = 0x023F; // 63
+    public static final int TAG_ORIGINATING_PROGRAM              = 0x0241; // 65
+    public static final int TAG_PROGRAM_VERSION                  = 0x0246; // 70
+    public static final int TAG_OBJECT_CYCLE                     = 0x024B; // 75
+    public static final int TAG_BY_LINE                          = 0x0250; // 80
+    public static final int TAG_BY_LINE_TITLE                    = 0x0255; // 85
+    public static final int TAG_CITY                             = 0x025A; // 90
+    public static final int TAG_SUB_LOCATION                     = 0x025C; // 92
+    public static final int TAG_PROVINCE_OR_STATE                = 0x025F; // 95
+    public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION_CODE = 0x0264; // 100
+    public static final int TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME = 0x0265; // 101
+    public static final int TAG_ORIGINAL_TRANSMISSION_REFERENCE  = 0x0267; // 103
+    public static final int TAG_HEADLINE                         = 0x0269; // 105
+    public static final int TAG_CREDIT                           = 0x026E; // 110
+    public static final int TAG_SOURCE                           = 0x0273; // 115
+    public static final int TAG_COPYRIGHT_NOTICE                 = 0x0274; // 116
+    public static final int TAG_CONTACT                          = 0x0276; // 118
+    public static final int TAG_CAPTION                          = 0x0278; // 120
+    public static final int TAG_LOCAL_CAPTION                    = 0x0279; // 121
+    public static final int TAG_CAPTION_WRITER                   = 0x027A; // 122
+    public static final int TAG_RASTERIZED_CAPTION               = 0x027D; // 125
+    public static final int TAG_IMAGE_TYPE                       = 0x0282; // 130
+    public static final int TAG_IMAGE_ORIENTATION                = 0x0283; // 131
+    public static final int TAG_LANGUAGE_IDENTIFIER              = 0x0287; // 135
+    public static final int TAG_AUDIO_TYPE                       = 0x0296; // 150
+    public static final int TAG_AUDIO_SAMPLING_RATE              = 0x0297; // 151
+    public static final int TAG_AUDIO_SAMPLING_RESOLUTION        = 0x0298; // 152
+    public static final int TAG_AUDIO_DURATION                   = 0x0299; // 153
+    public static final int TAG_AUDIO_OUTCUE                     = 0x029A; // 154
+
+    public static final int TAG_JOB_ID                           = 0x02B8; // 184
+    public static final int TAG_MASTER_DOCUMENT_ID               = 0x02B9; // 185
+    public static final int TAG_SHORT_DOCUMENT_ID                = 0x02BA; // 186
+    public static final int TAG_UNIQUE_DOCUMENT_ID               = 0x02BB; // 187
+    public static final int TAG_OWNER_ID                         = 0x02BC; // 188
+
+    public static final int TAG_OBJECT_PREVIEW_FILE_FORMAT       = 0x02C8; // 200
+    public static final int TAG_OBJECT_PREVIEW_FILE_FORMAT_VERSION  = 0x02C9; // 201
+    public static final int TAG_OBJECT_PREVIEW_DATA              = 0x02CA; // 202
+
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
     static
     {
-        tagNameMap.put(new Integer(TAG_RECORD_VERSION), "Directory Version");
-        tagNameMap.put(new Integer(TAG_CAPTION), "Caption/Abstract");
-        tagNameMap.put(new Integer(TAG_WRITER), "Writer/Editor");
-        tagNameMap.put(new Integer(TAG_HEADLINE), "Headline");
-        tagNameMap.put(new Integer(TAG_SPECIAL_INSTRUCTIONS), "Special Instructions");
-        tagNameMap.put(new Integer(TAG_BY_LINE), "By-line");
-        tagNameMap.put(new Integer(TAG_BY_LINE_TITLE), "By-line Title");
-        tagNameMap.put(new Integer(TAG_CREDIT), "Credit");
-        tagNameMap.put(new Integer(TAG_SOURCE), "Source");
-        tagNameMap.put(new Integer(TAG_OBJECT_NAME), "Object Name");
-        tagNameMap.put(new Integer(TAG_DATE_CREATED), "Date Created");
-        tagNameMap.put(new Integer(TAG_CITY), "City");
-        tagNameMap.put(new Integer(TAG_PROVINCE_OR_STATE), "Province/State");
-        tagNameMap.put(new Integer(TAG_COUNTRY_OR_PRIMARY_LOCATION), "Country/Primary Location");
-        tagNameMap.put(new Integer(TAG_ORIGINAL_TRANSMISSION_REFERENCE), "Original Transmission Reference");
-        tagNameMap.put(new Integer(TAG_CATEGORY), "Category");
-        tagNameMap.put(new Integer(TAG_SUPPLEMENTAL_CATEGORIES), "Supplemental Category(s)");
-        tagNameMap.put(new Integer(TAG_URGENCY), "Urgency");
-        tagNameMap.put(new Integer(TAG_KEYWORDS), "Keywords");
-        tagNameMap.put(new Integer(TAG_COPYRIGHT_NOTICE), "Copyright Notice");
-        tagNameMap.put(new Integer(TAG_RELEASE_DATE), "Release Date");
-        tagNameMap.put(new Integer(TAG_RELEASE_TIME), "Release Time");
-        tagNameMap.put(new Integer(TAG_TIME_CREATED), "Time Created");
-        tagNameMap.put(new Integer(TAG_ORIGINATING_PROGRAM), "Originating Program");
+        _tagNameMap.put(TAG_ENVELOPE_RECORD_VERSION, "Enveloped Record Version");
+        _tagNameMap.put(TAG_DESTINATION, "Destination");
+        _tagNameMap.put(TAG_FILE_FORMAT, "File Format");
+        _tagNameMap.put(TAG_FILE_VERSION, "File Version");
+        _tagNameMap.put(TAG_SERVICE_ID, "Service Identifier");
+        _tagNameMap.put(TAG_ENVELOPE_NUMBER, "Envelope Number");
+        _tagNameMap.put(TAG_PRODUCT_ID, "Product Identifier");
+        _tagNameMap.put(TAG_ENVELOPE_PRIORITY, "Envelope Priority");
+        _tagNameMap.put(TAG_DATE_SENT, "Date Sent");
+        _tagNameMap.put(TAG_TIME_SENT, "Time Sent");
+        _tagNameMap.put(TAG_CODED_CHARACTER_SET, "Coded Character Set");
+        _tagNameMap.put(TAG_UNIQUE_OBJECT_NAME, "Unique Object Name");
+        _tagNameMap.put(TAG_ARM_IDENTIFIER, "ARM Identifier");
+        _tagNameMap.put(TAG_ARM_VERSION, "ARM Version");
+
+        _tagNameMap.put(TAG_APPLICATION_RECORD_VERSION, "Application Record Version");
+        _tagNameMap.put(TAG_OBJECT_TYPE_REFERENCE, "Object Type Reference");
+        _tagNameMap.put(TAG_OBJECT_ATTRIBUTE_REFERENCE, "Object Attribute Reference");
+        _tagNameMap.put(TAG_OBJECT_NAME, "Object Name");
+        _tagNameMap.put(TAG_EDIT_STATUS, "Edit Status");
+        _tagNameMap.put(TAG_EDITORIAL_UPDATE, "Editorial Update");
+        _tagNameMap.put(TAG_URGENCY, "Urgency");
+        _tagNameMap.put(TAG_SUBJECT_REFERENCE, "Subject Reference");
+        _tagNameMap.put(TAG_CATEGORY, "Category");
+        _tagNameMap.put(TAG_SUPPLEMENTAL_CATEGORIES, "Supplemental Category(s)");
+        _tagNameMap.put(TAG_FIXTURE_ID, "Fixture Identifier");
+        _tagNameMap.put(TAG_KEYWORDS, "Keywords");
+        _tagNameMap.put(TAG_CONTENT_LOCATION_CODE, "Content Location Code");
+        _tagNameMap.put(TAG_CONTENT_LOCATION_NAME, "Content Location Name");
+        _tagNameMap.put(TAG_RELEASE_DATE, "Release Date");
+        _tagNameMap.put(TAG_RELEASE_TIME, "Release Time");
+        _tagNameMap.put(TAG_EXPIRATION_DATE, "Expiration Date");
+        _tagNameMap.put(TAG_EXPIRATION_TIME, "Expiration Time");
+        _tagNameMap.put(TAG_SPECIAL_INSTRUCTIONS, "Special Instructions");
+        _tagNameMap.put(TAG_ACTION_ADVISED, "Action Advised");
+        _tagNameMap.put(TAG_REFERENCE_SERVICE, "Reference Service");
+        _tagNameMap.put(TAG_REFERENCE_DATE, "Reference Date");
+        _tagNameMap.put(TAG_REFERENCE_NUMBER, "Reference Number");
+        _tagNameMap.put(TAG_DATE_CREATED, "Date Created");
+        _tagNameMap.put(TAG_TIME_CREATED, "Time Created");
+        _tagNameMap.put(TAG_DIGITAL_DATE_CREATED, "Digital Date Created");
+        _tagNameMap.put(TAG_DIGITAL_TIME_CREATED, "Digital Time Created");
+        _tagNameMap.put(TAG_ORIGINATING_PROGRAM, "Originating Program");
+        _tagNameMap.put(TAG_PROGRAM_VERSION, "Program Version");
+        _tagNameMap.put(TAG_OBJECT_CYCLE, "Object Cycle");
+        _tagNameMap.put(TAG_BY_LINE, "By-line");
+        _tagNameMap.put(TAG_BY_LINE_TITLE, "By-line Title");
+        _tagNameMap.put(TAG_CITY, "City");
+        _tagNameMap.put(TAG_SUB_LOCATION, "Sub-location");
+        _tagNameMap.put(TAG_PROVINCE_OR_STATE, "Province/State");
+        _tagNameMap.put(TAG_COUNTRY_OR_PRIMARY_LOCATION_CODE, "Country/Primary Location Code");
+        _tagNameMap.put(TAG_COUNTRY_OR_PRIMARY_LOCATION_NAME, "Country/Primary Location Name");
+        _tagNameMap.put(TAG_ORIGINAL_TRANSMISSION_REFERENCE, "Original Transmission Reference");
+        _tagNameMap.put(TAG_HEADLINE, "Headline");
+        _tagNameMap.put(TAG_CREDIT, "Credit");
+        _tagNameMap.put(TAG_SOURCE, "Source");
+        _tagNameMap.put(TAG_COPYRIGHT_NOTICE, "Copyright Notice");
+        _tagNameMap.put(TAG_CONTACT, "Contact");
+        _tagNameMap.put(TAG_CAPTION, "Caption/Abstract");
+        _tagNameMap.put(TAG_LOCAL_CAPTION, "Local Caption");
+        _tagNameMap.put(TAG_CAPTION_WRITER, "Caption Writer/Editor");
+        _tagNameMap.put(TAG_RASTERIZED_CAPTION, "Rasterized Caption");
+        _tagNameMap.put(TAG_IMAGE_TYPE, "Image Type");
+        _tagNameMap.put(TAG_IMAGE_ORIENTATION, "Image Orientation");
+        _tagNameMap.put(TAG_LANGUAGE_IDENTIFIER, "Language Identifier");
+        _tagNameMap.put(TAG_AUDIO_TYPE, "Audio Type");
+        _tagNameMap.put(TAG_AUDIO_SAMPLING_RATE, "Audio Sampling Rate");
+        _tagNameMap.put(TAG_AUDIO_SAMPLING_RESOLUTION, "Audio Sampling Resolution");
+        _tagNameMap.put(TAG_AUDIO_DURATION, "Audio Duration");
+        _tagNameMap.put(TAG_AUDIO_OUTCUE, "Audio Outcue");
+
+        _tagNameMap.put(TAG_JOB_ID, "Job Identifier");
+        _tagNameMap.put(TAG_MASTER_DOCUMENT_ID, "Master Document Identifier");
+        _tagNameMap.put(TAG_SHORT_DOCUMENT_ID, "Short Document Identifier");
+        _tagNameMap.put(TAG_UNIQUE_DOCUMENT_ID, "Unique Document Identifier");
+        _tagNameMap.put(TAG_OWNER_ID, "Owner Identifier");
+
+        _tagNameMap.put(TAG_OBJECT_PREVIEW_FILE_FORMAT, "Object Data Preview File Format");
+        _tagNameMap.put(TAG_OBJECT_PREVIEW_FILE_FORMAT_VERSION, "Object Data Preview File Format Version");
+        _tagNameMap.put(TAG_OBJECT_PREVIEW_DATA, "Object Data Preview Data");
     }
 
@@ -86,4 +211,5 @@
     }
 
+    @NotNull
     public String getName()
     {
@@ -91,7 +217,20 @@
     }
 
-    protected HashMap getTagNameMap()
-    {
-        return tagNameMap;
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
+
+    /**
+     * Returns any keywords contained in the IPTC data.  This value may be <code>null</code>.
+     */
+    @Nullable
+    public List<String> getKeywords()
+    {
+        final String[] array = getStringArray(IptcDirectory.TAG_KEYWORDS);
+        if (array==null)
+            return null;
+        return Arrays.asList(array);
     }
 }
Index: trunk/src/com/drew/metadata/iptc/IptcProcessingException.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcProcessingException.java	(revision 6002)
+++ 	(revision )
@@ -1,51 +1,0 @@
-/*
- * ExifProcessingException.java
- *
- * This class is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
- *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
- *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
- *
- * Created on 29 April 2002, 00:33
- */
-
-package com.drew.metadata.iptc;
-
-import com.drew.metadata.MetadataException;
-
-/**
- * The exception type raised during reading of Iptc data in the instance of
- * unexpected data conditions.
- * @author  Drew Noakes http://drewnoakes.com
- */
-public class IptcProcessingException extends MetadataException
-{
-    /**
-     * Constructs an instance of <code>ExifProcessingException</code> with the
-     * specified detail message.
-     * @param message the detail message
-     */
-    public IptcProcessingException(String message)
-    {
-        super(message);
-    }
-
-    /**
-     * Constructs an instance of <code>IptcProcessingException</code> with the
-     * specified detail message and inner exception.
-     * @param message the detail message
-     * @param cause an inner exception
-     */
-    public IptcProcessingException(String message, Throwable cause)
-    {
-        super(message, cause);
-    }
-}
Index: trunk/src/com/drew/metadata/iptc/IptcReader.java
===================================================================
--- trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 6002)
+++ trunk/src/com/drew/metadata/iptc/IptcReader.java	(revision 6127)
@@ -1,36 +1,41 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on 12-Nov-2002 19:00:03 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.iptc;
 
-import com.drew.imaging.jpeg.JpegProcessingException;
-import com.drew.imaging.jpeg.JpegSegmentReader;
+import com.drew.lang.BufferBoundsException;
+import com.drew.lang.BufferReader;
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataException;
 import com.drew.metadata.MetadataReader;
 
-import java.io.File;
-import java.io.InputStream;
 import java.util.Date;
 
 /**
+ * Decodes IPTC binary data, populating a <code>Metadata</code> object with tag values in an <code>IptcDirectory</code>.
  *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class IptcReader implements MetadataReader
 {
+    // TODO consider breaking the IPTC section up into multiple directories and providing segregation of each IPTC directory
 /*
     public static final int DIRECTORY_IPTC = 2;
@@ -46,70 +51,43 @@
     public static final int POST_DATA_RECORD = 9;
 */
-    /**
-     * The Iptc data segment.
-     */
-    private final byte[] _data;
 
-    /**
-     * Creates a new IptcReader for the specified Jpeg jpegFile.
-     */
-    public IptcReader(File jpegFile) throws JpegProcessingException
+    /** Performs the IPTC data extraction, adding found values to the specified instance of <code>Metadata</code>. */
+    public void extract(@NotNull final BufferReader reader, @NotNull final Metadata metadata)
     {
-        this(new JpegSegmentReader(jpegFile).readSegment(JpegSegmentReader.SEGMENT_APPD));
-    }
+        IptcDirectory directory = metadata.getOrCreateDirectory(IptcDirectory.class);
 
-    /** Creates an IptcReader for a JPEG stream.
-     *
-     * @param is JPEG stream. Stream will be closed.
-     */
-    public IptcReader(InputStream is) throws JpegProcessingException
-    {
-        this(new JpegSegmentReader(is).readSegment(JpegSegmentReader.SEGMENT_APPD));
-    }
+        int offset = 0;
 
-    public IptcReader(byte[] data)
-    {
-        _data = data;
-    }
-
-    /**
-     * Performs the Exif data extraction, returning a new instance of <code>Metadata</code>.
-     */
-    public Metadata extract()
-    {
-        return extract(new Metadata());
-    }
-
-    /**
-     * Performs the Exif data extraction, adding found values to the specified
-     * instance of <code>Metadata</code>.
-     */
-    public Metadata extract(Metadata metadata)
-    {
-        if (_data == null) {
-            return metadata;
+/*
+        // find start-of-segment marker (potentially need to skip some ASCII photoshop header info)
+        try {
+            while (offset < data.length - 1 && reader.getUInt16(offset) != 0x1c01 && reader.getUInt16(offset) != 0x1c02)
+                offset++;
+        } catch (BufferBoundsException e) {
+            directory.addError("Couldn't find start of IPTC data (invalid segment)");
+            return;
         }
-
-        Directory directory = metadata.getDirectory(IptcDirectory.class);
-
-        // find start of data
-        int offset = 0;
-        try {
-            while (offset < _data.length - 1 && get32Bits(offset) != 0x1c02) {
-                offset++;
-            }
-        } catch (MetadataException e) {
-            directory.addError("Couldn't find start of Iptc data (invalid segment)");
-            return metadata;
-        }
+*/
 
         // for each tag
-        while (offset < _data.length) {
+        while (offset < reader.getLength()) {
+
             // identifies start of a tag
-            if (_data[offset] != 0x1c) {
+            short startByte;
+            try {
+                startByte = reader.getUInt8(offset);
+            } catch (BufferBoundsException e) {
+                directory.addError("Unable to read starting byte of IPTC tag");
                 break;
             }
+
+            if (startByte != 0x1c) {
+                directory.addError("Invalid start to IPTC tag");
+                break;
+            }
+
             // we need at least five bytes left to read a tag
-            if ((offset + 5) >= _data.length) {
+            if (offset + 5 >= reader.getLength()) {
+                directory.addError("Too few bytes remain for a valid IPTC tag");
                 break;
             }
@@ -121,54 +99,42 @@
             int tagByteCount;
             try {
-                directoryType = _data[offset++];
-                tagType = _data[offset++];
-                tagByteCount = get32Bits(offset);
-            } catch (MetadataException e) {
-                directory.addError("Iptc data segment ended mid-way through tag descriptor");
-                return metadata;
+                directoryType = reader.getUInt8(offset++);
+                tagType = reader.getUInt8(offset++);
+                tagByteCount = reader.getUInt16(offset);
+                offset += 2;
+            } catch (BufferBoundsException e) {
+                directory.addError("IPTC data segment ended mid-way through tag descriptor");
+                return;
             }
-            offset += 2;
-            if ((offset + tagByteCount) > _data.length) {
-                directory.addError("data for tag extends beyond end of iptc segment");
+
+            if (offset + tagByteCount > reader.getLength()) {
+                directory.addError("Data for tag extends beyond end of IPTC segment");
                 break;
             }
 
-            processTag(directory, directoryType, tagType, offset, tagByteCount);
+            try {
+                processTag(reader, directory, directoryType, tagType, offset, tagByteCount);
+            } catch (BufferBoundsException e) {
+                directory.addError("Error processing IPTC tag");
+                break;
+            }
+
             offset += tagByteCount;
         }
-
-        return metadata;
     }
 
-    /**
-     * Returns an int calculated from two bytes of data at the specified offset (MSB, LSB).
-     * @param offset position within the data buffer to read first byte
-     * @return the 32 bit int value, between 0x0000 and 0xFFFF
-     */
-    private int get32Bits(int offset) throws MetadataException
-    {
-        if (offset >= _data.length) {
-            throw new MetadataException("Attempt to read bytes from outside Iptc data buffer");
-        }
-        return ((_data[offset] & 255) << 8) | (_data[offset + 1] & 255);
-    }
-
-    /**
-     * This method serves as marsheller of objects for dataset. It converts from IPTC
-     * octets to relevant java object.
-     */
-    private void processTag(Directory directory, int directoryType, int tagType, int offset, int tagByteCount)
+    private void processTag(@NotNull BufferReader reader, @NotNull Directory directory, int directoryType, int tagType, int offset, int tagByteCount) throws BufferBoundsException
     {
         int tagIdentifier = tagType | (directoryType << 8);
 
         switch (tagIdentifier) {
-            case IptcDirectory.TAG_RECORD_VERSION:
+            case IptcDirectory.TAG_APPLICATION_RECORD_VERSION:
                 // short
-                short shortValue = (short)((_data[offset] << 8) | _data[offset + 1]);
+                int shortValue = reader.getUInt16(offset);
                 directory.setInt(tagIdentifier, shortValue);
                 return;
             case IptcDirectory.TAG_URGENCY:
                 // byte
-                directory.setInt(tagIdentifier, _data[offset]);
+                directory.setInt(tagIdentifier, reader.getUInt8(offset));
                 return;
             case IptcDirectory.TAG_RELEASE_DATE:
@@ -176,10 +142,10 @@
                 // Date object
                 if (tagByteCount >= 8) {
-                    String dateStr = new String(_data, offset, tagByteCount);
+                    String dateStr = reader.getString(offset, tagByteCount);
                     try {
                         int year = Integer.parseInt(dateStr.substring(0, 4));
                         int month = Integer.parseInt(dateStr.substring(4, 6)) - 1;
                         int day = Integer.parseInt(dateStr.substring(6, 8));
-                        Date date = (new java.util.GregorianCalendar(year, month, day)).getTime();
+                        Date date = new java.util.GregorianCalendar(year, month, day).getTime();
                         directory.setDate(tagIdentifier, date);
                         return;
@@ -194,26 +160,22 @@
                 // fall through
         }
-        // If no special handling by now, treat it as a string
+
+        // If we haven't returned yet, treat it as a string
         String str;
         if (tagByteCount < 1) {
             str = "";
         } else {
-            str = new String(_data, offset, tagByteCount);
+            str = reader.getString(offset, tagByteCount, System.getProperty("file.encoding")); // "ISO-8859-1"
         }
+
         if (directory.containsTag(tagIdentifier)) {
-            String[] oldStrings;
+            // this fancy string[] business avoids using an ArrayList for performance reasons
+            String[] oldStrings = directory.getStringArray(tagIdentifier);
             String[] newStrings;
-            try {
-                oldStrings = directory.getStringArray(tagIdentifier);
-            } catch (MetadataException e) {
-                oldStrings = null;
-            }
             if (oldStrings == null) {
                 newStrings = new String[1];
             } else {
                 newStrings = new String[oldStrings.length + 1];
-                for (int i = 0; i < oldStrings.length; i++) {
-                    newStrings[i] = oldStrings[i];
-                }
+                System.arraycopy(oldStrings, 0, newStrings, 0, oldStrings.length);
             }
             newStrings[newStrings.length - 1] = str;
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentDescriptor.java	(revision 6127)
@@ -1,37 +1,44 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on Oct 10, 2003 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.jpeg;
 
-import com.drew.metadata.Directory;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
 /**
+ * Provides human-readable string representations of tag values stored in a <code>JpegCommentDirectory</code>.
  *
  * @author Drew Noakes http://drewnoakes.com
  */
-public class JpegCommentDescriptor extends TagDescriptor
+public class JpegCommentDescriptor extends TagDescriptor<JpegCommentDirectory>
 {
-    public JpegCommentDescriptor(Directory directory)
+    public JpegCommentDescriptor(@NotNull JpegCommentDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType)
+    @Nullable
+    public String getJpegCommentDescription()
     {
-        return _directory.getString(tagType);
+        return _directory.getString(JpegCommentDirectory.TAG_JPEG_COMMENT);
     }
 }
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentDirectory.java	(revision 6127)
@@ -1,20 +1,25 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on Oct 10, 2003 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.jpeg;
 
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Directory;
 
@@ -22,28 +27,38 @@
 
 /**
+ * Describes tags used by a JPEG file comment.
  *
  * @author Drew Noakes http://drewnoakes.com
  */
-public class JpegCommentDirectory extends Directory {
+public class JpegCommentDirectory extends Directory
+{
+    /**
+     * This value does not apply to a particular standard. Rather, this value has been fabricated to maintain
+     * consistency with other directory types.
+     */
+    public static final int TAG_JPEG_COMMENT = 0;
 
-	/** This is in bits/sample, usually 8 (12 and 16 not supported by most software). */
-	public static final int TAG_JPEG_COMMENT = 0;
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
-	protected static final HashMap tagNameMap = new HashMap();
+    static {
+        _tagNameMap.put(TAG_JPEG_COMMENT, "Jpeg Comment");
+    }
 
-	static {
-        tagNameMap.put(new Integer(TAG_JPEG_COMMENT), "Jpeg Comment");
-	}
+    public JpegCommentDirectory()
+    {
+        this.setDescriptor(new JpegCommentDescriptor(this));
+    }
 
-    public JpegCommentDirectory() {
-		this.setDescriptor(new JpegCommentDescriptor(this));
-	}
+    @NotNull
+    public String getName()
+    {
+        return "JpegComment";
+    }
 
-	public String getName() {
-		return "JpegComment";
-	}
-
-	protected HashMap getTagNameMap() {
-		return tagNameMap;
-	}
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
 }
Index: trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 6002)
+++ trunk/src/com/drew/metadata/jpeg/JpegCommentReader.java	(revision 6127)
@@ -1,29 +1,33 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on Oct 10, 2003 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.jpeg;
 
-import com.drew.imaging.jpeg.JpegProcessingException;
-import com.drew.imaging.jpeg.JpegSegmentReader;
+import com.drew.lang.BufferBoundsException;
+import com.drew.lang.BufferReader;
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
 import com.drew.metadata.MetadataReader;
 
-import java.io.File;
-import java.io.InputStream;
-
 /**
+ * Decodes the comment stored within Jpeg files, populating a <code>Metadata</code> object with tag values in a
+ * <code>JpegCommentDirectory</code>.
  *
  * @author Drew Noakes http://drewnoakes.com
@@ -32,53 +36,16 @@
 {
     /**
-     * The COM data segment.
-     */
-    private final byte[] _data;
-
-    /**
-     * Creates a new JpegReader for the specified Jpeg jpegFile.
-     */
-    public JpegCommentReader(File jpegFile) throws JpegProcessingException
-    {
-        this(new JpegSegmentReader(jpegFile).readSegment(JpegSegmentReader.SEGMENT_COM));
-    }
-
-    /** Creates a JpegCommentReader for a JPEG stream.
-     *
-     * @param is JPEG stream. Stream will be closed.
-     */
-    public JpegCommentReader(InputStream is) throws JpegProcessingException
-    {
-        this(new JpegSegmentReader(is).readSegment(JpegSegmentReader.SEGMENT_APPD));
-    }
-
-    public JpegCommentReader(byte[] data)
-    {
-        _data = data;
-    }
-
-    /**
-     * Performs the Jpeg data extraction, returning a new instance of <code>Metadata</code>.
-     */
-    public Metadata extract()
-    {
-        return extract(new Metadata());
-    }
-
-    /**
      * Performs the Jpeg data extraction, adding found values to the specified
      * instance of <code>Metadata</code>.
      */
-    public Metadata extract(Metadata metadata)
+    public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata)
     {
-        if (_data==null) {
-            return metadata;
+        JpegCommentDirectory directory = metadata.getOrCreateDirectory(JpegCommentDirectory.class);
+
+        try {
+            directory.setString(JpegCommentDirectory.TAG_JPEG_COMMENT, reader.getString(0, (int)reader.getLength()));
+        } catch (BufferBoundsException e) {
+            directory.addError("Exception reading JPEG comment string");
         }
-
-        JpegCommentDirectory directory = (JpegCommentDirectory)metadata.getDirectory(JpegCommentDirectory.class);
-
-        directory.setString(JpegCommentDirectory.TAG_JPEG_COMMENT, new String(_data));
-
-        return metadata;
     }
 }
Index: trunk/src/com/drew/metadata/jpeg/JpegComponent.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 6002)
+++ trunk/src/com/drew/metadata/jpeg/JpegComponent.java	(revision 6127)
@@ -1,33 +1,38 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on Oct 9, 17:04:07 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.jpeg;
 
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.Nullable;
 
 import java.io.Serializable;
 
 /**
- * Created by IntelliJ IDEA.
- * User: dnoakes
- * Date: 09-Oct-2003
- * Time: 17:04:07
- * To change this template use Options | File Templates.
+ * Stores information about a Jpeg image component such as the component id, horiz/vert sampling factor and
+ * quantization table number.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
 public class JpegComponent implements Serializable
 {
+    private static final long serialVersionUID = 61121257899091914L;
+
     private final int _componentId;
     private final int _samplingFactorByte;
@@ -46,5 +51,10 @@
     }
 
-    public String getComponentName() throws MetadataException
+    /**
+     * Returns the component name (one of: Y, Cb, Cr, I, or Q)
+     * @return the component name
+     */
+    @Nullable
+    public String getComponentName()
     {
         switch (_componentId)
@@ -61,6 +71,5 @@
                 return "Q";
         }
-
-        throw new MetadataException("Unsupported component id: " + _componentId);
+        return null;
     }
 
Index: trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 6002)
+++ trunk/src/com/drew/metadata/jpeg/JpegDescriptor.java	(revision 6127)
@@ -1,20 +1,26 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.jpeg;
 
-import com.drew.metadata.Directory;
-import com.drew.metadata.MetadataException;
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.TagDescriptor;
 
@@ -22,16 +28,21 @@
  * Provides human-readable string versions of the tags stored in a JpegDirectory.
  * Thanks to Darrell Silver (www.darrellsilver.com) for the initial version of this class.
+ *
+ * @author Drew Noakes http://drewnoakes.com
  */
-public class JpegDescriptor extends TagDescriptor
+public class JpegDescriptor extends TagDescriptor<JpegDirectory>
 {
-    public JpegDescriptor(Directory directory)
+    public JpegDescriptor(@NotNull JpegDirectory directory)
     {
         super(directory);
     }
 
-    public String getDescription(int tagType) throws MetadataException
+    @Nullable
+    public String getDescription(int tagType)
     {
         switch (tagType)
         {
+            case JpegDirectory.TAG_JPEG_COMPRESSION_TYPE:
+                return getImageCompressionTypeDescription();
             case JpegDirectory.TAG_JPEG_COMPONENT_DATA_1:
                 return getComponentDataDescription(0);
@@ -48,39 +59,78 @@
             case JpegDirectory.TAG_JPEG_IMAGE_WIDTH:
                 return getImageWidthDescription();
+            default:
+                return super.getDescription(tagType);
         }
-
-        return _directory.getString(tagType);
     }
 
+    @Nullable
+    public String getImageCompressionTypeDescription()
+    {
+        Integer value = _directory.getInteger(JpegDirectory.TAG_JPEG_COMPRESSION_TYPE);
+        if (value==null)
+            return null;
+        // Note there is no 2 or 12
+        switch (value) {
+            case 0: return "Baseline";
+            case 1: return "Extended sequential, Huffman";
+            case 2: return "Progressive, Huffman";
+            case 3: return "Lossless, Huffman";
+            case 5: return "Differential sequential, Huffman";
+            case 6: return "Differential progressive, Huffman";
+            case 7: return "Differential lossless, Huffman";
+            case 8: return "Reserved for JPEG extensions";
+            case 9: return "Extended sequential, arithmetic";
+            case 10: return "Progressive, arithmetic";
+            case 11: return "Lossless, arithmetic";
+            case 13: return "Differential sequential, arithmetic";
+            case 14: return "Differential progressive, arithmetic";
+            case 15: return "Differential lossless, arithmetic";
+            default:
+                return "Unknown type: "+ value;
+        }
+    }
+    @Nullable
     public String getImageWidthDescription()
     {
-        return _directory.getString(JpegDirectory.TAG_JPEG_IMAGE_WIDTH) + " pixels";
+        final String value = _directory.getString(JpegDirectory.TAG_JPEG_IMAGE_WIDTH);
+        if (value==null)
+            return null;
+        return value + " pixels";
     }
 
+    @Nullable
     public String getImageHeightDescription()
     {
-        return _directory.getString(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT) + " pixels";
+        final String value = _directory.getString(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT);
+        if (value==null)
+            return null;
+        return value + " pixels";
     }
 
+    @Nullable
     public String getDataPrecisionDescription()
     {
-        return _directory.getString(JpegDirectory.TAG_JPEG_DATA_PRECISION) + " bits";
+        final String value = _directory.getString(JpegDirectory.TAG_JPEG_DATA_PRECISION);
+        if (value==null)
+            return null;
+        return value + " bits";
     }
 
-    public String getComponentDataDescription(int componentNumber) throws MetadataException
+    @Nullable
+    public String getComponentDataDescription(int componentNumber)
     {
-        JpegComponent component = ((JpegDirectory)_directory).getComponent(componentNumber);
+        JpegComponent value = _directory.getComponent(componentNumber);
 
-        if (component==null)
-            throw new MetadataException("No Jpeg component exists with number " + componentNumber);
+        if (value==null)
+            return null;
 
-        StringBuffer sb = new StringBuffer();
-        sb.append(component.getComponentName());
+        StringBuilder sb = new StringBuilder();
+        sb.append(value.getComponentName());
         sb.append(" component: Quantization table ");
-        sb.append(component.getQuantizationTableNumber());
+        sb.append(value.getQuantizationTableNumber());
         sb.append(", Sampling factors ");
-        sb.append(component.getHorizontalSamplingFactor());
+        sb.append(value.getHorizontalSamplingFactor());
         sb.append(" horiz/");
-        sb.append(component.getVerticalSamplingFactor());
+        sb.append(value.getVerticalSamplingFactor());
         sb.append(" vert");
         return sb.toString();
Index: trunk/src/com/drew/metadata/jpeg/JpegDirectory.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 6002)
+++ trunk/src/com/drew/metadata/jpeg/JpegDirectory.java	(revision 6127)
@@ -1,20 +1,26 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created on Aug 2, 2003.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.jpeg;
 
+import com.drew.lang.annotations.NotNull;
+import com.drew.lang.annotations.Nullable;
 import com.drew.metadata.Directory;
 import com.drew.metadata.MetadataException;
@@ -24,73 +30,80 @@
 /**
  * Directory of tags and values for the SOF0 Jpeg segment.  This segment holds basic metadata about the image.
- * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes
+ *
+ * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes http://drewnoakes.com
  */
-public class JpegDirectory extends Directory {
-
-	/** This is in bits/sample, usually 8 (12 and 16 not supported by most software). */
-	public static final int TAG_JPEG_DATA_PRECISION = 0;
-	/** The image's height.  Necessary for decoding the image, so it should always be there. */
-	public static final int TAG_JPEG_IMAGE_HEIGHT = 1;
-	/** The image's width.  Necessary for decoding the image, so it should always be there. */
-	public static final int TAG_JPEG_IMAGE_WIDTH = 3;
-	/** Usually 1 = grey scaled, 3 = color YcbCr or YIQ, 4 = color CMYK
-	 * Each component TAG_COMPONENT_DATA_[1-4], has the following meaning:
-	 * component Id(1byte)(1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q),
-	 * sampling factors (1byte) (bit 0-3 vertical., 4-7 horizontal.),
-	 * quantization table number (1 byte).
-	 * <p>
-	 * This info is from http://www.funducode.com/freec/Fileformats/format3/format3b.htm
-	 */
-	public static final int TAG_JPEG_NUMBER_OF_COMPONENTS = 5;
+public class JpegDirectory extends Directory
+{
+    public static final int TAG_JPEG_COMPRESSION_TYPE = -3;
+    /** This is in bits/sample, usually 8 (12 and 16 not supported by most software). */
+    public static final int TAG_JPEG_DATA_PRECISION = 0;
+    /** The image's height.  Necessary for decoding the image, so it should always be there. */
+    public static final int TAG_JPEG_IMAGE_HEIGHT = 1;
+    /** The image's width.  Necessary for decoding the image, so it should always be there. */
+    public static final int TAG_JPEG_IMAGE_WIDTH = 3;
+    /**
+     * Usually 1 = grey scaled, 3 = color YcbCr or YIQ, 4 = color CMYK
+     * Each component TAG_COMPONENT_DATA_[1-4], has the following meaning:
+     * component Id(1byte)(1 = Y, 2 = Cb, 3 = Cr, 4 = I, 5 = Q),
+     * sampling factors (1byte) (bit 0-3 vertical., 4-7 horizontal.),
+     * quantization table number (1 byte).
+     * <p/>
+     * This info is from http://www.funducode.com/freec/Fileformats/format3/format3b.htm
+     */
+    public static final int TAG_JPEG_NUMBER_OF_COMPONENTS = 5;
 
     // NOTE!  Component tag type int values must increment in steps of 1
 
-	/** the first of a possible 4 color components.  Number of components specified in TAG_JPEG_NUMBER_OF_COMPONENTS.*/
-	public static final int TAG_JPEG_COMPONENT_DATA_1 = 6;
-	/** the second of a possible 4 color components.  Number of components specified in TAG_JPEG_NUMBER_OF_COMPONENTS.*/
-	public static final int TAG_JPEG_COMPONENT_DATA_2 = 7;
-	/** the third of a possible 4 color components.  Number of components specified in TAG_JPEG_NUMBER_OF_COMPONENTS.*/
-	public static final int TAG_JPEG_COMPONENT_DATA_3 = 8;
-	/** the fourth of a possible 4 color components.  Number of components specified in TAG_JPEG_NUMBER_OF_COMPONENTS.*/
-	public static final int TAG_JPEG_COMPONENT_DATA_4 = 9;
+    /** the first of a possible 4 color components.  Number of components specified in TAG_JPEG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_JPEG_COMPONENT_DATA_1 = 6;
+    /** the second of a possible 4 color components.  Number of components specified in TAG_JPEG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_JPEG_COMPONENT_DATA_2 = 7;
+    /** the third of a possible 4 color components.  Number of components specified in TAG_JPEG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_JPEG_COMPONENT_DATA_3 = 8;
+    /** the fourth of a possible 4 color components.  Number of components specified in TAG_JPEG_NUMBER_OF_COMPONENTS. */
+    public static final int TAG_JPEG_COMPONENT_DATA_4 = 9;
 
-	protected static final HashMap tagNameMap = new HashMap();
+    @NotNull
+    protected static final HashMap<Integer, String> _tagNameMap = new HashMap<Integer, String>();
 
-	static {
-        tagNameMap.put(new Integer(TAG_JPEG_DATA_PRECISION), "Data Precision");
-        tagNameMap.put(new Integer(TAG_JPEG_IMAGE_WIDTH), "Image Width");
-        tagNameMap.put(new Integer(TAG_JPEG_IMAGE_HEIGHT), "Image Height");
-		tagNameMap.put(new Integer(TAG_JPEG_NUMBER_OF_COMPONENTS), "Number of Components");
-		tagNameMap.put(new Integer(TAG_JPEG_COMPONENT_DATA_1), "Component 1");
-		tagNameMap.put(new Integer(TAG_JPEG_COMPONENT_DATA_2), "Component 2");
-		tagNameMap.put(new Integer(TAG_JPEG_COMPONENT_DATA_3), "Component 3");
-		tagNameMap.put(new Integer(TAG_JPEG_COMPONENT_DATA_4), "Component 4");
-	}
+    static {
+        _tagNameMap.put(TAG_JPEG_COMPRESSION_TYPE, "Compression Type");
+        _tagNameMap.put(TAG_JPEG_DATA_PRECISION, "Data Precision");
+        _tagNameMap.put(TAG_JPEG_IMAGE_WIDTH, "Image Width");
+        _tagNameMap.put(TAG_JPEG_IMAGE_HEIGHT, "Image Height");
+        _tagNameMap.put(TAG_JPEG_NUMBER_OF_COMPONENTS, "Number of Components");
+        _tagNameMap.put(TAG_JPEG_COMPONENT_DATA_1, "Component 1");
+        _tagNameMap.put(TAG_JPEG_COMPONENT_DATA_2, "Component 2");
+        _tagNameMap.put(TAG_JPEG_COMPONENT_DATA_3, "Component 3");
+        _tagNameMap.put(TAG_JPEG_COMPONENT_DATA_4, "Component 4");
+    }
 
-    public JpegDirectory() {
-		this.setDescriptor(new JpegDescriptor(this));
-	}
+    public JpegDirectory()
+    {
+        this.setDescriptor(new JpegDescriptor(this));
+    }
 
-	public String getName() {
-		return "Jpeg";
-	}
+    @NotNull
+    public String getName()
+    {
+        return "Jpeg";
+    }
 
-	protected HashMap getTagNameMap() {
-		return tagNameMap;
-	}
+    @NotNull
+    protected HashMap<Integer, String> getTagNameMap()
+    {
+        return _tagNameMap;
+    }
 
     /**
-     *
      * @param componentNumber The zero-based index of the component.  This number is normally between 0 and 3.
-     *        Use getNumberOfComponents for bounds-checking.
-     * @return
+     *                        Use getNumberOfComponents for bounds-checking.
+     * @return the JpegComponent having the specified number.
      */
+    @Nullable
     public JpegComponent getComponent(int componentNumber)
     {
         int tagType = JpegDirectory.TAG_JPEG_COMPONENT_DATA_1 + componentNumber;
-
-        JpegComponent component = (JpegComponent)getObject(tagType);
-
-        return component;
+        return (JpegComponent)getObject(tagType);
     }
 
Index: trunk/src/com/drew/metadata/jpeg/JpegReader.java
===================================================================
--- trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 6002)
+++ trunk/src/com/drew/metadata/jpeg/JpegReader.java	(revision 6127)
@@ -1,97 +1,60 @@
 /*
- * This is public domain software - that is, you can do whatever you want
- * with it, and include it software that is licensed under the GNU or the
- * BSD license, or whatever other licence you choose, including proprietary
- * closed source licenses.  I do ask that you leave this header in tact.
+ * Copyright 2002-2012 Drew Noakes
  *
- * If you make modifications to this code that you think would benefit the
- * wider community, please send me a copy and I'll post it on my site.
+ *    Licensed under the Apache License, Version 2.0 (the "License");
+ *    you may not use this file except in compliance with the License.
+ *    You may obtain a copy of the License at
  *
- * If you make use of this code, I'd appreciate hearing about it.
- *   drew@drewnoakes.com
- * Latest version of this software kept at
- *   http://drewnoakes.com/
+ *        http://www.apache.org/licenses/LICENSE-2.0
  *
- * Created by dnoakes on Aug 2, 2003 using IntelliJ IDEA.
+ *    Unless required by applicable law or agreed to in writing, software
+ *    distributed under the License is distributed on an "AS IS" BASIS,
+ *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *    See the License for the specific language governing permissions and
+ *    limitations under the License.
+ *
+ * More information about this project is available at:
+ *
+ *    http://drewnoakes.com/code/exif/
+ *    http://code.google.com/p/metadata-extractor/
  */
 package com.drew.metadata.jpeg;
 
-import com.drew.imaging.jpeg.JpegProcessingException;
-import com.drew.imaging.jpeg.JpegSegmentReader;
+import com.drew.lang.BufferBoundsException;
+import com.drew.lang.BufferReader;
+import com.drew.lang.annotations.NotNull;
 import com.drew.metadata.Metadata;
-import com.drew.metadata.MetadataException;
 import com.drew.metadata.MetadataReader;
 
-import java.io.File;
-import java.io.InputStream;
-
 /**
+ * Decodes Jpeg SOF0 data, populating a <code>Metadata</code> object with tag values in a <code>JpegDirectory</code>.
  *
- * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes
+ * @author Darrell Silver http://www.darrellsilver.com and Drew Noakes http://drewnoakes.com
  */
 public class JpegReader implements MetadataReader
 {
     /**
-     * The SOF0 data segment.
-     */
-    private final byte[] _data;
-
-    /**
-     * Creates a new JpegReader for the specified Jpeg jpegFile.
-     */
-    public JpegReader(File jpegFile) throws JpegProcessingException
-    {
-        this(new JpegSegmentReader(jpegFile).readSegment(JpegSegmentReader.SEGMENT_SOF0));
-    }
-
-    /** Creates a JpegReader for a JPEG stream.
-     *
-     * @param is JPEG stream. Stream will be closed.
-     */
-    public JpegReader(InputStream is) throws JpegProcessingException
-    {
-        this(new JpegSegmentReader(is).readSegment(JpegSegmentReader.SEGMENT_APPD));
-    }
-
-    public JpegReader(byte[] data)
-    {
-        _data = data;
-    }
-
-    /**
-     * Performs the Jpeg data extraction, returning a new instance of <code>Metadata</code>.
-     */
-    public Metadata extract()
-    {
-        return extract(new Metadata());
-    }
-
-    /**
      * Performs the Jpeg data extraction, adding found values to the specified
      * instance of <code>Metadata</code>.
      */
-    public Metadata extract(Metadata metadata)
+    public void extract(@NotNull final BufferReader reader, @NotNull Metadata metadata)
     {
-        if (_data==null) {
-            return metadata;
-        }
-
-        JpegDirectory directory = (JpegDirectory)metadata.getDirectory(JpegDirectory.class);
+        JpegDirectory directory = metadata.getOrCreateDirectory(JpegDirectory.class);
 
         try {
             // data precision
-            int dataPrecision = get16Bits(JpegDirectory.TAG_JPEG_DATA_PRECISION);
+            int dataPrecision = reader.getUInt8(JpegDirectory.TAG_JPEG_DATA_PRECISION);
             directory.setInt(JpegDirectory.TAG_JPEG_DATA_PRECISION, dataPrecision);
 
             // process height
-            int height = get32Bits(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT);
+            int height = reader.getUInt16(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT);
             directory.setInt(JpegDirectory.TAG_JPEG_IMAGE_HEIGHT, height);
 
             // process width
-            int width = get32Bits(JpegDirectory.TAG_JPEG_IMAGE_WIDTH);
+            int width = reader.getUInt16(JpegDirectory.TAG_JPEG_IMAGE_WIDTH);
             directory.setInt(JpegDirectory.TAG_JPEG_IMAGE_WIDTH, width);
 
             // number of components
-            int numberOfComponents = get16Bits(JpegDirectory.TAG_JPEG_NUMBER_OF_COMPONENTS);
+            int numberOfComponents = reader.getUInt8(JpegDirectory.TAG_JPEG_NUMBER_OF_COMPONENTS);
             directory.setInt(JpegDirectory.TAG_JPEG_NUMBER_OF_COMPONENTS, numberOfComponents);
 
@@ -101,46 +64,15 @@
             // 3 - Quantization table number
             int offset = 6;
-            for (int i=0; i<numberOfComponents; i++)
-            {
-                int componentId = get16Bits(offset++);
-                int samplingFactorByte = get16Bits(offset++);
-                int quantizationTableNumber = get16Bits(offset++);
+            for (int i = 0; i < numberOfComponents; i++) {
+                int componentId = reader.getUInt8(offset++);
+                int samplingFactorByte = reader.getUInt8(offset++);
+                int quantizationTableNumber = reader.getUInt8(offset++);
                 JpegComponent component = new JpegComponent(componentId, samplingFactorByte, quantizationTableNumber);
                 directory.setObject(JpegDirectory.TAG_JPEG_COMPONENT_DATA_1 + i, component);
             }
 
-        } catch (MetadataException me) {
-            directory.addError("MetadataException: " + me);
+        } catch (BufferBoundsException ex) {
+            directory.addError(ex.getMessage());
         }
-
-        return metadata;
-    }
-
-    /**
-     * Returns an int calculated from two bytes of data at the specified offset (MSB, LSB).
-     * @param offset position within the data buffer to read first byte
-     * @return the 32 bit int value, between 0x0000 and 0xFFFF
-     */
-    private int get32Bits(int offset) throws MetadataException
-    {
-        if (offset+1>=_data.length) {
-            throw new MetadataException("Attempt to read bytes from outside Jpeg segment data buffer");
-        }
-
-        return ((_data[offset] & 255) << 8) | (_data[offset + 1] & 255);
-    }
-
-    /**
-     * Returns an int calculated from one byte of data at the specified offset.
-     * @param offset position within the data buffer to read byte
-     * @return the 16 bit int value, between 0x00 and 0xFF
-     */
-    private int get16Bits(int offset) throws MetadataException
-    {
-        if (offset>=_data.length) {
-            throw new MetadataException("Attempt to read bytes from outside Jpeg segment data buffer");
-        }
-
-        return (_data[offset] & 255);
     }
 }
